Update src/streamlit_app.py
Browse files- src/streamlit_app.py +296 -1
src/streamlit_app.py
CHANGED
|
@@ -468,4 +468,299 @@ def display_timestamp_improvements(json_data):
|
|
| 468 |
column_config={
|
| 469 |
"Timestamp": st.column_config.TextColumn(width="small"),
|
| 470 |
"Current Element": st.column_config.TextColumn(width="medium"),
|
| 471 |
-
"Improvement Type": st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 468 |
column_config={
|
| 469 |
"Timestamp": st.column_config.TextColumn(width="small"),
|
| 470 |
"Current Element": st.column_config.TextColumn(width="medium"),
|
| 471 |
+
"Improvement Type": st.column_config.TextColumn(width="medium"),
|
| 472 |
+
"Recommended Change": st.column_config.TextColumn(width="large"),
|
| 473 |
+
"Expected Impact": st.column_config.TextColumn(width="medium"),
|
| 474 |
+
"Priority": st.column_config.TextColumn(width="small")
|
| 475 |
+
}
|
| 476 |
+
)
|
| 477 |
+
|
| 478 |
+
logger.info("Timestamp improvements displayed successfully")
|
| 479 |
+
|
| 480 |
+
except Exception as e:
|
| 481 |
+
error_msg = f"Error displaying timestamp improvements: {str(e)}"
|
| 482 |
+
logger.error(error_msg, exc_info=True)
|
| 483 |
+
st.error(error_msg)
|
| 484 |
+
|
| 485 |
+
def create_csv_download(json_data):
|
| 486 |
+
"""Create CSV content with all scripts combined"""
|
| 487 |
+
logger.info("Creating CSV download...")
|
| 488 |
+
|
| 489 |
+
try:
|
| 490 |
+
all_scripts_data = []
|
| 491 |
+
|
| 492 |
+
# Combine all script variations into one dataset
|
| 493 |
+
for i, variation in enumerate(json_data.get("script_variations", []), 1):
|
| 494 |
+
variation_name = variation.get("variation_name", f"Variation {i}")
|
| 495 |
+
logger.debug(f"Processing variation for CSV: {variation_name}")
|
| 496 |
+
|
| 497 |
+
for row in variation.get("script_table", []):
|
| 498 |
+
script_row = {
|
| 499 |
+
'Variation': variation_name,
|
| 500 |
+
'Timestamp': row.get('timestamp', ''),
|
| 501 |
+
'Script_Voiceover': row.get('script_voiceover', ''),
|
| 502 |
+
'Visual_Direction': row.get('visual_direction', ''),
|
| 503 |
+
'Psychological_Trigger': row.get('psychological_trigger', ''),
|
| 504 |
+
'CTA_Action': row.get('cta_action', '')
|
| 505 |
+
}
|
| 506 |
+
all_scripts_data.append(script_row)
|
| 507 |
+
|
| 508 |
+
# Convert to DataFrame and then to CSV
|
| 509 |
+
if all_scripts_data:
|
| 510 |
+
df = pd.DataFrame(all_scripts_data)
|
| 511 |
+
csv_content = df.to_csv(index=False)
|
| 512 |
+
logger.info(f"CSV created successfully with {len(all_scripts_data)} rows")
|
| 513 |
+
return csv_content
|
| 514 |
+
else:
|
| 515 |
+
logger.warning("No script data available for CSV")
|
| 516 |
+
return "No script data available"
|
| 517 |
+
|
| 518 |
+
except Exception as e:
|
| 519 |
+
error_msg = f"Error creating CSV: {str(e)}"
|
| 520 |
+
logger.error(error_msg, exc_info=True)
|
| 521 |
+
return f"Error creating CSV: {error_msg}"
|
| 522 |
+
|
| 523 |
+
def check_token(user_token):
|
| 524 |
+
logger.info("Checking access token...")
|
| 525 |
+
|
| 526 |
+
ACCESS_TOKEN = os.getenv("ACCESS_TOKEN")
|
| 527 |
+
if not ACCESS_TOKEN:
|
| 528 |
+
error_msg = "ACCESS_TOKEN not set in environment."
|
| 529 |
+
logger.critical(error_msg)
|
| 530 |
+
return False, "Server error: Access token not configured."
|
| 531 |
+
|
| 532 |
+
if user_token == ACCESS_TOKEN:
|
| 533 |
+
logger.info("Access token validated successfully.")
|
| 534 |
+
return True, ""
|
| 535 |
+
|
| 536 |
+
logger.warning("Invalid access token attempt.")
|
| 537 |
+
return False, "Invalid token."
|
| 538 |
+
|
| 539 |
+
def main():
|
| 540 |
+
"""Main application function"""
|
| 541 |
+
logger.info("Starting main application...")
|
| 542 |
+
|
| 543 |
+
if "authenticated" not in st.session_state:
|
| 544 |
+
st.session_state["authenticated"] = False
|
| 545 |
+
logger.debug("Authentication state initialized")
|
| 546 |
+
|
| 547 |
+
if not st.session_state["authenticated"]:
|
| 548 |
+
logger.info("User not authenticated, showing login screen")
|
| 549 |
+
st.markdown("## Access Required")
|
| 550 |
+
token_input = st.text_input("Enter Access Token", type="password")
|
| 551 |
+
if st.button("Unlock App"):
|
| 552 |
+
ok, error_msg = check_token(token_input)
|
| 553 |
+
if ok:
|
| 554 |
+
st.session_state["authenticated"] = True
|
| 555 |
+
logger.info("User authenticated successfully")
|
| 556 |
+
st.rerun()
|
| 557 |
+
else:
|
| 558 |
+
logger.warning(f"Authentication failed: {error_msg}")
|
| 559 |
+
st.error(error_msg)
|
| 560 |
+
return
|
| 561 |
+
|
| 562 |
+
# Add API test button for debugging
|
| 563 |
+
if st.sidebar.button("🔧 Test API Connection"):
|
| 564 |
+
logger.info("Testing API connection...")
|
| 565 |
+
try:
|
| 566 |
+
genai.configure(api_key=GEMINI_API_KEY)
|
| 567 |
+
models = list(genai.list_models())
|
| 568 |
+
st.sidebar.success(f"✅ API Working! Found {len(models)} models")
|
| 569 |
+
logger.info(f"API test successful, found {len(models)} models")
|
| 570 |
+
for model in models[:3]: # Show first 3 models
|
| 571 |
+
st.sidebar.text(f"• {model.name}")
|
| 572 |
+
except Exception as e:
|
| 573 |
+
error_msg = f"❌ API Test Failed: {str(e)}"
|
| 574 |
+
st.sidebar.error(error_msg)
|
| 575 |
+
logger.error(f"API test failed: {str(e)}", exc_info=True)
|
| 576 |
+
|
| 577 |
+
# Sidebar navigation
|
| 578 |
+
if st.session_state["authenticated"]:
|
| 579 |
+
logger.info("User authenticated, showing main interface")
|
| 580 |
+
|
| 581 |
+
selected_tab = st.sidebar.radio("Select Mode", ["Script Generator", "History"])
|
| 582 |
+
logger.debug(f"Selected tab: {selected_tab}")
|
| 583 |
+
|
| 584 |
+
# ========== SCRIPT GENERATOR ==========
|
| 585 |
+
if selected_tab == "Script Generator":
|
| 586 |
+
logger.info("Script Generator mode selected")
|
| 587 |
+
|
| 588 |
+
with st.expander("How to Use This Tool", expanded=False):
|
| 589 |
+
st.markdown("""
|
| 590 |
+
### Upload Guidelines:
|
| 591 |
+
- **Best videos to analyze**: Already profitable Facebook/TikTok ads in your niche
|
| 592 |
+
- **Video length**: 30–90 seconds work best for analysis
|
| 593 |
+
- **Quality**: Clear audio and visuals help with better analysis
|
| 594 |
+
|
| 595 |
+
### Context Tips:
|
| 596 |
+
- **Offer details**: Be specific about your main promise and mechanism
|
| 597 |
+
- **Audience**: Include demographics, pain points, and desires
|
| 598 |
+
- **Hooks**: Mention any specific angles that have worked for you
|
| 599 |
+
|
| 600 |
+
### Script Optimization:
|
| 601 |
+
- Generated scripts focus on stopping scroll and driving clicks
|
| 602 |
+
- Each variation tests different psychological triggers
|
| 603 |
+
- Use the timestamp format for precise video production
|
| 604 |
+
- Test multiple variations to find your best performer
|
| 605 |
+
""")
|
| 606 |
+
st.subheader("Input Configuration")
|
| 607 |
+
|
| 608 |
+
uploaded_video = st.file_uploader(
|
| 609 |
+
"Upload Reference Video",
|
| 610 |
+
type=['mp4', 'mov', 'avi', 'mkv'],
|
| 611 |
+
help="Upload a profitable ad video to analyze and create variations from"
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
if uploaded_video is not None:
|
| 615 |
+
logger.info(f"Video uploaded: {uploaded_video.name}, size: {uploaded_video.size} bytes")
|
| 616 |
+
else:
|
| 617 |
+
st.info("Please upload a reference video to begin analysis.")
|
| 618 |
+
|
| 619 |
+
st.subheader("Additional Context (Optional)")
|
| 620 |
+
|
| 621 |
+
offer_details = st.text_area(
|
| 622 |
+
"Offer Details",
|
| 623 |
+
placeholder="e.g., Solar installation with $0 down payment...",
|
| 624 |
+
height=80,
|
| 625 |
+
help="Describe the product/service and main promise"
|
| 626 |
+
)
|
| 627 |
+
|
| 628 |
+
target_audience = st.text_area(
|
| 629 |
+
"Target Audience",
|
| 630 |
+
placeholder="e.g., 40+ homeowners with high electricity bills...",
|
| 631 |
+
height=80,
|
| 632 |
+
help="Describe the ideal customer demographics and pain points"
|
| 633 |
+
)
|
| 634 |
+
|
| 635 |
+
specific_hooks = st.text_area(
|
| 636 |
+
"Specific Hooks to Test",
|
| 637 |
+
placeholder="e.g., Government rebate angle, celebrity endorsement...",
|
| 638 |
+
height=80,
|
| 639 |
+
help="Any specific angles or hooks you want to incorporate"
|
| 640 |
+
)
|
| 641 |
+
|
| 642 |
+
additional_context = st.text_area(
|
| 643 |
+
"Additional Context",
|
| 644 |
+
placeholder="Any other relevant information...",
|
| 645 |
+
height=100,
|
| 646 |
+
help="Compliance requirements, brand guidelines, or other notes"
|
| 647 |
+
)
|
| 648 |
+
|
| 649 |
+
generate_button = st.button("Generate Script Variations", use_container_width=True)
|
| 650 |
+
|
| 651 |
+
if "analysis_results" in st.session_state and st.session_state["analysis_results"]:
|
| 652 |
+
if st.button("Clear Results", use_container_width=True):
|
| 653 |
+
del st.session_state["analysis_results"]
|
| 654 |
+
logger.info("Analysis results cleared")
|
| 655 |
+
st.rerun()
|
| 656 |
+
|
| 657 |
+
# Generate & show results
|
| 658 |
+
if uploaded_video and generate_button:
|
| 659 |
+
logger.info("Starting video analysis process...")
|
| 660 |
+
|
| 661 |
+
with st.spinner("Analyzing video and generating scripts..."):
|
| 662 |
+
video_bytes = uploaded_video.read()
|
| 663 |
+
uploaded_video.seek(0)
|
| 664 |
+
|
| 665 |
+
json_response = analyze_video_and_generate_script(
|
| 666 |
+
video_bytes,
|
| 667 |
+
uploaded_video.name,
|
| 668 |
+
offer_details,
|
| 669 |
+
target_audience,
|
| 670 |
+
specific_hooks,
|
| 671 |
+
additional_context
|
| 672 |
+
)
|
| 673 |
+
|
| 674 |
+
if json_response:
|
| 675 |
+
logger.info("Analysis completed successfully, saving to database...")
|
| 676 |
+
try:
|
| 677 |
+
insert_analysis_result(
|
| 678 |
+
video_name=uploaded_video.name,
|
| 679 |
+
offer_details=offer_details,
|
| 680 |
+
target_audience=target_audience,
|
| 681 |
+
specific_hook=specific_hooks,
|
| 682 |
+
additional_context=additional_context,
|
| 683 |
+
response=json_response
|
| 684 |
+
)
|
| 685 |
+
logger.info("Results saved to database")
|
| 686 |
+
except Exception as db_error:
|
| 687 |
+
logger.error(f"Failed to save to database: {str(db_error)}", exc_info=True)
|
| 688 |
+
st.warning("Analysis completed but failed to save to database")
|
| 689 |
+
|
| 690 |
+
st.session_state["analysis_results"] = json_response
|
| 691 |
+
else:
|
| 692 |
+
logger.error("Analysis failed, no response received")
|
| 693 |
+
|
| 694 |
+
if "analysis_results" in st.session_state:
|
| 695 |
+
logger.info("Displaying analysis results...")
|
| 696 |
+
json_response = st.session_state["analysis_results"]
|
| 697 |
+
|
| 698 |
+
tab1, tab2, tab3 = st.tabs(["Script Variations", "Video Analysis", "Improvement Recommendations"])
|
| 699 |
+
|
| 700 |
+
with tab1:
|
| 701 |
+
display_script_variations(json_response)
|
| 702 |
+
csv_content = create_csv_download(json_response)
|
| 703 |
+
st.download_button("Download All Scripts (CSV)", data=csv_content,
|
| 704 |
+
file_name="video_script_variations.csv", mime="text/csv")
|
| 705 |
+
with tab2:
|
| 706 |
+
display_video_analysis(json_response)
|
| 707 |
+
with tab3:
|
| 708 |
+
display_timestamp_improvements(json_response)
|
| 709 |
+
|
| 710 |
+
# ========== HISTORY ==========
|
| 711 |
+
elif selected_tab == "History":
|
| 712 |
+
logger.info("History mode selected")
|
| 713 |
+
|
| 714 |
+
try:
|
| 715 |
+
from database import get_all_results
|
| 716 |
+
history_items = get_all_results(limit=20)
|
| 717 |
+
logger.info(f"Retrieved {len(history_items) if history_items else 0} history items")
|
| 718 |
+
|
| 719 |
+
if history_items:
|
| 720 |
+
video_titles = [
|
| 721 |
+
f"{item['video_name']} ({item['created_at'].strftime('%Y-%m-%d %H:%M')})"
|
| 722 |
+
for item in history_items
|
| 723 |
+
]
|
| 724 |
+
|
| 725 |
+
selected = st.sidebar.radio("History Items", video_titles, index=0)
|
| 726 |
+
selected_index = video_titles.index(selected)
|
| 727 |
+
selected_data = history_items[selected_index]
|
| 728 |
+
|
| 729 |
+
logger.info(f"Selected history item: {selected_data['video_name']}")
|
| 730 |
+
|
| 731 |
+
st.subheader(f"Analysis for: {selected_data['video_name']}")
|
| 732 |
+
json_response = selected_data.get("response")
|
| 733 |
+
|
| 734 |
+
if json_response:
|
| 735 |
+
tab1, tab2, tab3 = st.tabs(["Script Variations", "Video Analysis", "Improvement Recommendations"])
|
| 736 |
+
|
| 737 |
+
with tab1:
|
| 738 |
+
display_script_variations(json_response)
|
| 739 |
+
with tab2:
|
| 740 |
+
display_video_analysis(json_response)
|
| 741 |
+
with tab3:
|
| 742 |
+
display_timestamp_improvements(json_response)
|
| 743 |
+
else:
|
| 744 |
+
warning_msg = "No valid response data for this analysis."
|
| 745 |
+
logger.warning(warning_msg)
|
| 746 |
+
st.warning(warning_msg)
|
| 747 |
+
else:
|
| 748 |
+
logger.info("No history items found")
|
| 749 |
+
st.sidebar.info("No saved analyses found.")
|
| 750 |
+
st.info("No saved history available.")
|
| 751 |
+
|
| 752 |
+
except Exception as history_error:
|
| 753 |
+
error_msg = f"Error loading history: {str(history_error)}"
|
| 754 |
+
logger.error(error_msg, exc_info=True)
|
| 755 |
+
st.error(error_msg)
|
| 756 |
+
|
| 757 |
+
if __name__ == "__main__":
|
| 758 |
+
try:
|
| 759 |
+
logger.info("=" * 50)
|
| 760 |
+
logger.info("LAUNCHING VIDEO ANALYZER APPLICATION")
|
| 761 |
+
logger.info("=" * 50)
|
| 762 |
+
main()
|
| 763 |
+
except Exception as e:
|
| 764 |
+
logger.exception("CRITICAL ERROR: Unhandled error during app launch")
|
| 765 |
+
st.error(f"Critical application error: {str(e)}")
|
| 766 |
+
st.error("Please check the logs for more details.")
|