Claude commited on
Commit
abb3f8b
·
unverified ·
1 Parent(s): 23757ad

feat: Unify UI into single chatbot interface

Browse files

- Replace tabbed interface with single unified chatbot
- Auto-detect YouTube URLs vs questions in chat input
- Show personalized welcome messages based on login state
- Fix ChromaDB client reference (chroma_client -> _default_client)
- Remove unsupported type="messages" parameter from Chatbot
- Add handling for both video analysis and Q&A in one interface

Files changed (1) hide show
  1. app.py +188 -189
app.py CHANGED
@@ -23,22 +23,32 @@ try:
23
  except ImportError:
24
  ZEROGPU_AVAILABLE = False
25
 
26
- # Initialize ChromaDB client (persistent storage)
27
- chroma_client = chromadb.Client()
28
- collection = chroma_client.get_or_create_collection(
29
- name="video_knowledge",
30
- metadata={"hnsw:space": "cosine"}
31
- )
32
-
33
- # Global embedding model
34
- embedding_model = None
35
 
36
 
37
  def get_embedding_model():
38
- global embedding_model
39
- if embedding_model is None:
40
- embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
41
- return embedding_model
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
 
43
 
44
  def hello(profile: gr.OAuthProfile | None) -> str:
@@ -520,8 +530,8 @@ def clear_knowledge_base() -> str:
520
  global collection
521
  try:
522
  # Delete and recreate collection
523
- chroma_client.delete_collection("video_knowledge")
524
- collection = chroma_client.get_or_create_collection(
525
  name="video_knowledge",
526
  metadata={"hnsw:space": "cosine"}
527
  )
@@ -530,191 +540,180 @@ def clear_knowledge_base() -> str:
530
  return f"Error clearing knowledge base: {e!s}"
531
 
532
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
  def create_demo() -> gr.Blocks:
534
  """Create and configure the Gradio demo application."""
535
- with gr.Blocks(title="Video Analyzer") as demo:
536
- # Header - always visible
537
  gr.Markdown("# Video Analyzer")
538
- gr.Markdown("Download, transcribe, analyze, and chat with YouTube videos using AI")
539
-
540
- # Login section - shown when not logged in
541
- with gr.Column(visible=True) as login_section:
542
- gr.Markdown("---")
543
- gr.Markdown("## Welcome!")
544
- gr.Markdown(
545
- "This app lets you:\n"
546
- "- **Download** YouTube videos or playlists\n"
547
- "- **Transcribe** audio using OpenAI Whisper\n"
548
- "- **Analyze** video frames with AI vision\n"
549
- "- **Chat** about video content using Qwen2.5-72B\n\n"
550
- "**Sign in with HuggingFace to get started:**"
551
- )
552
- with gr.Row():
553
- gr.Column(scale=1)
554
- with gr.Column(scale=1):
555
- gr.LoginButton(size="lg")
556
- gr.Column(scale=1)
557
-
558
- # Main app section - shown after login
559
- with gr.Column(visible=False) as main_section:
560
- with gr.Row():
561
- with gr.Column(scale=3):
562
- user_info = gr.Markdown()
563
- with gr.Column(scale=1):
564
- gr.LoginButton(size="sm")
565
-
566
- gr.Markdown("---")
567
-
568
- with gr.Tabs():
569
- with gr.TabItem("Analyze Videos"):
570
- with gr.Row():
571
- with gr.Column(scale=2):
572
- gr.Markdown("### Enter a YouTube URL")
573
- url_input = gr.Textbox(
574
- label="YouTube URL",
575
- placeholder="https://www.youtube.com/watch?v=...",
576
- lines=1,
577
- )
578
-
579
- with gr.Row():
580
- submit_btn = gr.Button(
581
- "Analyze Video",
582
- variant="primary",
583
- size="lg",
584
- scale=3,
585
- )
586
- num_frames = gr.Slider(
587
- label="Frames to analyze",
588
- minimum=3,
589
- maximum=10,
590
- value=5,
591
- step=1,
592
- scale=1,
593
- )
594
-
595
- with gr.Column(scale=1):
596
- gr.Markdown("### What happens")
597
- gr.Markdown(
598
- "1. Download video\n"
599
- "2. Transcribe audio (Whisper)\n"
600
- "3. Analyze key frames (BLIP)\n"
601
- "4. Store in knowledge base"
602
- )
603
- gr.Markdown("### Knowledge Base")
604
- kb_status_analyze = gr.Markdown()
605
- with gr.Row():
606
- refresh_btn = gr.Button("Refresh", size="sm")
607
- clear_btn = gr.Button("Clear All", size="sm", variant="stop")
608
-
609
- clear_status = gr.Markdown()
610
-
611
- gr.Markdown("### Results")
612
- output = gr.Markdown(
613
- value="*Paste a YouTube URL and click Analyze Video*",
614
- )
615
 
616
- submit_btn.click(
617
- fn=process_youtube,
618
- inputs=[url_input, num_frames],
619
- outputs=[output],
620
- ).then(
621
- fn=get_knowledge_stats,
622
- outputs=[kb_status_analyze],
623
- )
624
 
625
- refresh_btn.click(
626
- fn=get_knowledge_stats,
627
- outputs=[kb_status_analyze],
628
- )
629
 
630
- clear_btn.click(
631
- fn=clear_knowledge_base,
632
- outputs=[clear_status],
633
- ).then(
634
- fn=get_knowledge_stats,
635
- outputs=[kb_status_analyze],
636
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
637
 
638
- with gr.TabItem("Chat with Videos"):
639
- with gr.Row():
640
- with gr.Column(scale=3):
641
- kb_stats = gr.Markdown()
642
-
643
- chatbot = gr.Chatbot(
644
- label="Video Chat",
645
- height=350,
646
- )
647
-
648
- with gr.Row():
649
- chat_input = gr.Textbox(
650
- label="Your Question",
651
- placeholder="What was discussed in the video?",
652
- scale=4,
653
- lines=1,
654
- )
655
- chat_btn = gr.Button("Ask", variant="primary", scale=1)
656
-
657
- with gr.Column(scale=1):
658
- gr.Markdown("### Sample Questions")
659
- gr.Markdown(
660
- "Try asking:\n"
661
- "- What are the main topics?\n"
662
- "- Summarize the key points\n"
663
- "- What was shown visually?\n"
664
- "- Any mentions of [topic]?"
665
- )
666
- clear_chat_btn = gr.Button("Clear Chat", size="sm")
667
-
668
- def respond(
669
- message: str,
670
- history: list[dict],
671
- profile: gr.OAuthProfile | None,
672
- oauth_token: gr.OAuthToken | None,
673
- ):
674
- response = chat_with_videos(message, history, profile, oauth_token)
675
- history = history or []
676
- history.append({"role": "user", "content": message})
677
- history.append({"role": "assistant", "content": response})
678
- return history, ""
679
-
680
- chat_btn.click(
681
- fn=respond,
682
- inputs=[chat_input, chatbot],
683
- outputs=[chatbot, chat_input],
684
- )
685
- chat_input.submit(
686
- fn=respond,
687
- inputs=[chat_input, chatbot],
688
- outputs=[chatbot, chat_input],
689
- )
690
- clear_chat_btn.click(
691
- fn=lambda: [],
692
- outputs=[chatbot],
693
- )
694
 
695
- # Handle login state changes
696
- def check_login(profile: gr.OAuthProfile | None):
697
- if profile:
698
- return (
699
- gr.update(visible=False), # Hide login section
700
- gr.update(visible=True), # Show main section
701
- f"Welcome, **{profile.name}**!",
702
- )
703
- return (
704
- gr.update(visible=True), # Show login section
705
- gr.update(visible=False), # Hide main section
706
- "",
707
- )
708
 
 
709
  demo.load(
710
- fn=check_login,
711
- inputs=None,
712
- outputs=[login_section, main_section, user_info],
 
 
 
713
  )
714
-
715
- # Also refresh knowledge base stats on load
716
- demo.load(get_knowledge_stats, outputs=kb_status_analyze)
717
- demo.load(get_knowledge_stats, outputs=kb_stats)
718
 
719
  return demo
720
 
 
23
  except ImportError:
24
  ZEROGPU_AVAILABLE = False
25
 
26
+ # Global embedding model (shared - stateless)
27
+ _embedding_model = None
 
 
 
 
 
 
 
28
 
29
 
30
  def get_embedding_model():
31
+ global _embedding_model
32
+ if _embedding_model is None:
33
+ _embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
34
+ return _embedding_model
35
+
36
+
37
+ def create_session_collection(session_id: str):
38
+ """Create a per-session ChromaDB collection."""
39
+ client = chromadb.Client()
40
+ return client.get_or_create_collection(
41
+ name=f"video_knowledge_{session_id}",
42
+ metadata={"hnsw:space": "cosine"}
43
+ )
44
+
45
+
46
+ # Default collection for backward compatibility
47
+ _default_client = chromadb.Client()
48
+ collection = _default_client.get_or_create_collection(
49
+ name="video_knowledge",
50
+ metadata={"hnsw:space": "cosine"}
51
+ )
52
 
53
 
54
  def hello(profile: gr.OAuthProfile | None) -> str:
 
530
  global collection
531
  try:
532
  # Delete and recreate collection
533
+ _default_client.delete_collection("video_knowledge")
534
+ collection = _default_client.get_or_create_collection(
535
  name="video_knowledge",
536
  metadata={"hnsw:space": "cosine"}
537
  )
 
540
  return f"Error clearing knowledge base: {e!s}"
541
 
542
 
543
+ def handle_chat(
544
+ message: str,
545
+ history: list[dict],
546
+ profile: gr.OAuthProfile | None,
547
+ oauth_token: gr.OAuthToken | None,
548
+ progress: gr.Progress = gr.Progress(),
549
+ ) -> tuple[list[dict], str]:
550
+ """Unified chat handler that processes URLs or answers questions."""
551
+ history = history or []
552
+
553
+ if not message or not message.strip():
554
+ return history, ""
555
+
556
+ # Add user message to history
557
+ history.append({"role": "user", "content": message})
558
+
559
+ # Check if user is logged in
560
+ if profile is None:
561
+ history.append({
562
+ "role": "assistant",
563
+ "content": "Please sign in with HuggingFace first using the button above."
564
+ })
565
+ return history, ""
566
+
567
+ message = message.strip()
568
+
569
+ # Check if it's a YouTube URL
570
+ is_url, normalized = is_valid_youtube_url(message)
571
+
572
+ if is_url:
573
+ # Process the YouTube video
574
+ history.append({
575
+ "role": "assistant",
576
+ "content": "I'll analyze that video for you. This may take a few minutes..."
577
+ })
578
+
579
+ try:
580
+ result = _process_youtube_impl(normalized, 5, profile, progress)
581
+
582
+ # Summarize the result for chat
583
+ if "Error" in result or "Please" in result:
584
+ history.append({"role": "assistant", "content": result})
585
+ else:
586
+ # Extract just the summary
587
+ lines = result.split("\n")
588
+ title = next((l.replace("## ", "") for l in lines if l.startswith("## ")), "the video")
589
+
590
+ history.append({
591
+ "role": "assistant",
592
+ "content": (
593
+ f"Done! I've analyzed **{title}** and added it to my knowledge base.\n\n"
594
+ f"I extracted the transcript and analyzed key visual frames. "
595
+ f"You can now ask me questions about this video!\n\n"
596
+ f"Try asking:\n"
597
+ f"- What are the main topics discussed?\n"
598
+ f"- Summarize the key points\n"
599
+ f"- What was shown in the video?"
600
+ )
601
+ })
602
+ except Exception as e:
603
+ history.append({
604
+ "role": "assistant",
605
+ "content": f"Sorry, I couldn't analyze that video: {e}"
606
+ })
607
+ else:
608
+ # Check if we have any analyzed videos
609
+ if collection.count() == 0:
610
+ history.append({
611
+ "role": "assistant",
612
+ "content": (
613
+ "I don't have any videos analyzed yet. "
614
+ "Please paste a YouTube URL and I'll analyze it for you!\n\n"
615
+ "Example: `https://youtube.com/watch?v=...`"
616
+ )
617
+ })
618
+ else:
619
+ # Answer question about videos
620
+ if oauth_token is None:
621
+ history.append({
622
+ "role": "assistant",
623
+ "content": "Authentication error. Please try refreshing the page."
624
+ })
625
+ else:
626
+ response = chat_with_videos(message, history, profile, oauth_token)
627
+ history.append({"role": "assistant", "content": response})
628
+
629
+ return history, ""
630
+
631
+
632
+ def get_welcome_message(profile: gr.OAuthProfile | None) -> list[dict]:
633
+ """Get initial chat message based on login state."""
634
+ if profile:
635
+ return [{
636
+ "role": "assistant",
637
+ "content": (
638
+ f"Hi **{profile.name}**! I'm your Video Analyzer assistant.\n\n"
639
+ f"**Here's how I work:**\n"
640
+ f"1. Paste a YouTube URL and I'll analyze it\n"
641
+ f"2. Ask me questions about the video content\n\n"
642
+ f"Let's get started - paste a YouTube video URL!"
643
+ )
644
+ }]
645
+ return [{
646
+ "role": "assistant",
647
+ "content": (
648
+ "Welcome to Video Analyzer!\n\n"
649
+ "I can analyze YouTube videos and answer questions about them.\n\n"
650
+ "**Please sign in with HuggingFace** to get started."
651
+ )
652
+ }]
653
+
654
+
655
  def create_demo() -> gr.Blocks:
656
  """Create and configure the Gradio demo application."""
657
+ with gr.Blocks(title="Video Analyzer", theme=gr.themes.Soft()) as demo:
 
658
  gr.Markdown("# Video Analyzer")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
 
660
+ with gr.Row():
661
+ with gr.Column(scale=4):
662
+ gr.Markdown("*Analyze YouTube videos and chat about their content*")
663
+ with gr.Column(scale=1):
664
+ gr.LoginButton()
 
 
 
665
 
666
+ chatbot = gr.Chatbot(
667
+ label="Video Analyzer",
668
+ height=500,
669
+ )
670
 
671
+ with gr.Row():
672
+ msg_input = gr.Textbox(
673
+ label="Message",
674
+ placeholder="Paste a YouTube URL or ask a question...",
675
+ scale=5,
676
+ lines=1,
677
+ )
678
+ send_btn = gr.Button("Send", variant="primary", scale=1)
679
+
680
+ with gr.Row():
681
+ kb_status = gr.Markdown()
682
+ clear_btn = gr.Button("Clear Chat", size="sm")
683
+
684
+ # Wire up chat
685
+ send_btn.click(
686
+ fn=handle_chat,
687
+ inputs=[msg_input, chatbot],
688
+ outputs=[chatbot, msg_input],
689
+ ).then(
690
+ fn=get_knowledge_stats,
691
+ outputs=[kb_status],
692
+ )
693
 
694
+ msg_input.submit(
695
+ fn=handle_chat,
696
+ inputs=[msg_input, chatbot],
697
+ outputs=[chatbot, msg_input],
698
+ ).then(
699
+ fn=get_knowledge_stats,
700
+ outputs=[kb_status],
701
+ )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
702
 
703
+ clear_btn.click(
704
+ fn=lambda: [],
705
+ outputs=[chatbot],
706
+ )
 
 
 
 
 
 
 
 
 
707
 
708
+ # Initialize chat with welcome message
709
  demo.load(
710
+ fn=get_welcome_message,
711
+ outputs=[chatbot],
712
+ )
713
+ demo.load(
714
+ fn=get_knowledge_stats,
715
+ outputs=[kb_status],
716
  )
 
 
 
 
717
 
718
  return demo
719