JustusI commited on
Commit
34cf186
Β·
verified Β·
1 Parent(s): f204ff2

test pdf and video app

Browse files
Files changed (1) hide show
  1. app.py +793 -209
app.py CHANGED
@@ -1,248 +1,832 @@
1
  import streamlit as st
2
  import tempfile
 
 
 
 
 
3
  import os
4
- import yt_dlp
5
- from moviepy.editor import VideoFileClip
6
- import socket
7
 
8
- from openai import OpenAI
9
- client = OpenAI()
 
 
 
 
10
 
 
 
 
11
 
12
 
13
- # Set your OpenAI API key (make sure it's set in Hugging Face Spaces secrets)
14
- #openai.api_key = os.getenv("OPENAI_API_KEY")
15
 
16
  # ---------------------------
17
- # Helper Functions
18
  # ---------------------------
19
- def check_internet_connection(host="8.8.8.8", port=53, timeout=3):
20
- """Check if there's an active internet connection."""
21
- try:
22
- socket.setdefaulttimeout(timeout)
23
- socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect((host, port))
24
- return True
25
- except socket.error:
26
- return False
27
-
28
- def check_youtube_access():
29
- """Check if YouTube is accessible."""
30
- try:
31
- socket.setdefaulttimeout(5)
32
- socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect(("www.youtube.com", 443))
33
- return True
34
- except socket.error:
35
- return False
36
- def download_video(youtube_url: str, output_path: str) -> str:
37
- """Download a YouTube video using yt-dlp and save it to the given output path."""
38
- try:
39
- ydl_opts = {
40
- 'format': 'bestvideo[ext=mp4]+bestaudio[ext=m4a]/best[ext=mp4]/best',
41
- 'outtmpl': os.path.join(output_path, 'video.%(ext)s'),
42
- 'noplaylist': True,
43
- 'quiet': False,
44
- 'no_warnings': False,
45
- 'merge_output_format': 'mp4',
46
- # Add network-related options to handle connectivity issues
47
- 'socket_timeout': 30,
48
- 'retries': 10,
49
- 'fragment_retries': 10,
50
- 'extractor_retries': 5,
51
- # Use IPv4 to avoid DNS issues
52
- 'source_address': '0.0.0.0',
53
- }
54
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
55
- info = ydl.extract_info(youtube_url, download=True)
56
- ext = info.get('ext', 'mp4')
57
- video_path = os.path.join(output_path, f"video.{ext}")
58
- return video_path
59
- except Exception as e:
60
- error_msg = str(e)
61
- if "Failed to resolve" in error_msg or "No address associated with hostname" in error_msg:
62
- st.error("⚠️ Network Error: Cannot connect to YouTube. Please check:")
63
- st.error("1. Your internet connection is active")
64
- st.error("2. YouTube is accessible in your network/region")
65
- st.error("3. No firewall/proxy is blocking YouTube")
66
- st.info("πŸ’‘ Alternative: Try uploading the video directly if you have it saved locally.")
67
- else:
68
- st.error(f"Error downloading video: {e}")
69
- return None
70
-
71
- def extract_audio(video_path: str) -> str:
72
- """Extract audio from the video file and save as MP3."""
73
- try:
74
- clip = VideoFileClip(video_path)
75
- audio_path = video_path.replace(".mp4", ".mp3").replace(".mkv", ".mp3").replace(".webm", ".mp3")
76
- clip.audio.write_audiofile(audio_path, codec='mp3', logger=None)
77
- clip.close()
78
- return audio_path
79
- except Exception as e:
80
- st.error(f"Error extracting audio: {e}")
81
- return None
82
-
83
- # audio_file = open("/path/to/file/speech.mp3", "rb")
84
- # transcription = client.audio.transcriptions.create(
85
- # model="whisper-1",
86
- # file=audio_file,
87
- # response_format="text"
88
- # )
89
-
90
- # print(transcription.text)
91
-
92
- def transcribe_audio(audio_path: str) -> str:
93
- """Transcribe the audio to text using OpenAI's Whisper API."""
94
- try:
95
- with open(audio_path, "rb") as audio_file:
96
- transcript = client.audio.transcriptions.create(
97
- model="whisper-1",
98
- file=audio_file
99
- #response_format="text"
100
- )
101
- return transcript.text
102
- except Exception as e:
103
- st.error(f"Error transcribing audio: {e}")
104
  return ""
 
 
 
 
 
 
 
105
 
106
-
107
-
108
- def generate_summary(transcript_text: str) -> str:
109
- """Generate a concise summary of the transcript using OpenAI."""
110
- prompt = f"Summarize the following video transcript in a concise manner, highlighting the key points that the user should know:\n\n{transcript_text}. Feel free to use bullet points, bold, italics and headers to empasize key points where necessary"
 
 
111
  messages = [
112
- {"role": "system", "content": "You are an helpful assistant."},
113
  {"role": "user", "content": prompt}
114
  ]
115
- completion = client.chat.completions.create(model="gpt-4o-mini", messages=messages)
 
 
 
116
  return completion.choices[0].message.content.strip()
117
 
118
- def get_chat_response(transcript_text: str, conversation_history: list, user_query: str) -> str:
119
- """Generate a chat response using the transcript as context."""
120
  messages = conversation_history + [
121
- {"role": "user", "content": f"Based on the video transcript:\n\n{transcript_text}\n\nQuestion: {user_query}"}
122
  ]
123
- completion = client.chat.completions.create(model="gpt-4o-mini", messages=messages)
 
 
 
124
  return completion.choices[0].message.content.strip()
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  # ---------------------------
127
- # Sidebar: Input Options
128
  # ---------------------------
129
- st.sidebar.title("Video Input")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
 
131
- input_mode = st.sidebar.radio("Select Input Type", ("Upload Video", "YouTube URL"))
 
132
 
133
- transcript_text = ""
134
- video_path = None
135
- audio_path = None
136
 
137
- if input_mode == "Upload Video":
138
- uploaded_video = st.sidebar.file_uploader("Upload a video file (MP4, max 200MB)", type=["mp4", "mkv", "mov", "avi"])
139
- if uploaded_video is not None:
140
- # Check file size (200MB = 200 * 1024 * 1024 bytes)
141
- if uploaded_video.size > 200 * 1024 * 1024:
142
- st.sidebar.error("File size exceeds 200MB. Please upload a smaller video.")
143
- else:
144
- with st.spinner("Processing uploaded video..."):
145
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
146
- tmp.write(uploaded_video.read())
147
- video_path = tmp.name
148
- st.sidebar.success("Video uploaded successfully!")
149
-
150
- elif input_mode == "YouTube URL":
151
- youtube_url = st.sidebar.text_input("Enter YouTube video URL:")
152
- if youtube_url:
153
- if st.sidebar.button("Download Video"):
154
- # Check internet connection first
155
- if not check_internet_connection():
156
- st.sidebar.error("❌ No internet connection detected. Please check your network.")
157
- elif not check_youtube_access():
158
- st.sidebar.error("❌ Cannot reach YouTube. It may be blocked by your network/firewall.")
159
- st.sidebar.info("πŸ’‘ Try using a VPN or upload the video directly instead.")
160
- else:
161
- with st.spinner("Downloading video from YouTube..."):
162
- with tempfile.TemporaryDirectory() as temp_dir:
163
- video_path = download_video(youtube_url, temp_dir)
164
- if video_path:
165
- # Copy to a persistent temp file
166
- with tempfile.NamedTemporaryFile(delete=False, suffix=".mp4") as tmp:
167
- with open(video_path, 'rb') as src:
168
- tmp.write(src.read())
169
- video_path = tmp.name
170
- st.sidebar.success("Video downloaded successfully!")
171
-
172
- # ---------------------------
173
- # Process Video to Extract Audio and Transcript
174
- # ---------------------------
175
- if video_path:
176
- if "processed_video" not in st.session_state or st.session_state.processed_video != video_path:
177
- with st.spinner("Extracting audio from video..."):
178
- audio_path = extract_audio(video_path)
179
- if audio_path:
180
- st.sidebar.success("Audio extracted successfully!")
181
- with st.spinner("Transcribing audio... This may take a few minutes."):
182
- transcript_text = transcribe_audio(audio_path)
183
- if transcript_text:
184
- st.sidebar.success("Audio transcribed successfully!")
185
- st.session_state.transcript_text = transcript_text
186
- st.session_state.processed_video = video_path
187
- # Clean up audio file
188
- try:
189
- os.unlink(audio_path)
190
- except:
191
- pass
 
 
 
 
 
 
 
 
 
 
192
  else:
193
- transcript_text = st.session_state.get("transcript_text", "")
194
 
195
  # ---------------------------
196
- # Sidebar: Action Selection
197
  # ---------------------------
198
- st.sidebar.title("Select Action")
199
- action_mode = st.sidebar.radio("Choose Action", ("Transcript", "Summary", "Chat"))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
200
 
201
  # ---------------------------
202
- # Session State Initialization for Chat
203
  # ---------------------------
204
- if "chat_history" not in st.session_state:
205
- st.session_state.chat_history = [{"role": "assistant", "content": "Hi, how can I help you with the video content?"}]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
  # ---------------------------
208
- # Main Display Area
209
  # ---------------------------
210
- st.title("Video Chat & Summarizer πŸŽ₯")
211
- st.write('Tired of watching long boring videos? Summarize your videos in seconds or just chat!')
212
 
213
- if "transcript_text" not in st.session_state or not st.session_state.transcript_text:
214
- st.info("Please provide a video input from the sidebar to begin.")
215
- else:
216
- transcript_text = st.session_state.transcript_text
217
-
218
- if action_mode == "Transcript":
219
- st.header("Full Transcript")
220
- st.write("Below is the complete transcript of the video:")
221
- st.text_area("Transcript", transcript_text, height=400, disabled=True)
222
-
223
- # Add download button for transcript
224
- st.download_button(
225
- label="Download Transcript",
226
- data=transcript_text,
227
- file_name="transcript.txt",
228
- mime="text/plain"
229
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
 
231
- elif action_mode == "Summary":
232
- st.header("Summary & Key Points")
233
- with st.spinner("Generating summary..."):
234
- summary = generate_summary(transcript_text)
235
- st.write(summary)
 
 
 
 
 
 
 
 
236
 
237
- elif action_mode == "Chat":
238
- st.header("Chat with Your Study Companion")
239
- for msg in st.session_state.chat_history:
240
- st.chat_message(msg["role"]).write(msg["content"])
241
- user_query = st.chat_input("Ask a question about the video content:")
242
- if user_query:
243
- st.session_state.chat_history.append({"role": "user", "content": user_query})
244
- st.chat_message("user").write(user_query)
245
- with st.spinner("Processing your question..."):
246
- response = get_chat_response(transcript_text, st.session_state.chat_history, user_query)
247
- st.session_state.chat_history.append({"role": "assistant", "content": response})
248
- st.chat_message("assistant").write(response)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import tempfile
3
+ import json
4
+ import random
5
+ from pathlib import Path
6
+ from PyPDF2 import PdfReader
7
+ from openai import OpenAI
8
  import os
9
+ from ast import literal_eval
 
 
10
 
11
+ # Import video processing functions
12
+ from video import (
13
+ process_uploaded_video,
14
+ generate_video_summary,
15
+ chat_with_video
16
+ )
17
 
18
+ # Initialize the OpenAI client
19
+ api_key = os.getenv("OPENAI_API_KEY")
20
+ client = OpenAI(api_key = api_key)
21
 
22
 
 
 
23
 
24
  # ---------------------------
25
+ # Helper Function: Extract text from PDF
26
  # ---------------------------
27
+ def extract_text(uploaded_file):
28
+ # Check file size (max 10MB)
29
+ uploaded_file.seek(0, os.SEEK_END)
30
+ file_size = uploaded_file.tell()
31
+ uploaded_file.seek(0)
32
+ if file_size > 10 * 1024 * 1024:
33
+ st.error("File size exceeds 10MB limit.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  return ""
35
+ pdf_reader = PdfReader(uploaded_file)
36
+ text = ""
37
+ for page in pdf_reader.pages:
38
+ page_text = page.extract_text()
39
+ if page_text:
40
+ text += page_text + "\n"
41
+ return text
42
 
43
+ # ---------------------------
44
+ # OpenAI Response Functions (using new style)
45
+ # ---------------------------
46
+ def generate_summary_from_text(text):
47
+ prompt = (
48
+ f"Summarize the following document in a concise manner, highlighting the key points that a student should know:\n\n{text}"
49
+ )
50
  messages = [
51
+ {"role": "system", "content": "You are an educational assistant."},
52
  {"role": "user", "content": prompt}
53
  ]
54
+ completion = client.chat.completions.create(
55
+ model="gpt-4o-mini",
56
+ messages=messages
57
+ )
58
  return completion.choices[0].message.content.strip()
59
 
60
+ def chat_with_document(text, conversation_history, user_query):
 
61
  messages = conversation_history + [
62
+ {"role": "user", "content": f"Based on the following document:\n\n{text}\n\nQuestion: {user_query}"}
63
  ]
64
+ completion = client.chat.completions.create(
65
+ model="gpt-4o-mini",
66
+ messages=messages
67
+ )
68
  return completion.choices[0].message.content.strip()
69
 
70
+ def generate_questions_from_text(text, num_questions):
71
+ prompt = (
72
+ f"Generate up to {num_questions} study questions with answers based on the following document.\n"
73
+ f"Return the output as a table with two columns: 'Question' and 'Answer'.\n\nDocument:\n\n{text}"
74
+ )
75
+ messages = [
76
+ {"role": "system", "content": "You are an educational assistant that generates study questions."},
77
+ {"role": "user", "content": prompt}
78
+ ]
79
+ completion = client.chat.completions.create(
80
+ model="gpt-5-mini",
81
+ messages=messages
82
+ )
83
+ return completion.choices[0].message.content.strip()
84
+
85
+ def generate_flashcards_from_text(text, num_cards):
86
+ prompt = (
87
+ f"Generate exactly {num_cards} flashcards based on the following document.\n\n"
88
+ f"Document:\n\n{text}\n\n"
89
+ "Return ONLY a valid JSON object (not a code block) where each key is a flashcard question and its value is the answer. "
90
+ "Format: {\"Question 1?\": \"Answer 1\", \"Question 2?\": \"Answer 2\"}. "
91
+ "Do not include ```json or any other text, just the raw JSON object."
92
+ )
93
+ messages = [
94
+ {"role": "system", "content": "You are an educational assistant that creates study flashcards. Always return valid JSON only, without code blocks or additional text."},
95
+ {"role": "user", "content": prompt}
96
+ ]
97
+ completion = client.chat.completions.create(
98
+ model="gpt-4o-mini",
99
+ messages=messages
100
+ )
101
+ output = completion.choices[0].message.content.strip()
102
+
103
+ # Clean up the output - remove code block markers if present
104
+ if output.startswith("```json"):
105
+ output = output[7:] # Remove ```json
106
+ elif output.startswith("```"):
107
+ output = output[3:] # Remove ```
108
+ if output.endswith("```"):
109
+ output = output[:-3] # Remove trailing ```
110
+ output = output.strip()
111
+
112
+ try:
113
+ # Try JSON first (more reliable)
114
+ flashcards = json.loads(output)
115
+ if isinstance(flashcards, dict) and len(flashcards) > 0:
116
+ return flashcards
117
+ else:
118
+ st.error("Generated flashcards are empty or invalid format.")
119
+ return {}
120
+ except json.JSONDecodeError:
121
+ # Fallback to literal_eval for Python dict syntax
122
+ try:
123
+ flashcards = literal_eval(output)
124
+ if isinstance(flashcards, dict) and len(flashcards) > 0:
125
+ return flashcards
126
+ else:
127
+ st.error("Generated flashcards are empty or invalid format.")
128
+ return {}
129
+ except Exception as e:
130
+ st.error(f"Error parsing flashcards: {e}")
131
+ st.error(f"Received output: {output[:200]}...") # Show first 200 chars for debugging
132
+ return {}
133
+
134
  # ---------------------------
135
+ # Sidebar: File Upload & Mode Selection
136
  # ---------------------------
137
+ st.sidebar.markdown("""
138
+ <style>
139
+ .sidebar-title {
140
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
141
+ padding: 20px;
142
+ border-radius: 10px;
143
+ color: white;
144
+ text-align: center;
145
+ margin-bottom: 20px;
146
+ font-size: 1.3em;
147
+ font-weight: bold;
148
+ }
149
+ </style>
150
+ <div class="sidebar-title">
151
+ βš™οΈ Study Companion Setup
152
+ </div>
153
+ """, unsafe_allow_html=True)
154
 
155
+ # Input source selection
156
+ input_source = st.sidebar.radio("πŸ“₯ Select Input Source", ("PDF Document", "Video"))
157
 
158
+ st.sidebar.markdown("---")
 
 
159
 
160
+ # Initialize variables
161
+ uploaded_pdf = None
162
+ uploaded_video = None
163
+
164
+ if input_source == "PDF Document":
165
+ uploaded_pdf = st.sidebar.file_uploader("πŸ“„ Upload your study PDF (max 10MB)", type="pdf")
166
+ mode = st.sidebar.radio("🎯 Select Mode", ("Chat", "Test Your Knowledge", "Flashcards"))
167
+ elif input_source == "Video":
168
+ uploaded_video = st.sidebar.file_uploader("πŸŽ₯ Upload your video (MP4, max 200MB)", type=["mp4", "mkv", "mov", "avi"])
169
+ mode = st.sidebar.radio("🎯 Select Mode", ("Transcript", "Summary", "Chat"))
170
+
171
+ st.sidebar.markdown("---")
172
+
173
+ # For Test Your Knowledge and Flashcards modes, allow number input.
174
+ num_questions = None
175
+ num_flashcards = None
176
+ if input_source == "PDF Document":
177
+ if mode == "Test Your Knowledge":
178
+ num_questions = st.sidebar.number_input("Number of questions to generate (max 50):", min_value=1, max_value=50, value=10, step=1)
179
+ elif mode == "Flashcards":
180
+ num_flashcards = st.sidebar.number_input("Number of flashcards to generate (max 20):", min_value=1, max_value=20, value=5, step=1)
181
+
182
+ # ---------------------------
183
+ # Session State Initialization
184
+ # ---------------------------
185
+ if "pdf_text" not in st.session_state:
186
+ st.session_state.pdf_text = None
187
+ if "summary" not in st.session_state:
188
+ st.session_state.summary = None
189
+ if "chat_history" not in st.session_state:
190
+ st.session_state.chat_history = [{"role": "assistant", "content": "Hi, how can I help you with your study material?"}]
191
+ if "questions_table" not in st.session_state:
192
+ st.session_state.questions_table = None
193
+ if "flashcards" not in st.session_state:
194
+ st.session_state.flashcards = {}
195
+ if "current_card" not in st.session_state:
196
+ st.session_state.current_card = 0
197
+ if "score" not in st.session_state:
198
+ st.session_state.score = 0
199
+ if "show_answer" not in st.session_state:
200
+ st.session_state.show_answer = False
201
+ if "flashcard_keys" not in st.session_state:
202
+ st.session_state.flashcard_keys = []
203
+ if "user_answers" not in st.session_state:
204
+ st.session_state.user_answers = {}
205
+ if "shuffle_cards" not in st.session_state:
206
+ st.session_state.shuffle_cards = False
207
+
208
+ # Video-related session states
209
+ if "video_transcript" not in st.session_state:
210
+ st.session_state.video_transcript = None
211
+ if "video_chat_history" not in st.session_state:
212
+ st.session_state.video_chat_history = [{"role": "assistant", "content": "Hi, how can I help you with the video content?"}]
213
+ if "processed_video_path" not in st.session_state:
214
+ st.session_state.processed_video_path = None
215
+ if "video_summary" not in st.session_state:
216
+ st.session_state.video_summary = None
217
+
218
+ # ---------------------------
219
+ # Process PDF Upload
220
+ # ---------------------------
221
+ if uploaded_pdf is not None:
222
+ st.session_state.pdf_text = extract_text(uploaded_pdf)
223
+ if st.session_state.pdf_text:
224
+ st.sidebar.success("PDF uploaded and processed successfully!")
225
  else:
226
+ st.sidebar.error("Failed to extract text. Please check your PDF file.")
227
 
228
  # ---------------------------
229
+ # Process Video Upload
230
  # ---------------------------
231
+ if uploaded_video is not None:
232
+ # Generate a unique identifier for the video
233
+ video_id = f"{uploaded_video.name}_{uploaded_video.size}"
234
+
235
+ # Only process if it's a new video
236
+ if st.session_state.processed_video_path != video_id:
237
+ transcript, video_path = process_uploaded_video(uploaded_video)
238
+ if transcript:
239
+ st.session_state.video_transcript = transcript
240
+ st.session_state.processed_video_path = video_id
241
+ st.session_state.video_summary = None # Reset summary for new video
242
+ st.sidebar.success("βœ… Video processed and transcribed successfully!")
243
+ else:
244
+ st.sidebar.error("Failed to process video.")
245
+ else:
246
+ st.sidebar.info("Using cached video transcript.")
247
 
248
  # ---------------------------
249
+ # Main Area: Mode-Based Display (all functions via side menu)
250
  # ---------------------------
251
+ # Page configuration and custom styling
252
+ st.markdown("""
253
+ <style>
254
+ /* Global styling */
255
+ .main {
256
+ background-color: #f5f7fa;
257
+ }
258
+
259
+ /* Header styling */
260
+ .main-header {
261
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
262
+ padding: 20px 15px;
263
+ border-radius: 15px;
264
+ text-align: center;
265
+ color: white;
266
+ margin-bottom: 20px;
267
+ box-shadow: 0 5px 20px rgba(0,0,0,0.2);
268
+ }
269
+
270
+ .main-header h1 {
271
+ margin: 0;
272
+ font-size: 1.8em;
273
+ }
274
+
275
+ .main-header p {
276
+ margin: 8px 0 0 0;
277
+ font-size: 0.95em;
278
+ opacity: 0.9;
279
+ }
280
+
281
+ /* Desktop view - larger header */
282
+ @media (min-width: 768px) {
283
+ .main-header {
284
+ padding: 30px;
285
+ margin-bottom: 30px;
286
+ }
287
+ .main-header h1 {
288
+ font-size: 2.5em;
289
+ }
290
+ .main-header p {
291
+ font-size: 1.1em;
292
+ margin: 10px 0 0 0;
293
+ }
294
+ }
295
+
296
+ /* Chat message styling */
297
+ .stChatMessage {
298
+ background-color: white;
299
+ border-radius: 10px;
300
+ padding: 15px;
301
+ margin: 10px 0;
302
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
303
+ }
304
+
305
+ /* Ensure chat text is visible on mobile */
306
+ .stChatMessage, .stChatMessage p, .stChatMessage div {
307
+ color: #333333 !important;
308
+ }
309
+
310
+ /* Chat mode header - smaller on mobile */
311
+ h2 {
312
+ font-size: 1.3em;
313
+ }
314
+
315
+ @media (min-width: 768px) {
316
+ h2 {
317
+ font-size: 1.75em;
318
+ }
319
+ }
320
+
321
+ /* Info box styling */
322
+ .stInfo {
323
+ background-color: #e3f2fd;
324
+ border-left: 5px solid #2196f3;
325
+ }
326
+
327
+ /* Success box styling */
328
+ .stSuccess {
329
+ background-color: #e8f5e9;
330
+ border-left: 5px solid #4caf50;
331
+ }
332
+
333
+ /* Questions table styling */
334
+ table {
335
+ background-color: white;
336
+ border-radius: 10px;
337
+ overflow: hidden;
338
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
339
+ }
340
+
341
+ th {
342
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
343
+ color: white;
344
+ padding: 15px;
345
+ }
346
+
347
+ td {
348
+ padding: 12px;
349
+ border-bottom: 1px solid #e0e0e0;
350
+ color: #333333 !important;
351
+ }
352
+
353
+ /* Ensure all text in tables is visible */
354
+ table, table p, table div, table span {
355
+ color: #333333 !important;
356
+ }
357
+
358
+ /* Question container - transparent background */
359
+ .question-container {
360
+ background: transparent;
361
+ border-radius: 10px;
362
+ padding: 0;
363
+ margin: 15px 0;
364
+ }
365
+
366
+ /* Make markdown text in questions visible */
367
+ .question-container, .question-container p, .question-container div {
368
+ color: #333333 !important;
369
+ }
370
+
371
+ /* Sidebar styling */
372
+ .css-1d391kg {
373
+ background-color: #f8f9fa;
374
+ }
375
+ </style>
376
+ """, unsafe_allow_html=True)
377
+
378
+ st.markdown("""
379
+ <div class="main-header">
380
+ <h1>πŸ“š Study Companion</h1>
381
+ <p>Your AI-powered learning assistant</p>
382
+ </div>
383
+ """, unsafe_allow_html=True)
384
 
385
  # ---------------------------
386
+ # Main Display - Handle PDF and Video inputs
387
  # ---------------------------
 
 
388
 
389
+ if input_source == "PDF Document":
390
+ if st.session_state.pdf_text is None:
391
+ st.info("Please upload a PDF from the sidebar to begin.")
392
+ else:
393
+ if mode == "Chat":
394
+ st.header("πŸ’¬ Chat with Your Study Companion")
395
+
396
+ # Display persistent chat history
397
+ for msg in st.session_state.chat_history:
398
+ st.chat_message(msg["role"]).write(msg["content"])
399
+
400
+ user_question = st.chat_input("πŸ’­ Ask a question about the document...")
401
+ if user_question:
402
+ st.session_state.chat_history.append({"role": "user", "content": user_question})
403
+ st.chat_message("user").write(user_question)
404
+ with st.spinner("πŸ€” Thinking..."):
405
+ response = chat_with_document(st.session_state.pdf_text, st.session_state.chat_history, user_question)
406
+ st.session_state.chat_history.append({"role": "assistant", "content": response})
407
+ st.chat_message("assistant").write(response)
408
+
409
+ # Add a clear chat button
410
+ if len(st.session_state.chat_history) > 1:
411
+ if st.button("πŸ—‘οΈ Clear Chat History"):
412
+ st.session_state.chat_history = [{"role": "assistant", "content": "Hi, how can I help you with your study material?"}]
413
+ st.rerun()
414
+
415
+ elif mode == "Test Your Knowledge":
416
+ st.header("πŸ“ Test Your Knowledge")
417
+
418
+ if num_questions is None:
419
+ st.info("Please specify the number of questions in the sidebar.")
420
+ else:
421
+ if st.button("πŸ“‹ Generate Questions", use_container_width=True, type="primary"):
422
+ with st.spinner("✨ Generating questions..."):
423
+ questions_output = generate_questions_from_text(st.session_state.pdf_text, num_questions)
424
+ st.session_state.questions_table = questions_output
425
+
426
+ if st.session_state.questions_table:
427
+ st.markdown(st.session_state.questions_table)
428
+
429
+ # Download button for questions
430
+ st.download_button(
431
+ label="πŸ“₯ Download Questions",
432
+ data=st.session_state.questions_table,
433
+ file_name="study_questions.md",
434
+ mime="text/markdown"
435
+ )
436
+ else:
437
+ st.info("πŸ’‘ Click 'Generate Questions' to create a quiz based on your document.")
438
+
439
+ elif mode == "Flashcards":
440
+ st.header("🎴 Interactive Flashcards")
441
+
442
+ # Custom CSS for flashcard styling with flip effect
443
+ st.markdown("""
444
+ <style>
445
+ .flashcard-3d-container {
446
+ perspective: 1000px;
447
+ margin: 20px 0 30px 0;
448
+ }
449
+ .flashcard-flip {
450
+ position: relative;
451
+ min-height: 300px;
452
+ transition: transform 0.6s;
453
+ transform-style: preserve-3d;
454
+ margin-bottom: 20px;
455
+ }
456
+ .flashcard-flip.flipped {
457
+ transform: rotateY(180deg);
458
+ }
459
+ .flashcard-face {
460
+ position: absolute;
461
+ width: 100%;
462
+ min-height: 300px;
463
+ backface-visibility: hidden;
464
+ border-radius: 15px;
465
+ padding: 40px;
466
+ box-shadow: 0 10px 30px rgba(0,0,0,0.3);
467
+ display: flex;
468
+ align-items: center;
469
+ justify-content: center;
470
+ text-align: center;
471
+ overflow-y: auto;
472
+ max-height: 400px;
473
+ }
474
+ .flashcard-front {
475
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
476
+ color: white;
477
+ }
478
+ .flashcard-back {
479
+ background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
480
+ color: white;
481
+ transform: rotateY(180deg);
482
+ }
483
+ .flashcard-question {
484
+ font-size: 22px;
485
+ font-weight: bold;
486
+ line-height: 1.4;
487
+ }
488
+ .flashcard-answer {
489
+ font-size: 18px;
490
+ line-height: 1.6;
491
+ }
492
+ .card-number {
493
+ position: absolute;
494
+ top: 15px;
495
+ left: 20px;
496
+ font-size: 14px;
497
+ opacity: 0.9;
498
+ }
499
+ .flip-instruction {
500
+ position: absolute;
501
+ bottom: 15px;
502
+ width: 100%;
503
+ text-align: center;
504
+ font-size: 14px;
505
+ opacity: 0.8;
506
+ }
507
+ .score-card {
508
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
509
+ border-radius: 10px;
510
+ padding: 20px;
511
+ color: white;
512
+ font-size: 18px;
513
+ font-weight: bold;
514
+ text-align: center;
515
+ margin: 20px 0;
516
+ }
517
+ .progress-bar-container {
518
+ background: #e0e0e0;
519
+ border-radius: 10px;
520
+ height: 25px;
521
+ margin: 20px 0;
522
+ overflow: hidden;
523
+ position: relative;
524
+ }
525
+ .progress-bar {
526
+ background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);
527
+ height: 100%;
528
+ transition: width 0.3s ease;
529
+ display: flex;
530
+ align-items: center;
531
+ justify-content: center;
532
+ color: white;
533
+ font-weight: bold;
534
+ font-size: 14px;
535
+ }
536
+ .navigation-buttons {
537
+ display: flex;
538
+ gap: 10px;
539
+ margin: 30px 0 20px 0;
540
+ }
541
+ .stButton > button {
542
+ border-radius: 8px;
543
+ font-weight: 600;
544
+ padding: 10px 24px;
545
+ transition: all 0.3s ease;
546
+ }
547
+ .stButton > button:hover {
548
+ transform: scale(1.05);
549
+ box-shadow: 0 4px 12px rgba(0,0,0,0.2);
550
+ }
551
+ /* Mobile responsive adjustments */
552
+ @media (max-width: 768px) {
553
+ .flashcard-face {
554
+ padding: 30px 20px;
555
+ min-height: 250px;
556
+ }
557
+ .flashcard-question {
558
+ font-size: 18px;
559
+ }
560
+ .flashcard-answer {
561
+ font-size: 16px;
562
+ }
563
+ }
564
+ </style>
565
+ """, unsafe_allow_html=True)
566
 
567
+ # Sidebar controls for flashcards
568
+ with st.sidebar:
569
+ st.markdown("---")
570
+ st.subheader("🎴 Flashcard Options")
571
+ shuffle_option = st.checkbox("Shuffle cards", value=st.session_state.shuffle_cards)
572
+ if shuffle_option != st.session_state.shuffle_cards:
573
+ st.session_state.shuffle_cards = shuffle_option
574
+ if st.session_state.flashcards and st.session_state.flashcard_keys:
575
+ if shuffle_option:
576
+ st.session_state.flashcard_keys = list(st.session_state.flashcards.keys())
577
+ random.shuffle(st.session_state.flashcard_keys)
578
+ else:
579
+ st.session_state.flashcard_keys = list(st.session_state.flashcards.keys())
580
 
581
+ if num_flashcards is None:
582
+ st.info("Please specify the number of flashcards in the sidebar.")
583
+ else:
584
+ col1, col2 = st.columns([3, 1])
585
+ with col1:
586
+ if st.button("🎯 Generate Flashcards", use_container_width=True):
587
+ with st.spinner("✨ Creating your flashcards..."):
588
+ flashcards = generate_flashcards_from_text(st.session_state.pdf_text, num_flashcards)
589
+ if flashcards:
590
+ st.session_state.flashcards = flashcards
591
+ st.session_state.flashcard_keys = list(flashcards.keys())
592
+ if st.session_state.shuffle_cards:
593
+ random.shuffle(st.session_state.flashcard_keys)
594
+ st.session_state.current_card = 0
595
+ st.session_state.score = 0
596
+ st.session_state.show_answer = False
597
+ st.session_state.user_answers = {}
598
+ st.success("βœ… Flashcards generated successfully!")
599
+ st.rerun()
600
+ else:
601
+ st.error("Failed to generate flashcards. Please try again.")
602
+
603
+ with col2:
604
+ if st.session_state.flashcards and st.button("πŸ”„ Reset", use_container_width=True):
605
+ st.session_state.current_card = 0
606
+ st.session_state.score = 0
607
+ st.session_state.show_answer = False
608
+ st.session_state.user_answers = {}
609
+ st.rerun()
610
+
611
+ if not st.session_state.flashcards:
612
+ st.info("πŸ’‘ No flashcards available. Click 'Generate Flashcards' to create a study set based on your document.")
613
+ else:
614
+ total_cards = len(st.session_state.flashcards)
615
+
616
+ if st.session_state.current_card >= total_cards:
617
+ # Completion screen
618
+ st.balloons()
619
+ st.markdown(f"""
620
+ <div class="score-card">
621
+ πŸŽ‰ Congratulations! You've completed all flashcards!<br>
622
+ Final Score: {st.session_state.score} / {total_cards} ({int(st.session_state.score/total_cards*100)}%)
623
+ </div>
624
+ """, unsafe_allow_html=True)
625
+
626
+ # Show review of incorrect answers
627
+ if st.session_state.user_answers:
628
+ st.subheader("πŸ“ Review Your Answers")
629
+ for i, (q, is_correct) in enumerate(st.session_state.user_answers.items(), 1):
630
+ if not is_correct:
631
+ with st.expander(f"❌ Card {i}: {q[:50]}..."):
632
+ st.write(f"**Question:** {q}")
633
+ st.write(f"**Answer:** {st.session_state.flashcards[q]}")
634
+
635
+ col1, col2 = st.columns(2)
636
+ with col1:
637
+ if st.button("πŸ”„ Practice Again", use_container_width=True):
638
+ st.session_state.current_card = 0
639
+ st.session_state.score = 0
640
+ st.session_state.show_answer = False
641
+ st.session_state.user_answers = {}
642
+ if st.session_state.shuffle_cards:
643
+ random.shuffle(st.session_state.flashcard_keys)
644
+ st.rerun()
645
+ with col2:
646
+ if st.button("πŸ“š New Set", use_container_width=True):
647
+ st.session_state.flashcards = {}
648
+ st.session_state.flashcard_keys = []
649
+ st.session_state.current_card = 0
650
+ st.session_state.score = 0
651
+ st.session_state.show_answer = False
652
+ st.session_state.user_answers = {}
653
+ st.rerun()
654
+ else:
655
+ # Progress bar with percentage
656
+ progress = (st.session_state.current_card / total_cards) * 100
657
+ st.markdown(f"""
658
+ <div class="progress-bar-container">
659
+ <div class="progress-bar" style="width: {progress}%">{int(progress)}%</div>
660
+ </div>
661
+ """, unsafe_allow_html=True)
662
+
663
+ # Display current card with flip effect
664
+ current_key = st.session_state.flashcard_keys[st.session_state.current_card]
665
+ current_answer = st.session_state.flashcards[current_key]
666
+
667
+ # Flip card display
668
+ flip_class = "flipped" if st.session_state.show_answer else ""
669
+ st.markdown(f"""
670
+ <div class="flashcard-3d-container">
671
+ <div class="flashcard-flip {flip_class}">
672
+ <div class="flashcard-face flashcard-front">
673
+ <div>
674
+ <div class="card-number">Card {st.session_state.current_card + 1} of {total_cards}</div>
675
+ <div class="flashcard-question">{current_key}</div>
676
+ <div class="flip-instruction">πŸ‘† Click below to flip</div>
677
+ </div>
678
+ </div>
679
+ <div class="flashcard-face flashcard-back">
680
+ <div>
681
+ <div class="card-number">Card {st.session_state.current_card + 1} of {total_cards}</div>
682
+ <div class="flashcard-answer"><strong>Answer:</strong><br><br>{current_answer}</div>
683
+ </div>
684
+ </div>
685
+ </div>
686
+ </div>
687
+ """, unsafe_allow_html=True)
688
+
689
+ # Flip button
690
+ col_flip1, col_flip2, col_flip3 = st.columns([1, 2, 1])
691
+ with col_flip2:
692
+ if st.button("πŸ”„ Flip Card", use_container_width=True, key="flip_btn"):
693
+ st.session_state.show_answer = not st.session_state.show_answer
694
+ st.rerun()
695
+
696
+ # Navigation buttons (Previous/Next)
697
+ nav_col1, nav_col2, nav_col3 = st.columns([1, 1, 1])
698
+
699
+ with nav_col1:
700
+ if st.button("⬅️ Previous", use_container_width=True, key="prev_btn",
701
+ disabled=(st.session_state.current_card == 0)):
702
+ st.session_state.current_card -= 1
703
+ st.session_state.show_answer = False
704
+ st.rerun()
705
+
706
+ with nav_col2:
707
+ # Show current position
708
+ st.markdown(f"""
709
+ <div style="text-align: center; padding: 10px; font-size: 16px; font-weight: bold;">
710
+ {st.session_state.current_card + 1} / {total_cards}
711
+ </div>
712
+ """, unsafe_allow_html=True)
713
+
714
+ with nav_col3:
715
+ if st.button("Next ➑️", use_container_width=True, key="next_btn",
716
+ disabled=(st.session_state.current_card >= total_cards - 1)):
717
+ st.session_state.current_card += 1
718
+ st.session_state.show_answer = False
719
+ st.rerun()
720
+
721
+ # Self-assessment buttons (only when answer is shown)
722
+ if st.session_state.show_answer:
723
+ st.markdown("### Did you get it right?")
724
+ assess_col1, assess_col2, assess_col3 = st.columns([1, 1, 1])
725
+
726
+ with assess_col1:
727
+ if st.button("βœ… Got it!", use_container_width=True, type="primary", key="correct_btn"):
728
+ st.session_state.score += 1
729
+ st.session_state.user_answers[current_key] = True
730
+ if st.session_state.current_card < total_cards - 1:
731
+ st.session_state.current_card += 1
732
+ st.session_state.show_answer = False
733
+ else:
734
+ st.session_state.current_card += 1
735
+ st.rerun()
736
+
737
+ with assess_col2:
738
+ if st.button("❌ Missed it", use_container_width=True, key="wrong_btn"):
739
+ st.session_state.user_answers[current_key] = False
740
+ if st.session_state.current_card < total_cards - 1:
741
+ st.session_state.current_card += 1
742
+ st.session_state.show_answer = False
743
+ else:
744
+ st.session_state.current_card += 1
745
+ st.rerun()
746
+
747
+ with assess_col3:
748
+ if st.button("⏭️ Skip", use_container_width=True, key="skip_btn"):
749
+ if st.session_state.current_card < total_cards - 1:
750
+ st.session_state.current_card += 1
751
+ st.session_state.show_answer = False
752
+ else:
753
+ st.session_state.current_card += 1
754
+ st.rerun()
755
+
756
+ # Current score display
757
+ st.markdown(f"""
758
+ <div class="score-card">
759
+ Current Score: {st.session_state.score} / {st.session_state.current_card}
760
+ {f"({int(st.session_state.score/st.session_state.current_card*100)}%)" if st.session_state.current_card > 0 else ""}
761
+ </div>
762
+ """, unsafe_allow_html=True)
763
+
764
+ # ---------------------------
765
+ # Video Input Display
766
+ # ---------------------------
767
+ elif input_source == "Video":
768
+ if st.session_state.video_transcript is None:
769
+ st.info("πŸŽ₯ Please upload a video from the sidebar to begin.")
770
+ else:
771
+ if mode == "Transcript":
772
+ st.header("πŸ“„ Video Transcript")
773
+ st.write("Below is the complete transcript of the video:")
774
+ st.text_area("Transcript", st.session_state.video_transcript, height=400, disabled=True, label_visibility="collapsed")
775
+
776
+ # Add download button for transcript
777
+ st.download_button(
778
+ label="πŸ“₯ Download Transcript",
779
+ data=st.session_state.video_transcript,
780
+ file_name="video_transcript.txt",
781
+ mime="text/plain"
782
+ )
783
+
784
+ elif mode == "Summary":
785
+ st.header("πŸ“ Video Summary & Key Points")
786
+
787
+ # Generate summary only once and cache it
788
+ if st.session_state.video_summary is None:
789
+ with st.spinner("✨ Generating summary..."):
790
+ summary = generate_video_summary(st.session_state.video_transcript)
791
+ st.session_state.video_summary = summary
792
+
793
+ st.markdown(st.session_state.video_summary)
794
+
795
+ # Download button for summary
796
+ st.download_button(
797
+ label="πŸ“₯ Download Summary",
798
+ data=st.session_state.video_summary,
799
+ file_name="video_summary.md",
800
+ mime="text/markdown"
801
+ )
802
+
803
+ elif mode == "Chat":
804
+ st.header("πŸ’¬ Chat About the Video")
805
+
806
+ # Display persistent chat history for video
807
+ for msg in st.session_state.video_chat_history:
808
+ st.chat_message(msg["role"]).write(msg["content"])
809
+
810
+ user_question = st.chat_input("πŸ’­ Ask a question about the video...")
811
+ if user_question:
812
+ st.session_state.video_chat_history.append({"role": "user", "content": user_question})
813
+ st.chat_message("user").write(user_question)
814
+ with st.spinner("πŸ€” Thinking..."):
815
+ response = chat_with_video(
816
+ st.session_state.video_transcript,
817
+ st.session_state.video_chat_history,
818
+ user_question
819
+ )
820
+ st.session_state.video_chat_history.append({"role": "assistant", "content": response})
821
+ st.chat_message("assistant").write(response)
822
+
823
+ # Add a clear chat button
824
+ if len(st.session_state.video_chat_history) > 1:
825
+ if st.button("πŸ—‘οΈ Clear Chat History", key="clear_video_chat"):
826
+ st.session_state.video_chat_history = [{"role": "assistant", "content": "Hi, how can I help you with the video content?"}]
827
+ st.rerun()
828
+
829
+
830
+
831
+
832
+