userIdc2024 commited on
Commit
170f2f2
·
verified ·
1 Parent(s): 9215752

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +306 -742
src/streamlit_app.py CHANGED
@@ -1,765 +1,329 @@
1
- import streamlit as st
2
- import google.generativeai as genai
3
- import tempfile
4
  import os
5
  import time
 
 
6
  import json
7
- from typing import Optional
 
8
  import pandas as pd
9
- import logging
10
- from database import insert_analysis_result
11
- from dotenv import load_dotenv
12
-
13
- load_dotenv()
14
-
15
- # Backend API Key Configuration
16
- GEMINI_API_KEY = os.getenv("GEMINI_KEY")
17
-
18
- # Page configuration
19
- st.set_page_config(
20
- page_title="Video Analyser and Script Generator",
21
- page_icon="🎥",
22
- layout="wide",
23
- initial_sidebar_state="expanded"
24
- )
25
-
26
- # Enhanced logging configuration
27
- logging.basicConfig(
28
- level=logging.DEBUG, # Changed to DEBUG for more detailed logs
29
- format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
30
- handlers=[
31
- logging.StreamHandler()
32
- ]
33
- )
34
  logger = logging.getLogger(__name__)
35
 
36
- def configure_gemini():
37
- """Configure Gemini API with backend key"""
38
- logger.info("Starting Gemini API configuration...")
39
-
 
40
  if not GEMINI_API_KEY:
41
- error_msg = "GEMINI_KEY not found in environment variables"
42
- logger.error(error_msg)
43
- st.error(error_msg)
44
- return False
45
-
46
- logger.info(f"API Key found, length: {len(GEMINI_API_KEY)}")
47
- logger.debug(f"API Key starts with: {GEMINI_API_KEY[:10]}..." if len(GEMINI_API_KEY) > 10 else "API Key too short")
48
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
  try:
50
- genai.configure(api_key=GEMINI_API_KEY)
51
- logger.info("Gemini API configured successfully")
52
-
53
- # Test API connection
54
- logger.info("Testing API connection...")
55
- models = list(genai.list_models())
56
- logger.info(f"Available models: {[model.name for model in models]}")
57
-
58
- return True
59
- except Exception as e:
60
- error_msg = f"Failed to configure Gemini API: {str(e)}"
61
- logger.error(error_msg, exc_info=True)
62
- st.error(error_msg)
63
- return False
64
-
65
- # Enhanced system prompt with timestamp-based improvements
66
- SYSTEM_PROMPT = f"""{os.getenv("SYS_PROMPT", "")}"""
67
- logger.info(f"System prompt loaded, length: {len(SYSTEM_PROMPT) if SYSTEM_PROMPT else 0}")
68
-
69
- def analyze_video_and_generate_script(
70
- video_bytes,
71
- video_name,
72
- offer_details: str = "",
73
- target_audience: str = "",
74
- specific_hooks: str = "",
75
- additional_context: str = ""
76
- ):
77
- """
78
- Analyze video and generate direct response script variations
79
- """
80
- logger.info(f"Starting video analysis for: {video_name}")
81
- logger.info(f"Video size: {len(video_bytes)} bytes")
82
-
83
- try:
84
- # Save uploaded video to temporary file
85
- logger.info("Creating temporary file...")
86
- with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(video_name)[1]) as tmp_file:
87
- tmp_file.write(video_bytes)
88
- tmp_file_path = tmp_file.name
89
-
90
- logger.info(f"Temporary file created: {tmp_file_path}")
91
- logger.info(f"File size on disk: {os.path.getsize(tmp_file_path)} bytes")
92
-
93
- # Configure Gemini
94
- logger.info("Configuring Gemini API...")
95
- if not configure_gemini():
96
- logger.error("Gemini configuration failed")
97
- return None
98
-
99
- # Show upload progress
100
- upload_progress = st.progress(0)
101
- upload_status = st.empty()
102
-
103
- upload_status.text("Uploading video to Google AI...")
104
- upload_progress.progress(20)
105
- logger.info("Starting file upload to Gemini...")
106
-
107
- try:
108
- # Upload video to Gemini
109
- video_file_obj = genai.upload_file(tmp_file_path)
110
- logger.info(f"File uploaded successfully. File URI: {video_file_obj.uri}")
111
- logger.info(f"File state: {video_file_obj.state.name}")
112
- upload_progress.progress(40)
113
-
114
- except Exception as upload_error:
115
- error_msg = f"File upload failed: {str(upload_error)}"
116
- logger.error(error_msg, exc_info=True)
117
- upload_status.error(error_msg)
118
- return None
119
-
120
- upload_status.text("Processing video...")
121
- logger.info("Waiting for video processing...")
122
-
123
- processing_attempts = 0
124
- max_processing_attempts = 30 # 1 minute timeout
125
-
126
- while video_file_obj.state.name == "PROCESSING":
127
- processing_attempts += 1
128
- logger.debug(f"Processing attempt {processing_attempts}/{max_processing_attempts}")
129
-
130
- if processing_attempts > max_processing_attempts:
131
- error_msg = "Video processing timed out after 1 minute"
132
- logger.error(error_msg)
133
- upload_status.error(error_msg)
134
- return None
135
-
136
  time.sleep(2)
137
- try:
138
- video_file_obj = genai.get_file(video_file_obj.name)
139
- logger.debug(f"Processing state: {video_file_obj.state.name}")
140
- except Exception as get_file_error:
141
- logger.error(f"Error checking file status: {str(get_file_error)}", exc_info=True)
142
- break
143
-
144
- upload_progress.progress(40 + (processing_attempts * 20 // max_processing_attempts))
145
-
146
- logger.info(f"Final file state: {video_file_obj.state.name}")
147
-
148
- if video_file_obj.state.name == "FAILED":
149
- error_msg = "Google AI file processing failed. Please try another video."
150
- logger.error(error_msg)
151
- upload_status.error(error_msg)
152
- return None
153
-
154
- if video_file_obj.state.name != "ACTIVE":
155
- error_msg = f"Unexpected file state: {video_file_obj.state.name}"
156
- logger.error(error_msg)
157
- upload_status.error(error_msg)
158
- return None
159
-
160
- upload_progress.progress(80)
161
- upload_status.text("Generating script variations...")
162
- logger.info("Starting content generation...")
163
-
164
- # Build the enhanced user prompt
165
- user_prompt = f"""Analyze this reference video and generate 3 high-converting direct response video script variations with detailed timestamp-based improvements.
166
-
167
- IMPORTANT CONTEXT TO FOLLOW WHEN CREATING OUTPUT:
168
- - Offer Details: {offer_details}
169
- - Target Audience: {target_audience}
170
- - Specific Hooks: {specific_hooks}
171
-
172
- ADDITIONAL CONTEXT (MANDATORY TO FOLLOW):
173
- {additional_context}
174
-
175
- You must reflect this additional context in:
176
- - The script tone, CTA, visuals
177
- - Compliance or branding constraints
178
- - Any assumptions about audience or product
179
-
180
- Failure to include this will be considered incomplete.
181
-
182
- Please provide a comprehensive analysis including:
183
-
184
- 1. DETAILED VIDEO ANALYSIS with timestamp-based metrics:
185
- - Break down the video into 5-10 second segments
186
- - Rate each segment's effectiveness (1-10 scale)
187
- - Identify specific elements (hook, transition, proof, CTA, etc.)
188
-
189
- 2. TIMESTAMP-BASED IMPROVEMENTS:
190
- - Specific recommendations for each time segment
191
- - Priority level for each improvement
192
- - Expected impact of implementing changes
193
-
194
- 3. SCRIPT VARIATIONS:
195
- - Create 2-3 complete script variations
196
- - Each with timestamp-by-timestamp breakdown
197
- - Different psychological triggers and approaches
198
-
199
- IMPORTANT: Return only valid JSON in the exact format specified in the system prompt. Analyze the video second-by-second for maximum detail."""
200
-
201
- logger.info(f"User prompt length: {len(user_prompt)}")
202
- logger.info(f"System prompt length: {len(SYSTEM_PROMPT) if SYSTEM_PROMPT else 0}")
203
-
204
- # Generate response
205
- try:
206
- logger.info("Creating GenerativeModel instance...")
207
- model = genai.GenerativeModel("gemini-2.0-flash-exp")
208
- logger.info("Model created successfully")
209
-
210
- logger.info("Generating content with video and prompts...")
211
- full_prompt = user_prompt + "\n\n" + (SYSTEM_PROMPT or "")
212
- logger.debug(f"Full prompt length: {len(full_prompt)}")
213
-
214
- response = model.generate_content([video_file_obj, full_prompt])
215
- logger.info("Content generation completed successfully")
216
- logger.debug(f"Response text length: {len(response.text) if hasattr(response, 'text') else 'No text attribute'}")
217
-
218
- except Exception as generation_error:
219
- error_msg = f"Error generating content with Gemini: {str(generation_error)}"
220
- logger.error(error_msg, exc_info=True)
221
- upload_status.error(error_msg)
222
- return None
223
-
224
- upload_progress.progress(100)
225
- upload_status.success("Analysis complete!")
226
- logger.info("Video analysis completed successfully")
227
-
228
- # Clean up temporary file
229
  try:
230
- os.unlink(tmp_file_path)
231
- logger.info(f"Temporary file deleted: {tmp_file_path}")
232
- except Exception as cleanup_error:
233
- logger.warning(f"Failed to delete temporary file: {str(cleanup_error)}")
234
-
235
- # Parse JSON response
236
- logger.info("Parsing JSON response...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  try:
238
- if not hasattr(response, 'text'):
239
- error_msg = "Response object has no text attribute"
240
- logger.error(error_msg)
241
- st.error(error_msg)
242
- return None
243
-
244
- response_text = response.text.strip()
245
- logger.debug(f"Raw response text preview: {response_text[:500]}...")
246
-
247
- if response_text.startswith('```json'):
248
- response_text = response_text[7:-3]
249
- logger.debug("Removed json code block markers")
250
- elif response_text.startswith('```'):
251
- response_text = response_text[3:-3]
252
- logger.debug("Removed generic code block markers")
253
-
254
- logger.debug(f"Cleaned response text preview: {response_text[:500]}...")
255
-
256
- json_response = json.loads(response_text)
257
- logger.info("JSON parsing successful")
258
- logger.debug(f"JSON keys: {list(json_response.keys()) if isinstance(json_response, dict) else 'Not a dict'}")
259
-
260
- return json_response
261
-
262
- except json.JSONDecodeError as json_error:
263
- error_msg = f"Error parsing AI response as JSON: {str(json_error)}"
264
- logger.error(error_msg)
265
- logger.error(f"Response text that failed to parse: {response_text[:1000]}...")
266
- st.error(error_msg)
267
- st.text_area("Raw Response (for debugging):", response_text, height=200)
268
- return None
269
-
270
- except Exception as e:
271
- error_msg = f"Unexpected error processing video: {str(e)}"
272
- logger.error(error_msg, exc_info=True)
273
- st.error(error_msg)
274
- return None
275
-
276
- def display_script_variations(json_data):
277
- """Display script variations in formatted tables"""
278
- logger.info("Displaying script variations...")
279
-
280
- if not json_data or "script_variations" not in json_data:
281
- error_msg = "No script variations found in the response"
282
- logger.error(error_msg)
283
- logger.debug(f"JSON data keys: {list(json_data.keys()) if isinstance(json_data, dict) else 'Not a dict'}")
284
- st.error(error_msg)
285
  return
286
-
287
- try:
288
- variations = json_data["script_variations"]
289
- logger.info(f"Found {len(variations)} script variations")
290
-
291
- for i, variation in enumerate(variations, 1):
292
- variation_name = variation.get("variation_name", f"Variation {i}")
293
- logger.debug(f"Processing variation {i}: {variation_name}")
294
-
295
- st.markdown(f"### Variation {i}: {variation_name}")
296
-
297
- # Convert script table to DataFrame for better display
298
- script_data = variation.get("script_table")
299
- if not script_data:
300
- warning_msg = f"No script data for {variation_name}"
301
- logger.warning(warning_msg)
302
- st.warning(warning_msg)
303
- continue
304
-
305
- logger.debug(f"Script data for {variation_name}: {len(script_data)} rows")
306
-
307
- df = pd.DataFrame(script_data)
308
-
309
- # Rename columns for better display
310
- df = df.rename(columns={
311
- 'timestamp': 'Timestamp',
312
- 'script_voiceover': 'Script / Voiceover',
313
- 'visual_direction': 'Visual Direction',
314
- 'psychological_trigger': 'Psychological Trigger',
315
- 'cta_action': 'CTA / Action'
316
- })
317
-
318
- st.table(df)
319
- st.markdown("---")
320
-
321
- logger.info("Script variations displayed successfully")
322
-
323
- except Exception as e:
324
- error_msg = f"Error displaying script variations: {str(e)}"
325
- logger.error(error_msg, exc_info=True)
326
- st.error(error_msg)
327
-
328
- def display_video_analysis(json_data):
329
- """Display video analysis in tabular format"""
330
- logger.info("Displaying video analysis...")
331
-
332
- if not json_data or "video_analysis" not in json_data:
333
- error_msg = "No video analysis found in the response"
334
- logger.error(error_msg)
335
- st.error(error_msg)
336
- return
337
-
338
- try:
339
- analysis = json_data["video_analysis"]
340
- logger.debug(f"Video analysis type: {type(analysis)}")
341
-
342
- # Display general analysis
343
- video_metrics = []
344
- if isinstance(analysis, dict):
345
- col1, col2 = st.columns(2)
346
-
347
- with col1:
348
- st.subheader("Effectiveness Factors")
349
- effectiveness = analysis.get('effectiveness_factors', 'N/A')
350
- st.write(effectiveness)
351
- logger.debug(f"Effectiveness factors: {effectiveness}")
352
-
353
- st.subheader("Target Audience")
354
- audience = analysis.get('target_audience', 'N/A')
355
- st.write(audience)
356
- logger.debug(f"Target audience: {audience}")
357
-
358
- with col2:
359
- st.subheader("Psychological Triggers")
360
- triggers = analysis.get('psychological_triggers', 'N/A')
361
- st.write(triggers)
362
- logger.debug(f"Psychological triggers: {triggers}")
363
-
364
- video_metrics = analysis.get("video_metrics", [])
365
- logger.debug(f"Video metrics count: {len(video_metrics)}")
366
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  else:
368
- warning_msg = "Unexpected format in video_analysis. Skipping metadata."
369
- logger.warning(warning_msg)
370
- st.warning(warning_msg)
371
- if isinstance(analysis, list):
372
- video_metrics = analysis
373
-
374
- if video_metrics:
375
- logger.info(f"Processing {len(video_metrics)} video metrics")
376
- metrics_df = pd.DataFrame(video_metrics)
377
-
378
- # Rename columns for better display
379
- column_mapping = {
380
- 'timestamp': 'Timestamp',
381
- 'element': 'Element',
382
- 'current_approach': 'Current Approach',
383
- 'effectiveness_score': 'Score',
384
- 'notes': 'Analysis Notes'
385
- }
386
-
387
- metrics_df = metrics_df.rename(columns=column_mapping)
388
- logger.debug(f"Metrics dataframe columns: {list(metrics_df.columns)}")
389
-
390
- st.dataframe(
391
- metrics_df,
392
- use_container_width=True,
393
- hide_index=True,
394
- column_config={
395
- "Timestamp": st.column_config.TextColumn(width="small"),
396
- "Element": st.column_config.TextColumn(width="medium"),
397
- "Current Approach": st.column_config.TextColumn(width="large"),
398
- "Score": st.column_config.TextColumn(width="small"),
399
- "Analysis Notes": st.column_config.TextColumn(width="large")
400
- }
401
- )
402
  else:
403
- warning_msg = "No detailed video metrics available"
404
- logger.warning(warning_msg)
405
- st.warning(warning_msg)
406
-
407
- logger.info("Video analysis displayed successfully")
408
-
409
- except Exception as e:
410
- error_msg = f"Error displaying video analysis: {str(e)}"
411
- logger.error(error_msg, exc_info=True)
412
- st.error(error_msg)
413
-
414
- def display_timestamp_improvements(json_data):
415
- """Display timestamp-based improvements in tabular format"""
416
- logger.info("Displaying timestamp improvements...")
417
-
418
- improvements = json_data.get("timestamp_improvements")
419
-
420
- if improvements is None:
421
- error_msg = "No timestamp improvements found in the response"
422
- logger.error(error_msg)
423
- st.error(error_msg)
424
- return
425
-
426
- if not improvements:
427
- warning_msg = "No timestamp improvements available"
428
- logger.warning(warning_msg)
429
- st.warning(warning_msg)
430
- return
431
-
432
- try:
433
- st.subheader("Timestamp-by-Timestamp Improvement Recommendations")
434
- logger.info(f"Processing {len(improvements)} improvement recommendations")
435
-
436
- improvements_df = pd.DataFrame(improvements)
437
-
438
- # Rename columns for better display
439
- column_mapping = {
440
- 'timestamp': 'Timestamp',
441
- 'current_element': 'Current Element',
442
- 'improvement_type': 'Improvement Type',
443
- 'recommended_change': 'Recommended Change',
444
- 'expected_impact': 'Expected Impact',
445
- 'priority': 'Priority'
446
- }
447
-
448
- improvements_df = improvements_df.rename(columns=column_mapping)
449
- logger.debug(f"Improvements dataframe columns: {list(improvements_df.columns)}")
450
-
451
- # Color code priority
452
- def color_priority(val):
453
- if val == 'High':
454
- return 'background-color: #ffcccb'
455
- elif val == 'Medium':
456
- return 'background-color: #ffffcc'
457
- elif val == 'Low':
458
- return 'background-color: #ccffcc'
459
- return ''
460
-
461
- styled_df = improvements_df.style.applymap(color_priority, subset=['Priority'])
462
-
463
- st.dataframe(
464
- styled_df,
465
- use_container_width=True,
466
- hide_index=True,
467
- column_config={
468
- "Timestamp": st.column_config.TextColumn(width="small"),
469
- "Current Element": st.column_config.TextColumn(width="medium"),
470
- "Improvement Type": st.column_config.TextColumn(width="medium"),
471
- "Recommended Change": st.column_config.TextColumn(width="large"),
472
- "Expected Impact": st.column_config.TextColumn(width="medium"),
473
- "Priority": st.column_config.TextColumn(width="small")
474
- }
475
- )
476
-
477
- logger.info("Timestamp improvements displayed successfully")
478
-
479
- except Exception as e:
480
- error_msg = f"Error displaying timestamp improvements: {str(e)}"
481
- logger.error(error_msg, exc_info=True)
482
- st.error(error_msg)
483
-
484
- def create_csv_download(json_data):
485
- """Create CSV content with all scripts combined"""
486
- logger.info("Creating CSV download...")
487
-
488
- try:
489
- all_scripts_data = []
490
-
491
- # Combine all script variations into one dataset
492
- for i, variation in enumerate(json_data.get("script_variations", []), 1):
493
- variation_name = variation.get("variation_name", f"Variation {i}")
494
- logger.debug(f"Processing variation for CSV: {variation_name}")
495
-
496
- for row in variation.get("script_table", []):
497
- script_row = {
498
- 'Variation': variation_name,
499
- 'Timestamp': row.get('timestamp', ''),
500
- 'Script_Voiceover': row.get('script_voiceover', ''),
501
- 'Visual_Direction': row.get('visual_direction', ''),
502
- 'Psychological_Trigger': row.get('psychological_trigger', ''),
503
- 'CTA_Action': row.get('cta_action', '')
504
- }
505
- all_scripts_data.append(script_row)
506
-
507
- # Convert to DataFrame and then to CSV
508
- if all_scripts_data:
509
- df = pd.DataFrame(all_scripts_data)
510
- csv_content = df.to_csv(index=False)
511
- logger.info(f"CSV created successfully with {len(all_scripts_data)} rows")
512
- return csv_content
513
  else:
514
- logger.warning("No script data available for CSV")
515
- return "No script data available"
516
-
517
- except Exception as e:
518
- error_msg = f"Error creating CSV: {str(e)}"
519
- logger.error(error_msg, exc_info=True)
520
- return f"Error creating CSV: {error_msg}"
521
-
522
- def check_token(user_token):
523
- logger.info("Checking access token...")
524
-
525
- ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")
526
- if not ACCESS_TOKEN:
527
- error_msg = "ACCESS_TOKEN not set in environment."
528
- logger.critical(error_msg)
529
- return False, "Server error: Access token not configured."
530
-
531
- if user_token == ACCESS_TOKEN:
532
- logger.info("Access token validated successfully.")
533
- return True, ""
534
-
535
- logger.warning("Invalid access token attempt.")
536
- return False, "Invalid token."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
537
 
538
  def main():
539
- """Main application function"""
540
- logger.info("Starting main application...")
541
-
542
- if "authenticated" not in st.session_state:
543
- st.session_state["authenticated"] = False
544
- logger.debug("Authentication state initialized")
545
-
546
- if not st.session_state["authenticated"]:
547
- logger.info("User not authenticated, showing login screen")
548
- st.markdown("## Access Required")
549
- token_input = st.text_input("Enter Access Token", type="password")
550
- if st.button("Unlock App"):
551
- ok, error_msg = check_token(token_input)
552
- if ok:
553
- st.session_state["authenticated"] = True
554
- logger.info("User authenticated successfully")
555
- st.rerun()
556
- else:
557
- logger.warning(f"Authentication failed: {error_msg}")
558
- st.error(error_msg)
559
- return
560
-
561
- # Add API test button for debugging
562
- if st.sidebar.button("🔧 Test API Connection"):
563
- logger.info("Testing API connection...")
564
- try:
565
- genai.configure(api_key=GEMINI_API_KEY)
566
- models = list(genai.list_models())
567
- st.sidebar.success(f"✅ API Working! Found {len(models)} models")
568
- logger.info(f"API test successful, found {len(models)} models")
569
- for model in models[:3]: # Show first 3 models
570
- st.sidebar.text(f"• {model.name}")
571
- except Exception as e:
572
- error_msg = f"❌ API Test Failed: {str(e)}"
573
- st.sidebar.error(error_msg)
574
- logger.error(f"API test failed: {str(e)}", exc_info=True)
575
-
576
- # Sidebar navigation
577
- if st.session_state["authenticated"]:
578
- logger.info("User authenticated, showing main interface")
579
-
580
- selected_tab = st.sidebar.radio("Select Mode", ["Script Generator", "History"])
581
- logger.debug(f"Selected tab: {selected_tab}")
582
-
583
- # ========== SCRIPT GENERATOR ==========
584
- if selected_tab == "Script Generator":
585
- logger.info("Script Generator mode selected")
586
-
587
- with st.expander("How to Use This Tool", expanded=False):
588
- st.markdown("""
589
- ### Upload Guidelines:
590
- - **Best videos to analyze**: Already profitable Facebook/TikTok ads in your niche
591
- - **Video length**: 30–90 seconds work best for analysis
592
- - **Quality**: Clear audio and visuals help with better analysis
593
-
594
- ### Context Tips:
595
- - **Offer details**: Be specific about your main promise and mechanism
596
- - **Audience**: Include demographics, pain points, and desires
597
- - **Hooks**: Mention any specific angles that have worked for you
598
-
599
- ### Script Optimization:
600
- - Generated scripts focus on stopping scroll and driving clicks
601
- - Each variation tests different psychological triggers
602
- - Use the timestamp format for precise video production
603
- - Test multiple variations to find your best performer
604
- """)
605
- st.subheader("Input Configuration")
606
-
607
- uploaded_video = st.file_uploader(
608
- "Upload Reference Video",
609
- type=['mp4', 'mov', 'avi', 'mkv'],
610
- help="Upload a profitable ad video to analyze and create variations from"
611
- )
612
-
613
- if uploaded_video is not None:
614
- logger.info(f"Video uploaded: {uploaded_video.name}, size: {uploaded_video.size} bytes")
615
- else:
616
- st.info("Please upload a reference video to begin analysis.")
617
-
618
- st.subheader("Additional Context (Optional)")
619
-
620
- offer_details = st.text_area(
621
- "Offer Details",
622
- placeholder="e.g., Solar installation with $0 down payment...",
623
- height=80,
624
- help="Describe the product/service and main promise"
625
- )
626
-
627
- target_audience = st.text_area(
628
- "Target Audience",
629
- placeholder="e.g., 40+ homeowners with high electricity bills...",
630
- height=80,
631
- help="Describe the ideal customer demographics and pain points"
632
- )
633
-
634
- specific_hooks = st.text_area(
635
- "Specific Hooks to Test",
636
- placeholder="e.g., Government rebate angle, celebrity endorsement...",
637
- height=80,
638
- help="Any specific angles or hooks you want to incorporate"
639
- )
640
-
641
- additional_context = st.text_area(
642
- "Additional Context",
643
- placeholder="Any other relevant information...",
644
- height=100,
645
- help="Compliance requirements, brand guidelines, or other notes"
646
- )
647
-
648
- generate_button = st.button("Generate Script Variations", use_container_width=True)
649
-
650
- if "analysis_results" in st.session_state and st.session_state["analysis_results"]:
651
- if st.button("Clear Results", use_container_width=True):
652
- del st.session_state["analysis_results"]
653
- logger.info("Analysis results cleared")
654
- st.rerun()
655
-
656
- # Generate & show results
657
- if uploaded_video and generate_button:
658
- logger.info("Starting video analysis process...")
659
-
660
- with st.spinner("Analyzing video and generating scripts..."):
661
- video_bytes = uploaded_video.read()
662
- uploaded_video.seek(0)
663
-
664
- json_response = analyze_video_and_generate_script(
665
- video_bytes,
666
- uploaded_video.name,
667
- offer_details,
668
- target_audience,
669
- specific_hooks,
670
- additional_context
671
- )
672
-
673
- if json_response:
674
- logger.info("Analysis completed successfully, saving to database...")
675
- try:
676
- insert_analysis_result(
677
- video_name=uploaded_video.name,
678
- offer_details=offer_details,
679
- target_audience=target_audience,
680
- specific_hook=specific_hooks,
681
- additional_context=additional_context,
682
- response=json_response
683
- )
684
- logger.info("Results saved to database")
685
- except Exception as db_error:
686
- logger.error(f"Failed to save to database: {str(db_error)}", exc_info=True)
687
- st.warning("Analysis completed but failed to save to database")
688
-
689
- st.session_state["analysis_results"] = json_response
690
- else:
691
- logger.error("Analysis failed, no response received")
692
-
693
- if "analysis_results" in st.session_state:
694
- logger.info("Displaying analysis results...")
695
- json_response = st.session_state["analysis_results"]
696
-
697
- tab1, tab2, tab3 = st.tabs(["Script Variations", "Video Analysis", "Improvement Recommendations"])
698
-
699
- with tab1:
700
- display_script_variations(json_response)
701
- csv_content = create_csv_download(json_response)
702
- st.download_button("Download All Scripts (CSV)", data=csv_content,
703
- file_name="video_script_variations.csv", mime="text/csv")
704
- with tab2:
705
- display_video_analysis(json_response)
706
- with tab3:
707
- display_timestamp_improvements(json_response)
708
-
709
- # ========== HISTORY ==========
710
- elif selected_tab == "History":
711
- logger.info("History mode selected")
712
-
713
- try:
714
- from database import get_all_results
715
- history_items = get_all_results(limit=20)
716
- logger.info(f"Retrieved {len(history_items) if history_items else 0} history items")
717
-
718
- if history_items:
719
- video_titles = [
720
- f"{item['video_name']} ({item['created_at'].strftime('%Y-%m-%d %H:%M')})"
721
- for item in history_items
722
- ]
723
-
724
- selected = st.sidebar.radio("History Items", video_titles, index=0)
725
- selected_index = video_titles.index(selected)
726
- selected_data = history_items[selected_index]
727
-
728
- logger.info(f"Selected history item: {selected_data['video_name']}")
729
-
730
- st.subheader(f"Analysis for: {selected_data['video_name']}")
731
- json_response = selected_data.get("response")
732
-
733
- if json_response:
734
- tab1, tab2, tab3 = st.tabs(["Script Variations", "Video Analysis", "Improvement Recommendations"])
735
-
736
- with tab1:
737
- display_script_variations(json_response)
738
- with tab2:
739
- display_video_analysis(json_response)
740
- with tab3:
741
- display_timestamp_improvements(json_response)
742
- else:
743
- warning_msg = "No valid response data for this analysis."
744
- logger.warning(warning_msg)
745
- st.warning(warning_msg)
746
- else:
747
- logger.info("No history items found")
748
- st.sidebar.info("No saved analyses found.")
749
- st.info("No saved history available.")
750
-
751
- except Exception as history_error:
752
- error_msg = f"Error loading history: {str(history_error)}"
753
- logger.error(error_msg, exc_info=True)
754
- st.error(error_msg)
755
 
756
  if __name__ == "__main__":
757
  try:
758
- logger.info("=" * 50)
759
- logger.info("LAUNCHING VIDEO ANALYZER APPLICATION")
760
- logger.info("=" * 50)
761
  main()
762
  except Exception as e:
763
- logger.exception("CRITICAL ERROR: Unhandled error during app launch")
764
- st.error(f"Critical application error: {str(e)}")
765
- st.error("Please check the logs for more details.")
 
 
 
 
1
  import os
2
  import time
3
+ import tempfile
4
+ import logging
5
  import json
6
+ from typing import Dict, Any, List, Literal
7
+
8
  import pandas as pd
9
+ import streamlit as st
10
+ from pydantic import BaseModel, constr
11
+ from google import genai
12
+
13
+ logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s", handlers=[logging.StreamHandler()])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  logger = logging.getLogger(__name__)
15
 
16
+ st.set_page_config(page_title="Video Ad Analyzer", page_icon="🎬", layout="wide")
17
+
18
+ GEMINI_API_KEY = os.getenv("GEMINI_KEY", "")
19
+
20
+ def configure_gemini() -> genai.Client:
21
  if not GEMINI_API_KEY:
22
+ raise RuntimeError("GEMINI_KEY is not set in environment variables.")
23
+ return genai.Client(api_key=GEMINI_API_KEY)
24
+
25
+ Timestamp = constr(pattern=r'^\d{2}:\d{2}$')
26
+ RangeTimestamp = constr(pattern=r'^\d{2}:\d{2}-\d{2}:\d{2}$')
27
+ Score010 = constr(pattern=r'^(?:10|[0-9])\/10$')
28
+
29
+ class Hook(BaseModel):
30
+ hook_text: str
31
+ principle: str
32
+ advantages: List[str]
33
+
34
+ class StoryboardItem(BaseModel):
35
+ timeline: Timestamp
36
+ scene: str
37
+ visuals: str
38
+ dialogue: str
39
+ camera: str
40
+ sound_effects: str
41
+
42
+ class ScriptLine(BaseModel):
43
+ timeline: Timestamp
44
+ dialogue: str
45
+
46
+ class VideoMetric(BaseModel):
47
+ timestamp: RangeTimestamp
48
+ element: str
49
+ current_approach: str
50
+ effectiveness_score: Score010
51
+ notes: str
52
+
53
+ class VideoAnalysis(BaseModel):
54
+ effectiveness_factors: str
55
+ psychological_triggers: str
56
+ target_audience: str
57
+ video_metrics: List[VideoMetric]
58
+
59
+ class TimestampImprovement(BaseModel):
60
+ timestamp: RangeTimestamp
61
+ current_element: str
62
+ improvement_type: str
63
+ recommended_change: str
64
+ expected_impact: str
65
+ priority: Literal["High", "Medium", "Low"]
66
+
67
+ class AdAnalysis(BaseModel):
68
+ brief: str
69
+ caption_details: str
70
+ hook: Hook
71
+ framework_analysis: str
72
+ storyboard: List[StoryboardItem]
73
+ script: List[ScriptLine]
74
+ video_analysis: VideoAnalysis
75
+ timestamp_improvements: List[TimestampImprovement]
76
+
77
+ 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. Populate:
78
+
79
+ 1. **brief** → A concise summary covering visual style, speaker, target audience, and marketing objective.
80
+ 2. **caption_details** → Description of captions (color/style/position) or exactly the string `"None"` if not visible.
81
+ 3. **hook** →
82
+ - `"hook_text"`: Exact opening line or, if no speech, the precise description of the opening visual.
83
+ - `"principle"`: Psychological/marketing principle that makes this hook effective.
84
+ - `"advantages"`: ARRAY of 3–6 concise benefit statements tied to the ad’s value proposition.
85
+ 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.
86
+ 5. **storyboard** → ARRAY of 4–10 objects. Each must include:
87
+ - `"timeline"` in `"MM:SS"` (zero-padded)
88
+ - `"scene"` (brief)
89
+ - `"visuals"` (detailed)
90
+ - `"dialogue"` (exact words; use `""` if none)
91
+ - `"camera"` (shot/angle)
92
+ - `"sound_effects"` (or `"None"`)
93
+ 6. **script** → ARRAY of dialogue objects, each with `"timeline"` (`"MM:SS"`) and `"dialogue"` (exact spoken line).
94
+ 7. **video_analysis** → OBJECT with:
95
+ - `"effectiveness_factors"`: Key factors that influence effectiveness
96
+ - `"psychological_triggers"`: Triggers used (e.g., scarcity, authority)
97
+ - `"target_audience"`: Audience profile inferred
98
+ - `"video_metrics"`: ARRAY of objects with:
99
+ - `"timestamp"`: `"MM:SS-MM:SS"`
100
+ - `"element"`: The aspect being evaluated (e.g., Hook Strategy)
101
+ - `"current_approach"`: Description of current execution
102
+ - `"effectiveness_score"`: String score `"X/10"` (integer X)
103
+ - `"notes"`: Analytical notes
104
+ 8. **timestamp_improvements** → ARRAY of recommendation objects with:
105
+ - `"timestamp"`: `"MM:SS-MM:SS"`
106
+ - `"current_element"`: Current content of the segment
107
+ - `"improvement_type"`: Category (e.g., Hook Enhancement)
108
+ - `"recommended_change"`: Specific recommendation
109
+ - `"expected_impact"`: Projected effect on metrics or perception
110
+ - `"priority"`: `"High"`, `"Medium"`, or `"Low"`
111
+
112
+ ⚠️ 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).
113
+ """
114
+
115
+ def analyze_video_only(video_path: str) -> Dict[str, Any]:
116
+ client = configure_gemini()
117
  try:
118
+ video_file = client.files.upload(file=video_path)
119
+ while getattr(video_file.state, "name", "") == "PROCESSING":
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  time.sleep(2)
121
+ video_file = client.files.get(name=video_file.name)
122
+ if getattr(video_file.state, "name", "") == "FAILED":
123
+ return {}
124
+ resp = client.models.generate_content(
125
+ model="gemini-2.0-flash",
126
+ contents=[analyser_prompt, video_file],
127
+ config={"response_mime_type": "application/json"}
128
+ )
129
+ raw = getattr(resp, "text", "") or ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  try:
131
+ model_obj = AdAnalysis.model_validate_json(raw)
132
+ return model_obj.model_dump()
133
+ except Exception:
134
+ try:
135
+ return json.loads(raw)
136
+ except Exception:
137
+ return {}
138
+ except Exception:
139
+ return {}
140
+
141
+ def _normalize_list(value: Any) -> List[str]:
142
+ if value is None:
143
+ return []
144
+ if isinstance(value, list):
145
+ return [str(v) for v in value]
146
+ return [s for s in str(value).splitlines() if s.strip()]
147
+
148
+ def _to_dataframe(items: Any, columns_map: Dict[str, str]) -> pd.DataFrame:
149
+ if not isinstance(items, list) or not items:
150
+ return pd.DataFrame(columns=list(columns_map.values()))
151
+ df = pd.DataFrame(items)
152
+ df = df.rename(columns=columns_map)
153
+ ordered_cols = [columns_map[k] for k in columns_map.keys() if columns_map[k] in df.columns]
154
+ df = df.reindex(columns=ordered_cols)
155
+ return df
156
+
157
+ def _mean_effectiveness(metrics: List[Dict[str, Any]]) -> float:
158
+ if not metrics:
159
+ return 0.0
160
+ scores = []
161
+ for m in metrics:
162
+ s = str(m.get("effectiveness_score", "0/10")).split("/")[0]
163
  try:
164
+ scores.append(int(s))
165
+ except Exception:
166
+ pass
167
+ return round(sum(scores) / len(scores), 2) if scores else 0.0
168
+
169
+ def _search_dataframe(df: pd.DataFrame, query: str) -> pd.DataFrame:
170
+ if not query or df.empty:
171
+ return df
172
+ mask = pd.Series([False]*len(df))
173
+ for col in df.columns:
174
+ mask = mask | df[col].astype(str).str.contains(query, case=False, na=False)
175
+ return df[mask]
176
+
177
+ def render_analyzer_results(analysis: Dict[str, Any]) -> None:
178
+ if not isinstance(analysis, dict) or not analysis:
179
+ st.warning("No analysis available.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
181
 
182
+ st.markdown("""
183
+ <style>
184
+ .metric-card {background: #0f172a; padding: 14px 16px; border-radius: 14px; border: 1px solid #1f2937;}
185
+ .section-card {background: #0b1220; padding: 18px; border-radius: 14px; border: 1px solid #1f2937;}
186
+ .label {font-size: 12px; color: #94a3b8; margin-bottom: 6px;}
187
+ .value {font-size: 16px; color: #e2e8f0;}
188
+ </style>
189
+ """, unsafe_allow_html=True)
190
+
191
+ va = analysis.get("video_analysis", {}) or {}
192
+ storyboard = analysis.get("storyboard", []) or []
193
+ script = analysis.get("script", []) or []
194
+ metrics = va.get("video_metrics", []) or []
195
+ mean_score = _mean_effectiveness(metrics)
196
+
197
+ mcol1, mcol2, mcol3, mcol4 = st.columns([1,1,1,1])
198
+ with mcol1:
199
+ st.markdown(f'<div class="metric-card"><div class="label">Scenes</div><div class="value">{len(storyboard)}</div></div>', unsafe_allow_html=True)
200
+ with mcol2:
201
+ st.markdown(f'<div class="metric-card"><div class="label">Dialogue Lines</div><div class="value">{len(script)}</div></div>', unsafe_allow_html=True)
202
+ with mcol3:
203
+ 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)
204
+ with mcol4:
205
+ 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)
206
+
207
+ colA, colB = st.columns([1.3,1])
208
+ with colA:
209
+ with st.container():
210
+ st.markdown("### Executive Summary")
211
+ c1, c2 = st.columns(2)
212
+ with c1:
213
+ with st.expander("Brief", expanded=True):
214
+ st.write(analysis.get("brief", "N/A"))
215
+ with st.expander("Caption Details", expanded=False):
216
+ st.write(analysis.get("caption_details", "N/A"))
217
+ with c2:
218
+ hook = analysis.get("hook", {}) or {}
219
+ with st.expander("Hook", expanded=True):
220
+ st.markdown(f"**Opening:** {hook.get('hook_text','N/A')}")
221
+ st.markdown(f"**Principle:** {hook.get('principle','N/A')}")
222
+ adv = _normalize_list(hook.get("advantages"))
223
+ if adv:
224
+ st.markdown("**Advantages:**")
225
+ st.markdown("\n".join([f"- {a}" for a in adv]))
226
+ st.divider()
227
+ st.markdown("### Narrative & Copy Frameworks")
228
+ with st.expander("Framework Analysis", expanded=True):
229
+ st.write(analysis.get("framework_analysis", "N/A"))
230
+
231
+ with colB:
232
+ st.markdown("### Snapshot")
233
+ with st.container():
234
+ st.caption("Top Drivers")
235
+ st.markdown(f'<div class="section-card">{va.get("effectiveness_factors","N/A")}</div>', unsafe_allow_html=True)
236
+ st.markdown("")
237
+ with st.container():
238
+ st.caption("Psychological Triggers")
239
+ st.markdown(f'<div class="section-card">{va.get("psychological_triggers","N/A")}</div>', unsafe_allow_html=True)
240
+ st.markdown("")
241
+ with st.container():
242
+ st.caption("Target Audience")
243
+ st.markdown(f'<div class="section-card">{va.get("target_audience","N/A")}</div>', unsafe_allow_html=True)
244
+
245
+ st.divider()
246
+
247
+ tabs = st.tabs(["Storyboard", "Script", "Scored Metrics", "Improvements", "Raw JSON"])
248
+
249
+ with tabs[0]:
250
+ q = st.text_input("Search storyboard")
251
+ if storyboard:
252
+ df = _to_dataframe(storyboard, {"timeline": "Timeline", "scene": "Scene", "visuals": "Visuals", "dialogue": "Dialogue", "camera": "Camera", "sound_effects": "Sound Effects"})
253
+ df = _search_dataframe(df, q)
254
+ st.dataframe(df, use_container_width=True, height=480)
255
  else:
256
+ st.info("No storyboard available.")
257
+
258
+ with tabs[1]:
259
+ q2 = st.text_input("Search script")
260
+ if script:
261
+ df = _to_dataframe(script, {"timeline": "Timeline", "dialogue": "Dialogue"})
262
+ df = _search_dataframe(df, q2)
263
+ st.dataframe(df, use_container_width=True, height=480)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
264
  else:
265
+ st.info("No script breakdown available.")
266
+
267
+ with tabs[2]:
268
+ q3 = st.text_input("Search metrics")
269
+ if metrics:
270
+ dfm = _to_dataframe(metrics, {"timestamp": "Timestamp", "element": "Element", "current_approach": "Current Approach", "effectiveness_score": "Effectiveness Score", "notes": "Notes"})
271
+ dfm = _search_dataframe(dfm, q3)
272
+ st.dataframe(dfm, use_container_width=True, height=480)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  else:
274
+ st.info("No video metrics available.")
275
+
276
+ with tabs[3]:
277
+ improvements = analysis.get("timestamp_improvements", []) or []
278
+ q4 = st.text_input("Search improvements")
279
+ if improvements:
280
+ 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"})
281
+ if "Priority" in imp_df.columns:
282
+ order = pd.CategoricalDtype(["High", "Medium", "Low"], ordered=True)
283
+ imp_df["Priority"] = imp_df["Priority"].astype(order)
284
+ if "Timestamp" in imp_df.columns:
285
+ imp_df = imp_df.sort_values(["Priority", "Timestamp"])
286
+ imp_df = _search_dataframe(imp_df, q4)
287
+ st.dataframe(imp_df, use_container_width=True, height=480)
288
+ else:
289
+ st.info("No timestamp-based improvements available.")
290
+
291
+ with tabs[4]:
292
+ pretty = json.dumps(analysis, indent=2, ensure_ascii=False)
293
+ st.code(pretty, language="json")
294
+ st.download_button("Download JSON", data=pretty.encode("utf-8"), file_name="ad_analysis.json", mime="application/json", use_container_width=True)
295
+
296
+ def workspace_tab():
297
+ with st.sidebar:
298
+ st.header("Input")
299
+ uploaded_video = st.file_uploader("Upload Video", type=["mp4", "mov", "avi", "mkv"], accept_multiple_files=False)
300
+ run_btn = st.button("Analyze Video", use_container_width=True)
301
+ st.markdown("---")
302
+ st.caption("Session")
303
+ clear = st.button("Clear Output", use_container_width=True)
304
+ st.title("🎬 Video Ad Analyzer")
305
+ if "analysis" not in st.session_state or clear:
306
+ st.session_state["analysis"] = None
307
+ if run_btn:
308
+ if not uploaded_video:
309
+ st.error("Please upload a video.")
310
+ return
311
+ with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(uploaded_video.name)[1]) as tmp:
312
+ tmp.write(uploaded_video.read())
313
+ video_path = tmp.name
314
+ with st.spinner("Analyzing video..."):
315
+ st.session_state["analysis"] = analyze_video_only(video_path)
316
+ if st.session_state.get("analysis"):
317
+ render_analyzer_results(st.session_state["analysis"])
318
+ else:
319
+ st.info("Upload a video and click Analyze to see results.")
320
 
321
  def main():
322
+ workspace_tab()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
323
 
324
  if __name__ == "__main__":
325
  try:
326
+ logger.info("Launching Streamlit app...")
 
 
327
  main()
328
  except Exception as e:
329
+ logger.exception("Unhandled error during app launch.")