userIdc2024 commited on
Commit
1924d43
·
verified ·
1 Parent(s): e28bea9

Upload 27 files

Browse files
app_pages/__pycache__/comparison.cpython-311.pyc ADDED
Binary file (22.4 kB). View file
 
app_pages/__pycache__/script_generator.cpython-311.pyc ADDED
Binary file (10.6 kB). View file
 
app_pages/__pycache__/video_analyzer.cpython-311.pyc ADDED
Binary file (7.68 kB). View file
 
app_pages/comparison.py ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import uuid
4
+ import logging
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import pandas as pd
8
+ import streamlit as st
9
+
10
+ from services.video_analyzer import analyze_multiple_videos
11
+ from services.comparison import generate_comparison_summary
12
+ from database import insert_comparison_result, get_all_comparisons
13
+
14
+
15
+ # ---------- Logging Setup ----------
16
+
17
+ LOGGER_NAME = "app_pages.comparison"
18
+ logger = logging.getLogger(LOGGER_NAME)
19
+ if not logger.handlers:
20
+ logging.basicConfig(
21
+ level=os.environ.get("LOG_LEVEL", "INFO"),
22
+ format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
23
+ )
24
+ logger.setLevel(os.environ.get("LOG_LEVEL", "INFO"))
25
+
26
+
27
+ def _log_exception(context: str, exc: Exception) -> None:
28
+ """Log exceptions with context and show a clean UI error."""
29
+ logger.exception("Exception in %s: %s", context, exc)
30
+ st.error(f"{context} failed: {exc}")
31
+
32
+
33
+ def _rerun_analyses(analyses: List[Dict[str, Any]]) -> str:
34
+ """Stable fingerprint of analyses to avoid recomputation across reruns."""
35
+ try:
36
+
37
+ slim = []
38
+ for item in analyses or []:
39
+ slim.append({
40
+ "video_name": item.get("video_name"),
41
+ # Avoid huge or non-deterministic blobs like thumbnails
42
+ "analysis": item.get("analysis", {}),
43
+ })
44
+ return json.dumps(slim, sort_keys=True, ensure_ascii=False)
45
+ except Exception as e:
46
+ _log_exception("_rerun_analyses", e)
47
+ # Fallback: random to force recompute
48
+ return str(uuid.uuid4())
49
+
50
+
51
+ # ---------- Helpers ----------
52
+
53
+ def _mean_effectiveness(metrics):
54
+ """Compute average effectiveness score from e.g. '7/10' style values."""
55
+ scores = []
56
+ for m in metrics or []:
57
+ try:
58
+ scores.append(int(str(m.get("effectiveness_score", "0/10")).split("/")[0]))
59
+ except Exception:
60
+ # Log and skip bad value
61
+ logger.debug("Bad effectiveness_score value: %s", m)
62
+ pass
63
+ return round(sum(scores) / len(scores), 2) if scores else 0.0
64
+
65
+
66
+ def compare_analyses(analyses):
67
+ """Build a structured dict from analyses for tabular display."""
68
+ comparison = {
69
+ "hooks": [],
70
+ "frameworks": [],
71
+ "audiences": [],
72
+ "metrics_summary": [],
73
+ "improvements": []
74
+ }
75
+ for item in analyses:
76
+ try:
77
+ name = item["video_name"]
78
+ analysis = item["analysis"]
79
+
80
+ hook = analysis.get("hook", {}) or {}
81
+ comparison["hooks"].append({
82
+ "video": name,
83
+ "hook_text": hook.get("hook_text"),
84
+ "principle": hook.get("principle")
85
+ })
86
+
87
+ comparison["frameworks"].append({
88
+ "video": name,
89
+ "framework_analysis": analysis.get("framework_analysis")
90
+ })
91
+
92
+ va = analysis.get("video_analysis", {}) or {}
93
+ comparison["audiences"].append({
94
+ "video": name,
95
+ "audience": va.get("target_audience")
96
+ })
97
+
98
+ metrics = va.get("video_metrics", []) or []
99
+ avg = _mean_effectiveness(metrics)
100
+ comparison["metrics_summary"].append({
101
+ "video": name,
102
+ "avg_score": avg
103
+ })
104
+
105
+ comparison["improvements"].append({
106
+ "video": name,
107
+ "recommendations": analysis.get("timestamp_improvements", []) or []
108
+ })
109
+ except Exception as e:
110
+ _log_exception("compare_analyses (per-item)", e)
111
+ return comparison
112
+
113
+
114
+ def _arrow_safe_df(df: pd.DataFrame) -> pd.DataFrame:
115
+ """
116
+ Make dataframe Arrow-compatible for Streamlit:
117
+ - Replace NaN with empty strings
118
+ - Coerce to string to avoid mixed object dtype issues
119
+ """
120
+ try:
121
+ return df.fillna("").astype(str)
122
+ except Exception as e:
123
+ _log_exception("_arrow_safe_df", e)
124
+ return df # last resort (may still error later)
125
+
126
+
127
+ def _ensure_state_keys():
128
+ """Initialize session_state keys used in this page."""
129
+ defaults = {
130
+ "comparison_prompt": "Compare these videos",
131
+ "analyses": None,
132
+ "summary": None,
133
+ "comparison_dict": None,
134
+ "_analyses_fp": None,
135
+ "_run_no": 0,
136
+ "_last_action": None,
137
+ "_last_tab": None,
138
+ }
139
+ for k, v in defaults.items():
140
+ if k not in st.session_state:
141
+ st.session_state[k] = v
142
+
143
+
144
+ def _log_run_header(selected_tab: str):
145
+ """Log a trace line at every rerun to make lifecycle obvious."""
146
+ st.session_state["_run_no"] = int(st.session_state.get("_run_no", 0)) + 1
147
+ run_no = st.session_state["_run_no"]
148
+ analyses_present = st.session_state.get("analyses") is not None
149
+ summary_present = st.session_state.get("summary") is not None
150
+ comp_present = st.session_state.get("comparison_dict") is not None
151
+ last_action = st.session_state.get("_last_action")
152
+ logger.info(
153
+ "RERUN #%d | tab=%s | analyses=%s | summary=%s | table=%s | last_action=%s",
154
+ run_no,
155
+ selected_tab,
156
+ analyses_present,
157
+ summary_present,
158
+ comp_present,
159
+ last_action,
160
+ )
161
+
162
+
163
+
164
+ def _save_comparison_callback():
165
+ """Button callback to save comparison to DB exactly once per click."""
166
+ try:
167
+ analyses = st.session_state.get("analyses")
168
+ comparison_dict = st.session_state.get("comparison_dict")
169
+ summary = st.session_state.get("summary")
170
+
171
+ logger.info("Save callback invoked | analyses=%s | table=%s | summary=%s",
172
+ analyses is not None, comparison_dict is not None, summary is not None)
173
+
174
+ if not analyses or not comparison_dict or summary is None:
175
+ st.warning("Nothing to save yet. Please run a comparison first.")
176
+ logger.warning("Save aborted: missing data (analyses/table/summary).")
177
+ return
178
+
179
+ video_names = [item["video_name"] for item in analyses]
180
+ thumbnails = {item["video_name"]: item.get("thumbnail", "") for item in analyses}
181
+
182
+ logger.info("Inserting comparison_result | videos=%s", video_names)
183
+ insert_comparison_result(
184
+ video_name="comparison_result",
185
+ video_names=video_names,
186
+ user_prompt=st.session_state.get("comparison_prompt", ""),
187
+ response={"comparison_table": comparison_dict, "summary": summary},
188
+ thumbnails=thumbnails
189
+ )
190
+ st.success("Comparison saved to database!")
191
+ st.session_state["_last_action"] = "saved_to_db"
192
+ logger.info("Save completed successfully.")
193
+
194
+ except Exception as e:
195
+ _log_exception("_save_comparison_callback", e)
196
+
197
+
198
+ # ---------- Page ----------
199
+
200
+ def comparison_page():
201
+ _ensure_state_keys()
202
+
203
+ selected_tab = st.sidebar.radio("Select Mode", ["Comparison", "History"], index=0, key="tab_radio")
204
+
205
+
206
+ if st.session_state.get("_last_tab") != selected_tab:
207
+ st.session_state["_last_tab"] = selected_tab
208
+ _log_run_header(selected_tab)
209
+
210
+ if selected_tab == "Comparison":
211
+ st.subheader("Video Comparison")
212
+
213
+
214
+ num_videos = st.slider("Select Number of Videos to Compare", 2, 5, 2, key="num_videos_slider")
215
+
216
+
217
+ uploaded_videos = []
218
+ for i in range(num_videos):
219
+ try:
220
+ file = st.file_uploader(
221
+ f"Upload Video {i+1}",
222
+ type=["mp4", "mov", "avi", "mkv"],
223
+ key=f"video_{i}"
224
+ )
225
+ if file:
226
+ uploaded_videos.append(file)
227
+ except Exception as e:
228
+ _log_exception(f"file_uploader[{i}]", e)
229
+
230
+ # Run comparison
231
+ if st.button("Run Comparison", use_container_width=True, key="run_comparison_btn"):
232
+ logger.info("Run Comparison clicked | uploaded=%d / expected=%d", len(uploaded_videos), num_videos)
233
+ if len(uploaded_videos) < num_videos:
234
+ st.error("Please upload all videos before running comparison.")
235
+ logger.warning("Run Comparison aborted: insufficient uploads.")
236
+ else:
237
+ try:
238
+ with st.spinner("Analyzing videos..."):
239
+ analyses = analyze_multiple_videos(uploaded_videos)
240
+ st.session_state["analyses"] = analyses
241
+ st.session_state["_analyses_fp"] = _rerun_analyses(analyses)
242
+ st.session_state["summary"] = None
243
+ st.session_state["comparison_dict"] = None
244
+ logger.info("Analyses computed. Set rerun and cleared summary/table.")
245
+ except Exception as e:
246
+ _log_exception("analyze_multiple_videos", e)
247
+
248
+
249
+ analyses = st.session_state.get("analyses")
250
+ if analyses:
251
+ st.divider()
252
+ st.subheader("Comparison")
253
+
254
+ current_fp = _rerun_analyses(analyses)
255
+ cached_fp = st.session_state.get("_analyses_fp")
256
+
257
+ # ---- Summary ----
258
+ st.markdown("#### Comparison Summary")
259
+ try:
260
+ if st.session_state.get("summary") is None or current_fp != cached_fp:
261
+ logger.info("Generating summary (fp changed? %s)", current_fp != cached_fp)
262
+ with st.spinner("Generating comparison..."):
263
+ summary = generate_comparison_summary(
264
+ analyses,
265
+ st.session_state.get("comparison_prompt", "Compare these videos")
266
+ )
267
+ st.session_state["summary"] = summary
268
+ st.session_state["_analyses_fp"] = current_fp
269
+ else:
270
+ logger.info("Reusing cached summary.")
271
+ st.markdown(st.session_state["summary"])
272
+ except Exception as e:
273
+ _log_exception("generate_comparison_summary", e)
274
+
275
+ # ---- Structured Comparison ----
276
+ st.markdown("#### Structured Comparison")
277
+ try:
278
+ if st.session_state.get("comparison_dict") is None or current_fp != cached_fp:
279
+ logger.info("Building comparison table (fp changed? %s)", current_fp != cached_fp)
280
+ comparison = compare_analyses(analyses)
281
+
282
+
283
+ comparison_dict = {}
284
+ for hook, fw, aud, met in zip(
285
+ comparison["hooks"],
286
+ comparison["frameworks"],
287
+ comparison["audiences"],
288
+ comparison["metrics_summary"]
289
+ ):
290
+ video = hook["video"]
291
+ comparison_dict[video] = {
292
+ "Hook Text": hook.get("hook_text", ""),
293
+ "Principle": hook.get("principle", ""),
294
+ "Framework Analysis": fw.get("framework_analysis", ""),
295
+ "Target Audience": aud.get("audience", ""),
296
+ "Avg Score": met.get("avg_score", ""),
297
+ }
298
+
299
+ st.session_state["comparison_dict"] = comparison_dict
300
+ st.session_state["_analyses_fp"] = current_fp
301
+ else:
302
+ logger.info("Reusing cached comparison table.")
303
+
304
+ df_horizontal = pd.DataFrame(st.session_state["comparison_dict"])
305
+ df_display = _arrow_safe_df(df_horizontal.copy())
306
+ st.dataframe(df_display, use_container_width=True)
307
+
308
+ csv_data = df_horizontal.to_csv(index=True).encode("utf-8")
309
+ st.download_button(
310
+ "Download CSV",
311
+ data=csv_data,
312
+ file_name="comparison_results.csv",
313
+ mime="text/csv",
314
+ use_container_width=True,
315
+ key="download_current_csv_btn"
316
+ )
317
+ except Exception as e:
318
+ _log_exception("Structured Comparison section", e)
319
+
320
+ st.button(
321
+ "Save to DB",
322
+ use_container_width=True,
323
+ key="save_to_db_btn",
324
+ on_click=_save_comparison_callback
325
+ )
326
+
327
+ if st.session_state.get("_last_action") == "saved_to_db":
328
+ st.info("Saved to DB")
329
+
330
+ else:
331
+ logger.info("No analyses in session_state yet.")
332
+
333
+ else:
334
+ # ---------- History ----------
335
+ logger.info("Entering History tab.")
336
+ try:
337
+ history_items = get_all_comparisons(limit=20)
338
+ logger.info("Fetched %d history items.", len(history_items) if history_items else 0)
339
+ except Exception as e:
340
+ _log_exception("get_all_comparisons", e)
341
+ history_items = []
342
+
343
+ if history_items:
344
+ # Titles for sidebar selection
345
+ try:
346
+ titles = [
347
+ f"{item['video_name']} ({item['created_at'].strftime('%Y-%m-%d %H:%M')})"
348
+ for item in history_items
349
+ ]
350
+ except Exception:
351
+ titles = [
352
+ f"{item.get('video_name', 'comparison_result')} ({item.get('created_at')})"
353
+ for item in history_items
354
+ ]
355
+
356
+ try:
357
+ selected = st.sidebar.radio("History Items", titles, index=0, key="history_select_radio")
358
+ idx = titles.index(selected)
359
+ selected_data = history_items[idx]
360
+ logger.info("History selection: index=%d title=%s", idx, selected)
361
+ except Exception as e:
362
+ _log_exception("History selection radio", e)
363
+ selected_data = history_items[0]
364
+
365
+ st.subheader("Comparison Result")
366
+
367
+
368
+ try:
369
+ if "video_names" in selected_data:
370
+ st.markdown("### Compared Videos")
371
+ cols = st.columns(len(selected_data["video_names"]))
372
+ for i, name in enumerate(selected_data["video_names"]):
373
+ with cols[i]:
374
+ thumb = selected_data.get("thumbnails", {}).get(name, "")
375
+ if thumb:
376
+ st.image("data:image/jpeg;base64," + thumb, width=120)
377
+ st.caption(name)
378
+ except Exception as e:
379
+ _log_exception("Rendering thumbnails", e)
380
+
381
+ # Response content
382
+ response = selected_data.get("response", {}) or {}
383
+
384
+ # Summary
385
+ try:
386
+ if "summary" in response:
387
+ st.markdown("### Comparison Summary")
388
+ st.markdown(response["summary"])
389
+ except Exception as e:
390
+ _log_exception("Rendering history summary", e)
391
+
392
+ # Table
393
+ try:
394
+ if "comparison_table" in response:
395
+ st.markdown("### Structured Comparison")
396
+ df_hist = pd.DataFrame(response["comparison_table"])
397
+ df_hist_display = _arrow_safe_df(df_hist.copy())
398
+ st.dataframe(df_hist_display, use_container_width=True)
399
+
400
+ csv_hist = df_hist.to_csv(index=True).encode("utf-8")
401
+ st.download_button(
402
+ "Download CSV",
403
+ data=csv_hist,
404
+ file_name="past_comparison.csv",
405
+ mime="text/csv",
406
+ use_container_width=True,
407
+ key="download_history_csv_btn"
408
+ )
409
+ except Exception as e:
410
+ _log_exception("Rendering history table", e)
411
+
412
+ else:
413
+ st.sidebar.info("No saved comparisons yet.")
414
+ st.info("No saved history available.")
415
+ logger.info("History tab: no items.")
416
+
app_pages/script_generator.py ADDED
@@ -0,0 +1,155 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import pandas as pd
4
+ import streamlit as st
5
+
6
+ from services.script_generator import generate_scripts
7
+ from utils.video import get_video_thumbnail_base64
8
+ from components.display_variations import display_script_variations
9
+ from database import insert_script_result, get_all_scripts
10
+
11
+ def generator_page():
12
+ selected_tab = st.sidebar.radio("Select Mode", ["Script Generator", "History"], index=0)
13
+
14
+ if selected_tab == "Script Generator":
15
+ st.subheader("Script Generator")
16
+
17
+ uploaded_video = st.file_uploader(
18
+ "Upload Video or ZIP (max 3 videos)",
19
+ type=['mp4','mov','avi','mkv','zip']
20
+ )
21
+ script_duration = st.slider("Script Duration (seconds)", 0, 180, 60, 5)
22
+ num_scripts = st.slider("Number of Scripts", 1, 5, 3)
23
+
24
+ st.markdown("Additional Information")
25
+ offer_details = st.text_area("Offer Details", placeholder="e.g., Solar installation with $0 down payment...")
26
+ target_audience = st.text_area("Target Audience", placeholder="e.g., 40+ homeowners with high electricity bills...")
27
+ specific_hooks = st.text_area("Specific Hooks to Test", placeholder="e.g., Government rebate angle...")
28
+ additional_context = st.text_area("Additional Context", placeholder="Compliance requirements, brand guidelines...")
29
+
30
+ script_button = st.button("Generate Scripts", use_container_width=True)
31
+ if script_button and uploaded_video:
32
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_video.name)[1]) as tmp:
33
+ tmp.write(uploaded_video.read())
34
+ video_path = tmp.name
35
+ with st.spinner("Generating scripts..."):
36
+ st.session_state.setdefault("scripts", [])
37
+ result = generate_scripts(
38
+ video_path,
39
+ offer_details,
40
+ target_audience,
41
+ specific_hooks,
42
+ additional_context,
43
+ num_scripts=num_scripts,
44
+ duration=script_duration
45
+ )
46
+ if result and "script_variations" in result:
47
+ st.session_state["scripts"].append({
48
+ "prompt_used": "Initial Generation",
49
+ "variations": result["script_variations"]
50
+ })
51
+ st.session_state["video_name"] = uploaded_video.name
52
+ st.session_state["video_path"] = video_path
53
+ st.session_state["thumbnail"] = get_video_thumbnail_base64(video_path)
54
+ st.session_state["meta"] = {
55
+ "offer_details": offer_details,
56
+ "target_audience": target_audience,
57
+ "specific_hook": specific_hooks,
58
+ "additional_context": additional_context
59
+ }
60
+
61
+ if "scripts" in st.session_state and st.session_state["scripts"]:
62
+ for round_idx, round_data in enumerate(st.session_state["scripts"], 1):
63
+ st.markdown(f"### Generation Round {round_idx}")
64
+ st.text_input("Prompt used:", round_data["prompt_used"], disabled=True, key=f"prompt_{round_idx}")
65
+ for i, variation in enumerate(round_data["variations"], 1):
66
+ st.markdown(f"#### Variation {i}: {variation.get('variation_name','Var')}")
67
+ df = pd.DataFrame(variation.get("script_table", []))
68
+ st.table(df)
69
+
70
+ st.divider()
71
+ save_button = st.button("Save to DB", use_container_width=True)
72
+ if save_button:
73
+ try:
74
+ insert_script_result(
75
+ video_name=st.session_state.get("video_name", "unknown"),
76
+ offer_details=st.session_state["meta"].get("offer_details", ""),
77
+ target_audience=st.session_state["meta"].get("target_audience", ""),
78
+ specific_hook=st.session_state["meta"].get("specific_hook", ""),
79
+ additional_context=st.session_state["meta"].get("additional_context", ""),
80
+ response=st.session_state["scripts"],
81
+ thumbnail=st.session_state.get("thumbnail", "")
82
+ )
83
+ st.success("Scripts saved to database!")
84
+ except Exception as e:
85
+ st.error(f"Failed to save scripts: {e}")
86
+
87
+ st.subheader("Generate More Scripts")
88
+ more_num = st.slider("How many more scripts?", 1, 5, 1, key="more_scripts_slider")
89
+ more_prompt = st.text_area("Required Prompt", placeholder="Add specific guidance")
90
+ if st.button("Generate More Scripts", use_container_width=True):
91
+ if not more_prompt.strip():
92
+ st.error("Please provide a prompt before generating more scripts.")
93
+ else:
94
+ video_path = st.session_state.get("video_path")
95
+ if not video_path:
96
+ st.error("No video available. Please upload again.")
97
+ else:
98
+ with st.spinner("Generating more scripts..."):
99
+ extra_result = generate_scripts(
100
+ video_path,
101
+ st.session_state["meta"]["offer_details"],
102
+ st.session_state["meta"]["target_audience"],
103
+ st.session_state["meta"]["specific_hook"],
104
+ st.session_state["meta"]["additional_context"] + "\n\n" + more_prompt,
105
+ num_scripts=more_num,
106
+ duration=script_duration
107
+ )
108
+ if extra_result and "script_variations" in extra_result:
109
+ st.session_state["scripts"].append({
110
+ "prompt_used": more_prompt,
111
+ "variations": extra_result["script_variations"]
112
+ })
113
+
114
+ else:
115
+ history_items = get_all_scripts(limit=20)
116
+ if history_items:
117
+ video_titles = [
118
+ f"{item['video_name']} ({item['created_at'].strftime('%Y-%m-%d %H:%M ')})"
119
+ for item in history_items
120
+ ]
121
+ selected = st.sidebar.radio("History Items", video_titles, index=0)
122
+ idx = video_titles.index(selected)
123
+ selected_data = history_items[idx]
124
+
125
+ st.subheader(f"Scripts for: {selected_data['video_name']}")
126
+ if selected_data.get("thumbnail"):
127
+ st.image("data:image/jpeg;base64," + selected_data["thumbnail"], width=150)
128
+
129
+ json_response = selected_data.get("response")
130
+ if json_response:
131
+ if isinstance(json_response, list):
132
+ all_tables = []
133
+ for round_idx, round_data in enumerate(json_response, 1):
134
+ st.markdown(f"### Generation Round {round_idx}")
135
+ st.text_input("Prompt used:", round_data.get("prompt_used", "N/A"), disabled=True)
136
+ for i, variation in enumerate(round_data.get("variations", []), 1):
137
+ st.markdown(f"#### Variation {i}: {variation.get('variation_name','Var')}")
138
+ df = pd.DataFrame(variation.get("script_table", []))
139
+ st.table(df)
140
+ if not df.empty:
141
+ df["Variation"] = variation.get("variation_name", f"Var{i}")
142
+ df["Round"] = round_idx
143
+ all_tables.append(df)
144
+
145
+ if all_tables:
146
+ csv_scripts = pd.concat(all_tables, ignore_index=True).to_csv(index=False)
147
+ st.download_button(
148
+ "Download CSV",
149
+ data=csv_scripts,
150
+ file_name=f"{selected_data['video_name']}_scripts.csv",
151
+ mime="text/csv",
152
+ use_container_width=True
153
+ )
154
+ else:
155
+ st.info("No saved history available.")
app_pages/video_analyzer.py ADDED
@@ -0,0 +1,112 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+ import pandas as pd
4
+ import streamlit as st
5
+
6
+ from services.video_analyzer import analyze_video_only
7
+ from components.render_analysis import render_analyzer_results
8
+ from utils.video import get_video_thumbnail_base64
9
+ from utils.dataframe import analysis_to_csv
10
+ from database import insert_video_analysis, get_all_video_analyses
11
+
12
+ def analyzer_page():
13
+ selected_tab = st.sidebar.radio("Select Mode", ["Video Analyser", "History"], index=0)
14
+
15
+ if selected_tab == "Video Analyser":
16
+ st.subheader(" Video Analyser")
17
+ uploaded_video = st.file_uploader("Upload Video",
18
+ type=['mp4','mov','avi','mkv'],
19
+ help="Upload a video for analysis")
20
+ analyse_button = st.button("Run Analysis", use_container_width=True)
21
+
22
+ if uploaded_video and analyse_button:
23
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_video.name)[1]) as tmp:
24
+ tmp.write(uploaded_video.read())
25
+ video_path = tmp.name
26
+ with st.spinner("Analyzing video..."):
27
+ st.session_state["analysis"] = analyze_video_only(video_path)
28
+ st.session_state["video_name"] = uploaded_video.name
29
+ st.session_state["video_path"] = video_path
30
+ st.session_state["thumbnail"] = get_video_thumbnail_base64(video_path)
31
+
32
+ if "analysis" in st.session_state and st.session_state["analysis"]:
33
+ render_analyzer_results(st.session_state["analysis"])
34
+
35
+ col1, col2 = st.columns(2)
36
+ with col1:
37
+ analysis = st.session_state["analysis"]
38
+ frames = []
39
+ if "storyboard" in analysis:
40
+ df_storyboard = pd.DataFrame(analysis["storyboard"])
41
+ df_storyboard["section"] = "Storyboard"
42
+ frames.append(df_storyboard)
43
+ if "script" in analysis:
44
+ df_script = pd.DataFrame(analysis["script"])
45
+ df_script["section"] = "Script"
46
+ frames.append(df_script)
47
+ if "video_analysis" in analysis and "video_metrics" in analysis["video_analysis"]:
48
+ df_metrics = pd.DataFrame(analysis["video_analysis"]["video_metrics"])
49
+ df_metrics["section"] = "Metrics"
50
+ frames.append(df_metrics)
51
+ if "timestamp_improvements" in analysis:
52
+ df_improvements = pd.DataFrame(analysis["timestamp_improvements"])
53
+ df_improvements["section"] = "Improvements"
54
+ frames.append(df_improvements)
55
+
56
+ if frames:
57
+ csv_content = pd.concat(frames, ignore_index=True).to_csv(index=False)
58
+ st.download_button(
59
+ "Download CSV",
60
+ data=csv_content,
61
+ file_name=f"{st.session_state.get('video_name','analysis')}.csv",
62
+ mime="text/csv",
63
+ use_container_width=True
64
+ )
65
+ else:
66
+ st.info("No tabular data available for CSV export.")
67
+
68
+ with col2:
69
+ if st.button("Save to DB", use_container_width=True):
70
+ try:
71
+ insert_video_analysis(
72
+ video_name=st.session_state.get("video_name", "unknown"),
73
+ response=st.session_state["analysis"],
74
+ thumbnail=st.session_state.get("thumbnail", "")
75
+ )
76
+ st.success("Analysis saved to database ")
77
+ except Exception as e:
78
+ st.error(f"Failed to save analysis: {e}")
79
+
80
+ else:
81
+ history_items = get_all_video_analyses(limit=20)
82
+ if history_items:
83
+ video_titles = [
84
+ f"{item['video_name']} ({item['created_at'].strftime('%Y-%m-%d %H:%M')})"
85
+ for item in history_items
86
+ ]
87
+ selected = st.sidebar.radio("History Items", video_titles, index=0)
88
+ idx = video_titles.index(selected)
89
+ selected_data = history_items[idx]
90
+
91
+ st.subheader(f"Analysis for: {selected_data['video_name']}")
92
+ if selected_data.get("thumbnail"):
93
+ st.image("data:image/jpeg;base64," + selected_data["thumbnail"], width=150)
94
+
95
+ json_response = selected_data.get("response")
96
+ if json_response:
97
+ tabs = st.tabs(["Video Analysis"])
98
+ with tabs[0]:
99
+ render_analyzer_results(json_response)
100
+ try:
101
+ csv_data = analysis_to_csv(json_response)
102
+ st.download_button(
103
+ "Download CSV",
104
+ data=csv_data,
105
+ file_name=f"{selected_data['video_name']}_analysis.csv",
106
+ mime="text/csv",
107
+ use_container_width=True
108
+ )
109
+ except Exception as e:
110
+ st.error(f"CSV export failed: {e}")
111
+ else:
112
+ st.info("No saved history available.")
components/__pycache__/display_variations.cpython-311.pyc ADDED
Binary file (2.06 kB). View file
 
components/__pycache__/render_analysis.cpython-311.pyc ADDED
Binary file (13.6 kB). View file
 
components/display_variations.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import streamlit as st
3
+
4
+ def display_script_variations(json_data: dict):
5
+ if not json_data or "script_variations" not in json_data:
6
+ st.error("No script variations found")
7
+ return
8
+ for i, variation in enumerate(json_data["script_variations"], 1):
9
+ st.markdown(f"### Variation {i}: {variation.get('variation_name','Var')}")
10
+ df = pd.DataFrame(variation.get("script_table", []))
11
+ st.table(df)
12
+ csv_content = pd.concat(
13
+ [pd.DataFrame(v.get("script_table", []))
14
+ .assign(Variation=v.get("variation_name", f"Var{i+1}"))
15
+ for i, v in enumerate(json_data["script_variations"])],
16
+ ignore_index=True
17
+ ).to_csv(index=False)
18
+ st.download_button("Download CSV", data=csv_content,
19
+ file_name="scripts.csv", mime="text/csv")
components/render_analysis.py ADDED
@@ -0,0 +1,124 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import pandas as pd
3
+ import streamlit as st
4
+ from utils.dataframe import (
5
+ _normalize_list, _to_dataframe, _mean_effectiveness, _search_dataframe, safe_dataframe
6
+ )
7
+
8
+ def render_analyzer_results(analysis: dict, prefix: str = "") -> None:
9
+ if not isinstance(analysis, dict) or not analysis:
10
+ st.warning("No analysis available.")
11
+ return
12
+
13
+ st.markdown("""
14
+ <style>
15
+ .metric-card {background: #0f172a; padding: 14px 16px; border-radius: 14px; border: 1px solid #1f2937;}
16
+ .section-card {background: #0b1220; padding: 18px; border-radius: 14px; border: 1px solid #1f2937;}
17
+ .label {font-size: 12px; color: #94a3b8; margin-bottom: 6px;}
18
+ .value {font-size: 16px; color: #e2e8f0;}
19
+ </style>
20
+ """, unsafe_allow_html=True)
21
+
22
+ va = analysis.get("video_analysis", {}) or {}
23
+ storyboard = analysis.get("storyboard", []) or []
24
+ script = analysis.get("script", []) or []
25
+ metrics = va.get("video_metrics", []) or []
26
+ mean_score = _mean_effectiveness(metrics)
27
+
28
+ mcol1, mcol2, mcol3, mcol4 = st.columns([1,1,1,1])
29
+ with mcol1:
30
+ st.markdown(f'<div class="metric-card"><div class="label">Scenes</div><div class="value">{len(storyboard)}</div></div>', unsafe_allow_html=True)
31
+ with mcol2:
32
+ st.markdown(f'<div class="metric-card"><div class="label">Dialogue Lines</div><div class="value">{len(script)}</div></div>', unsafe_allow_html=True)
33
+ with mcol3:
34
+ st.markdown(f'<div class="metric-card"><div class="label">Avg Effectiveness</div><div class="value">{mean_score}/10</div></div>', unsafe_allow_html=True)
35
+ with mcol4:
36
+ st.markdown(f'<div class="metric-card"><div class="label">Improvements</div><div class="value">{len(analysis.get("timestamp_improvements", []) or [])}</div></div>', unsafe_allow_html=True)
37
+
38
+ colA, colB = st.columns([1.3,1])
39
+ with colA:
40
+ with st.container():
41
+ st.markdown("### Executive Summary")
42
+ c1, c2 = st.columns(2)
43
+ with c1:
44
+ with st.expander("Brief", expanded=True):
45
+ st.write(analysis.get("brief", "N/A"))
46
+ with st.expander("Caption Details", expanded=False):
47
+ st.write(analysis.get("caption_details", "N/A"))
48
+ with c2:
49
+ hook = analysis.get("hook", {}) or {}
50
+ with st.expander("Hook", expanded=True):
51
+ st.markdown(f"**Opening:** {hook.get('hook_text','N/A')}")
52
+ st.markdown(f"**Principle:** {hook.get('principle','N/A')}")
53
+ adv = _normalize_list(hook.get("advantages"))
54
+ if adv:
55
+ st.markdown("**Advantages:**")
56
+ st.markdown("\n".join([f"- {a}" for a in adv]))
57
+ st.divider()
58
+ st.markdown("### Narrative & Copy Frameworks")
59
+ with st.expander("Framework Analysis", expanded=True):
60
+ st.write(analysis.get("framework_analysis", "N/A"))
61
+
62
+ with colB:
63
+ st.markdown("### Snapshot")
64
+ with st.container():
65
+ st.caption("Top Drivers")
66
+ st.markdown(f'{va.get("effectiveness_factors","N/A")}</div>', unsafe_allow_html=True)
67
+ st.markdown("")
68
+ with st.container():
69
+ st.caption("Psychological Triggers")
70
+ st.markdown(f'{va.get("psychological_triggers","N/A")}</div>', unsafe_allow_html=True)
71
+ st.markdown("")
72
+ with st.container():
73
+ st.caption("Target Audience")
74
+ st.markdown(f'{va.get("target_audience","N/A")}</div>', unsafe_allow_html=True)
75
+
76
+ st.divider()
77
+ tabs = st.tabs(["Storyboard", "Script", "Scored Metrics", "Improvements", "Raw JSON"])
78
+
79
+ with tabs[0]:
80
+ q = st.text_input("Search storyboard", key=f"{prefix}_storyboard")
81
+ if storyboard:
82
+ df = _to_dataframe(storyboard, {"timeline": "Timeline", "scene": "Scene", "visuals": "Visuals", "dialogue": "Dialogue", "camera": "Camera", "sound_effects": "Sound Effects"})
83
+ df = _search_dataframe(df, q)
84
+ st.dataframe(safe_dataframe(df), use_container_width=True, height=480)
85
+ else:
86
+ st.info("No storyboard available.")
87
+
88
+ with tabs[1]:
89
+ q2 = st.text_input("Search script", key=f"{prefix}_script")
90
+ if script:
91
+ df = _to_dataframe(script, {"timeline": "Timeline", "dialogue": "Dialogue"})
92
+ df = _search_dataframe(df, q2)
93
+ st.dataframe(safe_dataframe(df), use_container_width=True, height=480)
94
+ else:
95
+ st.info("No script breakdown available.")
96
+
97
+ with tabs[2]:
98
+ q3 = st.text_input("Search metrics", key=f"{prefix}_metrics")
99
+ if metrics:
100
+ dfm = _to_dataframe(metrics, {"timestamp": "Timestamp", "element": "Element", "current_approach": "Current Approach", "effectiveness_score": "Effectiveness Score", "notes": "Notes"})
101
+ dfm = _search_dataframe(dfm, q3)
102
+ st.dataframe(dfm, use_container_width=True, height=480)
103
+ else:
104
+ st.info("No video metrics available.")
105
+
106
+ with tabs[3]:
107
+ improvements = analysis.get("timestamp_improvements", []) or []
108
+ q4 = st.text_input("Search improvements", key=f"{prefix}_improvements")
109
+ if improvements:
110
+ imp_df = _to_dataframe(improvements, {"timestamp": "Timestamp", "current_element": "Current Element", "improvement_type": "Improvement Type", "recommended_change": "Recommended Change", "expected_impact": "Expected Impact", "priority": "Priority"})
111
+ if "Priority" in imp_df.columns:
112
+ order = pd.CategoricalDtype(["High", "Medium", "Low"], ordered=True)
113
+ imp_df["Priority"] = imp_df["Priority"].astype(order)
114
+ if "Timestamp" in imp_df.columns:
115
+ imp_df = imp_df.sort_values(["Priority", "Timestamp"])
116
+ imp_df = _search_dataframe(imp_df, q4)
117
+ st.dataframe(imp_df, use_container_width=True, height=480)
118
+ else:
119
+ st.info("No timestamp-based improvements available.")
120
+
121
+ with tabs[4]:
122
+ pretty = json.dumps(analysis, indent=2, ensure_ascii=False)
123
+ st.code(pretty, language="json")
124
+ st.download_button("Download JSON", data=pretty.encode("utf-8"), file_name="ad_analysis.json", mime="application/json", use_container_width=True)
prompt/__pycache__/analyser_prompt.cpython-311.pyc ADDED
Binary file (2.75 kB). View file
 
prompt/__pycache__/system_prompt.cpython-311.pyc ADDED
Binary file (2.47 kB). View file
 
prompt/analyser_prompt.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ analyser_prompt = """You are an expert video advertisement analyst. Analyze the provided video and give response conforms EXACTLY to the schema below with no extra text or markdown.
2
+ Populate:
3
+
4
+ 1. **brief** → A concise summary covering visual style, speaker, target audience, and marketing objective.
5
+ 2. **caption_details** → Description of captions (color/style/position) or exactly the string `"None"` if not visible.
6
+ 3. **hook** →
7
+ - `"hook_text"`: Exact opening line or, if no speech, the precise description of the opening visual.
8
+ - `"principle"`: Psychological/marketing principle that makes this hook effective.
9
+ - `"advantages"`: ARRAY of 3–6 concise benefit statements tied to the ad’s value proposition.
10
+ 4. **framework_analysis** → A detailed block identifying copywriting/psychology/storytelling frameworks (e.g., PAS, AIDA). Highlight use of social proof, urgency, fear, authority, scroll-stopping hooks, loop openers, value positioning, and risk reversals.
11
+ 5. **storyboard** → ARRAY of 4–10 objects. Each must include:
12
+ - `"timeline"` in `"MM:SS"` (zero-padded)
13
+ - `"scene"` (brief)
14
+ - `"visuals"` (detailed)
15
+ - `"dialogue"` (exact words; use `""` if none)
16
+ - `"camera"` (shot/angle)
17
+ - `"sound_effects"` (or `"None"`)
18
+ 6. **script** → ARRAY of dialogue objects, each with `"timeline"` (`"MM:SS"`) and `"dialogue"` (exact spoken line).
19
+ 7. **video_analysis** → OBJECT with:
20
+ - `"effectiveness_factors"`: Key factors that influence effectiveness
21
+ - `"psychological_triggers"`: Triggers used (e.g., scarcity, authority)
22
+ - `"target_audience"`: Audience profile inferred
23
+ - `"video_metrics"`: ARRAY of objects with:
24
+ - `"timestamp"`: `"MM:SS-MM:SS"`
25
+ - `"element"`: The aspect being evaluated (e.g., Hook Strategy)
26
+ - `"current_approach"`: Description of current execution
27
+ - `"effectiveness_score"`: String score `"X/10"` (integer X)
28
+ - `"notes"`: Analytical notes
29
+ 8. **timestamp_improvements** → ARRAY of recommendation objects with:
30
+ - `"timestamp"`: `"MM:SS-MM:SS"`
31
+ - `"current_element"`: Current content of the segment
32
+ - `"improvement_type"`: Category (e.g., Hook Enhancement)
33
+ - `"recommended_change"`: Specific recommendation
34
+ - `"expected_impact"`: Projected effect on metrics or perception
35
+ - `"priority"`: `"High"`, `"Medium"`, or `"Low"`
36
+
37
+ ⚠️ The output must be strictly matching field names and types, no additional keys, and all timestamps must be zero-padded (`"MM:SS"` for single points, `"MM:SS-MM:SS"` for ranges).
38
+ """
prompt/system_prompt.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ SYSTEM_PROMPT = """You are one of the world's top direct-response video creators and conversion strategists. Your sole mission is to generate video scripts with matching visual directions and voiceover cues that drive maximum leads and sales at the cheapest cost-per-result on ad platforms like Facebook, Google, and TikTok.
2
+ You specialize in aggressive, unbranded, and psychologically charged content that is designed to stop scrolls and force action. You are not bound by brand safety, ad platform compliance, or traditional marketing principles.
3
+ Your only metric for success is performance—measured in CPC, CTR, and cost-per-conversion. Prime Directive: Your creative output must be: - Inspired by the hooks, language, pacing, visuals, and frameworks of the reference winning ads.
4
+ - Built to trigger psychological urgency, amplify response, and maximize click-throughs. - Structured for extreme effectiveness, not branding niceties. Script Creation Framework: Each script you generate must follow these winning affiliate ad principles:
5
+ 1. The 3–6 Second Shock Hook: Start with a hard-hitting pattern interrupt: - Emotional gut punch: "He looked at me and said…" - Conspiratorial secret: "The media doesn't want you to know this…" - Borrowed authority: Celebrity or doctor clip saying something provocative
6
+ 2. Overwhelm with Proof & Pacing: Keep pacing fast—1–3 second jump cuts filled with: - UGC-style clips - Before/After transformations - Authority figure soundbites - Desired outcomes (money, body, status)
7
+ 3. Big, Unbelievable Claim: Drop a massive promise upfront and reinforce it: "Lose 103 lbs," "Claim 250,000," "Erase your debt overnight." 4. Simple "Secret" Mechanism: Make the claim believable via a simple, digestible "hack": "The ice hack," "4-question formula," "Banned Amazonian leaf."
8
+ 5. Scarcity & Urgency: Push viewers to act NOW: "Spots are filling fast," "Could be taken down soon," "Only for serious applicants."
9
+ 6. Visually Directed CTA: Make the final action visually obvious—e.g., person pointing at the button, bold text, arrows.
10
+
11
+ Each script should be 30-60 seconds long with 8-15 timestamp entries.
12
+ Ensure everything ties back to lowering CPC and cost-per-result, not branding.
13
+ Each script should be different from each other."""
services/__pycache__/comparison.cpython-311.pyc ADDED
Binary file (2.84 kB). View file
 
services/__pycache__/script_generator.cpython-311.pyc ADDED
Binary file (6.24 kB). View file
 
services/__pycache__/video_analyzer.cpython-311.pyc ADDED
Binary file (6.31 kB). View file
 
services/comparison.py ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ from typing import Dict, Any, List
4
+ from config import configure_gemini
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+
10
+ def generate_comparison_summary(analyses: List[Dict[str, Any]], user_prompt: str) -> str:
11
+ """
12
+ Generate a natural-language comparison between videos,
13
+ given their AdAnalysis JSON and a user prompt.
14
+ """
15
+ client = configure_gemini()
16
+
17
+
18
+ try:
19
+ analyses_json = json.dumps(
20
+ [{"video": a.get("video_name"), "analysis": a.get("analysis")} for a in analyses],
21
+ ensure_ascii=False, indent=2
22
+ )
23
+ except Exception:
24
+ logger.exception("Failed to serialize analyses for comparison")
25
+ analyses_json = "[]"
26
+
27
+ system_prompt = (
28
+ "You are an expert video ad strategist. Compare multiple video ad analyses and return "
29
+ "a clear, structured comparison. Highlight:\n"
30
+ "- Hooks and opening strategies\n"
31
+ "- Copywriting / psychology frameworks\n"
32
+ "- Target audience differences\n"
33
+ "- Average effectiveness scores\n"
34
+ "- Major timestamp improvements\n"
35
+ "- Strengths & weaknesses of each video\n\n"
36
+ "Always structure output into sections and provide actionable insights."
37
+ )
38
+
39
+ user_message = (
40
+ f"Here are the analyses for multiple videos:\n\n{analyses_json}\n\n"
41
+ f"Now, based on this data, {user_prompt}."
42
+ )
43
+
44
+ try:
45
+ resp = client.chat.completions.create(
46
+ model="gemini-2.0-flash",
47
+ messages=[
48
+ {"role": "system", "content": system_prompt},
49
+ {"role": "user", "content": user_message}
50
+ ],
51
+ temperature=0.2,
52
+ )
53
+ return resp.choices[0].message.content or "No summary generated."
54
+ except Exception:
55
+ logger.exception("Comparison summary generation failed")
56
+ return "Failed to generate comparison summary."
57
+
services/script_generator.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import time
4
+ import logging
5
+ from typing import Dict, Any
6
+
7
+ from prompt.system_prompt import SYSTEM_PROMPT
8
+ from schema_script import ScriptResponse
9
+ from google import genai
10
+ from dotenv import load_dotenv
11
+
12
+ load_dotenv()
13
+ GEMINI_API_KEY = os.getenv("GEMINI_KEY")
14
+
15
+ logging.basicConfig(
16
+ level=logging.INFO,
17
+ format="%(asctime)s [%(levelname)s] %(message)s",
18
+ handlers=[logging.StreamHandler()]
19
+ )
20
+ logger = logging.getLogger(__name__)
21
+
22
+
23
+ def configure_gemini():
24
+ return genai.Client(api_key=GEMINI_API_KEY)
25
+
26
+
27
+ def generate_scripts(
28
+ video_path: str,
29
+ offer_details: str,
30
+ target_audience: str,
31
+ specific_hooks: str,
32
+ additional_context: str,
33
+ num_scripts: int = 3,
34
+ duration: int = 60
35
+ ) -> Dict[str, Any]:
36
+ client = configure_gemini()
37
+
38
+ try:
39
+ user_prompt = f"""
40
+ Generate {num_scripts} high-converting direct response script variations,
41
+ each about {duration} seconds long.
42
+
43
+ CONTEXT TO FOLLOW:
44
+ - Offer Details: {offer_details}
45
+ - Target Audience: {target_audience}
46
+ - Specific Hooks: {specific_hooks}
47
+
48
+ ADDITIONAL CONTEXT:
49
+ {additional_context}
50
+ You must reflect this additional context in:
51
+ - The script tone, CTA, visuals
52
+ - Compliance or branding constraints
53
+ - Any assumptions about audience or product
54
+ Failure to include this will be considered incomplete.
55
+ Please provide a comprehensive analysis including:
56
+ 1. DETAILED VIDEO ANALYSIS with timestamp-based metrics:
57
+ - Break down the video into 5-10 second segments
58
+ - Rate each segment's effectiveness (1-10 scale)
59
+ - Identify specific elements (hook, transition, proof, CTA, etc.)
60
+ 2. TIMESTAMP-BASED IMPROVEMENTS:
61
+ - Specific recommendations for each time segment
62
+ - Priority level for each improvement
63
+ - Expected impact of implementing changes
64
+ 3. SCRIPT VARIATIONS:
65
+ - Create 2-3 complete script variations
66
+ - Each with timestamp-by-timestamp breakdown
67
+ - Different psychological triggers and approaches
68
+ IMPORTANT: Return only valid JSON in the exact format specified in the system prompt. Analyze the video second-by-second for maximum detail.
69
+
70
+ Return ONLY valid JSON that matches this schema:
71
+ {{
72
+ "script_variations": [
73
+ {{
74
+ "variation_name": "string",
75
+ "script_table": [
76
+ {{
77
+ "timestamp": "M:SS or MM:SS",
78
+ "script_voiceover": "string",
79
+ "visual_direction": "string",
80
+ "psychological_trigger": "string",
81
+ "cta_action": "string"
82
+ }}
83
+ ]
84
+ }}
85
+ ]
86
+ }}
87
+ """
88
+
89
+ video_file = client.files.upload(file=video_path)
90
+
91
+ while getattr(video_file.state, "name", "") == "PROCESSING":
92
+ time.sleep(1.0)
93
+ video_file = client.files.get(name=video_file.name)
94
+ if getattr(video_file.state, "name", "") == "FAILED":
95
+ logger.error("Video processing FAILED.")
96
+ return {}
97
+
98
+ resp = client.models.generate_content(
99
+ model="gemini-2.0-flash",
100
+ contents=[SYSTEM_PROMPT, user_prompt, video_file],
101
+ config={
102
+ "response_mime_type": "application/json",
103
+ "response_schema": ScriptResponse,
104
+
105
+ },
106
+ )
107
+
108
+
109
+ parsed = getattr(resp, "parsed", None)
110
+ if parsed is None:
111
+
112
+ raw_text = getattr(resp, "text", "") or ""
113
+ if not raw_text:
114
+ # Inspect parts for better debugging
115
+ parts = None
116
+ if getattr(resp, "candidates", None):
117
+ parts = getattr(resp.candidates[0].content, "parts", None)
118
+ raise RuntimeError(f"Model returned no JSON text. parts={parts}")
119
+ data = json.loads(raw_text)
120
+ return data
121
+
122
+
123
+ out = parsed.model_dump()
124
+ logger.info("Generated %d variations.", len(out.get("script_variations", [])))
125
+ return out
126
+
127
+ except Exception as e:
128
+ logger.exception("generate_scripts failed: %s", e)
129
+ return {}
130
+
131
+
132
+
133
+
134
+
135
+
136
+
137
+
138
+
139
+
services/video_analyzer.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import tempfile
4
+ import logging
5
+ import pandas as pd
6
+ import streamlit as st
7
+ from typing import Dict, Any, List
8
+ import cv2
9
+ import base64
10
+
11
+ from config import configure_gemini
12
+ from prompt.analyser_prompt import analyser_prompt
13
+ from schema import AdAnalysis
14
+ from utils.video import get_video_thumbnail_base64
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+
20
+ def _sample_frames_b64(video_path: str, num_frames: int = 6, max_side: int = 896) -> List[str]:
21
+ """
22
+ Return up to `num_frames` JPEG frames from the video as base64 strings.
23
+ Frames are spaced across the video duration and resized so the longer
24
+ side is <= max_side to keep payload smaller.
25
+ """
26
+ b64s: List[str] = []
27
+ cap = cv2.VideoCapture(video_path)
28
+ try:
29
+ total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
30
+ if total <= 0:
31
+ return b64s
32
+
33
+
34
+ idxs = [int(i * (total - 1) / max(num_frames - 1, 1)) for i in range(num_frames)]
35
+ for idx in idxs:
36
+ cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
37
+ ok, frame = cap.read()
38
+ if not ok or frame is None:
39
+ continue
40
+
41
+ h, w = frame.shape[:2]
42
+ scale = min(1.0, float(max_side) / max(h, w))
43
+ if scale < 1.0:
44
+ frame = cv2.resize(frame, (int(w * scale), int(h * scale)))
45
+
46
+ ok, buf = cv2.imencode(".jpg", frame, [cv2.IMWRITE_JPEG_QUALITY, 85])
47
+ if not ok:
48
+ continue
49
+ b64s.append(base64.b64encode(buf.tobytes()).decode("utf-8"))
50
+ return b64s
51
+ finally:
52
+ cap.release()
53
+
54
+
55
+ def analyze_video_only(video_path: str) -> Dict[str, Any]:
56
+ client = configure_gemini()
57
+ try:
58
+
59
+ frames_b64 = _sample_frames_b64(video_path, num_frames=8)
60
+ if not frames_b64:
61
+ logger.error("Could not extract frames from video.")
62
+ return {}
63
+
64
+ user_parts: List[Dict[str, Any]] = [
65
+ {
66
+ "type": "text",
67
+ "text": (
68
+ "Analyze these frames of the ad and return ONLY valid JSON "
69
+ "that conforms exactly to the schema described in the system message."
70
+ ),
71
+ }
72
+ ]
73
+ for b64 in frames_b64:
74
+ user_parts.append({
75
+ "type": "image_url",
76
+ "image_url": {"url": f"data:image/jpeg;base64,{b64}"}
77
+ })
78
+ resp = client.beta.chat.completions.parse(
79
+ model="gemini-2.0-flash",
80
+ messages=[
81
+ {"role": "system", "content": analyser_prompt},
82
+ {"role": "user", "content": user_parts},
83
+ ],
84
+ response_format=AdAnalysis,
85
+
86
+
87
+ )
88
+
89
+ raw = resp.choices[0].message.parsed
90
+
91
+ try:
92
+
93
+ return raw.model_dump()
94
+ except Exception:
95
+ return json.loads(raw)
96
+
97
+ except Exception as e:
98
+ logger.exception("Video analysis failed")
99
+ return {}
100
+
101
+
102
+ def analyze_multiple_videos(video_files: List[st.runtime.uploaded_file_manager.UploadedFile]) -> List[Dict[str, Any]]:
103
+ results = []
104
+ for file in video_files:
105
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(file.name)[1]) as tmp:
106
+ tmp.write(file.read())
107
+ video_path = tmp.name
108
+
109
+ analysis = analyze_video_only(video_path)
110
+ thumbnail_b64 = get_video_thumbnail_base64(video_path)
111
+
112
+ results.append({
113
+ "video_name": file.name,
114
+ "analysis": analysis,
115
+ "thumbnail": thumbnail_b64
116
+ })
117
+ return results
utils/.DS_Store ADDED
Binary file (6.15 kB). View file
 
utils/__pycache__/auth.cpython-311.pyc ADDED
Binary file (1.91 kB). View file
 
utils/__pycache__/dataframe.cpython-311.pyc ADDED
Binary file (5.34 kB). View file
 
utils/__pycache__/video.cpython-311.pyc ADDED
Binary file (1.38 kB). View file
 
utils/auth.py ADDED
@@ -0,0 +1,33 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ import streamlit as st
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ def check_token(user_token: str):
8
+ ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")
9
+ if not ACCESS_TOKEN:
10
+ logger.critical("ACCESS_TOKEN not set in environment.")
11
+ return False, "Server error: Access token not configured."
12
+ if user_token == ACCESS_TOKEN:
13
+ logger.info("Access token validated successfully.")
14
+ return True, ""
15
+ logger.warning("Invalid access token attempt.")
16
+ return False, "Invalid token."
17
+
18
+ def gated_access() -> bool:
19
+ if "authenticated" not in st.session_state:
20
+ st.session_state["authenticated"] = False
21
+
22
+ if not st.session_state["authenticated"]:
23
+ st.markdown("## Access Required")
24
+ token_input = st.text_input("Enter Access Token", type="password")
25
+ if st.button("Unlock App"):
26
+ ok, error_msg = check_token(token_input)
27
+ if ok:
28
+ st.session_state["authenticated"] = True
29
+ st.rerun()
30
+ else:
31
+ st.error(error_msg)
32
+ return False
33
+ return True
utils/dataframe.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import pandas as pd
3
+ from typing import Any, Dict, List
4
+
5
+ def safe_dataframe(df: pd.DataFrame) -> pd.DataFrame:
6
+ for col in df.columns:
7
+ df[col] = df[col].astype(str)
8
+ return df
9
+
10
+ def analysis_to_csv(analysis: Dict[str, Any]) -> str:
11
+ rows = []
12
+ for sb in analysis.get("storyboard", []):
13
+ rows.append({"Section": "Storyboard", **sb})
14
+ for sc in analysis.get("script", []):
15
+ rows.append({"Section": "Script", **sc})
16
+ for met in analysis.get("video_analysis", {}).get("video_metrics", []):
17
+ rows.append({"Section": "Metrics", **met})
18
+ for imp in analysis.get("timestamp_improvements", []):
19
+ rows.append({"Section": "Improvements", **imp})
20
+ if not rows:
21
+ return ""
22
+ df = pd.DataFrame(rows)
23
+ return df.to_csv(index=False)
24
+
25
+ def _normalize_list(value: Any) -> List[str]:
26
+ if value is None:
27
+ return []
28
+ if isinstance(value, list):
29
+ return [str(v) for v in value]
30
+ return [s for s in str(value).splitlines() if s.strip()]
31
+
32
+ def _to_dataframe(items: Any, columns_map: Dict[str, str]) -> pd.DataFrame:
33
+ if not isinstance(items, list) or not items:
34
+ return pd.DataFrame(columns=list(columns_map.values()))
35
+ df = pd.DataFrame(items)
36
+ df = df.rename(columns=columns_map)
37
+ ordered_cols = [columns_map[k] for k in columns_map.keys() if columns_map[k] in df.columns]
38
+ df = df.reindex(columns=ordered_cols)
39
+ return df
40
+
41
+ def _mean_effectiveness(metrics: List[Dict[str, Any]]) -> float:
42
+ if not metrics:
43
+ return 0.0
44
+ scores = []
45
+ for m in metrics:
46
+ s = str(m.get("effectiveness_score", "0/10")).split("/")[0]
47
+ try:
48
+ scores.append(int(s))
49
+ except Exception:
50
+ pass
51
+ return round(sum(scores) / len(scores), 2) if scores else 0.0
52
+
53
+ def _search_dataframe(df: pd.DataFrame, query: str) -> pd.DataFrame:
54
+ if not query or df.empty:
55
+ return df
56
+ mask = pd.Series([False]*len(df))
57
+ for col in df.columns:
58
+ mask = mask | df[col].astype(str).str.contains(query, case=False, na=False)
59
+ return df[mask]
utils/video.py ADDED
@@ -0,0 +1,19 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import cv2
2
+ import base64
3
+ import logging
4
+
5
+ logger = logging.getLogger(__name__)
6
+
7
+ def get_video_thumbnail_base64(video_path: str, time_sec: int = 1) -> str:
8
+ try:
9
+ cap = cv2.VideoCapture(video_path)
10
+ cap.set(cv2.CAP_PROP_POS_MSEC, time_sec * 1000)
11
+ success, frame = cap.read()
12
+ cap.release()
13
+ if not success:
14
+ return ""
15
+ _, buffer = cv2.imencode(".jpg", frame)
16
+ return base64.b64encode(buffer).decode("utf-8")
17
+ except Exception:
18
+ logger.exception("Thumbnail extraction failed")
19
+ return ""