CamiloVega commited on
Commit
e71af4a
Β·
verified Β·
1 Parent(s): 67e305d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +325 -537
app.py CHANGED
@@ -58,25 +58,80 @@ class ModelManager:
58
  logger.info("Initializing ModelManager attributes.")
59
  self.tokenizer = None
60
  self.model = None
61
- self.text_pipeline = None # Renamed for clarity
62
  self.whisper_model = None
63
- self._initialized = True
 
 
64
  self.last_used = time.time()
65
  self.llm_loading = False
66
  self.whisper_loading = False
67
 
68
- @spaces.GPU(duration=120) # Increased duration for potentially long loads
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  def initialize_llm(self):
70
  """Initialize LLM model with standard transformers"""
71
  logger.info("Attempting to initialize LLM.")
72
  if self.llm_loading:
73
  logger.info("LLM initialization already in progress. Skipping.")
74
- return True # Assume it will succeed or fail elsewhere
75
- if self.tokenizer and self.model and self.text_pipeline:
76
  logger.info("LLM already initialized.")
77
  self.last_used = time.time()
78
  return True
79
 
 
 
 
80
  self.llm_loading = True
81
  logger.info("Starting LLM initialization...")
82
  try:
@@ -84,114 +139,90 @@ class ModelManager:
84
  logger.info(f"Using LLM model: {MODEL_NAME}")
85
 
86
  logger.info("Loading LLM tokenizer...")
87
- self.tokenizer = AutoTokenizer.from_pretrained(
88
- MODEL_NAME,
89
- token=HUGGINGFACE_TOKEN,
90
- use_fast=True
91
- )
92
  logger.info("LLM tokenizer loaded.")
93
-
94
  if self.tokenizer.pad_token is None:
95
- logger.info("Setting pad_token to eos_token for LLM tokenizer.")
96
  self.tokenizer.pad_token = self.tokenizer.eos_token
97
 
98
  logger.info("Loading LLM model...")
99
  self.model = AutoModelForCausalLM.from_pretrained(
100
- MODEL_NAME,
101
- token=HUGGINGFACE_TOKEN,
102
- device_map="auto",
103
- torch_dtype=torch.float16,
104
- low_cpu_mem_usage=True,
105
- offload_folder="offload",
106
- offload_state_dict=True
107
  )
108
  logger.info("LLM model loaded.")
109
 
110
  logger.info("Creating LLM text generation pipeline...")
111
  self.text_pipeline = pipeline(
112
- "text-generation",
113
- model=self.model,
114
- tokenizer=self.tokenizer,
115
- torch_dtype=torch.float16,
116
- device_map="auto",
117
- max_length=1024 # Default max length
118
  )
119
  logger.info("LLM text generation pipeline created.")
120
 
121
  logger.info("LLM initialized successfully.")
122
  self.last_used = time.time()
 
123
  self.llm_loading = False
124
  return True
125
 
126
  except Exception as e:
127
  logger.error(f"!!! ERROR during LLM initialization: {str(e)}")
128
- logger.error(traceback.format_exc()) # Log full traceback
129
  logger.error("Resetting potentially partially loaded LLM components due to error.")
130
- self.tokenizer = None
131
- self.model = None
132
- self.text_pipeline = None
133
- if torch.cuda.is_available():
134
- logger.info("Clearing CUDA cache after LLM init error.")
135
- torch.cuda.empty_cache()
136
- gc.collect()
137
  self.llm_loading = False
138
- raise # Re-raise the exception to signal failure
139
 
140
- @spaces.GPU(duration=120) # Increased duration
141
  def initialize_whisper(self):
142
  """Initialize Whisper model for audio transcription"""
143
  logger.info("Attempting to initialize Whisper.")
144
  if self.whisper_loading:
145
  logger.info("Whisper initialization already in progress. Skipping.")
146
  return True
147
- if self.whisper_model:
148
  logger.info("Whisper already initialized.")
149
  self.last_used = time.time()
150
  return True
151
 
 
 
 
152
  self.whisper_loading = True
153
  logger.info("Starting Whisper initialization...")
154
  try:
155
- WHISPER_MODEL_NAME = "tiny" # Consider "base" for better accuracy if "tiny" struggles
156
  logger.info(f"Loading Whisper model: {WHISPER_MODEL_NAME}")
157
- # Specify weights_only=True to address the FutureWarning
158
- # Note: Whisper's load_model might not directly support weights_only yet.
159
- # If it errors, remove the weights_only=True. The warning is mainly informative.
160
- # Let's attempt without weights_only first as whisper might handle it internally
161
  self.whisper_model = whisper.load_model(
162
- WHISPER_MODEL_NAME,
163
- device="cuda" if torch.cuda.is_available() else "cpu",
164
- download_root="/tmp/whisper" # Use persistent storage if available/needed
165
  )
166
  logger.info(f"Whisper model '{WHISPER_MODEL_NAME}' loaded successfully.")
167
  self.last_used = time.time()
 
168
  self.whisper_loading = False
169
  return True
170
  except Exception as e:
171
  logger.error(f"!!! ERROR during Whisper initialization: {str(e)}")
172
  logger.error(traceback.format_exc())
173
  logger.error("Resetting potentially partially loaded Whisper components due to error.")
174
- self.whisper_model = None
175
- if torch.cuda.is_available():
176
- logger.info("Clearing CUDA cache after Whisper init error.")
177
- torch.cuda.empty_cache()
178
- gc.collect()
179
  self.whisper_loading = False
180
  raise
181
 
182
  def check_llm_initialized(self):
183
  """Check if LLM is initialized and initialize if needed"""
184
  logger.info("Checking if LLM is initialized.")
185
- if self.tokenizer is None or self.model is None or self.text_pipeline is None:
186
  logger.info("LLM not initialized, attempting initialization...")
187
- if not self.llm_loading: # Prevent re-entry if already loading
188
  self.initialize_llm() # This will raise error if it fails
189
  logger.info("LLM initialization completed by check_llm_initialized.")
190
  else:
 
191
  logger.info("LLM initialization is already in progress by another request. Waiting briefly.")
192
- # Optional: Wait a bit for the other process to finish
193
- time.sleep(10) # Increased wait time
194
- if self.tokenizer is None or self.model is None or self.text_pipeline is None:
195
  logger.error("LLM initialization timed out or failed after waiting.")
196
  raise RuntimeError("LLM initialization timed out or failed.")
197
  else:
@@ -200,18 +231,19 @@ class ModelManager:
200
  logger.info("LLM was already initialized.")
201
  self.last_used = time.time()
202
 
 
203
  def check_whisper_initialized(self):
204
  """Check if Whisper model is initialized and initialize if needed"""
205
  logger.info("Checking if Whisper is initialized.")
206
- if self.whisper_model is None:
207
  logger.info("Whisper model not initialized, attempting initialization...")
208
- if not self.whisper_loading: # Prevent re-entry
209
  self.initialize_whisper() # This will raise error if it fails
210
  logger.info("Whisper initialization completed by check_whisper_initialized.")
211
  else:
212
  logger.info("Whisper initialization is already in progress by another request. Waiting briefly.")
213
- time.sleep(10) # Increased wait time
214
- if self.whisper_model is None:
215
  logger.error("Whisper initialization timed out or failed after waiting.")
216
  raise RuntimeError("Whisper initialization timed out or failed.")
217
  else:
@@ -221,62 +253,23 @@ class ModelManager:
221
  self.last_used = time.time()
222
 
223
  def reset_models(self, force=False):
224
- """Reset models to free memory if they haven't been used recently"""
225
- current_time = time.time()
226
- should_reset = force or (current_time - self.last_used > 600) # 10 minutes idle threshold
227
- logger.info(f"Checking if models should be reset. Force: {force}, Idle time: {current_time - self.last_used:.0f}s, Should reset: {should_reset}")
228
-
229
- if should_reset:
230
- try:
231
- logger.info("--- Resetting models to free memory ---")
232
-
233
- if hasattr(self, 'model') and self.model is not None:
234
- del self.model
235
- self.model = None
236
- logger.info("LLM model deleted.")
237
- else: logger.info("LLM model was None or not found.")
238
-
239
- if hasattr(self, 'tokenizer') and self.tokenizer is not None:
240
- del self.tokenizer
241
- self.tokenizer = None
242
- logger.info("LLM tokenizer deleted.")
243
- else: logger.info("LLM tokenizer was None or not found.")
244
-
245
- if hasattr(self, 'text_pipeline') and self.text_pipeline is not None:
246
- del self.text_pipeline
247
- self.text_pipeline = None
248
- logger.info("LLM pipeline deleted.")
249
- else: logger.info("LLM pipeline was None or not found.")
250
-
251
- if hasattr(self, 'whisper_model') and self.whisper_model is not None:
252
- del self.whisper_model
253
- self.whisper_model = None
254
- logger.info("Whisper model deleted.")
255
- else: logger.info("Whisper model was None or not found.")
256
-
257
- # Explicitly clear CUDA cache and collect garbage
258
- if torch.cuda.is_available():
259
- logger.info("Clearing CUDA cache...")
260
- torch.cuda.empty_cache()
261
- logger.info("CUDA cache cleared.")
262
- else:
263
- logger.info("CUDA not available, skipping cache clear.")
264
-
265
- logger.info("Running garbage collection...")
266
- collected_count = gc.collect()
267
- logger.info(f"Garbage collected ({collected_count} objects). Models reset successfully.")
268
- self._initialized = False # Mark as uninitialized so they reload on next use
269
-
270
- except Exception as e:
271
- logger.error(f"!!! ERROR during model reset: {str(e)}")
272
- logger.error(traceback.format_exc())
273
- else:
274
- logger.info("Skipping model reset (not forced and not idle long enough).")
275
-
276
-
277
- # Create global model manager instance
278
- logger.info("Creating global ModelManager instance.")
279
- model_manager = ModelManager()
280
 
281
  @lru_cache(maxsize=16) # Reduced cache size slightly
282
  def download_social_media_video(url):
@@ -460,18 +453,22 @@ def transcribe_audio_or_video(file_input):
460
  original_input_path = None
461
  temp_files_to_clean = []
462
  processing_step = "Initialization"
 
463
 
464
  try:
465
  processing_step = "Whisper Model Check"
466
- logger.info("Checking/Initializing Whisper model...")
 
 
 
467
  model_manager.check_whisper_initialized() # Will raise error if fails
468
- logger.info("Whisper model is ready.")
469
 
470
  if file_input is None:
471
  logger.info("No file input provided for transcription. Returning empty string.")
472
- return "" # Return empty string for None input
473
 
474
- # Determine input type and get file path
475
  processing_step = "Input Type Handling"
476
  if isinstance(file_input, str): # Input is a path
477
  original_input_path = file_input
@@ -495,7 +492,6 @@ def transcribe_audio_or_video(file_input):
495
  file_extension = os.path.splitext(input_path)[1].lower()
496
  logger.info(f"File extension: {file_extension}")
497
 
498
- # Check if it's a video file that needs conversion
499
  processing_step = "Video Conversion Check"
500
  if file_extension in ['.mp4', '.avi', '.mov', '.mkv', '.webm']:
501
  logger.info(f"Detected video file ({file_extension}), attempting conversion to audio...")
@@ -510,12 +506,10 @@ def transcribe_audio_or_video(file_input):
510
  logger.error(f"Unsupported file extension for transcription: {file_extension}")
511
  raise ValueError(f"Unsupported file type: {file_extension}")
512
 
513
- # Preprocess the audio (optional)
514
  processing_step = "Audio Preprocessing"
515
  try:
516
  logger.info(f"Attempting to preprocess audio file: {audio_file_to_process}")
517
  preprocessed_audio_path = preprocess_audio(audio_file_to_process)
518
- # If preprocessing creates a new file different from the input, add it to cleanup
519
  if preprocessed_audio_path != audio_file_to_process:
520
  logger.info("Preprocessing created a new file, adding to cleanup list.")
521
  temp_files_to_clean.append(preprocessed_audio_path)
@@ -523,25 +517,22 @@ def transcribe_audio_or_video(file_input):
523
  logger.info(f"Audio preprocessing successful. File to transcribe: {audio_file_to_transcribe}")
524
  except Exception as preprocess_err:
525
  logger.warning(f"Audio preprocessing failed: {preprocess_err}. Using original/converted audio for transcription.")
526
- logger.warning(traceback.format_exc()) # Log warning traceback
527
- audio_file_to_transcribe = audio_file_to_process # Fallback
528
 
529
- processing_step = "Transcription"
530
- logger.info(f"Starting transcription for: {audio_file_to_transcribe}")
531
  if not os.path.exists(audio_file_to_transcribe):
532
  logger.error(f"Audio file to transcribe not found: {audio_file_to_transcribe}")
533
  raise FileNotFoundError(f"Audio file to transcribe not found: {audio_file_to_transcribe}")
534
 
535
- # Perform transcription
536
  logger.info("Calling Whisper model transcribe method...")
537
- with torch.inference_mode(): # Ensure inference mode for efficiency
538
- # Use fp16 if available on CUDA
539
  use_fp16 = torch.cuda.is_available()
540
  logger.info(f"Using fp16 for transcription: {use_fp16}")
 
541
  result = model_manager.whisper_model.transcribe(
542
- audio_file_to_transcribe,
543
- fp16=use_fp16
544
- # language="en" # Optional: specify language if known
545
  )
546
  logger.info("Whisper model transcribe method finished.")
547
  if not result or "text" not in result:
@@ -553,42 +544,46 @@ def transcribe_audio_or_video(file_input):
553
  logger.info(f"Transcription completed successfully: '{log_transcription}'")
554
 
555
  processing_step = "Success"
556
- return transcription
 
 
557
 
 
558
  except FileNotFoundError as e:
559
  logger.error(f"!!! File not found error during transcription (Step: {processing_step}): {e}")
560
  logger.error(traceback.format_exc())
561
- return f"Error: Input file not found ({e})"
562
  except ValueError as e:
563
  logger.error(f"!!! Value error during transcription (Step: {processing_step}): {e}")
564
  logger.error(traceback.format_exc())
565
- return f"Error: Unsupported file type ({e})"
566
  except TypeError as e:
567
  logger.error(f"!!! Type error during transcription setup (Step: {processing_step}): {e}")
568
  logger.error(traceback.format_exc())
569
- return f"Error: Invalid input provided ({e})"
570
  except RuntimeError as e:
571
  logger.error(f"!!! Runtime error during transcription (Step: {processing_step}): {e}")
572
  logger.error(traceback.format_exc())
573
- return f"Error during processing: {e}"
574
  except Exception as e:
575
  logger.error(f"!!! Unexpected error during transcription (Step: {processing_step}): {str(e)}")
576
  logger.error(traceback.format_exc())
577
- return f"Error processing the file: An unexpected error occurred."
578
-
579
  finally:
580
- # Clean up all temporary files created during the process
581
  logger.info(f"--- Cleaning up temporary files for transcription process ({len(temp_files_to_clean)} files) ---")
582
  for temp_file in temp_files_to_clean:
583
  try:
584
  if os.path.exists(temp_file):
585
  os.remove(temp_file)
586
  logger.info(f"Cleaned up temporary file: {temp_file}")
587
- else:
588
- logger.info(f"Temporary file already removed or never created: {temp_file}")
589
  except Exception as e:
590
  logger.warning(f"Could not remove temporary file {temp_file}: {str(e)}")
591
  logger.info("--- Finished transcription process cleanup ---")
 
 
592
 
593
 
594
  @lru_cache(maxsize=16)
@@ -607,6 +602,13 @@ def read_document(document_path):
607
  if file_extension == ".pdf":
608
  logger.info("Reading PDF document using PyMuPDF (fitz)...")
609
  doc = fitz.open(document_path)
 
 
 
 
 
 
 
610
  content = "\n".join([page.get_text() for page in doc])
611
  doc.close()
612
  logger.info(f"PDF read successfully. Length: {len(content)} chars.")
@@ -627,16 +629,26 @@ def read_document(document_path):
627
  logger.info(f"Excel read successfully. Length: {len(content)} chars.")
628
  elif file_extension == ".csv":
629
  logger.info("Reading CSV document using pandas...")
630
- # Try detecting separator
631
  try:
632
  logger.info("Attempting CSV read with comma separator...")
633
- df = pd.read_csv(document_path)
634
- except pd.errors.ParserError:
635
- logger.warning(f"Could not parse CSV {document_path} with comma separator, trying semicolon.")
636
- df = pd.read_csv(document_path, sep=';')
637
- except Exception as csv_err: # Catch other potential pandas errors
638
- logger.error(f"Error reading CSV {document_path}: {csv_err}")
639
- raise
 
 
 
 
 
 
 
 
 
 
 
640
  content = df.to_string()
641
  logger.info(f"CSV read successfully. Length: {len(content)} chars.")
642
  else:
@@ -647,8 +659,11 @@ def read_document(document_path):
647
 
648
  except FileNotFoundError as e:
649
  logger.error(f"!!! File not found error while reading document: {e}")
650
- # logger.error(traceback.format_exc()) # Traceback might be less useful here
651
  return f"Error: Document file not found at {document_path}"
 
 
 
 
652
  except Exception as e:
653
  logger.error(f"!!! Error reading document {document_path}: {str(e)}")
654
  logger.error(traceback.format_exc())
@@ -660,38 +675,41 @@ def read_url(url):
660
  logger.info(f"Attempting to read URL: {url}")
661
  if not url or not url.strip().startswith('http'):
662
  logger.warning(f"Invalid or empty URL provided: '{url}'")
663
- return "" # Return empty for invalid or empty URLs
664
 
665
  try:
666
  headers = {
667
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
 
 
 
668
  }
669
  logger.info(f"Sending GET request to {url} with headers: {headers}")
670
- # Increased timeout
671
  response = requests.get(url, headers=headers, timeout=20, allow_redirects=True)
672
- logger.info(f"Received response from {url}. Status code: {response.status_code}")
673
- response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
674
 
675
- # Check content type - proceed only if likely HTML/text
676
  content_type = response.headers.get('content-type', '').lower()
677
- logger.info(f"URL content type: {content_type}")
678
  if not ('html' in content_type or 'text' in content_type):
679
  logger.warning(f"URL {url} has non-text content type: {content_type}. Skipping.")
680
  return f"Error: URL content type ({content_type}) is not text/html."
681
 
 
 
 
 
 
682
  logger.info(f"Parsing HTML content from {url} using BeautifulSoup...")
683
- soup = BeautifulSoup(response.content, 'html.parser')
684
  logger.info("HTML parsed.")
685
 
686
- # Remove non-content elements like scripts, styles, nav, footers etc.
687
  logger.info("Removing script, style, and other non-content tags...")
688
- tags_to_remove = ["script", "style", "meta", "noscript", "iframe", "header", "footer", "nav", "aside", "form", "button"]
689
  for tag_name in tags_to_remove:
690
  for element in soup.find_all(tag_name):
691
  element.extract()
692
  logger.info("Non-content tags removed.")
693
 
694
- # Attempt to find main content area (common tags/attributes)
695
  logger.info("Attempting to find main content container...")
696
  main_content = (
697
  soup.find("main") or
@@ -710,23 +728,19 @@ def read_url(url):
710
  if body:
711
  logger.info("Extracting text from body.")
712
  text = body.get_text(separator='\n', strip=True)
713
- else: # Very basic fallback
714
  logger.warning(f"No body tag found for {url}. Falling back to all text.")
715
  text = soup.get_text(separator='\n', strip=True)
716
 
717
- # Clean up whitespace: replace multiple newlines/spaces with single ones
718
  logger.info("Cleaning extracted text whitespace...")
719
  lines = [line.strip() for line in text.split('\n') if line.strip()]
720
  cleaned_text = "\n".join(lines)
721
- # cleaned_text = ' '.join(cleaned_text.split()) # Consolidate spaces - might merge paragraphs inappropriately, use newline join instead
722
  logger.info(f"Text cleaning complete. Initial length: {len(text)}, Cleaned length: {len(cleaned_text)}")
723
 
724
-
725
  if not cleaned_text:
726
  logger.warning(f"Could not extract meaningful text from URL: {url}")
727
  return "Error: Could not extract text content from URL."
728
 
729
- # Limit content size to avoid overwhelming the LLM
730
  max_chars = 15000
731
  if len(cleaned_text) > max_chars:
732
  logger.info(f"URL content is long ({len(cleaned_text)} chars), truncating to {max_chars} characters.")
@@ -738,7 +752,6 @@ def read_url(url):
738
  return final_text
739
  except requests.exceptions.RequestException as e:
740
  logger.error(f"!!! Error fetching URL {url}: {str(e)}")
741
- # logger.error(traceback.format_exc()) # Traceback might not be needed for RequestException
742
  return f"Error reading URL: Could not fetch content ({e})"
743
  except Exception as e:
744
  logger.error(f"!!! Error parsing URL {url}: {str(e)}")
@@ -754,7 +767,6 @@ def process_social_media_url(url):
754
 
755
  text_content = None
756
  video_transcription = None
757
- error_occurred = False
758
  temp_audio_file = None
759
 
760
  # 1. Try extracting text content using read_url
@@ -766,13 +778,11 @@ def process_social_media_url(url):
766
  logger.info(f"Successfully read text content from {url}. Length: {len(text_content)}")
767
  elif text_content_result:
768
  logger.warning(f"read_url returned an error for {url}: {text_content_result}")
769
- error_occurred = True # Mark as error but continue
770
  else:
771
  logger.info(f"No text content extracted by read_url for {url}.")
772
  except Exception as e:
773
  logger.error(f"!!! Exception during text content extraction from social URL {url}: {e}")
774
  logger.error(traceback.format_exc())
775
- error_occurred = True
776
 
777
  # 2. Try downloading and transcribing potential video/audio content
778
  logger.info(f"Attempting to download audio/video content from social URL: {url}")
@@ -780,14 +790,12 @@ def process_social_media_url(url):
780
  temp_audio_file = download_social_media_video(url) # Returns path or None
781
  if temp_audio_file:
782
  logger.info(f"Audio downloaded from {url} to {temp_audio_file}. Proceeding to transcription.")
783
- # Transcribe the downloaded audio file
784
  transcription_result = transcribe_audio_or_video(temp_audio_file) # Handles errors internally
785
  if transcription_result and not transcription_result.startswith("Error"):
786
  video_transcription = transcription_result
787
  logger.info(f"Successfully transcribed audio from {url}. Length: {len(video_transcription)}")
788
  elif transcription_result:
789
  logger.warning(f"Transcription returned an error for audio from {url}: {transcription_result}")
790
- error_occurred = True # Mark as error but maybe text content worked
791
  else:
792
  logger.warning(f"Transcription returned empty result for audio from {url}.")
793
  else:
@@ -795,7 +803,6 @@ def process_social_media_url(url):
795
  except Exception as e:
796
  logger.error(f"!!! Exception during video/audio processing for social URL {url}: {e}")
797
  logger.error(traceback.format_exc())
798
- error_occurred = True
799
  finally:
800
  # Clean up downloaded file if it exists
801
  if temp_audio_file and os.path.exists(temp_audio_file):
@@ -808,11 +815,16 @@ def process_social_media_url(url):
808
 
809
  # Return results
810
  logger.info(f"--- Finished processing social media URL: {url} ---")
811
- # Return dict even if empty, let caller decide if it's useful
812
- return {
813
- "text": text_content or "", # Ensure string type
814
- "video": video_transcription or "" # Ensure string type
815
- }
 
 
 
 
 
816
 
817
 
818
  @spaces.GPU(duration=300) # Allow more time for generation
@@ -825,6 +837,7 @@ def generate_news(instructions, facts, size, tone, *args):
825
 
826
  try:
827
  # --- Parameter Logging & Basic Validation ---
 
828
  logger.info(f"Received Instructions: {'Yes' if instructions else 'No'}")
829
  logger.info(f"Received Facts: {'Yes' if facts else 'No'}")
830
  logger.info(f"Requested Size: {size}, Tone: {tone}")
@@ -836,8 +849,8 @@ def generate_news(instructions, facts, size, tone, *args):
836
  size = 250
837
  logger.info(f"Using Size: {size}")
838
 
839
-
840
  # --- Argument Parsing ---
 
841
  logger.info("Parsing dynamic arguments...")
842
  num_docs = 5
843
  num_audio_sources = 5
@@ -855,7 +868,6 @@ def generate_news(instructions, facts, size, tone, *args):
855
  logger.warning(f"Received more arguments ({len(args_list)}) than expected ({total_expected_args}). Truncating.")
856
  args_list = args_list[:total_expected_args]
857
 
858
- # Slice arguments based on the expected order
859
  doc_files = args_list[0:num_docs]
860
  audio_inputs_flat = args_list[num_docs : num_docs + (num_audio_sources * num_audio_inputs_per_source)]
861
  url_inputs = args_list[num_docs + (num_audio_sources * num_audio_inputs_per_source) : num_docs + (num_audio_sources * num_audio_inputs_per_source) + num_urls]
@@ -865,14 +877,12 @@ def generate_news(instructions, facts, size, tone, *args):
865
  knowledge_base = {
866
  "instructions": instructions or "No specific instructions provided.",
867
  "facts": facts or "No specific facts provided.",
868
- "document_content": [],
869
- "audio_data": [], # Will store dicts: {file_path, name, position, original_filename}
870
- "url_content": [],
871
- "social_content": [] # Will store dicts from process_social_media_url
872
  }
873
 
874
-
875
- # --- Process Document Inputs ---
 
876
  logger.info("--- Processing document inputs ---")
877
  doc_counter = 0
878
  for i, doc_file in enumerate(doc_files):
@@ -880,31 +890,26 @@ def generate_news(instructions, facts, size, tone, *args):
880
  doc_filename = os.path.basename(doc_file.name)
881
  logger.info(f"Attempting to read document {i+1}: {doc_filename} (Path: {doc_file.name})")
882
  try:
883
- content = read_document(doc_file.name) # doc_file.name is the temp path
884
  if content and content.startswith("Error:"):
885
  logger.warning(f"Skipping document {i+1} ({doc_filename}) due to read error: {content}")
886
  raw_transcriptions += f"[Document {i+1}: {doc_filename}] Error reading: {content}\n\n"
887
  elif content:
888
  doc_excerpt = (content[:1000] + "... [document truncated]") if len(content) > 1000 else content
889
  knowledge_base["document_content"].append(f"[Document {i+1} Source: {doc_filename}]\n{doc_excerpt}")
890
- logger.info(f"Successfully processed document {i+1}. Added excerpt to knowledge base.")
891
  doc_counter += 1
892
- # Add full content to raw_transcriptions log? Might be too verbose.
893
- # raw_transcriptions += f"[Document {i+1}: {doc_filename}]\n{content}\n\n"
894
  else:
895
- logger.warning(f"Skipping document {i+1} ({doc_filename}) because content is empty after reading.")
896
  raw_transcriptions += f"[Document {i+1}: {doc_filename}] Read successfully but content is empty.\n\n"
897
  except Exception as e:
898
  logger.error(f"!!! FAILED to process document {i+1} ({doc_filename}): {e}")
899
  logger.error(traceback.format_exc())
900
  raw_transcriptions += f"[Document {i+1}: {doc_filename}] CRITICAL Error during processing: {e}\n\n"
901
- else:
902
- logger.info(f"Skipping document slot {i+1}: No file provided or invalid file object.")
903
- logger.info(f"--- Finished processing document inputs. {doc_counter} documents added. ---")
904
- # Gradio handles cleanup of the uploaded temp file doc_file.name
905
-
906
 
907
- # --- Process URL Inputs ---
908
  logger.info("--- Processing URL inputs ---")
909
  url_counter = 0
910
  for i, url in enumerate(url_inputs):
@@ -916,59 +921,42 @@ def generate_news(instructions, facts, size, tone, *args):
916
  logger.warning(f"Skipping URL {i+1} ({url}) due to read error: {content}")
917
  raw_transcriptions += f"[URL {i+1}: {url}] Error reading: {content}\n\n"
918
  elif content:
919
- # Content is already truncated in read_url if needed
920
  knowledge_base["url_content"].append(f"[URL {i+1} Source: {url}]\n{content}")
921
- logger.info(f"Successfully processed URL {i+1}. Added content to knowledge base.")
922
  url_counter += 1
923
  else:
924
- logger.warning(f"Skipping URL {i+1} ({url}) because content is empty after reading.")
925
  raw_transcriptions += f"[URL {i+1}: {url}] Read successfully but content is empty.\n\n"
926
  except Exception as e:
927
  logger.error(f"!!! FAILED to process URL {i+1} ({url}): {e}")
928
  logger.error(traceback.format_exc())
929
  raw_transcriptions += f"[URL {i+1}: {url}] CRITICAL Error during processing: {e}\n\n"
930
- elif url and isinstance(url, str) and url.strip():
931
- logger.warning(f"Skipping URL slot {i+1}: Input '{url}' is not a valid HTTP/HTTPS URL.")
932
- else:
933
- logger.info(f"Skipping URL slot {i+1}: No URL provided.")
934
- logger.info(f"--- Finished processing URL inputs. {url_counter} URLs added. ---")
935
 
936
-
937
- # --- Process Audio/Video Inputs ---
938
  logger.info("--- Processing audio/video inputs (collecting info) ---")
939
  has_audio_source = False
940
  audio_counter = 0
941
  for i in range(num_audio_sources):
942
  start_idx = i * num_audio_inputs_per_source
943
- # Check if indices are valid before accessing
944
  if start_idx + 2 < len(audio_inputs_flat):
945
  audio_file = audio_inputs_flat[start_idx]
946
  name = audio_inputs_flat[start_idx + 1] or f"Unnamed Audio Source {i+1}"
947
  position = audio_inputs_flat[start_idx + 2] or "Role N/A"
948
-
949
  if audio_file and hasattr(audio_file, 'name') and audio_file.name:
950
  audio_filename = os.path.basename(audio_file.name)
951
  logger.info(f"Found audio/video source {i+1}: {name} ({position}) - File: {audio_filename} (Path: {audio_file.name})")
952
- # Store info for transcription later
953
- knowledge_base["audio_data"].append({
954
- "file_path": audio_file.name, # Use the temp path
955
- "name": name,
956
- "position": position,
957
- "original_filename": audio_filename
958
- })
959
  has_audio_source = True
960
  audio_counter += 1
961
- else:
962
- logger.info(f"Skipping audio source slot {i+1}: No file provided or invalid file object.")
963
- else:
964
- logger.warning(f"Index out of bounds when processing audio source {i+1}. Check argument parsing logic.")
965
- break # Stop processing further audio if indexing is wrong
966
- logger.info(f"--- Finished collecting audio/video input info. {audio_counter} sources found. Transcription needed: {has_audio_source} ---")
967
 
968
-
969
- # --- Process Social Media Inputs ---
970
  logger.info("--- Processing social media inputs ---")
971
- has_social_source = False
972
  social_counter = 0
973
  for i in range(num_social_sources):
974
  start_idx = i * num_social_inputs_per_source
@@ -976,168 +964,93 @@ def generate_news(instructions, facts, size, tone, *args):
976
  social_url = social_inputs_flat[start_idx]
977
  social_name = social_inputs_flat[start_idx + 1] or f"Unnamed Social Source {i+1}"
978
  social_context = social_inputs_flat[start_idx + 2] or "Context N/A"
979
-
980
  if social_url and isinstance(social_url, str) and social_url.strip().startswith('http'):
981
  logger.info(f"Attempting to process social media URL {i+1}: {social_url} ({social_name}, {social_context})")
982
  try:
983
- social_data = process_social_media_url(social_url) # Returns dict or None
984
- if social_data and (social_data.get("text") or social_data.get("video")):
985
- logger.info(f"Successfully processed social URL {i+1}. Text found: {bool(social_data.get('text'))}, Video transcription found: {bool(social_data.get('video'))}")
986
- knowledge_base["social_content"].append({
987
- "url": social_url,
988
- "name": social_name,
989
- "context": social_context,
990
- "text": social_data.get("text", ""),
991
- "video_transcription": social_data.get("video", "") # Store potential transcription
992
- })
993
- has_social_source = True # Mark even if only text is found
994
- social_counter += 1
995
- elif social_data:
996
- logger.warning(f"Processed social URL {i+1} ({social_url}) but found no text or video content.")
997
- raw_transcriptions += f"[Social Media {i+1}: {social_url} ({social_name})] Processed but no content found.\n\n"
998
- else:
999
- # process_social_media_url returning None implies an error occurred during processing
1000
- logger.error(f"Processing failed for social URL {i+1} ({social_url}). See previous logs.")
1001
- raw_transcriptions += f"[Social Media {i+1}: {social_url} ({social_name})] Error during processing.\n\n"
1002
  except Exception as e:
1003
  logger.error(f"!!! FAILED to process social URL {i+1} ({social_url}): {e}")
1004
  logger.error(traceback.format_exc())
1005
  raw_transcriptions += f"[Social Media {i+1}: {social_url} ({social_name})] CRITICAL Error during processing: {e}\n\n"
1006
- elif social_url and isinstance(social_url, str) and social_url.strip():
1007
- logger.warning(f"Skipping social media slot {i+1}: Input '{social_url}' is not a valid HTTP/HTTPS URL.")
1008
- else:
1009
- logger.info(f"Skipping social media slot {i+1}: No URL provided.")
1010
- else:
1011
- logger.warning(f"Index out of bounds when processing social source {i+1}. Check argument parsing logic.")
1012
- break
1013
- logger.info(f"--- Finished processing social media inputs. {social_counter} sources added. ---")
1014
 
1015
 
1016
  # --- Transcribe Audio/Video (Conditional) ---
1017
  transcriptions_for_prompt = ""
1018
  if has_audio_source:
1019
  logger.info("--- Starting Audio Transcription Phase ---")
1020
- try:
1021
- # Ensure Whisper is ready (check_whisper_initialized raises error if fails)
1022
- logger.info("Ensuring Whisper model is initialized for transcription...")
1023
- model_manager.check_whisper_initialized()
1024
- logger.info("Whisper model confirmed ready.")
1025
-
1026
- for idx, data in enumerate(knowledge_base["audio_data"]):
1027
- audio_filename = data['original_filename']
1028
- logger.info(f"Attempting transcription for audio source {idx+1}: {audio_filename} ({data['name']}, {data['position']})")
1029
- try:
1030
- # Call the robust transcription function
1031
- transcription = transcribe_audio_or_video(data["file_path"])
1032
- if transcription and not transcription.startswith("Error"):
1033
- logger.info(f"Transcription successful for audio {idx+1}. Length: {len(transcription)}")
1034
- quote = f'"{transcription}" - {data["name"]}, {data["position"]}'
1035
- transcriptions_for_prompt += f"{quote}\n\n"
1036
- raw_transcriptions += f'[Audio/Video {idx + 1}: {audio_filename} ({data["name"]}, {data["position"]})]\n"{transcription}"\n\n'
1037
- elif transcription:
1038
- logger.warning(f"Transcription failed or returned error for audio source {idx+1} ({audio_filename}): {transcription}")
1039
- raw_transcriptions += f'[Audio/Video {idx + 1}: {audio_filename} ({data["name"]}, {data["position"]})]\n[Error during transcription: {transcription}]\n\n'
1040
- else:
1041
- logger.warning(f"Transcription returned empty result for audio source {idx+1} ({audio_filename}).")
1042
- raw_transcriptions += f'[Audio/Video {idx + 1}: {audio_filename} ({data["name"]}, {data["position"]})]\n[Transcription result was empty.]\n\n'
1043
- except Exception as e:
1044
- logger.error(f"!!! CRITICAL Error during transcription call for audio source {idx+1} ({audio_filename}): {e}")
1045
- logger.error(traceback.format_exc())
1046
- raw_transcriptions += f'[Audio/Video {idx + 1}: {audio_filename} ({data["name"]}, {data["position"]})]\n[CRITICAL Error during transcription: {e}]\n\n'
1047
- # Gradio handles cleanup of the uploaded temp file audio_file.name based on the path stored
1048
-
1049
- except Exception as whisper_init_err:
1050
- # This catches errors from check_whisper_initialized if it failed
1051
- logger.error(f"!!! FATAL: Whisper model could not be initialized. Skipping all audio transcriptions.")
1052
- logger.error(traceback.format_exc())
1053
- raw_transcriptions += f"\n\n[CRITICAL ERROR] Whisper model failed to load. Audio sources could not be transcribed: {whisper_init_err}\n\n"
1054
- # Decide whether to continue without audio or return error immediately
1055
- # For now, we continue and log the error.
1056
-
1057
  logger.info("--- Finished Audio Transcription Phase ---")
1058
  else:
1059
  logger.info("--- Skipping Audio Transcription Phase (no audio sources found) ---")
1060
 
1061
 
1062
  # --- Add Social Media Content to Prompt Data ---
 
1063
  logger.info("--- Adding social media content to prompt data ---")
1064
  social_content_added_to_prompt = False
1065
  for idx, data in enumerate(knowledge_base["social_content"]):
1066
  source_id_log = f'[Social Media {idx+1}: {data["url"]} ({data["name"]}, {data["context"]})]'
1067
  source_id_prompt = f'Social Media Post ({data["name"]}, {data["context"]} at {data["url"]}):'
1068
  content_added_this_source = False
1069
-
1070
- # Add text content if available
1071
  if data["text"]:
1072
  text_excerpt = (data["text"][:500] + "...[text truncated]") if len(data["text"]) > 500 else data["text"]
1073
  social_text_prompt = f'{source_id_prompt}\nText Content:\n"{text_excerpt}"\n\n'
1074
  transcriptions_for_prompt += social_text_prompt
1075
- raw_transcriptions += f"{source_id_log}\nText Content:\n{data['text']}\n\n" # Log full text
1076
- logger.info(f"Added text excerpt from social source {idx+1} to prompt data.")
1077
- content_added_this_source = True
1078
- social_content_added_to_prompt = True
1079
-
1080
- # Add video transcription if available
1081
  if data["video_transcription"]:
1082
  social_video_prompt = f'{source_id_prompt}\nVideo Transcription:\n"{data["video_transcription"]}"\n\n'
1083
  transcriptions_for_prompt += social_video_prompt
1084
  raw_transcriptions += f"{source_id_log}\nVideo Transcription:\n{data['video_transcription']}\n\n"
1085
- logger.info(f"Added video transcription from social source {idx+1} to prompt data.")
1086
- content_added_this_source = True
1087
- social_content_added_to_prompt = True
1088
-
1089
- if not content_added_this_source:
1090
- logger.info(f"No usable text or video transcription found for social source {idx+1} ({data['url']}).")
1091
- # No need to add error to raw_transcriptions here, lack of content is logged earlier
1092
-
1093
- if not social_content_added_to_prompt:
1094
- logger.info("No content from social media sources was added to the prompt data.")
1095
- logger.info("--- Finished adding social media content to prompt data ---")
1096
 
1097
 
1098
  # --- Prepare Final Prompt ---
 
1099
  logger.info("--- Preparing final prompt for LLM ---")
1100
  document_summary = "\n\n".join(knowledge_base["document_content"]) if knowledge_base["document_content"] else "No document content provided or processed successfully."
1101
  url_summary = "\n\n".join(knowledge_base["url_content"]) if knowledge_base["url_content"] else "No URL content provided or processed successfully."
1102
  transcription_summary = transcriptions_for_prompt if transcriptions_for_prompt else "No usable transcriptions or social media content available."
1103
-
1104
- # Construct the prompt for the LLM
1105
- prompt = f"""<s>[INST] You are a professional news writer. Your task is to synthesize information from various sources into a coherent news article.
1106
-
1107
- Primary Instructions: {knowledge_base["instructions"]}
1108
- Key Facts to Include: {knowledge_base["facts"]}
1109
-
1110
- Supporting Information:
1111
-
1112
- Document Content Summary:
1113
- {document_summary}
1114
-
1115
- Web Content Summary (from URLs):
1116
- {url_summary}
1117
-
1118
- Transcribed Quotes/Content (Use these directly or indirectly):
1119
- {transcription_summary}
1120
-
1121
- Article Requirements:
1122
- - Title: Create a concise and informative title for the article.
1123
- - Hook: Write a compelling 15-word (approx.) hook sentence that complements the title.
1124
- - Body: Write the main news article body, aiming for approximately {size} words.
1125
- - Tone: Adopt a {tone} tone throughout the article.
1126
- - 5 Ws: Ensure the first paragraph addresses the core questions (Who, What, When, Where, Why).
1127
- - Quotes: Incorporate relevant information from the 'Transcribed Quotes/Content' section. Aim to use quotes where appropriate, but synthesize information rather than just listing quotes. Use quotation marks (" ") for direct quotes attributed correctly (e.g., based on name/position provided).
1128
- - Style: Adhere to a professional journalistic style. Be objective and factual.
1129
- - Accuracy: Do NOT invent information. Stick strictly to the provided facts, instructions, and source materials. If information is contradictory or missing, state that or omit the detail.
1130
- - Structure: Organize the article logically with clear paragraphs.
1131
-
1132
- Begin the article now. [/INST]
1133
- Article Draft:
1134
- """
1135
-
1136
- # Log prompt length details
1137
- prompt_words = len(prompt.split())
1138
- prompt_chars = len(prompt)
1139
  logger.info(f"Generated prompt length: {prompt_words} words / {prompt_chars} characters.")
1140
- # Log first/last few chars for verification, avoid logging full potentially huge prompt
1141
  logger.debug(f"Prompt Start: {prompt[:200]}...")
1142
  logger.debug(f"...Prompt End: {prompt[-200:]}")
1143
  logger.info("--- Finished preparing final prompt ---")
@@ -1147,11 +1060,14 @@ Article Draft:
1147
  logger.info("--- Starting LLM Generation Phase ---")
1148
  generation_start_time = time.time()
1149
 
1150
- # Ensure LLM is ready
1151
  logger.info("Ensuring LLM is initialized for generation...")
1152
  try:
 
 
 
1153
  model_manager.check_llm_initialized() # Raises error if fails
1154
- logger.info("LLM confirmed ready.")
1155
  except Exception as llm_init_err:
1156
  logger.error(f"!!! FATAL: LLM could not be initialized. Cannot generate article.")
1157
  logger.error(traceback.format_exc())
@@ -1159,29 +1075,24 @@ Article Draft:
1159
 
1160
 
1161
  # Estimate max_new_tokens
 
1162
  estimated_tokens_per_word = 1.5
1163
- max_new_tokens = int(size * estimated_tokens_per_word + 150) # size words + buffer for title/hook/etc.
1164
- model_max_length = 2048 # Check model card if different
1165
- # Simple length check for prompt tokens (more accurate requires tokenizer)
1166
- prompt_tokens_estimate = prompt_chars // 3 # Very rough estimate
1167
- available_tokens = model_max_length - prompt_tokens_estimate - 50 # Leave buffer
1168
  max_new_tokens = min(max_new_tokens, available_tokens)
1169
- max_new_tokens = max(max_new_tokens, 100) # Ensure at least minimum generation length
1170
-
1171
  logger.info(f"Estimated prompt tokens: ~{prompt_tokens_estimate}. Model max length: {model_max_length}. Requesting max_new_tokens: {max_new_tokens}")
1172
 
1173
  try:
 
 
1174
  logger.info("Calling LLM text generation pipeline...")
1175
  outputs = model_manager.text_pipeline(
1176
- prompt,
1177
- max_new_tokens=max_new_tokens,
1178
- do_sample=True,
1179
- temperature=0.7,
1180
- top_p=0.95,
1181
- top_k=50,
1182
- repetition_penalty=1.15,
1183
- pad_token_id=model_manager.tokenizer.eos_token_id,
1184
- num_return_sequences=1
1185
  )
1186
  logger.info("LLM pipeline call finished.")
1187
 
@@ -1189,40 +1100,39 @@ Article Draft:
1189
  logger.error("LLM pipeline returned invalid or empty output.")
1190
  raise RuntimeError("LLM generation failed: Pipeline returned empty or invalid output.")
1191
 
1192
- # Extract generated text
1193
  full_generated_text = outputs[0]['generated_text']
1194
  logger.info(f"Raw generated text length: {len(full_generated_text)} chars.")
1195
- # logger.debug(f"Raw LLM Output:\n{full_generated_text}") # Careful logging full output
1196
 
1197
- # Clean up the result by removing the prompt
 
1198
  logger.info("Cleaning LLM output (removing prompt)...")
1199
  inst_marker = "[/INST]"
1200
  marker_pos = full_generated_text.find(inst_marker)
1201
  if marker_pos != -1:
1202
  generated_article = full_generated_text[marker_pos + len(inst_marker):].strip()
1203
- # Further clean potentially leading "Article Draft:" if model included it
1204
  if generated_article.startswith("Article Draft:"):
1205
  generated_article = generated_article[len("Article Draft:"):].strip()
1206
  logger.info("Prompt removed successfully using '[/INST]' marker.")
1207
  else:
1208
- logger.warning("Prompt marker '[/INST]' not found in LLM output. Attempting fallback cleaning.")
1209
- # Fallback: Try removing the input prompt string itself (less reliable for long prompts)
1210
- # This is risky and might remove actual generated content if prompt is somehow repeated.
1211
- # Let's just return the full output with a warning if marker not found.
1212
- generated_article = full_generated_text # Keep full output
1213
- logger.warning("Could not reliably remove prompt. Returning full generated text.")
1214
 
1215
  generation_time = time.time() - generation_start_time
1216
  logger.info(f"News generation completed in {generation_time:.2f} seconds.")
1217
  logger.info(f"Final article length: {len(generated_article)} characters.")
1218
  logger.info("--- Finished LLM Generation Phase ---")
 
 
 
1219
 
 
1220
  except torch.cuda.OutOfMemoryError as oom_error:
1221
  logger.error(f"!!! CUDA Out of Memory error during LLM generation: {oom_error}")
1222
  logger.error(traceback.format_exc())
1223
  logger.info("Attempting to reset models after OOM error...")
1224
- model_manager.reset_models(force=True) # Attempt to recover
1225
- raise RuntimeError("Generation failed due to insufficient GPU memory. Please try reducing article size or complexity.") from oom_error
1226
  except Exception as gen_error:
1227
  logger.error(f"!!! Error during text generation pipeline: {str(gen_error)}")
1228
  logger.error(traceback.format_exc())
@@ -1230,79 +1140,53 @@ Article Draft:
1230
 
1231
  total_time = time.time() - request_start_time
1232
  logger.info(f"--- generate_news function completed successfully in {total_time:.2f} seconds. ---")
1233
-
1234
- # Return the generated article and the log of raw transcriptions
1235
  return generated_article.strip(), raw_transcriptions.strip()
1236
 
1237
  except Exception as e:
1238
  # Catch-all for any unexpected error during the entire generate_news flow
 
1239
  total_time = time.time() - request_start_time
1240
  logger.error(f"!!! UNHANDLED Error in generate_news function after {total_time:.2f} seconds: {str(e)}")
1241
  logger.error(traceback.format_exc())
1242
- # Attempt to reset models to recover state if possible
1243
  try:
1244
  logger.info("Attempting model reset due to unhandled error in generate_news.")
1245
  model_manager.reset_models(force=True)
1246
  except Exception as reset_error:
1247
  logger.error(f"Failed to reset models after error: {str(reset_error)}")
1248
- # Return error messages to the UI
1249
  error_message = f"Error generating the news article: An unexpected error occurred. Please check logs. ({str(e)})"
1250
  transcription_log = raw_transcriptions.strip() + f"\n\n[CRITICAL ERROR] News generation failed unexpectedly: {str(e)}"
1251
  return error_message, transcription_log
1252
  finally:
1253
- # Optional: Log resource usage here if possible/needed
1254
  logger.info("--- generate_news function finished execution (either success or error) ---")
 
 
 
1255
 
1256
 
 
1257
  def create_demo():
1258
  """Creates the Gradio interface"""
1259
  logger.info("--- Creating Gradio interface ---")
1260
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
1261
  gr.Markdown("# πŸ“° NewsIA - AI News Generator")
1262
  gr.Markdown("Create professional news articles from multiple information sources.")
1263
-
1264
- # Store all input components for easy access/reset
1265
  all_inputs = []
1266
-
1267
  with gr.Row():
1268
  with gr.Column(scale=2):
1269
  logger.info("Creating instruction input.")
1270
- instructions = gr.Textbox(
1271
- label="Instructions for the News Article",
1272
- placeholder="Enter specific instructions for generating your news article (e.g., focus on the economic impact)",
1273
- lines=2,
1274
- value=""
1275
- )
1276
  all_inputs.append(instructions)
1277
-
1278
  logger.info("Creating facts input.")
1279
- facts = gr.Textbox(
1280
- label="Main Facts",
1281
- placeholder="Describe the most important facts the news should include (e.g., Event name, date, location, key people involved)",
1282
- lines=4,
1283
- value=""
1284
- )
1285
  all_inputs.append(facts)
1286
-
1287
  with gr.Row():
1288
  logger.info("Creating size slider.")
1289
- size_slider = gr.Slider(
1290
- label="Approximate Length (words)",
1291
- minimum=100,
1292
- maximum=700, # Increased max size
1293
- value=250,
1294
- step=50
1295
- )
1296
  all_inputs.append(size_slider)
1297
-
1298
  logger.info("Creating tone dropdown.")
1299
- tone_dropdown = gr.Dropdown(
1300
- label="Tone of the News Article",
1301
- choices=["neutral", "serious", "formal", "urgent", "investigative", "human-interest", "lighthearted"],
1302
- value="neutral"
1303
- )
1304
  all_inputs.append(tone_dropdown)
1305
-
1306
  with gr.Column(scale=3):
1307
  with gr.Tabs():
1308
  with gr.TabItem("πŸ“ Documents"):
@@ -1310,201 +1194,105 @@ def create_demo():
1310
  gr.Markdown("Upload relevant documents (PDF, DOCX, XLSX, CSV). Max 5.")
1311
  doc_inputs = []
1312
  for i in range(1, 6):
1313
- doc_file = gr.File(
1314
- label=f"Document {i}",
1315
- file_types=["pdf", ".docx", ".xlsx", ".csv"], # Explicit extensions for clarity
1316
- file_count="single" # Ensure single file per component
1317
- )
1318
  doc_inputs.append(doc_file)
1319
  all_inputs.extend(doc_inputs)
1320
  logger.info(f"{len(doc_inputs)} document inputs created.")
1321
-
1322
  with gr.TabItem("πŸ”Š Audio/Video"):
1323
  logger.info("Creating audio/video input tabs.")
1324
- gr.Markdown("Upload audio or video files for transcription (MP3, WAV, MP4, MOV, etc.). Max 5 sources.")
1325
  audio_video_inputs = []
1326
  for i in range(1, 6):
1327
  with gr.Group():
1328
  gr.Markdown(f"**Source {i}**")
1329
- audio_file = gr.File(
1330
- label=f"Audio/Video File {i}",
1331
- file_types=["audio", "video"]
1332
- )
1333
  with gr.Row():
1334
- speaker_name = gr.Textbox(
1335
- label="Speaker Name",
1336
- placeholder="Name of the interviewee or speaker",
1337
- value=""
1338
- )
1339
- speaker_role = gr.Textbox(
1340
- label="Role/Position",
1341
- placeholder="Speaker's title or role",
1342
- value=""
1343
- )
1344
- audio_video_inputs.append(audio_file)
1345
- audio_video_inputs.append(speaker_name)
1346
- audio_video_inputs.append(speaker_role)
1347
  all_inputs.extend(audio_video_inputs)
1348
- logger.info(f"{len(audio_video_inputs)} audio/video inputs created (file + 2 textboxes per source).")
1349
-
1350
-
1351
  with gr.TabItem("🌐 URLs"):
1352
  logger.info("Creating URL input tabs.")
1353
- gr.Markdown("Add URLs to relevant web pages or articles. Max 5.")
1354
  url_inputs = []
1355
  for i in range(1, 6):
1356
- url_textbox = gr.Textbox(
1357
- label=f"URL {i}",
1358
- placeholder="https://example.com/article",
1359
- value=""
1360
- )
1361
  url_inputs.append(url_textbox)
1362
  all_inputs.extend(url_inputs)
1363
  logger.info(f"{len(url_inputs)} URL inputs created.")
1364
-
1365
  with gr.TabItem("πŸ“± Social Media"):
1366
  logger.info("Creating social media input tabs.")
1367
- gr.Markdown("Add URLs to social media posts (e.g., Twitter, YouTube, TikTok). Max 3.")
1368
  social_inputs = []
1369
  for i in range(1, 4):
1370
  with gr.Group():
1371
  gr.Markdown(f"**Social Media Source {i}**")
1372
- social_url_textbox = gr.Textbox(
1373
- label=f"Post URL",
1374
- placeholder="https://twitter.com/user/status/...",
1375
- value=""
1376
- )
1377
  with gr.Row():
1378
- social_name_textbox = gr.Textbox(
1379
- label=f"Account Name/User",
1380
- placeholder="Name or handle (e.g., @username)",
1381
- value=""
1382
- )
1383
- social_context_textbox = gr.Textbox(
1384
- label=f"Context",
1385
- placeholder="Brief context (e.g., statement on event X)",
1386
- value=""
1387
- )
1388
- social_inputs.append(social_url_textbox)
1389
- social_inputs.append(social_name_textbox)
1390
- social_inputs.append(social_context_textbox)
1391
  all_inputs.extend(social_inputs)
1392
- logger.info(f"{len(social_inputs)} social media inputs created (URL + 2 textboxes per source).")
1393
-
1394
 
1395
  logger.info(f"Total number of input components collected: {len(all_inputs)}")
1396
-
1397
  with gr.Row():
1398
  logger.info("Creating generate and clear buttons.")
1399
  generate_button = gr.Button("✨ Generate News Article", variant="primary")
1400
  clear_button = gr.Button("πŸ”„ Clear All Inputs")
1401
-
1402
  with gr.Tabs():
1403
  with gr.TabItem("πŸ“„ Generated News Article"):
1404
  logger.info("Creating news output textbox.")
1405
- news_output = gr.Textbox(
1406
- label="Draft News Article",
1407
- lines=20, # Increased lines
1408
- show_copy_button=True,
1409
- value="",
1410
- interactive=False # Make non-editable initially
1411
- )
1412
  with gr.TabItem("πŸŽ™οΈ Source Transcriptions & Logs"):
1413
  logger.info("Creating transcriptions/log output textbox.")
1414
- transcriptions_output = gr.Textbox(
1415
- label="Transcriptions and Processing Log",
1416
- lines=15, # Increased lines
1417
- show_copy_button=True,
1418
- value="",
1419
- interactive=False # Make non-editable initially
1420
- )
1421
-
1422
- # --- Event Handlers ---
1423
  outputs_list = [news_output, transcriptions_output]
1424
  logger.info("Setting up event handlers.")
1425
-
1426
- # Generate button click
1427
- generate_button.click(
1428
- fn=generate_news,
1429
- inputs=all_inputs, # Pass the consolidated list
1430
- outputs=outputs_list
1431
- )
1432
  logger.info("Generate button click handler set.")
1433
 
1434
- # Clear button click
1435
  def clear_all_inputs_and_outputs():
1436
  logger.info("--- Clear All button clicked ---")
1437
  reset_values = []
1438
- # Generate default values based on input component types
1439
  for input_comp in all_inputs:
1440
- if isinstance(input_comp, (gr.Textbox, gr.Dropdown)):
1441
- reset_values.append("")
1442
- elif isinstance(input_comp, gr.Slider):
1443
- reset_values.append(250) # Reset slider to default
1444
- elif isinstance(input_comp, gr.File):
1445
- reset_values.append(None)
1446
- else:
1447
- logger.warning(f"Unhandled input type for reset: {type(input_comp)}. Resetting to None.")
1448
- reset_values.append(None)
1449
-
1450
- # Add default values for the output fields (empty strings for textboxes)
1451
  reset_values.extend(["", ""])
1452
  logger.info(f"Generated {len(reset_values)} reset values for UI components.")
1453
-
1454
- # Also reset the models in the background (optional, but good for freeing resources)
1455
  try:
1456
  logger.info("Calling model reset from clear button handler.")
1457
  model_manager.reset_models(force=True)
1458
  except Exception as e:
1459
  logger.error(f"Error resetting models during clear operation: {e}")
1460
  logger.error(traceback.format_exc())
1461
-
1462
  logger.info("--- Clear All operation finished ---")
1463
  return reset_values
1464
 
1465
- clear_button.click(
1466
- fn=clear_all_inputs_and_outputs,
1467
- inputs=None, # No inputs needed for the clear function itself
1468
- outputs=all_inputs + outputs_list # The list of components to clear
1469
- )
1470
  logger.info("Clear button click handler set.")
1471
  logger.info("--- Gradio interface creation complete ---")
1472
  return demo
1473
 
 
 
1474
  if __name__ == "__main__":
1475
  logger.info("--- Running main execution block ---")
1476
-
1477
- # Optional: Pre-initialize Whisper on startup (consider trade-offs)
1478
- # try:
1479
- # logger.info("Attempting to pre-initialize Whisper model on startup...")
1480
- # model_manager.initialize_whisper()
1481
- # logger.info("Whisper pre-initialization successful.")
1482
- # except Exception as e:
1483
- # logger.warning(f"Pre-initialization of Whisper model failed (will load on demand): {str(e)}")
1484
- # logger.warning(traceback.format_exc())
1485
-
1486
- # Create the Gradio Demo
1487
  logger.info("Creating Gradio demo instance...")
1488
  news_demo = create_demo()
1489
  logger.info("Gradio demo instance created.")
1490
-
1491
- # Configure the queue
1492
  logger.info("Configuring Gradio queue...")
1493
- news_demo.queue() # Use default queue settings
1494
  logger.info("Gradio queue configured.")
1495
-
1496
- # Launch the Gradio app
1497
  logger.info("Launching Gradio interface...")
1498
  try:
1499
- news_demo.launch(
1500
- server_name="0.0.0.0", # Necessary for Docker/Spaces
1501
- server_port=7860,
1502
- # share=False, # Usually set by Spaces automatically
1503
- # debug=True # Enable for more Gradio-specific logs if needed
1504
- )
1505
  logger.info("Gradio launch called. Application running.")
1506
  except Exception as launch_err:
1507
  logger.error(f"!!! CRITICAL Error during Gradio launch: {launch_err}")
1508
  logger.error(traceback.format_exc())
1509
-
1510
- logger.info("--- Main execution block finished ---") # May not be reached if launch blocks
 
58
  logger.info("Initializing ModelManager attributes.")
59
  self.tokenizer = None
60
  self.model = None
61
+ self.text_pipeline = None
62
  self.whisper_model = None
63
+ # self._initialized remains False until a model is successfully loaded
64
+ self.llm_loaded = False
65
+ self.whisper_loaded = False
66
  self.last_used = time.time()
67
  self.llm_loading = False
68
  self.whisper_loading = False
69
 
70
+ def _cleanup_memory(self):
71
+ """Utility function to force memory cleanup"""
72
+ logger.info("Running garbage collection...")
73
+ collected_count = gc.collect()
74
+ logger.info(f"Garbage collected ({collected_count} objects).")
75
+ if torch.cuda.is_available():
76
+ logger.info("Clearing CUDA cache...")
77
+ torch.cuda.empty_cache()
78
+ logger.info("CUDA cache cleared.")
79
+
80
+ def reset_llm(self):
81
+ """Explicitly resets the LLM components."""
82
+ logger.info("--- Attempting to reset LLM ---")
83
+ try:
84
+ if hasattr(self, 'model') and self.model is not None:
85
+ del self.model
86
+ logger.info("LLM model deleted.")
87
+ if hasattr(self, 'tokenizer') and self.tokenizer is not None:
88
+ del self.tokenizer
89
+ logger.info("LLM tokenizer deleted.")
90
+ if hasattr(self, 'text_pipeline') and self.text_pipeline is not None:
91
+ del self.text_pipeline
92
+ logger.info("LLM pipeline deleted.")
93
+
94
+ self.model = None
95
+ self.tokenizer = None
96
+ self.text_pipeline = None
97
+ self.llm_loaded = False
98
+ self._cleanup_memory()
99
+ logger.info("LLM components reset successfully.")
100
+ except Exception as e:
101
+ logger.error(f"!!! ERROR during LLM reset: {e}")
102
+ logger.error(traceback.format_exc())
103
+
104
+ def reset_whisper(self):
105
+ """Explicitly resets the Whisper model."""
106
+ logger.info("--- Attempting to reset Whisper ---")
107
+ try:
108
+ if hasattr(self, 'whisper_model') and self.whisper_model is not None:
109
+ del self.whisper_model
110
+ logger.info("Whisper model deleted.")
111
+
112
+ self.whisper_model = None
113
+ self.whisper_loaded = False
114
+ self._cleanup_memory()
115
+ logger.info("Whisper component reset successfully.")
116
+ except Exception as e:
117
+ logger.error(f"!!! ERROR during Whisper reset: {e}")
118
+ logger.error(traceback.format_exc())
119
+
120
+ @spaces.GPU(duration=120)
121
  def initialize_llm(self):
122
  """Initialize LLM model with standard transformers"""
123
  logger.info("Attempting to initialize LLM.")
124
  if self.llm_loading:
125
  logger.info("LLM initialization already in progress. Skipping.")
126
+ return True
127
+ if self.llm_loaded:
128
  logger.info("LLM already initialized.")
129
  self.last_used = time.time()
130
  return True
131
 
132
+ # Explicitly try to free Whisper memory before loading LLM
133
+ self.reset_whisper()
134
+
135
  self.llm_loading = True
136
  logger.info("Starting LLM initialization...")
137
  try:
 
139
  logger.info(f"Using LLM model: {MODEL_NAME}")
140
 
141
  logger.info("Loading LLM tokenizer...")
142
+ self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, token=HUGGINGFACE_TOKEN, use_fast=True)
 
 
 
 
143
  logger.info("LLM tokenizer loaded.")
 
144
  if self.tokenizer.pad_token is None:
 
145
  self.tokenizer.pad_token = self.tokenizer.eos_token
146
 
147
  logger.info("Loading LLM model...")
148
  self.model = AutoModelForCausalLM.from_pretrained(
149
+ MODEL_NAME, token=HUGGINGFACE_TOKEN, device_map="auto",
150
+ torch_dtype=torch.float16, low_cpu_mem_usage=True,
151
+ offload_folder="offload", offload_state_dict=True
 
 
 
 
152
  )
153
  logger.info("LLM model loaded.")
154
 
155
  logger.info("Creating LLM text generation pipeline...")
156
  self.text_pipeline = pipeline(
157
+ "text-generation", model=self.model, tokenizer=self.tokenizer,
158
+ torch_dtype=torch.float16, device_map="auto", max_length=1024
 
 
 
 
159
  )
160
  logger.info("LLM text generation pipeline created.")
161
 
162
  logger.info("LLM initialized successfully.")
163
  self.last_used = time.time()
164
+ self.llm_loaded = True
165
  self.llm_loading = False
166
  return True
167
 
168
  except Exception as e:
169
  logger.error(f"!!! ERROR during LLM initialization: {str(e)}")
170
+ logger.error(traceback.format_exc())
171
  logger.error("Resetting potentially partially loaded LLM components due to error.")
172
+ self.reset_llm() # Use the specific reset function
 
 
 
 
 
 
173
  self.llm_loading = False
174
+ raise
175
 
176
+ @spaces.GPU(duration=120)
177
  def initialize_whisper(self):
178
  """Initialize Whisper model for audio transcription"""
179
  logger.info("Attempting to initialize Whisper.")
180
  if self.whisper_loading:
181
  logger.info("Whisper initialization already in progress. Skipping.")
182
  return True
183
+ if self.whisper_loaded:
184
  logger.info("Whisper already initialized.")
185
  self.last_used = time.time()
186
  return True
187
 
188
+ # Explicitly try to free LLM memory before loading Whisper
189
+ self.reset_llm()
190
+
191
  self.whisper_loading = True
192
  logger.info("Starting Whisper initialization...")
193
  try:
194
+ WHISPER_MODEL_NAME = "tiny"
195
  logger.info(f"Loading Whisper model: {WHISPER_MODEL_NAME}")
 
 
 
 
196
  self.whisper_model = whisper.load_model(
197
+ WHISPER_MODEL_NAME, device="cuda" if torch.cuda.is_available() else "cpu",
198
+ download_root="/tmp/whisper"
 
199
  )
200
  logger.info(f"Whisper model '{WHISPER_MODEL_NAME}' loaded successfully.")
201
  self.last_used = time.time()
202
+ self.whisper_loaded = True
203
  self.whisper_loading = False
204
  return True
205
  except Exception as e:
206
  logger.error(f"!!! ERROR during Whisper initialization: {str(e)}")
207
  logger.error(traceback.format_exc())
208
  logger.error("Resetting potentially partially loaded Whisper components due to error.")
209
+ self.reset_whisper() # Use the specific reset function
 
 
 
 
210
  self.whisper_loading = False
211
  raise
212
 
213
  def check_llm_initialized(self):
214
  """Check if LLM is initialized and initialize if needed"""
215
  logger.info("Checking if LLM is initialized.")
216
+ if not self.llm_loaded:
217
  logger.info("LLM not initialized, attempting initialization...")
218
+ if not self.llm_loading:
219
  self.initialize_llm() # This will raise error if it fails
220
  logger.info("LLM initialization completed by check_llm_initialized.")
221
  else:
222
+ # This state should ideally be avoided by sequential logic, but handle anyway
223
  logger.info("LLM initialization is already in progress by another request. Waiting briefly.")
224
+ time.sleep(10)
225
+ if not self.llm_loaded:
 
226
  logger.error("LLM initialization timed out or failed after waiting.")
227
  raise RuntimeError("LLM initialization timed out or failed.")
228
  else:
 
231
  logger.info("LLM was already initialized.")
232
  self.last_used = time.time()
233
 
234
+
235
  def check_whisper_initialized(self):
236
  """Check if Whisper model is initialized and initialize if needed"""
237
  logger.info("Checking if Whisper is initialized.")
238
+ if not self.whisper_loaded:
239
  logger.info("Whisper model not initialized, attempting initialization...")
240
+ if not self.whisper_loading:
241
  self.initialize_whisper() # This will raise error if it fails
242
  logger.info("Whisper initialization completed by check_whisper_initialized.")
243
  else:
244
  logger.info("Whisper initialization is already in progress by another request. Waiting briefly.")
245
+ time.sleep(10)
246
+ if not self.whisper_loaded:
247
  logger.error("Whisper initialization timed out or failed after waiting.")
248
  raise RuntimeError("Whisper initialization timed out or failed.")
249
  else:
 
253
  self.last_used = time.time()
254
 
255
  def reset_models(self, force=False):
256
+ """Reset models if idle or forced."""
257
+ # This function now just calls the specific resets.
258
+ # Idle logic could be added back if needed, but explicit resets might be better for ZeroGPU.
259
+ if force:
260
+ logger.info("Forcing reset of all models.")
261
+ self.reset_llm()
262
+ self.reset_whisper()
263
+ # else: # Optional: Add idle check back if desired
264
+ # current_time = time.time()
265
+ # if current_time - self.last_used > 600:
266
+ # logger.info("Resetting models due to inactivity.")
267
+ # self.reset_llm()
268
+ # self.reset_whisper()
269
+
270
+
271
+ # --- Rest of the functions (download_social_media_video, convert_video_to_audio, etc.) remain the same as the previous version with detailed logging ---
272
+ # --- Paste the functions from the previous answer here, starting from @lru_cache...download_social_media_video down to the end of process_social_media_url ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
 
274
  @lru_cache(maxsize=16) # Reduced cache size slightly
275
  def download_social_media_video(url):
 
453
  original_input_path = None
454
  temp_files_to_clean = []
455
  processing_step = "Initialization"
456
+ transcription = "" # Default value
457
 
458
  try:
459
  processing_step = "Whisper Model Check"
460
+ logger.info("Checking/Initializing Whisper model for transcription...")
461
+ # *** Crucial Change: Reset LLM before ensuring Whisper is ready ***
462
+ # model_manager.reset_llm()
463
+ # *** Let's try NOT resetting LLM here, maybe both can fit? Check logs if fails ***
464
  model_manager.check_whisper_initialized() # Will raise error if fails
465
+ logger.info("Whisper model is ready for transcription.")
466
 
467
  if file_input is None:
468
  logger.info("No file input provided for transcription. Returning empty string.")
469
+ return ""
470
 
471
+ # ... (rest of the input type handling, conversion, preprocessing - same as before) ...
472
  processing_step = "Input Type Handling"
473
  if isinstance(file_input, str): # Input is a path
474
  original_input_path = file_input
 
492
  file_extension = os.path.splitext(input_path)[1].lower()
493
  logger.info(f"File extension: {file_extension}")
494
 
 
495
  processing_step = "Video Conversion Check"
496
  if file_extension in ['.mp4', '.avi', '.mov', '.mkv', '.webm']:
497
  logger.info(f"Detected video file ({file_extension}), attempting conversion to audio...")
 
506
  logger.error(f"Unsupported file extension for transcription: {file_extension}")
507
  raise ValueError(f"Unsupported file type: {file_extension}")
508
 
 
509
  processing_step = "Audio Preprocessing"
510
  try:
511
  logger.info(f"Attempting to preprocess audio file: {audio_file_to_process}")
512
  preprocessed_audio_path = preprocess_audio(audio_file_to_process)
 
513
  if preprocessed_audio_path != audio_file_to_process:
514
  logger.info("Preprocessing created a new file, adding to cleanup list.")
515
  temp_files_to_clean.append(preprocessed_audio_path)
 
517
  logger.info(f"Audio preprocessing successful. File to transcribe: {audio_file_to_transcribe}")
518
  except Exception as preprocess_err:
519
  logger.warning(f"Audio preprocessing failed: {preprocess_err}. Using original/converted audio for transcription.")
520
+ logger.warning(traceback.format_exc())
521
+ audio_file_to_transcribe = audio_file_to_process
522
 
523
+ processing_step = "Transcription Execution"
524
+ logger.info(f"Starting transcription execution for: {audio_file_to_transcribe}")
525
  if not os.path.exists(audio_file_to_transcribe):
526
  logger.error(f"Audio file to transcribe not found: {audio_file_to_transcribe}")
527
  raise FileNotFoundError(f"Audio file to transcribe not found: {audio_file_to_transcribe}")
528
 
 
529
  logger.info("Calling Whisper model transcribe method...")
530
+ with torch.inference_mode():
 
531
  use_fp16 = torch.cuda.is_available()
532
  logger.info(f"Using fp16 for transcription: {use_fp16}")
533
+ # Add language='en' if most input is English, might improve speed/accuracy
534
  result = model_manager.whisper_model.transcribe(
535
+ audio_file_to_transcribe, fp16=use_fp16 #, language="en"
 
 
536
  )
537
  logger.info("Whisper model transcribe method finished.")
538
  if not result or "text" not in result:
 
544
  logger.info(f"Transcription completed successfully: '{log_transcription}'")
545
 
546
  processing_step = "Success"
547
+ # *** Optional: Reset Whisper immediately after use if memory is tight ***
548
+ # logger.info("Resetting Whisper model after successful transcription.")
549
+ # model_manager.reset_whisper()
550
 
551
+ # ... (keep the except blocks same as before) ...
552
  except FileNotFoundError as e:
553
  logger.error(f"!!! File not found error during transcription (Step: {processing_step}): {e}")
554
  logger.error(traceback.format_exc())
555
+ transcription = f"Error: Input file not found ({e})"
556
  except ValueError as e:
557
  logger.error(f"!!! Value error during transcription (Step: {processing_step}): {e}")
558
  logger.error(traceback.format_exc())
559
+ transcription = f"Error: Unsupported file type ({e})"
560
  except TypeError as e:
561
  logger.error(f"!!! Type error during transcription setup (Step: {processing_step}): {e}")
562
  logger.error(traceback.format_exc())
563
+ transcription = f"Error: Invalid input provided ({e})"
564
  except RuntimeError as e:
565
  logger.error(f"!!! Runtime error during transcription (Step: {processing_step}): {e}")
566
  logger.error(traceback.format_exc())
567
+ transcription = f"Error during processing: {e}"
568
  except Exception as e:
569
  logger.error(f"!!! Unexpected error during transcription (Step: {processing_step}): {str(e)}")
570
  logger.error(traceback.format_exc())
571
+ transcription = f"Error processing the file: An unexpected error occurred."
 
572
  finally:
573
+ # Clean up temporary files
574
  logger.info(f"--- Cleaning up temporary files for transcription process ({len(temp_files_to_clean)} files) ---")
575
  for temp_file in temp_files_to_clean:
576
  try:
577
  if os.path.exists(temp_file):
578
  os.remove(temp_file)
579
  logger.info(f"Cleaned up temporary file: {temp_file}")
580
+ # else:
581
+ # logger.info(f"Temporary file already removed or never created: {temp_file}")
582
  except Exception as e:
583
  logger.warning(f"Could not remove temporary file {temp_file}: {str(e)}")
584
  logger.info("--- Finished transcription process cleanup ---")
585
+ # Return the result (could be transcription or error message)
586
+ return transcription
587
 
588
 
589
  @lru_cache(maxsize=16)
 
602
  if file_extension == ".pdf":
603
  logger.info("Reading PDF document using PyMuPDF (fitz)...")
604
  doc = fitz.open(document_path)
605
+ # Check for encryption first
606
+ if doc.is_encrypted:
607
+ logger.warning(f"PDF document {document_path} is encrypted. Attempting to decrypt with empty password.")
608
+ if not doc.authenticate(""):
609
+ logger.error(f"Failed to decrypt PDF {document_path} with empty password.")
610
+ doc.close()
611
+ raise ValueError("Encrypted PDF cannot be read without password.")
612
  content = "\n".join([page.get_text() for page in doc])
613
  doc.close()
614
  logger.info(f"PDF read successfully. Length: {len(content)} chars.")
 
629
  logger.info(f"Excel read successfully. Length: {len(content)} chars.")
630
  elif file_extension == ".csv":
631
  logger.info("Reading CSV document using pandas...")
 
632
  try:
633
  logger.info("Attempting CSV read with comma separator...")
634
+ # Try to sniff encoding
635
+ with open(document_path, 'rb') as f:
636
+ import chardet
637
+ encoding = chardet.detect(f.read())['encoding']
638
+ logger.info(f"Detected CSV encoding: {encoding}")
639
+ df = pd.read_csv(document_path, encoding=encoding)
640
+ except (pd.errors.ParserError, UnicodeDecodeError) as e1:
641
+ logger.warning(f"Could not parse CSV {document_path} with comma/detected encoding ({e1}), trying semicolon.")
642
+ try:
643
+ df = pd.read_csv(document_path, sep=';', encoding=encoding)
644
+ except Exception as e2:
645
+ logger.error(f"Also failed with semicolon separator: {e2}. Trying latin1 encoding.")
646
+ try:
647
+ df = pd.read_csv(document_path, encoding='latin1')
648
+ except Exception as e3:
649
+ logger.error(f"Also failed with latin1: {e3}. Giving up.")
650
+ raise ValueError(f"Failed to parse CSV: {e1}, {e2}, {e3}")
651
+
652
  content = df.to_string()
653
  logger.info(f"CSV read successfully. Length: {len(content)} chars.")
654
  else:
 
659
 
660
  except FileNotFoundError as e:
661
  logger.error(f"!!! File not found error while reading document: {e}")
 
662
  return f"Error: Document file not found at {document_path}"
663
+ except ValueError as e: # Catch specific errors like encryption or CSV parsing
664
+ logger.error(f"!!! Value error reading document {document_path}: {e}")
665
+ logger.error(traceback.format_exc())
666
+ return f"Error reading document: {e}"
667
  except Exception as e:
668
  logger.error(f"!!! Error reading document {document_path}: {str(e)}")
669
  logger.error(traceback.format_exc())
 
675
  logger.info(f"Attempting to read URL: {url}")
676
  if not url or not url.strip().startswith('http'):
677
  logger.warning(f"Invalid or empty URL provided: '{url}'")
678
+ return ""
679
 
680
  try:
681
  headers = {
682
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
683
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
684
+ 'Accept-Language': 'en-US,en;q=0.9',
685
+ 'Connection': 'keep-alive'
686
  }
687
  logger.info(f"Sending GET request to {url} with headers: {headers}")
 
688
  response = requests.get(url, headers=headers, timeout=20, allow_redirects=True)
689
+ logger.info(f"Received response from {url}. Status code: {response.status_code}, Content-Type: {response.headers.get('content-type')}")
690
+ response.raise_for_status()
691
 
 
692
  content_type = response.headers.get('content-type', '').lower()
 
693
  if not ('html' in content_type or 'text' in content_type):
694
  logger.warning(f"URL {url} has non-text content type: {content_type}. Skipping.")
695
  return f"Error: URL content type ({content_type}) is not text/html."
696
 
697
+ # Decode content carefully
698
+ detected_encoding = response.encoding if response.encoding else response.apparent_encoding
699
+ logger.info(f"Decoding response content with encoding: {detected_encoding}")
700
+ html_content = response.content.decode(detected_encoding or 'utf-8', errors='ignore')
701
+
702
  logger.info(f"Parsing HTML content from {url} using BeautifulSoup...")
703
+ soup = BeautifulSoup(html_content, 'html.parser')
704
  logger.info("HTML parsed.")
705
 
 
706
  logger.info("Removing script, style, and other non-content tags...")
707
+ tags_to_remove = ["script", "style", "meta", "noscript", "iframe", "header", "footer", "nav", "aside", "form", "button", "link", "head"]
708
  for tag_name in tags_to_remove:
709
  for element in soup.find_all(tag_name):
710
  element.extract()
711
  logger.info("Non-content tags removed.")
712
 
 
713
  logger.info("Attempting to find main content container...")
714
  main_content = (
715
  soup.find("main") or
 
728
  if body:
729
  logger.info("Extracting text from body.")
730
  text = body.get_text(separator='\n', strip=True)
731
+ else:
732
  logger.warning(f"No body tag found for {url}. Falling back to all text.")
733
  text = soup.get_text(separator='\n', strip=True)
734
 
 
735
  logger.info("Cleaning extracted text whitespace...")
736
  lines = [line.strip() for line in text.split('\n') if line.strip()]
737
  cleaned_text = "\n".join(lines)
 
738
  logger.info(f"Text cleaning complete. Initial length: {len(text)}, Cleaned length: {len(cleaned_text)}")
739
 
 
740
  if not cleaned_text:
741
  logger.warning(f"Could not extract meaningful text from URL: {url}")
742
  return "Error: Could not extract text content from URL."
743
 
 
744
  max_chars = 15000
745
  if len(cleaned_text) > max_chars:
746
  logger.info(f"URL content is long ({len(cleaned_text)} chars), truncating to {max_chars} characters.")
 
752
  return final_text
753
  except requests.exceptions.RequestException as e:
754
  logger.error(f"!!! Error fetching URL {url}: {str(e)}")
 
755
  return f"Error reading URL: Could not fetch content ({e})"
756
  except Exception as e:
757
  logger.error(f"!!! Error parsing URL {url}: {str(e)}")
 
767
 
768
  text_content = None
769
  video_transcription = None
 
770
  temp_audio_file = None
771
 
772
  # 1. Try extracting text content using read_url
 
778
  logger.info(f"Successfully read text content from {url}. Length: {len(text_content)}")
779
  elif text_content_result:
780
  logger.warning(f"read_url returned an error for {url}: {text_content_result}")
 
781
  else:
782
  logger.info(f"No text content extracted by read_url for {url}.")
783
  except Exception as e:
784
  logger.error(f"!!! Exception during text content extraction from social URL {url}: {e}")
785
  logger.error(traceback.format_exc())
 
786
 
787
  # 2. Try downloading and transcribing potential video/audio content
788
  logger.info(f"Attempting to download audio/video content from social URL: {url}")
 
790
  temp_audio_file = download_social_media_video(url) # Returns path or None
791
  if temp_audio_file:
792
  logger.info(f"Audio downloaded from {url} to {temp_audio_file}. Proceeding to transcription.")
 
793
  transcription_result = transcribe_audio_or_video(temp_audio_file) # Handles errors internally
794
  if transcription_result and not transcription_result.startswith("Error"):
795
  video_transcription = transcription_result
796
  logger.info(f"Successfully transcribed audio from {url}. Length: {len(video_transcription)}")
797
  elif transcription_result:
798
  logger.warning(f"Transcription returned an error for audio from {url}: {transcription_result}")
 
799
  else:
800
  logger.warning(f"Transcription returned empty result for audio from {url}.")
801
  else:
 
803
  except Exception as e:
804
  logger.error(f"!!! Exception during video/audio processing for social URL {url}: {e}")
805
  logger.error(traceback.format_exc())
 
806
  finally:
807
  # Clean up downloaded file if it exists
808
  if temp_audio_file and os.path.exists(temp_audio_file):
 
815
 
816
  # Return results
817
  logger.info(f"--- Finished processing social media URL: {url} ---")
818
+ if text_content or video_transcription:
819
+ return {"text": text_content or "", "video": video_transcription or ""}
820
+ else:
821
+ # Return None only if BOTH failed and no content was retrieved
822
+ logger.info(f"No usable content retrieved for social URL: {url}")
823
+ return None
824
+
825
+ # Create global model manager instance
826
+ logger.info("Creating global ModelManager instance.")
827
+ model_manager = ModelManager()
828
 
829
 
830
  @spaces.GPU(duration=300) # Allow more time for generation
 
837
 
838
  try:
839
  # --- Parameter Logging & Basic Validation ---
840
+ # (Same as before)
841
  logger.info(f"Received Instructions: {'Yes' if instructions else 'No'}")
842
  logger.info(f"Received Facts: {'Yes' if facts else 'No'}")
843
  logger.info(f"Requested Size: {size}, Tone: {tone}")
 
849
  size = 250
850
  logger.info(f"Using Size: {size}")
851
 
 
852
  # --- Argument Parsing ---
853
+ # (Same as before)
854
  logger.info("Parsing dynamic arguments...")
855
  num_docs = 5
856
  num_audio_sources = 5
 
868
  logger.warning(f"Received more arguments ({len(args_list)}) than expected ({total_expected_args}). Truncating.")
869
  args_list = args_list[:total_expected_args]
870
 
 
871
  doc_files = args_list[0:num_docs]
872
  audio_inputs_flat = args_list[num_docs : num_docs + (num_audio_sources * num_audio_inputs_per_source)]
873
  url_inputs = args_list[num_docs + (num_audio_sources * num_audio_inputs_per_source) : num_docs + (num_audio_sources * num_audio_inputs_per_source) + num_urls]
 
877
  knowledge_base = {
878
  "instructions": instructions or "No specific instructions provided.",
879
  "facts": facts or "No specific facts provided.",
880
+ "document_content": [], "audio_data": [], "url_content": [], "social_content": []
 
 
 
881
  }
882
 
883
+ # --- Process Inputs (Documents, URLs, Collect Audio Info, Social Media) ---
884
+ # (Keep the processing loops same as previous version with detailed logging)
885
+ # --- Processing document inputs ---
886
  logger.info("--- Processing document inputs ---")
887
  doc_counter = 0
888
  for i, doc_file in enumerate(doc_files):
 
890
  doc_filename = os.path.basename(doc_file.name)
891
  logger.info(f"Attempting to read document {i+1}: {doc_filename} (Path: {doc_file.name})")
892
  try:
893
+ content = read_document(doc_file.name)
894
  if content and content.startswith("Error:"):
895
  logger.warning(f"Skipping document {i+1} ({doc_filename}) due to read error: {content}")
896
  raw_transcriptions += f"[Document {i+1}: {doc_filename}] Error reading: {content}\n\n"
897
  elif content:
898
  doc_excerpt = (content[:1000] + "... [document truncated]") if len(content) > 1000 else content
899
  knowledge_base["document_content"].append(f"[Document {i+1} Source: {doc_filename}]\n{doc_excerpt}")
900
+ logger.info(f"Successfully processed document {i+1}. Added excerpt.")
901
  doc_counter += 1
 
 
902
  else:
903
+ logger.warning(f"Skipping document {i+1} ({doc_filename}) because content is empty.")
904
  raw_transcriptions += f"[Document {i+1}: {doc_filename}] Read successfully but content is empty.\n\n"
905
  except Exception as e:
906
  logger.error(f"!!! FAILED to process document {i+1} ({doc_filename}): {e}")
907
  logger.error(traceback.format_exc())
908
  raw_transcriptions += f"[Document {i+1}: {doc_filename}] CRITICAL Error during processing: {e}\n\n"
909
+ # else: logger.info(f"Skipping document slot {i+1}: No file.")
910
+ logger.info(f"--- Finished processing {doc_counter} documents. ---")
 
 
 
911
 
912
+ # --- Processing URL inputs ---
913
  logger.info("--- Processing URL inputs ---")
914
  url_counter = 0
915
  for i, url in enumerate(url_inputs):
 
921
  logger.warning(f"Skipping URL {i+1} ({url}) due to read error: {content}")
922
  raw_transcriptions += f"[URL {i+1}: {url}] Error reading: {content}\n\n"
923
  elif content:
 
924
  knowledge_base["url_content"].append(f"[URL {i+1} Source: {url}]\n{content}")
925
+ logger.info(f"Successfully processed URL {i+1}. Added content.")
926
  url_counter += 1
927
  else:
928
+ logger.warning(f"Skipping URL {i+1} ({url}) because content is empty.")
929
  raw_transcriptions += f"[URL {i+1}: {url}] Read successfully but content is empty.\n\n"
930
  except Exception as e:
931
  logger.error(f"!!! FAILED to process URL {i+1} ({url}): {e}")
932
  logger.error(traceback.format_exc())
933
  raw_transcriptions += f"[URL {i+1}: {url}] CRITICAL Error during processing: {e}\n\n"
934
+ # elif url: logger.warning(f"Skipping URL slot {i+1}: Invalid URL '{url}'.")
935
+ # else: logger.info(f"Skipping URL slot {i+1}: No URL.")
936
+ logger.info(f"--- Finished processing {url_counter} URLs. ---")
 
 
937
 
938
+ # --- Processing audio/video inputs (collecting info) ---
 
939
  logger.info("--- Processing audio/video inputs (collecting info) ---")
940
  has_audio_source = False
941
  audio_counter = 0
942
  for i in range(num_audio_sources):
943
  start_idx = i * num_audio_inputs_per_source
 
944
  if start_idx + 2 < len(audio_inputs_flat):
945
  audio_file = audio_inputs_flat[start_idx]
946
  name = audio_inputs_flat[start_idx + 1] or f"Unnamed Audio Source {i+1}"
947
  position = audio_inputs_flat[start_idx + 2] or "Role N/A"
 
948
  if audio_file and hasattr(audio_file, 'name') and audio_file.name:
949
  audio_filename = os.path.basename(audio_file.name)
950
  logger.info(f"Found audio/video source {i+1}: {name} ({position}) - File: {audio_filename} (Path: {audio_file.name})")
951
+ knowledge_base["audio_data"].append({"file_path": audio_file.name, "name": name, "position": position, "original_filename": audio_filename})
 
 
 
 
 
 
952
  has_audio_source = True
953
  audio_counter += 1
954
+ # else: logger.info(f"Skipping audio source slot {i+1}: No file.")
955
+ else: logger.warning(f"Index out of bounds for audio source {i+1}."); break
956
+ logger.info(f"--- Finished collecting audio/video info. {audio_counter} sources found. Transcription needed: {has_audio_source} ---")
 
 
 
957
 
958
+ # --- Processing social media inputs ---
 
959
  logger.info("--- Processing social media inputs ---")
 
960
  social_counter = 0
961
  for i in range(num_social_sources):
962
  start_idx = i * num_social_inputs_per_source
 
964
  social_url = social_inputs_flat[start_idx]
965
  social_name = social_inputs_flat[start_idx + 1] or f"Unnamed Social Source {i+1}"
966
  social_context = social_inputs_flat[start_idx + 2] or "Context N/A"
 
967
  if social_url and isinstance(social_url, str) and social_url.strip().startswith('http'):
968
  logger.info(f"Attempting to process social media URL {i+1}: {social_url} ({social_name}, {social_context})")
969
  try:
970
+ social_data = process_social_media_url(social_url)
971
+ if social_data: # process_social_media_url now returns dict even if empty
972
+ if social_data.get("text") or social_data.get("video"):
973
+ logger.info(f"Successfully processed social URL {i+1}. Text: {bool(social_data.get('text'))}, Video: {bool(social_data.get('video'))}")
974
+ knowledge_base["social_content"].append({"url": social_url, "name": social_name, "context": social_context, "text": social_data.get("text", ""), "video_transcription": social_data.get("video", "")})
975
+ social_counter += 1
976
+ else:
977
+ logger.warning(f"Processed social URL {i+1} ({social_url}) but found no text or video content.")
978
+ raw_transcriptions += f"[Social Media {i+1}: {social_url} ({social_name})] Processed but no content found.\n\n"
979
+ # No 'else' needed as process_social_media_url handles internal errors and returns dict
 
 
 
 
 
 
 
 
 
980
  except Exception as e:
981
  logger.error(f"!!! FAILED to process social URL {i+1} ({social_url}): {e}")
982
  logger.error(traceback.format_exc())
983
  raw_transcriptions += f"[Social Media {i+1}: {social_url} ({social_name})] CRITICAL Error during processing: {e}\n\n"
984
+ # elif social_url: logger.warning(f"Skipping social slot {i+1}: Invalid URL '{social_url}'.")
985
+ # else: logger.info(f"Skipping social slot {i+1}: No URL.")
986
+ else: logger.warning(f"Index out of bounds for social source {i+1}."); break
987
+ logger.info(f"--- Finished processing {social_counter} social media sources. ---")
 
 
 
 
988
 
989
 
990
  # --- Transcribe Audio/Video (Conditional) ---
991
  transcriptions_for_prompt = ""
992
  if has_audio_source:
993
  logger.info("--- Starting Audio Transcription Phase ---")
994
+ # Whisper check/initialization happens INSIDE transcribe_audio_or_video now
995
+ for idx, data in enumerate(knowledge_base["audio_data"]):
996
+ audio_filename = data['original_filename']
997
+ logger.info(f"Attempting transcription for audio source {idx+1}: {audio_filename} ({data['name']}, {data['position']})")
998
+ try:
999
+ # transcribe_audio_or_video now includes model check and returns error string on failure
1000
+ transcription = transcribe_audio_or_video(data["file_path"])
1001
+ if transcription and not transcription.startswith("Error"):
1002
+ logger.info(f"Transcription successful for audio {idx+1}. Length: {len(transcription)}")
1003
+ quote = f'"{transcription}" - {data["name"]}, {data["position"]}'
1004
+ transcriptions_for_prompt += f"{quote}\n\n"
1005
+ raw_transcriptions += f'[Audio/Video {idx + 1}: {audio_filename} ({data["name"]}, {data["position"]})]\n"{transcription}"\n\n'
1006
+ else:
1007
+ # Log the error message returned by the function
1008
+ logger.warning(f"Transcription failed or returned error for audio source {idx+1} ({audio_filename}): {transcription}")
1009
+ raw_transcriptions += f'[Audio/Video {idx + 1}: {audio_filename} ({data["name"]}, {data["position"]})]\n[Transcription Error: {transcription}]\n\n'
1010
+ except Exception as e:
1011
+ # Catch unexpected errors during the call itself
1012
+ logger.error(f"!!! CRITICAL Error during transcription call for audio source {idx+1} ({audio_filename}): {e}")
1013
+ logger.error(traceback.format_exc())
1014
+ raw_transcriptions += f'[Audio/Video {idx + 1}: {audio_filename} ({data["name"]}, {data["position"]})]\n[CRITICAL Error during transcription call: {e}]\n\n'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1015
  logger.info("--- Finished Audio Transcription Phase ---")
1016
  else:
1017
  logger.info("--- Skipping Audio Transcription Phase (no audio sources found) ---")
1018
 
1019
 
1020
  # --- Add Social Media Content to Prompt Data ---
1021
+ # (Same as before)
1022
  logger.info("--- Adding social media content to prompt data ---")
1023
  social_content_added_to_prompt = False
1024
  for idx, data in enumerate(knowledge_base["social_content"]):
1025
  source_id_log = f'[Social Media {idx+1}: {data["url"]} ({data["name"]}, {data["context"]})]'
1026
  source_id_prompt = f'Social Media Post ({data["name"]}, {data["context"]} at {data["url"]}):'
1027
  content_added_this_source = False
 
 
1028
  if data["text"]:
1029
  text_excerpt = (data["text"][:500] + "...[text truncated]") if len(data["text"]) > 500 else data["text"]
1030
  social_text_prompt = f'{source_id_prompt}\nText Content:\n"{text_excerpt}"\n\n'
1031
  transcriptions_for_prompt += social_text_prompt
1032
+ raw_transcriptions += f"{source_id_log}\nText Content:\n{data['text']}\n\n"
1033
+ content_added_this_source = True; social_content_added_to_prompt = True
 
 
 
 
1034
  if data["video_transcription"]:
1035
  social_video_prompt = f'{source_id_prompt}\nVideo Transcription:\n"{data["video_transcription"]}"\n\n'
1036
  transcriptions_for_prompt += social_video_prompt
1037
  raw_transcriptions += f"{source_id_log}\nVideo Transcription:\n{data['video_transcription']}\n\n"
1038
+ content_added_this_source = True; social_content_added_to_prompt = True
1039
+ if content_added_this_source: logger.info(f"Added content from social source {idx+1} to prompt data.")
1040
+ # else: logger.info(f"No usable content found for social source {idx+1} ({data['url']}).")
1041
+ if not social_content_added_to_prompt: logger.info("No content from social media sources was added to the prompt data.")
1042
+ logger.info("--- Finished adding social media content ---")
 
 
 
 
 
 
1043
 
1044
 
1045
  # --- Prepare Final Prompt ---
1046
+ # (Same as before)
1047
  logger.info("--- Preparing final prompt for LLM ---")
1048
  document_summary = "\n\n".join(knowledge_base["document_content"]) if knowledge_base["document_content"] else "No document content provided or processed successfully."
1049
  url_summary = "\n\n".join(knowledge_base["url_content"]) if knowledge_base["url_content"] else "No URL content provided or processed successfully."
1050
  transcription_summary = transcriptions_for_prompt if transcriptions_for_prompt else "No usable transcriptions or social media content available."
1051
+ prompt = f"""<s>[INST] You are a professional news writer... [SAME PROMPT AS BEFORE] ...Begin the article now. [/INST]\nArticle Draft:\n""" # Keep prompt structure
1052
+ prompt_words = len(prompt.split()); prompt_chars = len(prompt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1053
  logger.info(f"Generated prompt length: {prompt_words} words / {prompt_chars} characters.")
 
1054
  logger.debug(f"Prompt Start: {prompt[:200]}...")
1055
  logger.debug(f"...Prompt End: {prompt[-200:]}")
1056
  logger.info("--- Finished preparing final prompt ---")
 
1060
  logger.info("--- Starting LLM Generation Phase ---")
1061
  generation_start_time = time.time()
1062
 
1063
+ # Ensure LLM is ready (will also reset Whisper if loaded)
1064
  logger.info("Ensuring LLM is initialized for generation...")
1065
  try:
1066
+ # *** Crucial Change: Reset Whisper before ensuring LLM is ready ***
1067
+ # model_manager.reset_whisper()
1068
+ # *** Let's try NOT resetting whisper, check logs if fails ***
1069
  model_manager.check_llm_initialized() # Raises error if fails
1070
+ logger.info("LLM confirmed ready for generation.")
1071
  except Exception as llm_init_err:
1072
  logger.error(f"!!! FATAL: LLM could not be initialized. Cannot generate article.")
1073
  logger.error(traceback.format_exc())
 
1075
 
1076
 
1077
  # Estimate max_new_tokens
1078
+ # (Same as before)
1079
  estimated_tokens_per_word = 1.5
1080
+ max_new_tokens = int(size * estimated_tokens_per_word + 150)
1081
+ model_max_length = 2048
1082
+ prompt_tokens_estimate = prompt_chars // 3
1083
+ available_tokens = model_max_length - prompt_tokens_estimate - 50
 
1084
  max_new_tokens = min(max_new_tokens, available_tokens)
1085
+ max_new_tokens = max(max_new_tokens, 100)
 
1086
  logger.info(f"Estimated prompt tokens: ~{prompt_tokens_estimate}. Model max length: {model_max_length}. Requesting max_new_tokens: {max_new_tokens}")
1087
 
1088
  try:
1089
+ # Generate text
1090
+ # (Same pipeline call as before)
1091
  logger.info("Calling LLM text generation pipeline...")
1092
  outputs = model_manager.text_pipeline(
1093
+ prompt, max_new_tokens=max_new_tokens, do_sample=True, temperature=0.7,
1094
+ top_p=0.95, top_k=50, repetition_penalty=1.15,
1095
+ pad_token_id=model_manager.tokenizer.eos_token_id, num_return_sequences=1
 
 
 
 
 
 
1096
  )
1097
  logger.info("LLM pipeline call finished.")
1098
 
 
1100
  logger.error("LLM pipeline returned invalid or empty output.")
1101
  raise RuntimeError("LLM generation failed: Pipeline returned empty or invalid output.")
1102
 
 
1103
  full_generated_text = outputs[0]['generated_text']
1104
  logger.info(f"Raw generated text length: {len(full_generated_text)} chars.")
 
1105
 
1106
+ # Clean output
1107
+ # (Same cleaning logic as before)
1108
  logger.info("Cleaning LLM output (removing prompt)...")
1109
  inst_marker = "[/INST]"
1110
  marker_pos = full_generated_text.find(inst_marker)
1111
  if marker_pos != -1:
1112
  generated_article = full_generated_text[marker_pos + len(inst_marker):].strip()
 
1113
  if generated_article.startswith("Article Draft:"):
1114
  generated_article = generated_article[len("Article Draft:"):].strip()
1115
  logger.info("Prompt removed successfully using '[/INST]' marker.")
1116
  else:
1117
+ generated_article = full_generated_text
1118
+ logger.warning("Prompt marker '[/INST]' not found in LLM output. Returning full generated text.")
1119
+
 
 
 
1120
 
1121
  generation_time = time.time() - generation_start_time
1122
  logger.info(f"News generation completed in {generation_time:.2f} seconds.")
1123
  logger.info(f"Final article length: {len(generated_article)} characters.")
1124
  logger.info("--- Finished LLM Generation Phase ---")
1125
+ # *** Optional: Reset LLM immediately after generation ***
1126
+ # logger.info("Resetting LLM model after successful generation.")
1127
+ # model_manager.reset_llm()
1128
 
1129
+ # ... (keep OOM and general Exception handling for generation same as before) ...
1130
  except torch.cuda.OutOfMemoryError as oom_error:
1131
  logger.error(f"!!! CUDA Out of Memory error during LLM generation: {oom_error}")
1132
  logger.error(traceback.format_exc())
1133
  logger.info("Attempting to reset models after OOM error...")
1134
+ model_manager.reset_models(force=True)
1135
+ raise RuntimeError("Generation failed due to insufficient GPU memory.") from oom_error
1136
  except Exception as gen_error:
1137
  logger.error(f"!!! Error during text generation pipeline: {str(gen_error)}")
1138
  logger.error(traceback.format_exc())
 
1140
 
1141
  total_time = time.time() - request_start_time
1142
  logger.info(f"--- generate_news function completed successfully in {total_time:.2f} seconds. ---")
 
 
1143
  return generated_article.strip(), raw_transcriptions.strip()
1144
 
1145
  except Exception as e:
1146
  # Catch-all for any unexpected error during the entire generate_news flow
1147
+ # (Same as before)
1148
  total_time = time.time() - request_start_time
1149
  logger.error(f"!!! UNHANDLED Error in generate_news function after {total_time:.2f} seconds: {str(e)}")
1150
  logger.error(traceback.format_exc())
 
1151
  try:
1152
  logger.info("Attempting model reset due to unhandled error in generate_news.")
1153
  model_manager.reset_models(force=True)
1154
  except Exception as reset_error:
1155
  logger.error(f"Failed to reset models after error: {str(reset_error)}")
 
1156
  error_message = f"Error generating the news article: An unexpected error occurred. Please check logs. ({str(e)})"
1157
  transcription_log = raw_transcriptions.strip() + f"\n\n[CRITICAL ERROR] News generation failed unexpectedly: {str(e)}"
1158
  return error_message, transcription_log
1159
  finally:
1160
+ # Final cleanup/logging
1161
  logger.info("--- generate_news function finished execution (either success or error) ---")
1162
+ # Force cleanup after every run attempt on ZeroGPU
1163
+ logger.info("Forcing model reset at the end of generate_news call.")
1164
+ model_manager.reset_models(force=True)
1165
 
1166
 
1167
+ # --- create_demo function remains the same as the previous version ---
1168
  def create_demo():
1169
  """Creates the Gradio interface"""
1170
  logger.info("--- Creating Gradio interface ---")
1171
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
1172
  gr.Markdown("# πŸ“° NewsIA - AI News Generator")
1173
  gr.Markdown("Create professional news articles from multiple information sources.")
 
 
1174
  all_inputs = []
 
1175
  with gr.Row():
1176
  with gr.Column(scale=2):
1177
  logger.info("Creating instruction input.")
1178
+ instructions = gr.Textbox(label="Instructions for the News Article", placeholder="Enter specific instructions...", lines=2)
 
 
 
 
 
1179
  all_inputs.append(instructions)
 
1180
  logger.info("Creating facts input.")
1181
+ facts = gr.Textbox(label="Main Facts", placeholder="Describe the most important facts...", lines=4)
 
 
 
 
 
1182
  all_inputs.append(facts)
 
1183
  with gr.Row():
1184
  logger.info("Creating size slider.")
1185
+ size_slider = gr.Slider(label="Approximate Length (words)", minimum=100, maximum=700, value=250, step=50)
 
 
 
 
 
 
1186
  all_inputs.append(size_slider)
 
1187
  logger.info("Creating tone dropdown.")
1188
+ tone_dropdown = gr.Dropdown(label="Tone of the News Article", choices=["neutral", "serious", "formal", "urgent", "investigative", "human-interest", "lighthearted"], value="neutral")
 
 
 
 
1189
  all_inputs.append(tone_dropdown)
 
1190
  with gr.Column(scale=3):
1191
  with gr.Tabs():
1192
  with gr.TabItem("πŸ“ Documents"):
 
1194
  gr.Markdown("Upload relevant documents (PDF, DOCX, XLSX, CSV). Max 5.")
1195
  doc_inputs = []
1196
  for i in range(1, 6):
1197
+ doc_file = gr.File(label=f"Document {i}", file_types=["pdf", ".docx", ".xlsx", ".csv"], file_count="single")
 
 
 
 
1198
  doc_inputs.append(doc_file)
1199
  all_inputs.extend(doc_inputs)
1200
  logger.info(f"{len(doc_inputs)} document inputs created.")
 
1201
  with gr.TabItem("πŸ”Š Audio/Video"):
1202
  logger.info("Creating audio/video input tabs.")
1203
+ gr.Markdown("Upload audio or video files... Max 5 sources.")
1204
  audio_video_inputs = []
1205
  for i in range(1, 6):
1206
  with gr.Group():
1207
  gr.Markdown(f"**Source {i}**")
1208
+ audio_file = gr.File(label=f"Audio/Video File {i}", file_types=["audio", "video"])
 
 
 
1209
  with gr.Row():
1210
+ speaker_name = gr.Textbox(label="Speaker Name", placeholder="Name...")
1211
+ speaker_role = gr.Textbox(label="Role/Position", placeholder="Role...")
1212
+ audio_video_inputs.extend([audio_file, speaker_name, speaker_role])
 
 
 
 
 
 
 
 
 
 
1213
  all_inputs.extend(audio_video_inputs)
1214
+ logger.info(f"{len(audio_video_inputs)} audio/video inputs created.")
 
 
1215
  with gr.TabItem("🌐 URLs"):
1216
  logger.info("Creating URL input tabs.")
1217
+ gr.Markdown("Add URLs to relevant web pages... Max 5.")
1218
  url_inputs = []
1219
  for i in range(1, 6):
1220
+ url_textbox = gr.Textbox(label=f"URL {i}", placeholder="https://...")
 
 
 
 
1221
  url_inputs.append(url_textbox)
1222
  all_inputs.extend(url_inputs)
1223
  logger.info(f"{len(url_inputs)} URL inputs created.")
 
1224
  with gr.TabItem("πŸ“± Social Media"):
1225
  logger.info("Creating social media input tabs.")
1226
+ gr.Markdown("Add URLs to social media posts... Max 3.")
1227
  social_inputs = []
1228
  for i in range(1, 4):
1229
  with gr.Group():
1230
  gr.Markdown(f"**Social Media Source {i}**")
1231
+ social_url_textbox = gr.Textbox(label=f"Post URL", placeholder="https://...")
 
 
 
 
1232
  with gr.Row():
1233
+ social_name_textbox = gr.Textbox(label=f"Account Name/User", placeholder="@username")
1234
+ social_context_textbox = gr.Textbox(label=f"Context", placeholder="Context...")
1235
+ social_inputs.extend([social_url_textbox, social_name_textbox, social_context_textbox])
 
 
 
 
 
 
 
 
 
 
1236
  all_inputs.extend(social_inputs)
1237
+ logger.info(f"{len(social_inputs)} social media inputs created.")
 
1238
 
1239
  logger.info(f"Total number of input components collected: {len(all_inputs)}")
 
1240
  with gr.Row():
1241
  logger.info("Creating generate and clear buttons.")
1242
  generate_button = gr.Button("✨ Generate News Article", variant="primary")
1243
  clear_button = gr.Button("πŸ”„ Clear All Inputs")
 
1244
  with gr.Tabs():
1245
  with gr.TabItem("πŸ“„ Generated News Article"):
1246
  logger.info("Creating news output textbox.")
1247
+ news_output = gr.Textbox(label="Draft News Article", lines=20, show_copy_button=True, interactive=False)
 
 
 
 
 
 
1248
  with gr.TabItem("πŸŽ™οΈ Source Transcriptions & Logs"):
1249
  logger.info("Creating transcriptions/log output textbox.")
1250
+ transcriptions_output = gr.Textbox(label="Transcriptions and Processing Log", lines=15, show_copy_button=True, interactive=False)
1251
+
 
 
 
 
 
 
 
1252
  outputs_list = [news_output, transcriptions_output]
1253
  logger.info("Setting up event handlers.")
1254
+ generate_button.click(fn=generate_news, inputs=all_inputs, outputs=outputs_list)
 
 
 
 
 
 
1255
  logger.info("Generate button click handler set.")
1256
 
 
1257
  def clear_all_inputs_and_outputs():
1258
  logger.info("--- Clear All button clicked ---")
1259
  reset_values = []
 
1260
  for input_comp in all_inputs:
1261
+ if isinstance(input_comp, (gr.Textbox, gr.Dropdown)): reset_values.append("")
1262
+ elif isinstance(input_comp, gr.Slider): reset_values.append(250)
1263
+ elif isinstance(input_comp, gr.File): reset_values.append(None)
1264
+ else: reset_values.append(None)
 
 
 
 
 
 
 
1265
  reset_values.extend(["", ""])
1266
  logger.info(f"Generated {len(reset_values)} reset values for UI components.")
 
 
1267
  try:
1268
  logger.info("Calling model reset from clear button handler.")
1269
  model_manager.reset_models(force=True)
1270
  except Exception as e:
1271
  logger.error(f"Error resetting models during clear operation: {e}")
1272
  logger.error(traceback.format_exc())
 
1273
  logger.info("--- Clear All operation finished ---")
1274
  return reset_values
1275
 
1276
+ clear_button.click(fn=clear_all_inputs_and_outputs, inputs=None, outputs=all_inputs + outputs_list)
 
 
 
 
1277
  logger.info("Clear button click handler set.")
1278
  logger.info("--- Gradio interface creation complete ---")
1279
  return demo
1280
 
1281
+
1282
+ # --- main execution block remains the same ---
1283
  if __name__ == "__main__":
1284
  logger.info("--- Running main execution block ---")
 
 
 
 
 
 
 
 
 
 
 
1285
  logger.info("Creating Gradio demo instance...")
1286
  news_demo = create_demo()
1287
  logger.info("Gradio demo instance created.")
 
 
1288
  logger.info("Configuring Gradio queue...")
1289
+ news_demo.queue()
1290
  logger.info("Gradio queue configured.")
 
 
1291
  logger.info("Launching Gradio interface...")
1292
  try:
1293
+ news_demo.launch(server_name="0.0.0.0", server_port=7860)
 
 
 
 
 
1294
  logger.info("Gradio launch called. Application running.")
1295
  except Exception as launch_err:
1296
  logger.error(f"!!! CRITICAL Error during Gradio launch: {launch_err}")
1297
  logger.error(traceback.format_exc())
1298
+ logger.info("--- Main execution block potentially finished (if launch doesn't block indefinitely) ---")