abhishekjoel commited on
Commit
bc5cfc4
·
verified ·
1 Parent(s): 4415aaf

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +192 -112
app.py CHANGED
@@ -10,14 +10,10 @@ import traceback # For detailed error logging
10
 
11
  # --- Configuration ---
12
  # Models chosen for speed and capability balance
13
- # whisper-1 is standard for transcription via API.
14
- # gpt-3.5-turbo is generally fast for summarization/chat.
15
  TRANSCRIPTION_MODEL = "whisper-1"
16
  LANGUAGE_MODEL = "gpt-3.5-turbo"
17
  # Approximate context window limit for the language model (input tokens)
18
- # Leaving space for prompt overhead and response generation.
19
- # Check OpenAI docs for the specific version deployed if needed.
20
- MAX_TOKENS_FOR_SUMMARY_INPUT = 3500 # Adjusted slightly for safety margin
21
  MAX_TOKENS_FOR_CHAT_INPUT = 3500 # Context + Question
22
  AUDIO_SIZE_LIMIT_MB = 25 # OpenAI API limit
23
 
@@ -57,14 +53,12 @@ def truncate_text_by_tokens(text, max_tokens):
57
  return text
58
  except Exception as e:
59
  st.warning(f"Token encoding/decoding failed during truncation: {e}. Using word count fallback.")
60
- # Fallback truncation
61
  words = text.split()
62
- estimated_words = int(max_tokens * 0.7) # Rough estimate words per token
63
  return " ".join(words[:estimated_words])
64
  else:
65
- # Fallback truncation if tiktoken failed to initialize
66
  words = text.split()
67
- estimated_words = int(max_tokens * 0.7) # Rough estimate
68
  return " ".join(words[:estimated_words])
69
 
70
  # --- Core Functions ---
@@ -72,7 +66,6 @@ def truncate_text_by_tokens(text, max_tokens):
72
  def initialize_openai():
73
  """Initializes OpenAI API key from Streamlit secrets."""
74
  try:
75
- # Fetch API key from Hugging Face secrets
76
  api_key = st.secrets["OPENAI_API_KEY"]
77
  if not api_key:
78
  st.error("OpenAI API Key not found in Secrets. Please add 'OPENAI_API_KEY' to your Hugging Face Space secrets.")
@@ -96,7 +89,6 @@ def transcribe_audio(audio_file):
96
  try:
97
  audio = AudioSegment.from_file(audio_file)
98
  buffer = io.BytesIO()
99
- # Export as WAV for broad compatibility with Whisper
100
  audio.export(buffer, format="wav")
101
  buffer.seek(0)
102
  buffer.name = "audio.wav" # Required by OpenAI API
@@ -118,14 +110,12 @@ def transcribe_audio(audio_file):
118
  return None
119
  except Exception as e:
120
  st.error(f"Error during audio transcription: {str(e)}")
121
- # Log detailed error for debugging if needed (visible in Hugging Face logs)
122
  print(f"Transcription Error Traceback:\n{traceback.format_exc()}")
123
  return None
124
 
125
  def extract_text_from_pdf(pdf_file):
126
  """Extracts text from a PDF using PyMuPDF."""
127
  try:
128
- # Read file bytes directly for PyMuPDF
129
  pdf_bytes = pdf_file.getvalue()
130
  doc = fitz.open(stream=pdf_bytes, filetype="pdf")
131
  text = ""
@@ -134,7 +124,7 @@ def extract_text_from_pdf(pdf_file):
134
  doc.close()
135
  if not text.strip():
136
  st.warning("No text could be extracted. The PDF might be image-based (scanned) or empty.")
137
- return "" # Return empty string, not None, to avoid downstream errors
138
  return text
139
  except Exception as e:
140
  st.error(f"Error reading PDF: {str(e)}")
@@ -144,28 +134,41 @@ def extract_text_from_pdf(pdf_file):
144
  def get_youtube_transcript(url):
145
  """Gets English transcript from a YouTube video."""
146
  try:
 
147
  if "watch?v=" in url:
148
  video_id = url.split("watch?v=")[1].split("&")[0]
149
- # Add other common formats if needed, e.g., youtu.be/
150
- elif "youtu.be//" in url:
151
- # Be careful with splitting logic for different URL structures
152
- # This is a guess, adjust based on actual URLs you encounter
153
- video_id = url.split("youtu.be//")[1].split("?")[0]
154
  elif "youtu.be//" in url:
155
  video_id = url.split("/")[-1].split("?")[0]
156
  else:
157
- # Basic check for other potential valid IDs (e.g., short URLs)
158
- # This might need refinement
159
  parts = url.split("/")
160
- if len(parts[-1]) == 11: # Common length for YouTube IDs
161
- video_id = parts[-1].split("?")[0]
 
162
  else:
163
- st.error("Could not automatically determine Video ID from URL.")
164
  return None
165
 
 
 
 
 
166
  transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
167
- # Try fetching English first, fallback might be needed if desired
168
- transcript = transcript_list.find_generated_transcript(['en']) # Or find_manually_created_transcript
 
 
 
 
 
 
 
 
 
169
  transcript_data = transcript.fetch()
170
  transcription_text = "\n".join(
171
  [f"[{entry['start']:.2f}-{entry['start']+entry['duration']:.2f}] {entry['text']}" for entry in transcript_data]
@@ -174,9 +177,6 @@ def get_youtube_transcript(url):
174
  except TranscriptsDisabled:
175
  st.error(f"Transcripts are disabled for video: {url}")
176
  return None
177
- except NoTranscriptFound:
178
- st.warning(f"No English transcript found for video: {url}. Auto-generated transcripts might exist in other languages.")
179
- return None
180
  except Exception as e:
181
  st.error(f"Error fetching YouTube transcript: {str(e)}")
182
  print(f"YouTube Transcript Error Traceback:\n{traceback.format_exc()}")
@@ -186,37 +186,33 @@ def generate_summary(text_to_summarize, max_output_tokens=800):
186
  """Generates summary using OpenAI API, handling potential truncation."""
187
  input_token_count = count_tokens(text_to_summarize)
188
 
189
- # Check if input text needs truncation BEFORE sending to API
190
  if input_token_count > MAX_TOKENS_FOR_SUMMARY_INPUT:
191
  st.warning(f"Input text ({input_token_count} tokens) exceeds the limit ({MAX_TOKENS_FOR_SUMMARY_INPUT} tokens) for the summarization model. Truncating input.")
192
  text_to_summarize = truncate_text_by_tokens(text_to_summarize, MAX_TOKENS_FOR_SUMMARY_INPUT)
193
- input_token_count = count_tokens(text_to_summarize) # Recount after truncation
194
 
195
  if not text_to_summarize:
196
  st.error("Input text for summarization is empty.")
197
  return None
198
 
199
- # Ensure we leave enough tokens for the output
200
- # The API calculates this, but good practice to have a buffer
201
- # max_tokens in create() limits the *output* length
202
  prompt = f"Summarize the following text comprehensively, focusing on key points, concepts, and conclusions. Aim for a detailed summary but keep it concise where possible:\n\n{text_to_summarize}"
203
 
204
  try:
205
  response = openai.ChatCompletion.create(
206
  model=LANGUAGE_MODEL,
207
  messages=[{'role': 'user', 'content': prompt}],
208
- max_tokens=max_output_tokens, # Limit the length of the generated summary
209
- temperature=0.5 # Adjust temperature for creativity vs factuality
210
  )
211
  return response.choices[0].message.content.strip()
212
  except openai.error.AuthenticationError:
213
  st.error("Authentication Error: Invalid OpenAI API Key provided in Secrets.")
214
  return None
215
  except openai.error.RateLimitError:
216
- st.error("OpenAI API Rate Limit Exceeded during summarization. Please check your usage or wait.")
217
  return None
218
  except openai.error.InvalidRequestError as e:
219
- st.error(f"Invalid Request during summarization: {e}. This might be due to content policy or exceeding model limits.")
220
  return None
221
  except Exception as e:
222
  st.error(f"Error during summary generation: {str(e)}")
@@ -229,36 +225,32 @@ def chat_with_ai(question, context, max_output_tokens=500):
229
  st.warning("Please enter a question.")
230
  return None
231
  if not context:
232
- st.error("Cannot answer question: No context (summary or text) available.")
233
  return None
234
 
235
  prompt = f"Based *only* on the following content:\n\n---\n{context}\n---\n\nAnswer the question: {question}"
236
  prompt_token_count = count_tokens(prompt)
237
 
238
- # Check if prompt exceeds model limits
239
  if prompt_token_count > MAX_TOKENS_FOR_CHAT_INPUT:
240
- st.error(f"The question and context combined ({prompt_token_count} tokens) exceed the model's input limit ({MAX_TOKENS_FOR_CHAT_INPUT} tokens). Please shorten the context or ask a more concise question.")
241
- # Alternative: Truncate context here if desired, but might lose info
242
- # context = truncate_text_by_tokens(context, MAX_TOKENS_FOR_CHAT_INPUT - count_tokens(f"Answer the question: {question}") - 50) # Rough context truncation
243
- # prompt = f"Based *only* on the following content:\n\n---\n{context}\n---\n\nAnswer the question: {question}"
244
  return None
245
 
246
  try:
247
  response = openai.ChatCompletion.create(
248
  model=LANGUAGE_MODEL,
249
  messages=[{'role': 'user', 'content': prompt}],
250
- max_tokens=max_output_tokens, # Limit answer length
251
- temperature=0.3 # Lower temperature for more factual answers based on context
252
  )
253
  return response.choices[0].message.content.strip()
254
  except openai.error.AuthenticationError:
255
  st.error("Authentication Error: Invalid OpenAI API Key provided in Secrets.")
256
  return None
257
  except openai.error.RateLimitError:
258
- st.error("OpenAI API Rate Limit Exceeded during chat. Please check your usage or wait.")
259
  return None
260
  except openai.error.InvalidRequestError as e:
261
- st.error(f"Invalid Request during chat: {e}. This might be due to content policy or exceeding model limits.")
262
  return None
263
  except Exception as e:
264
  st.error(f"Error during AI chat: {str(e)}")
@@ -269,39 +261,87 @@ def chat_with_ai(question, context, max_output_tokens=500):
269
  def main():
270
  st.set_page_config(layout="wide", page_title="AI Summarization Bot")
271
 
272
- # --- Styling (Optional) ---
273
  st.markdown("""
274
  <style>
275
- /* Add your custom CSS here if needed */
276
  .stApp {
277
- /* background: linear-gradient(....); */ /* Example */
 
 
 
 
278
  }
 
 
 
 
 
 
 
 
 
279
  .stTextArea textarea {
280
- /* background-color: #f0f0f0 !important; */ /* Example */
281
- /* color: #333 !important; */ /* Example */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
  }
283
- h1, h2, h3 {
284
- /* color: #your_color !important; */ /* Example */
 
285
  }
286
- /* Make text areas scrollable */
 
 
 
 
 
 
 
 
 
 
 
287
  div[data-baseweb="textarea"] > div > textarea {
288
- max-height: 400px; /* Adjust as needed */
289
  overflow-y: auto !important;
290
  }
291
  </style>
292
  """, unsafe_allow_html=True)
293
 
294
  st.markdown("<h1 style='text-align: center;'>AI Summarization Bot 🤖</h1>", unsafe_allow_html=True)
295
- st.markdown("<p style='text-align: center;'>Upload Audio/PDF or provide YouTube URL for Transcription & Summary</p>", unsafe_allow_html=True)
296
 
297
  # Initialize OpenAI API Key
298
- # This should run early, ideally once per session if key doesn't change
299
  if 'openai_initialized' not in st.session_state:
300
  st.session_state['openai_initialized'] = initialize_openai()
301
 
302
  if not st.session_state.get('openai_initialized'):
303
  st.warning("OpenAI initialization failed. Please ensure your API key is correctly set in Hugging Face secrets and refresh.")
304
- st.stop() # Stop execution if key is not valid/found
305
 
306
  # --- Sidebar for Inputs ---
307
  st.sidebar.header("Input Options")
@@ -314,16 +354,20 @@ def main():
314
  st.session_state['summary'] = None
315
  if 'last_input_type' not in st.session_state:
316
  st.session_state['last_input_type'] = None
317
- if 'last_input_data_key' not in st.session_state: # Track input data reference
318
  st.session_state['last_input_data_key'] = None
 
 
 
319
 
320
  # Clear results if input type changes
321
  if st.session_state['last_input_type'] != input_type:
322
  st.session_state['full_text'] = None
323
  st.session_state['summary'] = None
324
- st.session_state['last_input_data_key'] = None # Reset input tracker too
 
325
 
326
- st.session_state['last_input_type'] = input_type # Update current type
327
 
328
  # --- Input Elements ---
329
  uploaded_file = None
@@ -331,41 +375,60 @@ def main():
331
  process_button_pressed = False
332
 
333
  if input_type == "Audio File":
334
- uploaded_file = st.sidebar.file_uploader("Upload audio file (Max 25MB)", type=["mp3", "wav", "m4a", "ogg", "webm"], key="audio_uploader") # Added more types pydub might handle
335
  if uploaded_file:
336
- st.session_state['current_input_key'] = uploaded_file.id # Use file ID as key
 
337
  elif input_type == "PDF Document":
338
  uploaded_file = st.sidebar.file_uploader("Upload PDF document", type=["pdf"], key="pdf_uploader")
339
  if uploaded_file:
340
- st.session_state['current_input_key'] = uploaded_file.id
 
341
  elif input_type == "YouTube URL":
342
- youtube_url = st.sidebar.text_input("Enter YouTube URL (must have subtitles)", key="youtube_input")
343
  if youtube_url:
344
  st.session_state['current_input_key'] = youtube_url # Use URL as key
345
 
 
 
 
 
 
 
 
346
  # Single "Generate" button
347
- if st.sidebar.button("Generate Summary & Notes", key="generate_button"):
348
- # Check if new input is provided or if it's the same as last time
349
- if 'current_input_key' in st.session_state and st.session_state['current_input_key'] != st.session_state['last_input_data_key']:
350
- # New input detected, clear old results and process
351
- st.session_state['full_text'] = None
352
- st.session_state['summary'] = None
353
- st.session_state['last_input_data_key'] = st.session_state['current_input_key'] # Update tracker
354
- process_button_pressed = True
355
- elif 'current_input_key' not in st.session_state or not st.session_state['current_input_key']:
356
- st.warning("Please provide input (upload file or enter URL) before generating.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
357
  else:
358
- # Same input as before, maybe re-process? For now, just indicate it's done if results exist
359
- if st.session_state['full_text'] or st.session_state['summary']:
360
- st.info("Results for the current input are already displayed.")
361
- else: # If results somehow got cleared, reprocess
362
- process_button_pressed = True
363
 
364
 
365
  # --- Processing Logic ---
366
  if process_button_pressed:
367
  extracted_text = None
368
- input_valid = False
369
 
370
  if input_type == "Audio File" and uploaded_file:
371
  input_valid = True
@@ -380,58 +443,63 @@ def main():
380
  with st.spinner('Fetching YouTube transcript...'):
381
  extracted_text = get_youtube_transcript(youtube_url)
382
 
383
- if input_valid and extracted_text is not None: # Check for None from failed extraction
384
  st.session_state['full_text'] = extracted_text
385
  if extracted_text: # Only summarize if text extraction was successful
386
  with st.spinner('Generating summary...'):
387
  summary_text = generate_summary(extracted_text)
388
  st.session_state['summary'] = summary_text
389
  if not summary_text:
390
- st.error("Summary generation failed.")
391
  else:
392
  st.warning("Text extraction resulted in empty content. Cannot generate summary.")
393
- st.session_state['summary'] = None # Ensure summary is None if text is empty
394
  elif input_valid and extracted_text is None:
395
- # Error message already shown in extraction function
396
  st.session_state['full_text'] = None
397
  st.session_state['summary'] = None
398
 
399
 
400
  # --- Display Results ---
 
401
  if st.session_state.get('full_text') or st.session_state.get('summary'):
402
- st.markdown("---")
403
- col1, col2 = st.columns([1, 1]) # Equal columns
404
 
405
  with col1:
406
  st.markdown("<h3>Full Text / Transcription</h3>", unsafe_allow_html=True)
407
- if st.session_state.get('full_text'):
408
- # Display full text, truncate display if extremely long for UI performance
409
- display_text = st.session_state['full_text']
410
- if len(display_text) > 100000: # Arbitrary limit for UI
411
- display_text = display_text[:100000] + "\n\n... (Text truncated for display)"
412
- st.text_area("Full Content", display_text, height=400, key="full_text_area")
 
413
  else:
414
- st.info("No text extracted or transcribed.")
 
 
415
 
416
  with col2:
417
  st.markdown("<h3>Generated Summary</h3>", unsafe_allow_html=True)
418
- if st.session_state.get('summary'):
419
- st.text_area("Summary", st.session_state['summary'], height=400, key="summary_area")
420
- elif st.session_state.get('full_text'):
421
- st.warning("Summary could not be generated.")
422
  else:
423
- st.info("Generate content first to see summary.")
 
 
424
 
425
  # --- Chat Section ---
426
  st.markdown("---")
427
  st.markdown("<h3>Chat with AI about the Content</h3>", unsafe_allow_html=True)
428
 
429
- # Option to choose context for chat
430
  context_option = st.radio(
431
  "Use as chat context:",
432
  ('Generated Summary', 'Full Text'),
433
  key='chat_context_option',
434
- horizontal=True
 
435
  )
436
 
437
  chat_context = None
@@ -444,31 +512,43 @@ def main():
444
  st.warning("Summary not available for chat context.")
445
  else: # Full Text option
446
  if st.session_state.get('full_text'):
447
- # Important: Use truncated text if original was too long for chat model
448
- chat_context = truncate_text_by_tokens(st.session_state['full_text'], MAX_TOKENS_FOR_CHAT_INPUT - 500) # Leave room for question+response
449
- if len(st.session_state['full_text']) > len(chat_context):
450
- context_name = "Full Text (Truncated)"
 
 
 
 
451
  else:
452
  context_name = "Full Text"
453
  else:
454
  st.warning("Full text not available for chat context.")
455
 
456
  if chat_context:
457
- st.info(f"Chatting based on: **{context_name}**")
458
- question = st.text_input("Ask a question:", key="chat_question")
459
- if st.button("Ask AI", key="ask_ai_button"):
 
460
  if question:
461
  with st.spinner("AI is thinking..."):
462
  answer = chat_with_ai(question, chat_context)
463
  if answer:
464
  st.markdown("**AI Answer:**")
465
- st.write(answer)
 
466
  else:
467
  st.error("Failed to get an answer from the AI.")
468
  else:
469
  st.warning("Please enter a question first.")
470
  else:
471
- st.markdown("_(Generate content or summary first to enable chat)_")
 
 
 
 
 
 
472
 
473
 
474
  if __name__ == "__main__":
 
10
 
11
  # --- Configuration ---
12
  # Models chosen for speed and capability balance
 
 
13
  TRANSCRIPTION_MODEL = "whisper-1"
14
  LANGUAGE_MODEL = "gpt-3.5-turbo"
15
  # Approximate context window limit for the language model (input tokens)
16
+ MAX_TOKENS_FOR_SUMMARY_INPUT = 3500
 
 
17
  MAX_TOKENS_FOR_CHAT_INPUT = 3500 # Context + Question
18
  AUDIO_SIZE_LIMIT_MB = 25 # OpenAI API limit
19
 
 
53
  return text
54
  except Exception as e:
55
  st.warning(f"Token encoding/decoding failed during truncation: {e}. Using word count fallback.")
 
56
  words = text.split()
57
+ estimated_words = int(max_tokens * 0.7)
58
  return " ".join(words[:estimated_words])
59
  else:
 
60
  words = text.split()
61
+ estimated_words = int(max_tokens * 0.7)
62
  return " ".join(words[:estimated_words])
63
 
64
  # --- Core Functions ---
 
66
  def initialize_openai():
67
  """Initializes OpenAI API key from Streamlit secrets."""
68
  try:
 
69
  api_key = st.secrets["OPENAI_API_KEY"]
70
  if not api_key:
71
  st.error("OpenAI API Key not found in Secrets. Please add 'OPENAI_API_KEY' to your Hugging Face Space secrets.")
 
89
  try:
90
  audio = AudioSegment.from_file(audio_file)
91
  buffer = io.BytesIO()
 
92
  audio.export(buffer, format="wav")
93
  buffer.seek(0)
94
  buffer.name = "audio.wav" # Required by OpenAI API
 
110
  return None
111
  except Exception as e:
112
  st.error(f"Error during audio transcription: {str(e)}")
 
113
  print(f"Transcription Error Traceback:\n{traceback.format_exc()}")
114
  return None
115
 
116
  def extract_text_from_pdf(pdf_file):
117
  """Extracts text from a PDF using PyMuPDF."""
118
  try:
 
119
  pdf_bytes = pdf_file.getvalue()
120
  doc = fitz.open(stream=pdf_bytes, filetype="pdf")
121
  text = ""
 
124
  doc.close()
125
  if not text.strip():
126
  st.warning("No text could be extracted. The PDF might be image-based (scanned) or empty.")
127
+ return ""
128
  return text
129
  except Exception as e:
130
  st.error(f"Error reading PDF: {str(e)}")
 
134
  def get_youtube_transcript(url):
135
  """Gets English transcript from a YouTube video."""
136
  try:
137
+ video_id = None
138
  if "watch?v=" in url:
139
  video_id = url.split("watch?v=")[1].split("&")[0]
140
+ elif "youtu.be/" in url:
141
+ video_id = url.split("youtu.be/")[1].split("?")[0]
142
+ elif "youtu.be/" in url:
143
+ video_id = url.split("/")[-1].split("?")[0]
 
144
  elif "youtu.be//" in url:
145
  video_id = url.split("/")[-1].split("?")[0]
146
  else:
147
+ # Basic check for other potential valid IDs (e.g., youtu.be links)
 
148
  parts = url.split("/")
149
+ potential_id = parts[-1].split("?")[0]
150
+ if len(potential_id) == 11: # Common length for YouTube IDs
151
+ video_id = potential_id
152
  else:
153
+ st.error("Could not automatically determine Video ID from URL. Please use standard 'watch?v=' URL.")
154
  return None
155
 
156
+ if not video_id:
157
+ st.error("Failed to extract video ID.")
158
+ return None
159
+
160
  transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
161
+ try:
162
+ # Prioritize manual transcripts, fallback to generated
163
+ transcript = transcript_list.find_manually_created_transcript(['en'])
164
+ except NoTranscriptFound:
165
+ try:
166
+ transcript = transcript_list.find_generated_transcript(['en'])
167
+ st.info("Using auto-generated English transcript.")
168
+ except NoTranscriptFound:
169
+ st.warning(f"No English transcript (manual or generated) found for video: {url}")
170
+ return None
171
+
172
  transcript_data = transcript.fetch()
173
  transcription_text = "\n".join(
174
  [f"[{entry['start']:.2f}-{entry['start']+entry['duration']:.2f}] {entry['text']}" for entry in transcript_data]
 
177
  except TranscriptsDisabled:
178
  st.error(f"Transcripts are disabled for video: {url}")
179
  return None
 
 
 
180
  except Exception as e:
181
  st.error(f"Error fetching YouTube transcript: {str(e)}")
182
  print(f"YouTube Transcript Error Traceback:\n{traceback.format_exc()}")
 
186
  """Generates summary using OpenAI API, handling potential truncation."""
187
  input_token_count = count_tokens(text_to_summarize)
188
 
 
189
  if input_token_count > MAX_TOKENS_FOR_SUMMARY_INPUT:
190
  st.warning(f"Input text ({input_token_count} tokens) exceeds the limit ({MAX_TOKENS_FOR_SUMMARY_INPUT} tokens) for the summarization model. Truncating input.")
191
  text_to_summarize = truncate_text_by_tokens(text_to_summarize, MAX_TOKENS_FOR_SUMMARY_INPUT)
192
+ input_token_count = count_tokens(text_to_summarize) # Recount
193
 
194
  if not text_to_summarize:
195
  st.error("Input text for summarization is empty.")
196
  return None
197
 
 
 
 
198
  prompt = f"Summarize the following text comprehensively, focusing on key points, concepts, and conclusions. Aim for a detailed summary but keep it concise where possible:\n\n{text_to_summarize}"
199
 
200
  try:
201
  response = openai.ChatCompletion.create(
202
  model=LANGUAGE_MODEL,
203
  messages=[{'role': 'user', 'content': prompt}],
204
+ max_tokens=max_output_tokens,
205
+ temperature=0.5
206
  )
207
  return response.choices[0].message.content.strip()
208
  except openai.error.AuthenticationError:
209
  st.error("Authentication Error: Invalid OpenAI API Key provided in Secrets.")
210
  return None
211
  except openai.error.RateLimitError:
212
+ st.error("OpenAI API Rate Limit Exceeded during summarization.")
213
  return None
214
  except openai.error.InvalidRequestError as e:
215
+ st.error(f"Invalid Request during summarization: {e}.")
216
  return None
217
  except Exception as e:
218
  st.error(f"Error during summary generation: {str(e)}")
 
225
  st.warning("Please enter a question.")
226
  return None
227
  if not context:
228
+ st.error("Cannot answer question: No context available.")
229
  return None
230
 
231
  prompt = f"Based *only* on the following content:\n\n---\n{context}\n---\n\nAnswer the question: {question}"
232
  prompt_token_count = count_tokens(prompt)
233
 
 
234
  if prompt_token_count > MAX_TOKENS_FOR_CHAT_INPUT:
235
+ st.error(f"The question and context combined ({prompt_token_count} tokens) exceed the model's input limit ({MAX_TOKENS_FOR_CHAT_INPUT} tokens). Try using the summary as context or ask a shorter question.")
 
 
 
236
  return None
237
 
238
  try:
239
  response = openai.ChatCompletion.create(
240
  model=LANGUAGE_MODEL,
241
  messages=[{'role': 'user', 'content': prompt}],
242
+ max_tokens=max_output_tokens,
243
+ temperature=0.3
244
  )
245
  return response.choices[0].message.content.strip()
246
  except openai.error.AuthenticationError:
247
  st.error("Authentication Error: Invalid OpenAI API Key provided in Secrets.")
248
  return None
249
  except openai.error.RateLimitError:
250
+ st.error("OpenAI API Rate Limit Exceeded during chat.")
251
  return None
252
  except openai.error.InvalidRequestError as e:
253
+ st.error(f"Invalid Request during chat: {e}.")
254
  return None
255
  except Exception as e:
256
  st.error(f"Error during AI chat: {str(e)}")
 
261
  def main():
262
  st.set_page_config(layout="wide", page_title="AI Summarization Bot")
263
 
264
+ # --- Styling (Restored Original CSS) ---
265
  st.markdown("""
266
  <style>
 
267
  .stApp {
268
+ background: linear-gradient(180deg,
269
+ rgba(64,224,208,0.7) 0%,
270
+ rgba(32,112,104,0.4) 35%,
271
+ rgba(0,0,0,0) 100%
272
+ );
273
  }
274
+ /* Attempt to make sidebar slightly transparent if needed */
275
+ div[data-testid="stSidebarContent"] {
276
+ background-color: rgba(255,255,255,0.1) !important; /* May need tweaking */
277
+ }
278
+ /* Style markdown text */
279
+ .stMarkdown p, .stMarkdown li, .stText, .stAlert p {
280
+ color: #ffffff !important; /* White text for markdown, etc. */
281
+ }
282
+ /* Text Area Styling */
283
  .stTextArea textarea {
284
+ background-color: rgba(0, 0, 0, 0.6) !important; /* Darker transparent background */
285
+ color: #ffffff !important; /* White text */
286
+ border: 1px solid rgba(255, 255, 255, 0.3); /* Subtle border */
287
+ max-height: 400px; /* Ensure scroll height */
288
+ overflow-y: auto !important;
289
+ }
290
+ /* Input Text Styling */
291
+ .stTextInput input {
292
+ color: white !important;
293
+ background-color: rgba(0, 0, 0, 0.5) !important;
294
+ border: 1px solid rgba(255, 255, 255, 0.3);
295
+ }
296
+ /* Button Styling */
297
+ .stButton button {
298
+ background-color: #40E0D0; /* Turquoise */
299
+ color: black;
300
+ border: none;
301
+ padding: 0.5rem 1rem;
302
+ border-radius: 5px;
303
+ font-weight: bold;
304
+ }
305
+ .stButton button:hover {
306
+ background-color: #48D1CC; /* Slightly darker turquoise */
307
+ color: black;
308
+ }
309
+ /* Headings */
310
+ h1, h2, h3, h4, h5, h6 {
311
+ color: white !important;
312
  }
313
+ /* Specific text elements like radio buttons, selectbox labels */
314
+ .stRadio label, .stSelectbox label, .stFileUploader label {
315
+ color: white !important;
316
  }
317
+ /* Sidebar Header */
318
+ [data-testid="stSidebar"] [data-testid="stVerticalBlock"] {
319
+ color: white !important;
320
+ }
321
+ [data-testid="stSidebar"] h1, [data-testid="stSidebar"] h2, [data-testid="stSidebar"] h3 {
322
+ color: white !important;
323
+ }
324
+ [data-testid="stSidebar"] p, [data-testid="stSidebar"] li {
325
+ color: white !important;
326
+ }
327
+
328
+ /* Make text areas scrollable if content exceeds max-height */
329
  div[data-baseweb="textarea"] > div > textarea {
 
330
  overflow-y: auto !important;
331
  }
332
  </style>
333
  """, unsafe_allow_html=True)
334
 
335
  st.markdown("<h1 style='text-align: center;'>AI Summarization Bot 🤖</h1>", unsafe_allow_html=True)
336
+ # Removed redundant description paragraph as title is descriptive
337
 
338
  # Initialize OpenAI API Key
 
339
  if 'openai_initialized' not in st.session_state:
340
  st.session_state['openai_initialized'] = initialize_openai()
341
 
342
  if not st.session_state.get('openai_initialized'):
343
  st.warning("OpenAI initialization failed. Please ensure your API key is correctly set in Hugging Face secrets and refresh.")
344
+ st.stop()
345
 
346
  # --- Sidebar for Inputs ---
347
  st.sidebar.header("Input Options")
 
354
  st.session_state['summary'] = None
355
  if 'last_input_type' not in st.session_state:
356
  st.session_state['last_input_type'] = None
357
+ if 'last_input_data_key' not in st.session_state:
358
  st.session_state['last_input_data_key'] = None
359
+ if 'current_input_key' not in st.session_state:
360
+ st.session_state['current_input_key'] = None
361
+
362
 
363
  # Clear results if input type changes
364
  if st.session_state['last_input_type'] != input_type:
365
  st.session_state['full_text'] = None
366
  st.session_state['summary'] = None
367
+ st.session_state['last_input_data_key'] = None
368
+ st.session_state['current_input_key'] = None # Reset current key too
369
 
370
+ st.session_state['last_input_type'] = input_type
371
 
372
  # --- Input Elements ---
373
  uploaded_file = None
 
375
  process_button_pressed = False
376
 
377
  if input_type == "Audio File":
378
+ uploaded_file = st.sidebar.file_uploader("Upload audio file (Max 25MB)", type=["mp3", "wav", "m4a", "ogg", "webm"], key="audio_uploader")
379
  if uploaded_file:
380
+ # Use file name and size as the key instead of non-existent .id
381
+ st.session_state['current_input_key'] = f"{uploaded_file.name}-{uploaded_file.size}"
382
  elif input_type == "PDF Document":
383
  uploaded_file = st.sidebar.file_uploader("Upload PDF document", type=["pdf"], key="pdf_uploader")
384
  if uploaded_file:
385
+ # Use file name and size as the key
386
+ st.session_state['current_input_key'] = f"{uploaded_file.name}-{uploaded_file.size}"
387
  elif input_type == "YouTube URL":
388
+ youtube_url = st.sidebar.text_input("Enter YouTube URL", key="youtube_input", placeholder="e.g., https://www.youtube.com/watch?v=...")
389
  if youtube_url:
390
  st.session_state['current_input_key'] = youtube_url # Use URL as key
391
 
392
+ st.sidebar.markdown("---") # Separator
393
+ st.sidebar.markdown("### Steps:")
394
+ st.sidebar.markdown("1. Select input type & provide source.")
395
+ st.sidebar.markdown("2. Click 'Generate Summary & Notes'.")
396
+ st.sidebar.markdown("3. Review results and use chat if needed.")
397
+
398
+
399
  # Single "Generate" button
400
+ if st.sidebar.button("Generate Summary & Notes", key="generate_button", use_container_width=True): # Make button wider
401
+ current_key = st.session_state.get('current_input_key')
402
+ # Check if input is provided for the selected type
403
+ valid_input_provided = False
404
+ if input_type == "Audio File" and uploaded_file:
405
+ valid_input_provided = True
406
+ elif input_type == "PDF Document" and uploaded_file:
407
+ valid_input_provided = True
408
+ elif input_type == "YouTube URL" and youtube_url:
409
+ valid_input_provided = True
410
+
411
+ if valid_input_provided:
412
+ # Check if it's a *new* input compared to the last processed one
413
+ if current_key != st.session_state.get('last_input_data_key'):
414
+ st.session_state['full_text'] = None
415
+ st.session_state['summary'] = None
416
+ st.session_state['last_input_data_key'] = current_key
417
+ process_button_pressed = True
418
+ else:
419
+ # Input hasn't changed, check if results already exist
420
+ if st.session_state.get('full_text') or st.session_state.get('summary'):
421
+ st.info("Results for the current input are already displayed. Upload a new file or URL to generate again.")
422
+ else: # Results don't exist for some reason, re-process
423
+ process_button_pressed = True
424
  else:
425
+ st.warning("Please provide input (upload file or enter URL) before generating.")
 
 
 
 
426
 
427
 
428
  # --- Processing Logic ---
429
  if process_button_pressed:
430
  extracted_text = None
431
+ input_valid = False # Re-check validity just before processing
432
 
433
  if input_type == "Audio File" and uploaded_file:
434
  input_valid = True
 
443
  with st.spinner('Fetching YouTube transcript...'):
444
  extracted_text = get_youtube_transcript(youtube_url)
445
 
446
+ if input_valid and extracted_text is not None:
447
  st.session_state['full_text'] = extracted_text
448
  if extracted_text: # Only summarize if text extraction was successful
449
  with st.spinner('Generating summary...'):
450
  summary_text = generate_summary(extracted_text)
451
  st.session_state['summary'] = summary_text
452
  if not summary_text:
453
+ st.error("Summary generation failed.") # Keep error message if summary is None
454
  else:
455
  st.warning("Text extraction resulted in empty content. Cannot generate summary.")
456
+ st.session_state['summary'] = None
457
  elif input_valid and extracted_text is None:
458
+ # Error already shown in extraction func OR warning shown if text was empty
459
  st.session_state['full_text'] = None
460
  st.session_state['summary'] = None
461
 
462
 
463
  # --- Display Results ---
464
+ # Use columns only if there's something to display to avoid empty columns
465
  if st.session_state.get('full_text') or st.session_state.get('summary'):
466
+ st.markdown("---") # Separator before results
467
+ col1, col2 = st.columns([1, 1])
468
 
469
  with col1:
470
  st.markdown("<h3>Full Text / Transcription</h3>", unsafe_allow_html=True)
471
+ full_text_content = st.session_state.get('full_text')
472
+ if full_text_content:
473
+ display_text = full_text_content
474
+ # Simple truncation for display performance, not affecting summary/chat context
475
+ if len(display_text) > 150000:
476
+ display_text = display_text[:150000] + "\n\n... (Text truncated for display performance)"
477
+ st.text_area("Full Content:", display_text, height=400, key="full_text_area", label_visibility="collapsed")
478
  else:
479
+ # Show placeholder only if generation was attempted but failed/empty
480
+ if st.session_state.get('last_input_data_key') and process_button_pressed: # Check if process was triggered
481
+ st.info("No text extracted or transcribed.")
482
 
483
  with col2:
484
  st.markdown("<h3>Generated Summary</h3>", unsafe_allow_html=True)
485
+ summary_content = st.session_state.get('summary')
486
+ if summary_content:
487
+ st.text_area("Summary:", summary_content, height=400, key="summary_area", label_visibility="collapsed")
 
488
  else:
489
+ # Show placeholder only if generation was attempted but failed/empty
490
+ if st.session_state.get('last_input_data_key') and process_button_pressed:
491
+ st.warning("Summary could not be generated.")
492
 
493
  # --- Chat Section ---
494
  st.markdown("---")
495
  st.markdown("<h3>Chat with AI about the Content</h3>", unsafe_allow_html=True)
496
 
 
497
  context_option = st.radio(
498
  "Use as chat context:",
499
  ('Generated Summary', 'Full Text'),
500
  key='chat_context_option',
501
+ horizontal=True,
502
+ label_visibility="collapsed" # Hide label for radio itself
503
  )
504
 
505
  chat_context = None
 
512
  st.warning("Summary not available for chat context.")
513
  else: # Full Text option
514
  if st.session_state.get('full_text'):
515
+ full_text_for_chat = st.session_state['full_text']
516
+ # Truncate context *before* passing to chat if needed
517
+ # Estimate tokens needed for question + response buffer
518
+ max_context_tokens = MAX_TOKENS_FOR_CHAT_INPUT - 500
519
+ chat_context = truncate_text_by_tokens(full_text_for_chat, max_context_tokens)
520
+
521
+ if len(full_text_for_chat) > len(chat_context):
522
+ context_name = "Full Text (Truncated for Chat)"
523
  else:
524
  context_name = "Full Text"
525
  else:
526
  st.warning("Full text not available for chat context.")
527
 
528
  if chat_context:
529
+ # Display which context is being used subtly
530
+ st.markdown(f"<small style='color: #cccccc;'>Chatting based on: **{context_name}**</small>", unsafe_allow_html=True)
531
+ question = st.text_input("Ask a question:", key="chat_question", placeholder="Ask anything about the selected context...")
532
+ if st.button("Ask AI", key="ask_ai_button", use_container_width=True):
533
  if question:
534
  with st.spinner("AI is thinking..."):
535
  answer = chat_with_ai(question, chat_context)
536
  if answer:
537
  st.markdown("**AI Answer:**")
538
+ # Use markdown for potentially better formatting of AI response
539
+ st.markdown(answer)
540
  else:
541
  st.error("Failed to get an answer from the AI.")
542
  else:
543
  st.warning("Please enter a question first.")
544
  else:
545
+ # Only show message if processing was attempted for current input
546
+ if st.session_state.get('last_input_data_key'):
547
+ st.markdown("_(Generate content or summary first to enable chat)_")
548
+
549
+ # Add footer or instructions if desired
550
+ st.sidebar.markdown("---")
551
+ st.sidebar.info("Powered by OpenAI Whisper & GPT models.")
552
 
553
 
554
  if __name__ == "__main__":