CamiloVega commited on
Commit
43f972f
Β·
verified Β·
1 Parent(s): 48d2a37

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +151 -417
app.py CHANGED
@@ -22,12 +22,12 @@ import traceback # For detailed error logging
22
 
23
  # Configure logging
24
  logging.basicConfig(
25
- level=logging.INFO, # Set to INFO, can change to DEBUG for more verbosity if needed
26
- format='%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s' # Added filename/lineno
27
  )
28
  logger = logging.getLogger(__name__)
29
 
30
- logger.info("--- Starting App ---") # Log app start
31
 
32
  # Login to Hugging Face Hub if token is available
33
  HUGGINGFACE_TOKEN = os.environ.get('HUGGINGFACE_TOKEN')
@@ -40,7 +40,7 @@ if HUGGINGFACE_TOKEN:
40
  logger.error(f"Failed to login to Hugging Face Hub: {e}")
41
  logger.error(traceback.format_exc())
42
  else:
43
- logger.warning("HUGGINGFACE_TOKEN environment variable not set. Model loading might fail if private.")
44
 
45
 
46
  class ModelManager:
@@ -54,7 +54,7 @@ class ModelManager:
54
  return cls._instance
55
 
56
  def __init__(self):
57
- if not hasattr(self, '_initialized') or not self._initialized: # Ensure init runs only once
58
  logger.info("Initializing ModelManager attributes.")
59
  self.tokenizer = None
60
  self.model = None
@@ -65,10 +65,9 @@ class ModelManager:
65
  self.last_used = time.time()
66
  self.llm_loading = False
67
  self.whisper_loading = False
68
- self._initialized = True # Mark as initialized
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).")
@@ -78,500 +77,262 @@ class ModelManager:
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
- # Check attributes before deleting
85
- if hasattr(self, 'model') and self.model is not None:
86
- del self.model
87
- logger.info("LLM model deleted.")
88
- if hasattr(self, 'tokenizer') and self.tokenizer is not None:
89
- del self.tokenizer
90
- logger.info("LLM tokenizer deleted.")
91
- if hasattr(self, 'text_pipeline') and self.text_pipeline is not None:
92
- del self.text_pipeline
93
- logger.info("LLM pipeline deleted.")
94
-
95
- # Reset attributes
96
- self.model = None
97
- self.tokenizer = None
98
- self.text_pipeline = None
99
- self.llm_loaded = False # Mark as not loaded
100
  self._cleanup_memory()
101
  logger.info("LLM components reset successfully.")
102
- except Exception as e:
103
- logger.error(f"!!! ERROR during LLM reset: {e}")
104
- logger.error(traceback.format_exc())
105
 
106
  def reset_whisper(self):
107
- """Explicitly resets the Whisper model."""
108
  logger.info("--- Attempting to reset Whisper ---")
109
  try:
110
- if hasattr(self, 'whisper_model') and self.whisper_model is not None:
111
- del self.whisper_model
112
- logger.info("Whisper model deleted.")
113
-
114
  self.whisper_model = None
115
- self.whisper_loaded = False # Mark as not loaded
116
  self._cleanup_memory()
117
  logger.info("Whisper component reset successfully.")
118
- except Exception as e:
119
- logger.error(f"!!! ERROR during Whisper reset: {e}")
120
- logger.error(traceback.format_exc())
121
 
122
  @spaces.GPU(duration=120)
123
  def initialize_llm(self):
124
- """Initialize LLM model with standard transformers"""
125
  logger.info("Attempting to initialize LLM.")
126
- if self.llm_loading:
127
- logger.info("LLM initialization already in progress. Skipping.")
128
- return True
129
- if self.llm_loaded:
130
- logger.info("LLM already initialized.")
131
- self.last_used = time.time()
132
- return True
133
-
134
- # Explicitly try to free Whisper memory before loading LLM
135
- # self.reset_whisper() # Optional: Uncomment if severe memory pressure
136
-
137
  self.llm_loading = True
138
  logger.info("Starting LLM initialization...")
139
  try:
140
  MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
141
  logger.info(f"Using LLM model: {MODEL_NAME}")
142
-
143
- logger.info("Loading LLM tokenizer...")
144
  self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, token=HUGGINGFACE_TOKEN, use_fast=True)
145
- logger.info("LLM tokenizer loaded.")
146
- if self.tokenizer.pad_token is None:
147
- self.tokenizer.pad_token = self.tokenizer.eos_token
148
-
149
- logger.info("Loading LLM model...")
150
- self.model = AutoModelForCausalLM.from_pretrained(
151
- MODEL_NAME, token=HUGGINGFACE_TOKEN, device_map="auto",
152
- torch_dtype=torch.float16, low_cpu_mem_usage=True,
153
- offload_folder="offload", offload_state_dict=True
154
- )
155
- logger.info("LLM model loaded.")
156
-
157
- logger.info("Creating LLM text generation pipeline...")
158
- self.text_pipeline = pipeline(
159
- "text-generation", model=self.model, tokenizer=self.tokenizer,
160
- torch_dtype=torch.float16, device_map="auto", max_length=1024
161
- )
162
- logger.info("LLM text generation pipeline created.")
163
-
164
  logger.info("LLM initialized successfully.")
165
- self.last_used = time.time()
166
- self.llm_loaded = True
167
- self.llm_loading = False
168
- return True
169
-
170
- except Exception as e:
171
- logger.error(f"!!! ERROR during LLM initialization: {str(e)}")
172
- logger.error(traceback.format_exc())
173
- logger.error("Resetting potentially partially loaded LLM components due to error.")
174
- self.reset_llm() # Use the specific reset function
175
- self.llm_loading = False
176
- raise
177
 
178
  @spaces.GPU(duration=120)
179
  def initialize_whisper(self):
180
- """Initialize Whisper model for audio transcription"""
181
  logger.info("Attempting to initialize Whisper.")
182
- if self.whisper_loading:
183
- logger.info("Whisper initialization already in progress. Skipping.")
184
- return True
185
- if self.whisper_loaded:
186
- logger.info("Whisper already initialized.")
187
- self.last_used = time.time()
188
- return True
189
-
190
- # Explicitly try to free LLM memory before loading Whisper
191
- # self.reset_llm() # Optional: Uncomment if severe memory pressure
192
-
193
  self.whisper_loading = True
194
  logger.info("Starting Whisper initialization...")
195
  try:
196
  WHISPER_MODEL_NAME = "tiny"
197
- logger.info(f"Loading Whisper model: {WHISPER_MODEL_NAME}")
198
- self.whisper_model = whisper.load_model(
199
- WHISPER_MODEL_NAME, device="cuda" if torch.cuda.is_available() else "cpu",
200
- download_root="/tmp/whisper"
201
- )
202
  logger.info(f"Whisper model '{WHISPER_MODEL_NAME}' loaded successfully.")
203
- self.last_used = time.time()
204
- self.whisper_loaded = True
205
- self.whisper_loading = False
206
- return True
207
- except Exception as e:
208
- logger.error(f"!!! ERROR during Whisper initialization: {str(e)}")
209
- logger.error(traceback.format_exc())
210
- logger.error("Resetting potentially partially loaded Whisper components due to error.")
211
- self.reset_whisper() # Use the specific reset function
212
- self.whisper_loading = False
213
- raise
214
 
215
  def check_llm_initialized(self):
216
- """Check if LLM is initialized and initialize if needed"""
217
  logger.info("Checking if LLM is initialized.")
218
  if not self.llm_loaded:
219
  logger.info("LLM not initialized, attempting initialization...")
220
- if not self.llm_loading:
221
- self.initialize_llm() # This will raise error if it fails
222
- logger.info("LLM initialization completed by check_llm_initialized.")
223
  else:
224
- logger.info("LLM initialization is already in progress. Waiting briefly.")
225
  time.sleep(10)
226
- if not self.llm_loaded:
227
- logger.error("LLM initialization timed out or failed after waiting.")
228
- raise RuntimeError("LLM initialization timed out or failed.")
229
- else:
230
- logger.info("LLM seems initialized now after waiting.")
231
- else:
232
- logger.info("LLM was already initialized.")
233
  self.last_used = time.time()
234
 
235
-
236
  def check_whisper_initialized(self):
237
- """Check if Whisper model is initialized and initialize if needed"""
238
  logger.info("Checking if Whisper is initialized.")
239
  if not self.whisper_loaded:
240
  logger.info("Whisper model not initialized, attempting initialization...")
241
- if not self.whisper_loading:
242
- self.initialize_whisper() # This will raise error if it fails
243
- logger.info("Whisper initialization completed by check_whisper_initialized.")
244
  else:
245
- logger.info("Whisper initialization is already in progress. Waiting briefly.")
246
  time.sleep(10)
247
- if not self.whisper_loaded:
248
- logger.error("Whisper initialization timed out or failed after waiting.")
249
- raise RuntimeError("Whisper initialization timed out or failed.")
250
- else:
251
- logger.info("Whisper seems initialized now after waiting.")
252
- else:
253
- logger.info("Whisper was already initialized.")
254
  self.last_used = time.time()
255
 
256
  def reset_models(self, force=False):
257
- """Reset models if idle or forced."""
258
- if force:
259
- logger.info("Forcing reset of all models.")
260
- self.reset_llm()
261
- self.reset_whisper()
262
-
263
 
264
  # Create global model manager instance
265
  logger.info("Creating global ModelManager instance.")
266
  model_manager = ModelManager()
267
 
268
-
269
  # --- Functions: download_social_media_video, convert_video_to_audio, etc. ---
270
- # --- These functions are kept exactly the same as the previous full version ---
271
- # --- with detailed logging. Paste them here. ---
272
-
273
  @lru_cache(maxsize=16)
274
  def download_social_media_video(url):
275
- """Download audio from a social media video URL."""
276
- logger.info(f"Attempting to download audio from social media URL: {url}")
277
  temp_dir = tempfile.mkdtemp()
278
  output_template = os.path.join(temp_dir, '%(id)s.%(ext)s')
279
  final_audio_file_path = None
280
- ydl_opts = {
281
- 'format': 'bestaudio/best', 'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192'}],
282
- 'outtmpl': output_template, 'quiet': True, 'no_warnings': True, 'nocheckcertificate': True, 'retries': 3, 'socket_timeout': 15, 'cachedir': False
283
- }
284
  try:
285
- logger.debug(f"yt-dlp options: {ydl_opts}")
286
- with yt_dlp.YoutubeDL(ydl_opts) as ydl:
287
- logger.debug("Extracting info and downloading...")
288
- info_dict = ydl.extract_info(url, download=True)
289
- logger.debug(f"yt-dlp extraction complete for {url}. ID: {info_dict.get('id')}")
290
- found_files = [f for f in os.listdir(temp_dir) if f.endswith('.mp3')]
291
- if found_files:
292
- final_audio_file_path = os.path.join(temp_dir, found_files[0])
293
- logger.debug(f"Found downloaded MP3: {final_audio_file_path}")
294
- else:
295
- logger.error(f"Could not find downloaded MP3 file in {temp_dir} for URL {url}")
296
- raise FileNotFoundError(f"Downloaded MP3 not found in {temp_dir}")
297
- logger.debug(f"Reading content of {final_audio_file_path}")
298
  with open(final_audio_file_path, 'rb') as f: audio_content = f.read()
299
- logger.debug("Saving audio content to a new temporary file...")
300
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_output_file:
301
- temp_output_file.write(audio_content)
302
- final_path_for_gradio = temp_output_file.name
303
- logger.info(f"Audio content saved to temporary file for processing: {final_path_for_gradio}")
304
  return final_path_for_gradio
305
- except yt_dlp.utils.DownloadError as e:
306
- logger.error(f"!!! yt-dlp download error for {url}: {str(e)}")
307
- return None
308
- except Exception as e:
309
- logger.error(f"!!! Unexpected error downloading video from {url}: {str(e)}")
310
- logger.error(traceback.format_exc())
311
- return None
312
  finally:
313
  if os.path.exists(temp_dir):
314
- logger.debug(f"Cleaning up temporary download directory: {temp_dir}")
315
- try:
316
- import shutil
317
- shutil.rmtree(temp_dir)
318
- except Exception as cleanup_e: logger.warning(f"Could not clean up {temp_dir}: {cleanup_e}")
319
 
320
  def convert_video_to_audio(video_file_path):
321
- """Convert a video file to audio using ffmpeg directly."""
322
- logger.info(f"Attempting to convert video to audio: {video_file_path}")
323
  output_file_path = None
324
  try:
325
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file: output_file_path = temp_file.name
326
- logger.debug(f"Output audio path will be: {output_file_path}")
327
  command = ["ffmpeg", "-i", video_file_path, "-vn", "-acodec", "libmp3lame", "-ab", "192k", "-ar", "44100", "-ac", "2", output_file_path, "-y", "-loglevel", "error"]
328
- logger.debug(f"Executing ffmpeg command: {' '.join(command)}")
329
- process = subprocess.run(command, check=True, capture_output=True, text=True, timeout=120)
330
- logger.debug(f"ffmpeg conversion successful for {video_file_path}.")
331
- if not os.path.exists(output_file_path) or os.path.getsize(output_file_path) == 0:
332
- logger.error(f"ffmpeg conversion failed: Output file '{output_file_path}' not created or is empty.")
333
- raise RuntimeError(f"ffmpeg conversion failed: Output file '{output_file_path}' not created or is empty.")
334
- logger.info(f"Video successfully converted to audio: {output_file_path}")
335
  return output_file_path
336
- except subprocess.CalledProcessError as e:
337
- logger.error(f"!!! ffmpeg command failed with exit code {e.returncode} for video: {video_file_path}")
338
- logger.error(f"ffmpeg stderr: {e.stderr}")
339
- if output_file_path and os.path.exists(output_file_path):
340
- try: os.remove(output_file_path)
341
- except: pass
342
- raise RuntimeError(f"ffmpeg conversion failed: {e.stderr}") from e
343
- except subprocess.TimeoutExpired as e:
344
- logger.error(f"!!! ffmpeg command timed out after {e.timeout} seconds for video: {video_file_path}")
345
- if output_file_path and os.path.exists(output_file_path):
346
- try: os.remove(output_file_path)
347
- except: pass
348
- raise RuntimeError(f"ffmpeg conversion timed out after {e.timeout} seconds.") from e
349
- except Exception as e:
350
- logger.error(f"!!! Error converting video '{video_file_path}': {str(e)}")
351
- logger.error(traceback.format_exc())
352
- if output_file_path and os.path.exists(output_file_path):
353
  try: os.remove(output_file_path)
354
  except: pass
355
- raise
356
 
357
  def preprocess_audio(input_audio_path):
358
- """Preprocess the audio file (e.g., normalize volume)."""
359
- logger.info(f"Attempting to preprocess audio file: {input_audio_path}")
360
  output_path = None
361
  try:
362
- if not os.path.exists(input_audio_path):
363
- logger.error(f"Input audio file for preprocessing not found: {input_audio_path}")
364
- raise FileNotFoundError(f"Input audio file not found: {input_audio_path}")
365
- logger.debug("Loading audio with pydub...")
366
  audio = AudioSegment.from_file(input_audio_path)
367
- logger.debug("Audio loaded.")
368
- # Optional normalization can be added here
369
- logger.debug("Exporting preprocessed audio...")
370
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
371
- output_path = temp_file.name
372
- audio.export(output_path, format="mp3")
373
- logger.info(f"Audio preprocessed and saved to: {output_path}")
374
  return output_path
375
- except FileNotFoundError as e:
376
- logger.error(f"!!! File not found during audio preprocessing: {e}")
377
- raise
378
- except Exception as e:
379
- logger.error(f"!!! Error preprocessing audio '{input_audio_path}': {str(e)}")
380
- logger.error(traceback.format_exc())
381
- if output_path and os.path.exists(output_path):
382
- try: os.remove(output_path)
383
- except: pass
384
- raise
385
 
386
  @spaces.GPU(duration=300)
387
  def transcribe_audio_or_video(file_input):
388
- """Transcribe an audio or video file (local path or Gradio File object)."""
389
- logger.info(f"--- Starting transcription process for input: {type(file_input)} ---")
390
- audio_file_to_transcribe = None; original_input_path = None
391
- temp_files_to_clean = []; processing_step = "Initialization"; transcription = ""
392
  try:
393
- processing_step = "Whisper Model Check"
394
- logger.info("Checking/Initializing Whisper model for transcription...")
395
- model_manager.check_whisper_initialized()
396
- logger.info("Whisper model is ready for transcription.")
397
  if file_input is None: return ""
398
- processing_step = "Input Type Handling"
399
- if isinstance(file_input, str):
400
- original_input_path = file_input
401
- if not os.path.exists(original_input_path): raise FileNotFoundError(f"Input file not found: {original_input_path}")
402
- input_path = original_input_path
403
- elif hasattr(file_input, 'name') and file_input.name:
404
- original_input_path = file_input.name
405
- if not os.path.exists(original_input_path): raise FileNotFoundError(f"Gradio temporary file not found: {original_input_path}")
406
- input_path = original_input_path
407
- else: raise TypeError("Invalid input type for transcription.")
408
- logger.debug(f"Input path identified: {input_path}")
409
  file_extension = os.path.splitext(input_path)[1].lower()
410
- logger.debug(f"File extension: {file_extension}")
411
- processing_step = "Video Conversion Check"
412
  if file_extension in ['.mp4', '.avi', '.mov', '.mkv', '.webm']:
413
- logger.info(f"Detected video file ({file_extension}), converting...")
414
  converted_audio_path = convert_video_to_audio(input_path)
415
  temp_files_to_clean.append(converted_audio_path); audio_file_to_process = converted_audio_path
416
- elif file_extension in ['.mp3', '.wav', '.ogg', '.flac', '.m4a', '.aac']:
417
- logger.info(f"Detected audio file ({file_extension}).")
418
- audio_file_to_process = input_path
419
- else: raise ValueError(f"Unsupported file type: {file_extension}")
420
- processing_step = "Audio Preprocessing"
421
  try:
422
- logger.debug(f"Attempting to preprocess audio file: {audio_file_to_process}")
423
  preprocessed_audio_path = preprocess_audio(audio_file_to_process)
424
  if preprocessed_audio_path != audio_file_to_process: temp_files_to_clean.append(preprocessed_audio_path)
425
  audio_file_to_transcribe = preprocessed_audio_path
426
- logger.debug(f"Using preprocessed audio: {audio_file_to_transcribe}")
427
- except Exception as preprocess_err:
428
- logger.warning(f"Audio preprocessing failed: {preprocess_err}. Using original/converted audio.")
429
- audio_file_to_transcribe = audio_file_to_process
430
- processing_step = "Transcription Execution"
431
- logger.info(f"Starting transcription execution for: {audio_file_to_transcribe}")
432
- if not os.path.exists(audio_file_to_transcribe): raise FileNotFoundError(f"Audio file to transcribe not found: {audio_file_to_transcribe}")
433
- logger.debug("Calling Whisper model transcribe method...")
434
  with torch.inference_mode():
435
- use_fp16 = torch.cuda.is_available(); logger.debug(f"Using fp16: {use_fp16}")
436
  result = model_manager.whisper_model.transcribe(audio_file_to_transcribe, fp16=use_fp16)
437
- logger.debug("Whisper transcribe method finished.")
438
- if not result or "text" not in result: raise RuntimeError("Transcription failed to produce results")
439
- transcription = result.get("text", "Error: Transcription result empty")
440
- logger.info(f"Transcription completed successfully: '{transcription[:100]}...'")
441
- processing_step = "Success"
442
- except FileNotFoundError as e:
443
- logger.error(f"!!! File not found error (Step: {processing_step}): {e}"); transcription = f"Error: Input file not found ({e})"
444
- except ValueError as e:
445
- logger.error(f"!!! Value error (Step: {processing_step}): {e}"); transcription = f"Error: Unsupported file type ({e})"
446
- except TypeError as e:
447
- logger.error(f"!!! Type error (Step: {processing_step}): {e}"); transcription = f"Error: Invalid input provided ({e})"
448
- except RuntimeError as e:
449
- logger.error(f"!!! Runtime error (Step: {processing_step}): {e}"); logger.error(traceback.format_exc()); transcription = f"Error during processing: {e}"
450
- except Exception as e:
451
- logger.error(f"!!! Unexpected error (Step: {processing_step}): {str(e)}"); logger.error(traceback.format_exc()); transcription = f"Error processing the file: An unexpected error occurred."
452
  finally:
453
- logger.debug(f"--- Cleaning up {len(temp_files_to_clean)} temp files for transcription ---")
454
  for temp_file in temp_files_to_clean:
455
  try:
456
- if os.path.exists(temp_file): os.remove(temp_file); logger.debug(f"Cleaned: {temp_file}")
457
- except Exception as e: logger.warning(f"Could not remove temp file {temp_file}: {e}")
458
- logger.debug("--- Finished transcription cleanup ---")
459
  return transcription
460
 
461
  @lru_cache(maxsize=16)
462
  def read_document(document_path):
463
- """Read the content of a document (PDF, DOCX, XLSX, CSV)."""
464
- logger.info(f"Attempting to read document: {document_path}")
465
  try:
466
- if not os.path.exists(document_path): raise FileNotFoundError(f"Document not found: {document_path}")
467
- file_extension = os.path.splitext(document_path)[1].lower(); logger.debug(f"Doc type: {file_extension}")
468
  content = ""
469
- if file_extension == ".pdf":
470
- logger.debug("Reading PDF using PyMuPDF...")
471
  doc = fitz.open(document_path)
472
- if doc.is_encrypted:
473
- logger.warning(f"PDF {document_path} encrypted. Trying empty password.")
474
- if not doc.authenticate(""): raise ValueError("Encrypted PDF cannot be read.")
475
  content = "\n".join([page.get_text() for page in doc]); doc.close()
476
- elif file_extension == ".docx":
477
- logger.debug("Reading DOCX using python-docx...")
478
- doc = docx.Document(document_path); content = "\n".join([p.text for p in doc.paragraphs])
479
- elif file_extension in (".xlsx", ".xls"):
480
- logger.debug("Reading Excel using pandas...")
481
- xls = pd.ExcelFile(document_path); text_parts = []
482
- for sheet_name in xls.sheet_names:
483
- logger.debug(f"Reading sheet: {sheet_name}")
484
- df = pd.read_excel(xls, sheet_name=sheet_name); text_parts.append(f"--- Sheet: {sheet_name} ---\n{df.to_string()}")
485
- content = "\n\n".join(text_parts).strip()
486
- elif file_extension == ".csv":
487
- logger.debug("Reading CSV using pandas...")
488
  try:
489
- with open(document_path, 'rb') as f: import chardet; encoding = chardet.detect(f.read())['encoding']
490
- logger.debug(f"Detected CSV encoding: {encoding}")
491
- df = pd.read_csv(document_path, encoding=encoding)
492
- except (pd.errors.ParserError, UnicodeDecodeError, LookupError) as e1:
493
- logger.warning(f"CSV parse failed ({e1}), trying semicolon.")
494
- try: df = pd.read_csv(document_path, sep=';', encoding=encoding)
495
- except Exception as e2:
496
- logger.error(f"Also failed with semicolon ({e2}). Trying latin1.")
497
- try: df = pd.read_csv(document_path, encoding='latin1')
498
- except Exception as e3: raise ValueError(f"Failed to parse CSV: {e1}, {e2}, {e3}")
499
  content = df.to_string()
500
- else: return "Unsupported file type. Please upload a PDF, DOCX, XLSX or CSV document."
501
- logger.info(f"Document read successfully. Length: {len(content)} chars.")
502
  return content
503
- except FileNotFoundError as e: logger.error(f"!!! File not found reading doc: {e}"); return f"Error: Document file not found: {e}"
504
- except ValueError as e: logger.error(f"!!! Value error reading doc: {e}"); return f"Error reading document: {e}"
505
- except Exception as e: logger.error(f"!!! Error reading doc: {str(e)}"); logger.error(traceback.format_exc()); return f"Error reading document: {str(e)}"
506
 
507
  @lru_cache(maxsize=16)
508
  def read_url(url):
509
- """Read the main textual content of a URL."""
510
- logger.info(f"Attempting to read URL: {url}")
511
  if not url or not url.strip().startswith('http'): return ""
512
  try:
513
- headers = {'User-Agent': 'Mozilla/5.0 ... Chrome/91...', 'Accept': 'text/html...', 'Accept-Language': 'en-US,en;q=0.9', 'Connection': 'keep-alive'}
514
- logger.debug(f"Sending GET to {url}")
515
  response = requests.get(url, headers=headers, timeout=20, allow_redirects=True)
516
- logger.debug(f"Response from {url}: {response.status_code}, CT: {response.headers.get('content-type')}")
517
  response.raise_for_status()
518
- content_type = response.headers.get('content-type', '').lower()
519
- if not ('html' in content_type or 'text' in content_type): return f"Error: URL content type ({content_type}) is not text/html."
520
- detected_encoding = response.encoding if response.encoding else response.apparent_encoding
521
- html_content = response.content.decode(detected_encoding or 'utf-8', errors='ignore')
522
- logger.debug(f"Parsing HTML ({len(html_content)} bytes) from {url}...")
523
- soup = BeautifulSoup(html_content, 'html.parser')
524
- tags_to_remove = ["script", "style", "meta", "noscript", "iframe", "header", "footer", "nav", "aside", "form", "button", "link", "head"]
525
- for tag_name in tags_to_remove:
526
- for element in soup.find_all(tag_name): element.extract()
527
- logger.debug("Finding main content container...")
528
- main_content = (soup.find("main") or soup.find("article") or soup.find("div", class_=["content", "main", "post-content", "entry-content", "article-body", "story-content"]) or soup.find("div", id=["content", "main", "article", "story"]))
529
- text = ""
530
- if main_content: text = main_content.get_text(separator='\n', strip=True)
531
- else:
532
- body = soup.find("body")
533
- if body: text = body.get_text(separator='\n', strip=True)
534
- else: text = soup.get_text(separator='\n', strip=True)
535
- lines = [line.strip() for line in text.split('\n') if line.strip()]; cleaned_text = "\n".join(lines)
536
- if not cleaned_text: return "Error: Could not extract text content from URL."
537
- max_chars = 15000
538
- final_text = (cleaned_text[:max_chars] + "... [content truncated]") if len(cleaned_text) > max_chars else cleaned_text
539
- logger.info(f"Successfully read URL {url}. Final length: {len(final_text)}")
540
- return final_text
541
- except requests.exceptions.RequestException as e: logger.error(f"!!! Error fetching URL {url}: {e}"); return f"Error reading URL: Could not fetch content ({e})"
542
- except Exception as e: logger.error(f"!!! Error parsing URL {url}: {e}"); logger.error(traceback.format_exc()); return f"Error reading URL: Could not parse content ({e})"
543
 
544
  def process_social_media_url(url):
545
- """Process a social media URL, attempting to get text and transcribe video/audio."""
546
- logger.info(f"--- Starting processing for social media URL: {url} ---")
547
  if not url or not url.strip().startswith('http'): return None
548
- text_content = None; video_transcription = None; temp_audio_file = None
549
- try:
550
- logger.debug(f"Attempting text read from social URL: {url}")
551
- text_content_result = read_url(url)
552
- if text_content_result and not text_content_result.startswith("Error:"): text_content = text_content_result; logger.debug("Text read success.")
553
- elif text_content_result: logger.warning(f"read_url error for {url}: {text_content_result}")
554
- else: logger.debug("No text via read_url.")
555
- except Exception as e: logger.error(f"!!! Exception text reading social URL {url}: {e}"); logger.error(traceback.format_exc())
556
  try:
557
- logger.debug(f"Attempting audio download from social URL: {url}")
558
- temp_audio_file = download_social_media_video(url)
559
- if temp_audio_file:
560
- logger.info(f"Audio downloaded from {url} to {temp_audio_file}. Transcribing...")
561
- transcription_result = transcribe_audio_or_video(temp_audio_file)
562
- if transcription_result and not transcription_result.startswith("Error"): video_transcription = transcription_result; logger.info("Transcription success.")
563
- elif transcription_result: logger.warning(f"Transcription error for {url}: {transcription_result}")
564
- else: logger.warning(f"Empty transcription for {url}.")
565
- else: logger.debug("No downloadable audio found.")
566
- except Exception as e: logger.error(f"!!! Exception audio processing social URL {url}: {e}"); logger.error(traceback.format_exc())
567
  finally:
568
- if temp_audio_file and os.path.exists(temp_audio_file):
569
- logger.debug(f"Cleaning up social temp audio: {temp_audio_file}")
570
- try: os.remove(temp_audio_file)
571
- except Exception as e: logger.warning(f"Failed cleanup {temp_audio_file}: {e}")
572
- logger.debug(f"--- Finished processing social URL: {url} ---")
573
- if text_content or video_transcription: return {"text": text_content or "", "video": video_transcription or ""}
574
- else: logger.info(f"No usable content retrieved for social URL: {url}"); return None
575
 
576
  # ==============================================================
577
  # ========= SIMPLIFIED generate_news FOR DEBUGGING =============
@@ -613,7 +374,8 @@ def generate_news(instructions, facts, size, tone, *args):
613
  # ==============================================================
614
 
615
 
616
- # --- create_demo function remains the same as the previous version ---
 
617
  def create_demo():
618
  """Creates the Gradio interface"""
619
  logger.info("--- Creating Gradio interface ---")
@@ -623,55 +385,46 @@ def create_demo():
623
  all_inputs = []
624
  with gr.Row():
625
  with gr.Column(scale=2):
626
- logger.info("Creating instruction input.")
627
  instructions = gr.Textbox(label="Instructions for the News Article", placeholder="Enter specific instructions...", lines=2)
628
  all_inputs.append(instructions)
629
- logger.info("Creating facts input.")
630
  facts = gr.Textbox(label="Main Facts", placeholder="Describe the most important facts...", lines=4)
631
  all_inputs.append(facts)
632
  with gr.Row():
633
- logger.info("Creating size slider.")
634
  size_slider = gr.Slider(label="Approximate Length (words)", minimum=100, maximum=700, value=250, step=50)
635
  all_inputs.append(size_slider)
636
- logger.info("Creating tone dropdown.")
637
  tone_dropdown = gr.Dropdown(label="Tone of the News Article", choices=["neutral", "serious", "formal", "urgent", "investigative", "human-interest", "lighthearted"], value="neutral")
638
  all_inputs.append(tone_dropdown)
639
  with gr.Column(scale=3):
640
  with gr.Tabs():
641
  with gr.TabItem("πŸ“ Documents"):
642
- logger.info("Creating document input tabs.")
643
  gr.Markdown("Upload relevant documents (PDF, DOCX, XLSX, CSV). Max 5.")
644
  doc_inputs = []
645
  for i in range(1, 6):
646
- doc_file = gr.File(label=f"Document {i}", file_types=["pdf", ".docx", ".xlsx", ".csv"], file_count="single")
 
647
  doc_inputs.append(doc_file)
648
  all_inputs.extend(doc_inputs)
649
- logger.info(f"{len(doc_inputs)} document inputs created.")
650
  with gr.TabItem("πŸ”Š Audio/Video"):
651
- logger.info("Creating audio/video input tabs.")
652
  gr.Markdown("Upload audio or video files... Max 5 sources.")
653
  audio_video_inputs = []
654
  for i in range(1, 6):
655
  with gr.Group():
656
  gr.Markdown(f"**Source {i}**")
657
- audio_file = gr.File(label=f"Audio/Video File {i}", file_types=["audio", "video"])
 
658
  with gr.Row():
659
  speaker_name = gr.Textbox(label="Speaker Name", placeholder="Name...")
660
  speaker_role = gr.Textbox(label="Role/Position", placeholder="Role...")
661
  audio_video_inputs.extend([audio_file, speaker_name, speaker_role])
662
  all_inputs.extend(audio_video_inputs)
663
- logger.info(f"{len(audio_video_inputs)} audio/video inputs created.")
664
  with gr.TabItem("🌐 URLs"):
665
- logger.info("Creating URL input tabs.")
666
  gr.Markdown("Add URLs to relevant web pages... Max 5.")
667
  url_inputs = []
668
  for i in range(1, 6):
669
  url_textbox = gr.Textbox(label=f"URL {i}", placeholder="https://...")
670
  url_inputs.append(url_textbox)
671
  all_inputs.extend(url_inputs)
672
- logger.info(f"{len(url_inputs)} URL inputs created.")
673
  with gr.TabItem("πŸ“± Social Media"):
674
- logger.info("Creating social media input tabs.")
675
  gr.Markdown("Add URLs to social media posts... Max 3.")
676
  social_inputs = []
677
  for i in range(1, 4):
@@ -683,26 +436,17 @@ def create_demo():
683
  social_context_textbox = gr.Textbox(label=f"Context", placeholder="Context...")
684
  social_inputs.extend([social_url_textbox, social_name_textbox, social_context_textbox])
685
  all_inputs.extend(social_inputs)
686
- logger.info(f"{len(social_inputs)} social media inputs created.")
687
 
688
- logger.info(f"Total number of input components collected: {len(all_inputs)}")
689
- with gr.Row():
690
- logger.info("Creating generate and clear buttons.")
691
- generate_button = gr.Button("✨ Generate News Article", variant="primary")
692
- clear_button = gr.Button("πŸ”„ Clear All Inputs")
693
  with gr.Tabs():
694
  with gr.TabItem("πŸ“„ Generated News Article"):
695
- logger.info("Creating news output textbox.")
696
  news_output = gr.Textbox(label="Draft News Article", lines=20, show_copy_button=True, interactive=False)
697
  with gr.TabItem("πŸŽ™οΈ Source Transcriptions & Logs"):
698
- logger.info("Creating transcriptions/log output textbox.")
699
  transcriptions_output = gr.Textbox(label="Transcriptions and Processing Log", lines=15, show_copy_button=True, interactive=False)
700
 
701
  outputs_list = [news_output, transcriptions_output]
702
- logger.info("Setting up event handlers.")
703
- # AsegΓΊrate de que el botΓ³n llama a la funciΓ³n generate_news (aunque ahora estΓ© simplificada)
704
  generate_button.click(fn=generate_news, inputs=all_inputs, outputs=outputs_list)
705
- logger.info("Generate button click handler set.")
706
 
707
  def clear_all_inputs_and_outputs():
708
  logger.info("--- Clear All button clicked ---")
@@ -713,31 +457,21 @@ def create_demo():
713
  elif isinstance(input_comp, gr.File): reset_values.append(None)
714
  else: reset_values.append(None)
715
  reset_values.extend(["", ""])
716
- logger.info(f"Generated {len(reset_values)} reset values for UI components.")
717
- try:
718
- logger.info("Calling model reset from clear button handler.")
719
- model_manager.reset_models(force=True)
720
- except Exception as e:
721
- logger.error(f"Error resetting models during clear operation: {e}")
722
- logger.error(traceback.format_exc())
723
  logger.info("--- Clear All operation finished ---")
724
  return reset_values
725
 
726
  clear_button.click(fn=clear_all_inputs_and_outputs, inputs=None, outputs=all_inputs + outputs_list)
727
- logger.info("Clear button click handler set.")
728
- logger.info("--- Gradio interface creation complete ---")
729
  return demo
730
 
731
 
732
  # --- main execution block remains the same ---
733
  if __name__ == "__main__":
734
  logger.info("--- Running main execution block ---")
735
- logger.info("Creating Gradio demo instance...")
736
  news_demo = create_demo()
737
- logger.info("Gradio demo instance created.")
738
- logger.info("Configuring Gradio queue...")
739
  news_demo.queue()
740
- logger.info("Gradio queue configured.")
741
  logger.info("Launching Gradio interface...")
742
  try:
743
  news_demo.launch(server_name="0.0.0.0", server_port=7860)
@@ -745,4 +479,4 @@ if __name__ == "__main__":
745
  except Exception as launch_err:
746
  logger.error(f"!!! CRITICAL Error during Gradio launch: {launch_err}")
747
  logger.error(traceback.format_exc())
748
- logger.info("--- Main execution block potentially finished (if launch doesn't block indefinitely) ---")
 
22
 
23
  # Configure logging
24
  logging.basicConfig(
25
+ level=logging.INFO,
26
+ format='%(asctime)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s'
27
  )
28
  logger = logging.getLogger(__name__)
29
 
30
+ logger.info("--- Starting App ---")
31
 
32
  # Login to Hugging Face Hub if token is available
33
  HUGGINGFACE_TOKEN = os.environ.get('HUGGINGFACE_TOKEN')
 
40
  logger.error(f"Failed to login to Hugging Face Hub: {e}")
41
  logger.error(traceback.format_exc())
42
  else:
43
+ logger.warning("HUGGINGFACE_TOKEN environment variable not set.")
44
 
45
 
46
  class ModelManager:
 
54
  return cls._instance
55
 
56
  def __init__(self):
57
+ if not hasattr(self, '_initialized') or not self._initialized:
58
  logger.info("Initializing ModelManager attributes.")
59
  self.tokenizer = None
60
  self.model = None
 
65
  self.last_used = time.time()
66
  self.llm_loading = False
67
  self.whisper_loading = False
68
+ self._initialized = True
69
 
70
  def _cleanup_memory(self):
 
71
  logger.info("Running garbage collection...")
72
  collected_count = gc.collect()
73
  logger.info(f"Garbage collected ({collected_count} objects).")
 
77
  logger.info("CUDA cache cleared.")
78
 
79
  def reset_llm(self):
 
80
  logger.info("--- Attempting to reset LLM ---")
81
  try:
82
+ if hasattr(self, 'model') and self.model is not None: del self.model; logger.info("LLM model deleted.")
83
+ if hasattr(self, 'tokenizer') and self.tokenizer is not None: del self.tokenizer; logger.info("LLM tokenizer deleted.")
84
+ if hasattr(self, 'text_pipeline') and self.text_pipeline is not None: del self.text_pipeline; logger.info("LLM pipeline deleted.")
85
+ self.model = None; self.tokenizer = None; self.text_pipeline = None
86
+ self.llm_loaded = False
 
 
 
 
 
 
 
 
 
 
 
87
  self._cleanup_memory()
88
  logger.info("LLM components reset successfully.")
89
+ except Exception as e: logger.error(f"!!! ERROR during LLM reset: {e}"); logger.error(traceback.format_exc())
 
 
90
 
91
  def reset_whisper(self):
 
92
  logger.info("--- Attempting to reset Whisper ---")
93
  try:
94
+ if hasattr(self, 'whisper_model') and self.whisper_model is not None: del self.whisper_model; logger.info("Whisper model deleted.")
 
 
 
95
  self.whisper_model = None
96
+ self.whisper_loaded = False
97
  self._cleanup_memory()
98
  logger.info("Whisper component reset successfully.")
99
+ except Exception as e: logger.error(f"!!! ERROR during Whisper reset: {e}"); logger.error(traceback.format_exc())
 
 
100
 
101
  @spaces.GPU(duration=120)
102
  def initialize_llm(self):
 
103
  logger.info("Attempting to initialize LLM.")
104
+ if self.llm_loading: logger.info("LLM initialization already in progress."); return True
105
+ if self.llm_loaded: logger.info("LLM already initialized."); self.last_used = time.time(); return True
 
 
 
 
 
 
 
 
 
106
  self.llm_loading = True
107
  logger.info("Starting LLM initialization...")
108
  try:
109
  MODEL_NAME = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
110
  logger.info(f"Using LLM model: {MODEL_NAME}")
 
 
111
  self.tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, token=HUGGINGFACE_TOKEN, use_fast=True)
112
+ if self.tokenizer.pad_token is None: self.tokenizer.pad_token = self.tokenizer.eos_token
113
+ self.model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, token=HUGGINGFACE_TOKEN, device_map="auto", torch_dtype=torch.float16, low_cpu_mem_usage=True, offload_folder="offload", offload_state_dict=True)
114
+ self.text_pipeline = pipeline("text-generation", model=self.model, tokenizer=self.tokenizer, torch_dtype=torch.float16, device_map="auto", max_length=1024)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
115
  logger.info("LLM initialized successfully.")
116
+ self.last_used = time.time(); self.llm_loaded = True; self.llm_loading = False; return True
117
+ except Exception as e: logger.error(f"!!! ERROR during LLM initialization: {e}"); logger.error(traceback.format_exc()); self.reset_llm(); self.llm_loading = False; raise
 
 
 
 
 
 
 
 
 
 
118
 
119
  @spaces.GPU(duration=120)
120
  def initialize_whisper(self):
 
121
  logger.info("Attempting to initialize Whisper.")
122
+ if self.whisper_loading: logger.info("Whisper initialization already in progress."); return True
123
+ if self.whisper_loaded: logger.info("Whisper already initialized."); self.last_used = time.time(); return True
 
 
 
 
 
 
 
 
 
124
  self.whisper_loading = True
125
  logger.info("Starting Whisper initialization...")
126
  try:
127
  WHISPER_MODEL_NAME = "tiny"
128
+ self.whisper_model = whisper.load_model(WHISPER_MODEL_NAME, device="cuda" if torch.cuda.is_available() else "cpu", download_root="/tmp/whisper")
 
 
 
 
129
  logger.info(f"Whisper model '{WHISPER_MODEL_NAME}' loaded successfully.")
130
+ self.last_used = time.time(); self.whisper_loaded = True; self.whisper_loading = False; return True
131
+ except Exception as e: logger.error(f"!!! ERROR during Whisper initialization: {e}"); logger.error(traceback.format_exc()); self.reset_whisper(); self.whisper_loading = False; raise
 
 
 
 
 
 
 
 
 
132
 
133
  def check_llm_initialized(self):
 
134
  logger.info("Checking if LLM is initialized.")
135
  if not self.llm_loaded:
136
  logger.info("LLM not initialized, attempting initialization...")
137
+ if not self.llm_loading: self.initialize_llm(); logger.info("LLM initialization completed by check_llm_initialized.")
 
 
138
  else:
139
+ logger.info("LLM initialization already in progress. Waiting briefly.")
140
  time.sleep(10)
141
+ if not self.llm_loaded: raise RuntimeError("LLM initialization timed out or failed after waiting.")
142
+ else: logger.info("LLM seems initialized now after waiting.")
143
+ else: logger.info("LLM was already initialized.")
 
 
 
 
144
  self.last_used = time.time()
145
 
 
146
  def check_whisper_initialized(self):
 
147
  logger.info("Checking if Whisper is initialized.")
148
  if not self.whisper_loaded:
149
  logger.info("Whisper model not initialized, attempting initialization...")
150
+ if not self.whisper_loading: self.initialize_whisper(); logger.info("Whisper initialization completed by check_whisper_initialized.")
 
 
151
  else:
152
+ logger.info("Whisper initialization already in progress. Waiting briefly.")
153
  time.sleep(10)
154
+ if not self.whisper_loaded: raise RuntimeError("Whisper initialization timed out or failed after waiting.")
155
+ else: logger.info("Whisper seems initialized now after waiting.")
156
+ else: logger.info("Whisper was already initialized.")
 
 
 
 
157
  self.last_used = time.time()
158
 
159
  def reset_models(self, force=False):
160
+ if force: logger.info("Forcing reset of all models."); self.reset_llm(); self.reset_whisper()
 
 
 
 
 
161
 
162
  # Create global model manager instance
163
  logger.info("Creating global ModelManager instance.")
164
  model_manager = ModelManager()
165
 
 
166
  # --- Functions: download_social_media_video, convert_video_to_audio, etc. ---
167
+ # --- Kept exactly the same as the previous full version ---
 
 
168
  @lru_cache(maxsize=16)
169
  def download_social_media_video(url):
170
+ logger.info(f"Attempting social download: {url}")
 
171
  temp_dir = tempfile.mkdtemp()
172
  output_template = os.path.join(temp_dir, '%(id)s.%(ext)s')
173
  final_audio_file_path = None
174
+ ydl_opts = {'format': 'bestaudio/best', 'postprocessors': [{'key': 'FFmpegExtractAudio', 'preferredcodec': 'mp3', 'preferredquality': '192'}], 'outtmpl': output_template, 'quiet': True, 'no_warnings': True, 'nocheckcertificate': True, 'retries': 3, 'socket_timeout': 15, 'cachedir': False}
 
 
 
175
  try:
176
+ with yt_dlp.YoutubeDL(ydl_opts) as ydl: info_dict = ydl.extract_info(url, download=True)
177
+ found_files = [f for f in os.listdir(temp_dir) if f.endswith('.mp3')]
178
+ if not found_files: raise FileNotFoundError(f"Downloaded MP3 not found in {temp_dir}")
179
+ final_audio_file_path = os.path.join(temp_dir, found_files[0])
 
 
 
 
 
 
 
 
 
180
  with open(final_audio_file_path, 'rb') as f: audio_content = f.read()
 
181
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_output_file:
182
+ temp_output_file.write(audio_content); final_path_for_gradio = temp_output_file.name
183
+ logger.info(f"Social audio saved to: {final_path_for_gradio}")
 
184
  return final_path_for_gradio
185
+ except yt_dlp.utils.DownloadError as e: logger.error(f"yt-dlp error {url}: {e}"); return None
186
+ except Exception as e: logger.error(f"Download error {url}: {e}"); logger.error(traceback.format_exc()); return None
 
 
 
 
 
187
  finally:
188
  if os.path.exists(temp_dir):
189
+ try: import shutil; shutil.rmtree(temp_dir)
190
+ except Exception as cleanup_e: logger.warning(f"Cleanup failed {temp_dir}: {cleanup_e}")
 
 
 
191
 
192
  def convert_video_to_audio(video_file_path):
193
+ logger.info(f"Converting video: {video_file_path}")
 
194
  output_file_path = None
195
  try:
196
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file: output_file_path = temp_file.name
 
197
  command = ["ffmpeg", "-i", video_file_path, "-vn", "-acodec", "libmp3lame", "-ab", "192k", "-ar", "44100", "-ac", "2", output_file_path, "-y", "-loglevel", "error"]
198
+ subprocess.run(command, check=True, capture_output=True, text=True, timeout=120)
199
+ if not os.path.exists(output_file_path) or os.path.getsize(output_file_path) == 0: raise RuntimeError("ffmpeg output empty")
200
+ logger.info(f"Video converted to: {output_file_path}")
 
 
 
 
201
  return output_file_path
202
+ except subprocess.CalledProcessError as e: logger.error(f"ffmpeg fail {video_file_path}: {e.stderr}"); raise RuntimeError(f"ffmpeg failed: {e.stderr}") from e
203
+ except subprocess.TimeoutExpired as e: logger.error(f"ffmpeg timeout {video_file_path}"); raise RuntimeError("ffmpeg timed out") from e
204
+ except Exception as e: logger.error(f"Video conversion error {video_file_path}: {e}"); logger.error(traceback.format_exc()); raise
205
+ finally:
206
+ if output_file_path and os.path.exists(output_file_path) and ( 'e' in locals() or (not os.path.exists(output_file_path) or os.path.getsize(output_file_path) == 0)):
 
 
 
 
 
 
 
 
 
 
 
 
207
  try: os.remove(output_file_path)
208
  except: pass
 
209
 
210
  def preprocess_audio(input_audio_path):
211
+ logger.info(f"Preprocessing audio: {input_audio_path}")
 
212
  output_path = None
213
  try:
214
+ if not os.path.exists(input_audio_path): raise FileNotFoundError(f"Preprocessing input not found: {input_audio_path}")
 
 
 
215
  audio = AudioSegment.from_file(input_audio_path)
 
 
 
216
  with tempfile.NamedTemporaryFile(delete=False, suffix=".mp3") as temp_file:
217
+ output_path = temp_file.name; audio.export(output_path, format="mp3")
218
+ logger.info(f"Audio preprocessed to: {output_path}")
 
219
  return output_path
220
+ except FileNotFoundError as e: logger.error(f"Preprocessing file not found: {e}"); raise
221
+ except Exception as e: logger.error(f"Preprocessing error {input_audio_path}: {e}"); logger.error(traceback.format_exc()); raise
222
+ finally:
223
+ if 'e' in locals() and output_path and os.path.exists(output_path):
224
+ try: os.remove(output_path)
225
+ except: pass
 
 
 
 
226
 
227
  @spaces.GPU(duration=300)
228
  def transcribe_audio_or_video(file_input):
229
+ logger.info(f"--- Starting transcription: {type(file_input)} ---")
230
+ audio_file_to_transcribe = None; temp_files_to_clean = []; transcription = ""
 
 
231
  try:
232
+ logger.info("Checking Whisper model..."); model_manager.check_whisper_initialized()
 
 
 
233
  if file_input is None: return ""
234
+ if isinstance(file_input, str): input_path = file_input
235
+ elif hasattr(file_input, 'name') and file_input.name: input_path = file_input.name
236
+ else: raise TypeError("Invalid input type.")
237
+ if not os.path.exists(input_path): raise FileNotFoundError(f"Input not found: {input_path}")
 
 
 
 
 
 
 
238
  file_extension = os.path.splitext(input_path)[1].lower()
 
 
239
  if file_extension in ['.mp4', '.avi', '.mov', '.mkv', '.webm']:
 
240
  converted_audio_path = convert_video_to_audio(input_path)
241
  temp_files_to_clean.append(converted_audio_path); audio_file_to_process = converted_audio_path
242
+ elif file_extension in ['.mp3', '.wav', '.ogg', '.flac', '.m4a', '.aac']: audio_file_to_process = input_path
243
+ else: raise ValueError(f"Unsupported type: {file_extension}")
 
 
 
244
  try:
 
245
  preprocessed_audio_path = preprocess_audio(audio_file_to_process)
246
  if preprocessed_audio_path != audio_file_to_process: temp_files_to_clean.append(preprocessed_audio_path)
247
  audio_file_to_transcribe = preprocessed_audio_path
248
+ except Exception as preprocess_err: logger.warning(f"Preprocessing failed ({preprocess_err}), using original."); audio_file_to_transcribe = audio_file_to_process
249
+ if not os.path.exists(audio_file_to_transcribe): raise FileNotFoundError(f"File to transcribe lost: {audio_file_to_transcribe}")
250
+ logger.info(f"Transcribing: {audio_file_to_transcribe}")
 
 
 
 
 
251
  with torch.inference_mode():
252
+ use_fp16 = torch.cuda.is_available()
253
  result = model_manager.whisper_model.transcribe(audio_file_to_transcribe, fp16=use_fp16)
254
+ if not result or "text" not in result: raise RuntimeError("Transcription empty result")
255
+ transcription = result.get("text", "")
256
+ logger.info(f"Transcription success: '{transcription[:100]}...'")
257
+ except Exception as e: logger.error(f"!!! Transcription failed: {e}"); logger.error(traceback.format_exc()); transcription = f"Error during transcription: {e}"
 
 
 
 
 
 
 
 
 
 
 
258
  finally:
259
+ logger.debug(f"--- Cleaning {len(temp_files_to_clean)} temp transcription files ---")
260
  for temp_file in temp_files_to_clean:
261
  try:
262
+ if os.path.exists(temp_file): os.remove(temp_file)
263
+ except Exception as e: logger.warning(f"Cleanup failed {temp_file}: {e}")
 
264
  return transcription
265
 
266
  @lru_cache(maxsize=16)
267
  def read_document(document_path):
268
+ logger.info(f"Reading document: {document_path}")
 
269
  try:
270
+ if not os.path.exists(document_path): raise FileNotFoundError(f"Doc not found: {document_path}")
271
+ ext = os.path.splitext(document_path)[1].lower(); logger.debug(f"Doc type: {ext}")
272
  content = ""
273
+ if ext == ".pdf":
 
274
  doc = fitz.open(document_path)
275
+ if doc.is_encrypted and not doc.authenticate(""): raise ValueError("Encrypted PDF")
 
 
276
  content = "\n".join([page.get_text() for page in doc]); doc.close()
277
+ elif ext == ".docx": doc = docx.Document(document_path); content = "\n".join([p.text for p in doc.paragraphs])
278
+ elif ext in (".xlsx", ".xls"):
279
+ xls = pd.ExcelFile(document_path); parts = []
280
+ for sheet in xls.sheet_names: df = pd.read_excel(xls, sheet_name=sheet); parts.append(f"--- {sheet} ---\n{df.to_string()}")
281
+ content = "\n\n".join(parts).strip()
282
+ elif ext == ".csv":
 
 
 
 
 
 
283
  try:
284
+ with open(document_path, 'rb') as f: import chardet; enc = chardet.detect(f.read())['encoding']
285
+ df = pd.read_csv(document_path, encoding=enc)
286
+ except Exception as e1:
287
+ logger.warning(f"CSV parse failed ({e1}), trying alternatives...")
288
+ try: df = pd.read_csv(document_path, sep=';', encoding=enc)
289
+ except Exception as e2: df = pd.read_csv(document_path, encoding='latin1') # Last resort
 
 
 
 
290
  content = df.to_string()
291
+ else: return "Unsupported file type."
292
+ logger.info(f"Doc read success. Length: {len(content)}")
293
  return content
294
+ except Exception as e: logger.error(f"!!! Read doc error: {e}"); logger.error(traceback.format_exc()); return f"Error reading document: {e}"
 
 
295
 
296
  @lru_cache(maxsize=16)
297
  def read_url(url):
298
+ logger.info(f"Reading URL: {url}")
 
299
  if not url or not url.strip().startswith('http'): return ""
300
  try:
301
+ headers = {'User-Agent': 'Mozilla/5.0 ...', 'Accept': 'text/html...', 'Accept-Language': 'en-US,en;q=0.9', 'Connection': 'keep-alive'}
 
302
  response = requests.get(url, headers=headers, timeout=20, allow_redirects=True)
 
303
  response.raise_for_status()
304
+ ct = response.headers.get('content-type', '').lower()
305
+ if not ('html' in ct or 'text' in ct): return f"Error: Non-text content type: {ct}"
306
+ enc = response.encoding if response.encoding else response.apparent_encoding
307
+ html = response.content.decode(enc or 'utf-8', errors='ignore')
308
+ soup = BeautifulSoup(html, 'html.parser')
309
+ for tag in soup(["script", "style", "meta", "noscript", "iframe", "header", "footer", "nav", "aside", "form", "button", "link", "head"]): tag.extract()
310
+ main = (soup.find("main") or soup.find("article") or soup.find("div", class_=["content", "main", "post-content", "entry-content", "article-body", "story-content"]) or soup.find("div", id=["content", "main", "article", "story"]))
311
+ text = main.get_text(separator='\n', strip=True) if main else soup.body.get_text(separator='\n', strip=True) if soup.body else soup.get_text(separator='\n', strip=True)
312
+ lines = [line.strip() for line in text.split('\n') if line.strip()]; cleaned = "\n".join(lines)
313
+ if not cleaned: return "Error: Could not extract text."
314
+ max_c = 15000; final = (cleaned[:max_c] + "... [truncated]") if len(cleaned) > max_c else cleaned
315
+ logger.info(f"URL read success. Length: {len(final)}")
316
+ return final
317
+ except Exception as e: logger.error(f"!!! Read URL error: {e}"); logger.error(traceback.format_exc()); return f"Error reading URL: {e}"
 
 
 
 
 
 
 
 
 
 
 
318
 
319
  def process_social_media_url(url):
320
+ logger.info(f"--- Processing social URL: {url} ---")
 
321
  if not url or not url.strip().startswith('http'): return None
322
+ text = None; video = None; audio_file = None
323
+ try: text_res = read_url(url); text = text_res if text_res and not text_res.startswith("Error:") else None
324
+ except Exception as e: logger.error(f"Social text read error: {e}")
 
 
 
 
 
325
  try:
326
+ audio_file = download_social_media_video(url)
327
+ if audio_file: video_res = transcribe_audio_or_video(audio_file); video = video_res if video_res and not video_res.startswith("Error:") else None
328
+ except Exception as e: logger.error(f"Social audio proc error: {e}")
 
 
 
 
 
 
 
329
  finally:
330
+ if audio_file and os.path.exists(audio_file):
331
+ try: os.remove(audio_file)
332
+ except Exception as e: logger.warning(f"Social cleanup fail {audio_file}: {e}")
333
+ logger.debug(f"--- Finished social URL: {url} ---")
334
+ if text or video: return {"text": text or "", "video": video or ""}
335
+ else: return None
 
336
 
337
  # ==============================================================
338
  # ========= SIMPLIFIED generate_news FOR DEBUGGING =============
 
374
  # ==============================================================
375
 
376
 
377
+ # --- create_demo function ---
378
+ # --- MODIFIED: Removed file_types from gr.File ---
379
  def create_demo():
380
  """Creates the Gradio interface"""
381
  logger.info("--- Creating Gradio interface ---")
 
385
  all_inputs = []
386
  with gr.Row():
387
  with gr.Column(scale=2):
 
388
  instructions = gr.Textbox(label="Instructions for the News Article", placeholder="Enter specific instructions...", lines=2)
389
  all_inputs.append(instructions)
 
390
  facts = gr.Textbox(label="Main Facts", placeholder="Describe the most important facts...", lines=4)
391
  all_inputs.append(facts)
392
  with gr.Row():
 
393
  size_slider = gr.Slider(label="Approximate Length (words)", minimum=100, maximum=700, value=250, step=50)
394
  all_inputs.append(size_slider)
 
395
  tone_dropdown = gr.Dropdown(label="Tone of the News Article", choices=["neutral", "serious", "formal", "urgent", "investigative", "human-interest", "lighthearted"], value="neutral")
396
  all_inputs.append(tone_dropdown)
397
  with gr.Column(scale=3):
398
  with gr.Tabs():
399
  with gr.TabItem("πŸ“ Documents"):
 
400
  gr.Markdown("Upload relevant documents (PDF, DOCX, XLSX, CSV). Max 5.")
401
  doc_inputs = []
402
  for i in range(1, 6):
403
+ # *** CHANGED: Removed file_types ***
404
+ doc_file = gr.File(label=f"Document {i}", file_count="single")
405
  doc_inputs.append(doc_file)
406
  all_inputs.extend(doc_inputs)
 
407
  with gr.TabItem("πŸ”Š Audio/Video"):
 
408
  gr.Markdown("Upload audio or video files... Max 5 sources.")
409
  audio_video_inputs = []
410
  for i in range(1, 6):
411
  with gr.Group():
412
  gr.Markdown(f"**Source {i}**")
413
+ # *** CHANGED: Removed file_types ***
414
+ audio_file = gr.File(label=f"Audio/Video File {i}")
415
  with gr.Row():
416
  speaker_name = gr.Textbox(label="Speaker Name", placeholder="Name...")
417
  speaker_role = gr.Textbox(label="Role/Position", placeholder="Role...")
418
  audio_video_inputs.extend([audio_file, speaker_name, speaker_role])
419
  all_inputs.extend(audio_video_inputs)
 
420
  with gr.TabItem("🌐 URLs"):
 
421
  gr.Markdown("Add URLs to relevant web pages... Max 5.")
422
  url_inputs = []
423
  for i in range(1, 6):
424
  url_textbox = gr.Textbox(label=f"URL {i}", placeholder="https://...")
425
  url_inputs.append(url_textbox)
426
  all_inputs.extend(url_inputs)
 
427
  with gr.TabItem("πŸ“± Social Media"):
 
428
  gr.Markdown("Add URLs to social media posts... Max 3.")
429
  social_inputs = []
430
  for i in range(1, 4):
 
436
  social_context_textbox = gr.Textbox(label=f"Context", placeholder="Context...")
437
  social_inputs.extend([social_url_textbox, social_name_textbox, social_context_textbox])
438
  all_inputs.extend(social_inputs)
 
439
 
440
+ generate_button = gr.Button("✨ Generate News Article", variant="primary")
441
+ clear_button = gr.Button("πŸ”„ Clear All Inputs")
 
 
 
442
  with gr.Tabs():
443
  with gr.TabItem("πŸ“„ Generated News Article"):
 
444
  news_output = gr.Textbox(label="Draft News Article", lines=20, show_copy_button=True, interactive=False)
445
  with gr.TabItem("πŸŽ™οΈ Source Transcriptions & Logs"):
 
446
  transcriptions_output = gr.Textbox(label="Transcriptions and Processing Log", lines=15, show_copy_button=True, interactive=False)
447
 
448
  outputs_list = [news_output, transcriptions_output]
 
 
449
  generate_button.click(fn=generate_news, inputs=all_inputs, outputs=outputs_list)
 
450
 
451
  def clear_all_inputs_and_outputs():
452
  logger.info("--- Clear All button clicked ---")
 
457
  elif isinstance(input_comp, gr.File): reset_values.append(None)
458
  else: reset_values.append(None)
459
  reset_values.extend(["", ""])
460
+ try: logger.info("Calling model reset from clear button handler."); model_manager.reset_models(force=True)
461
+ except Exception as e: logger.error(f"Error resetting models during clear: {e}")
 
 
 
 
 
462
  logger.info("--- Clear All operation finished ---")
463
  return reset_values
464
 
465
  clear_button.click(fn=clear_all_inputs_and_outputs, inputs=None, outputs=all_inputs + outputs_list)
466
+ logger.info("--- Gradio interface creation complete ---")
 
467
  return demo
468
 
469
 
470
  # --- main execution block remains the same ---
471
  if __name__ == "__main__":
472
  logger.info("--- Running main execution block ---")
 
473
  news_demo = create_demo()
 
 
474
  news_demo.queue()
 
475
  logger.info("Launching Gradio interface...")
476
  try:
477
  news_demo.launch(server_name="0.0.0.0", server_port=7860)
 
479
  except Exception as launch_err:
480
  logger.error(f"!!! CRITICAL Error during Gradio launch: {launch_err}")
481
  logger.error(traceback.format_exc())
482
+ logger.info("--- Main execution block potentially finished ---")