Vineeth Sai
Initial deploy to HF Spaces (Docker)
501847e
# #!/usr/bin/env python3
# """
# Article Summarizer with Text-to-Speech
# Scrapes articles, summarizes with Qwen3-0.6B, and reads aloud with Kokoro TTS
# """
# import sys
# import torch
# import trafilatura
# import soundfile as sf
# import time
# from transformers import AutoModelForCausalLM, AutoTokenizer
# from kokoro import KPipeline
# # --- Part 1: Web Scraping Function ---
# def scrape_article_text(url: str) -> str | None:
# """
# Downloads a webpage and extracts the main article text, removing ads,
# menus, and other boilerplate.
# Args:
# url: The URL of the article to scrape.
# Returns:
# The cleaned article text as a string, or None if it fails.
# """
# print(f"🌐 Scraping article from: {url}")
# # fetch_url downloads the content of the URL
# downloaded = trafilatura.fetch_url(url)
# if downloaded is None:
# print("❌ Error: Failed to download the article content.")
# return None
# # extract the main text, ignoring comments and tables for a cleaner summary
# article_text = trafilatura.extract(downloaded, include_comments=False, include_tables=False)
# if article_text:
# print("βœ… Successfully extracted article text.")
# return article_text
# else:
# print("❌ Error: Could not find main article text on the page.")
# return None
# # --- Part 2: Summarization Function ---
# def summarize_with_qwen(text: str, model, tokenizer) -> str:
# """
# Generates a summary for the given text using the Qwen3-0.6B model.
# Args:
# text: The article text to summarize.
# model: The pre-loaded transformer model.
# tokenizer: The pre-loaded tokenizer.
# Returns:
# The generated summary as a string.
# """
# print("πŸ€– Summarizing text with Qwen3-0.6B...")
# # 1. Create a detailed prompt for the summarization task
# prompt = f"""
# Please provide a concise and clear summary of the following article.
# Focus on the main points, key findings, and conclusions. The summary should be
# easy to understand for someone who has not read the original text.
# ARTICLE:
# {text}
# """
# messages = [{"role": "user", "content": prompt}]
# # 2. Apply the chat template. We set `enable_thinking=False` for direct summarization.
# # This is more efficient than the default reasoning mode for this task.
# text_input = tokenizer.apply_chat_template(
# messages,
# tokenize=False,
# add_generation_prompt=True,
# enable_thinking=False
# )
# # 3. Tokenize the formatted prompt and move it to the correct device (CPU or MPS on Mac)
# model_inputs = tokenizer([text_input], return_tensors="pt").to(model.device)
# # 4. Generate the summary using parameters recommended for non-thinking mode
# generated_ids = model.generate(
# **model_inputs,
# max_new_tokens=512, # Limit summary length
# temperature=0.7,
# top_p=0.8,
# top_k=20
# )
# # 5. Slice the output to remove the input prompt, leaving only the generated response
# output_ids = generated_ids[0][len(model_inputs.input_ids[0]):]
# # 6. Decode the token IDs back into a readable string
# summary = tokenizer.decode(output_ids, skip_special_tokens=True).strip()
# print("βœ… Summary generated successfully.")
# return summary
# # --- Part 3: Text-to-Speech Function ---
# def speak_summary_with_kokoro(summary: str, voice: str = "af_heart") -> str:
# """
# Converts the summary text to speech using Kokoro TTS and saves as audio file.
# Args:
# summary: The text summary to convert to speech.
# voice: The voice to use (default: "af_heart").
# Returns:
# The filename of the generated audio file.
# """
# print("🎡 Converting summary to speech with Kokoro TTS...")
# try:
# # Initialize Kokoro TTS pipeline
# pipeline = KPipeline(lang_code='a') # 'a' for English
# # Generate speech
# generator = pipeline(summary, voice=voice)
# # Process audio chunks
# audio_chunks = []
# total_duration = 0
# for i, (gs, ps, audio) in enumerate(generator):
# audio_chunks.append(audio)
# chunk_duration = len(audio) / 24000
# total_duration += chunk_duration
# print(f" πŸ“Š Generated chunk {i+1}: {chunk_duration:.2f}s")
# # Combine all audio chunks
# if len(audio_chunks) > 1:
# combined_audio = torch.cat(audio_chunks, dim=0)
# else:
# combined_audio = audio_chunks[0]
# # Generate filename with timestamp
# timestamp = int(time.time())
# filename = f"summary_audio_{timestamp}.wav"
# # Save audio file
# sf.write(filename, combined_audio.numpy(), 24000)
# print(f"βœ… Audio generated successfully!")
# print(f"πŸ’Ύ Saved as: {filename}")
# print(f"⏱️ Duration: {total_duration:.2f} seconds")
# print(f"🎭 Voice used: {voice}")
# return filename
# except Exception as e:
# print(f"❌ Error generating speech: {e}")
# return None
# # --- Part 4: Voice Selection Function ---
# def select_voice() -> str:
# """
# Allows user to select from available voices or use default.
# Returns:
# Selected voice name.
# """
# available_voices = {
# '1': ('af_heart', 'Female - Heart (Grade A, default) ❀️'),
# '2': ('af_bella', 'Female - Bella (Grade A-) πŸ”₯'),
# '3': ('af_nicole', 'Female - Nicole (Grade B-) 🎧'),
# '4': ('am_michael', 'Male - Michael (Grade C+)'),
# '5': ('am_fenrir', 'Male - Fenrir (Grade C+)'),
# '6': ('af_sarah', 'Female - Sarah (Grade C+)'),
# '7': ('bf_emma', 'British Female - Emma (Grade B-)'),
# '8': ('bm_george', 'British Male - George (Grade C)')
# }
# print("\n🎭 Available voices (sorted by quality):")
# for key, (voice_id, description) in available_voices.items():
# print(f" {key}. {description}")
# print(" Enter: Use default voice (af_heart)")
# choice = input("\nSelect voice (1-8 or Enter): ").strip()
# if choice in available_voices:
# selected_voice, description = available_voices[choice]
# print(f"🎡 Selected: {description}")
# return selected_voice
# else:
# print("🎡 Using default voice: Female - Heart")
# return 'af_heart'
# # --- Main Execution Block ---
# if __name__ == "__main__":
# print("πŸš€ Article Summarizer with Text-to-Speech")
# print("=" * 50)
# # Check if a URL was provided as a command-line argument
# if len(sys.argv) < 2:
# print("Usage: python qwen_kokoro_summarizer.py <URL_OF_ARTICLE>")
# print("Example: python qwen_kokoro_summarizer.py https://example.com/article")
# sys.exit(1)
# article_url = sys.argv[1]
# # --- Load Qwen Model and Tokenizer ---
# print("\nπŸ“š Setting up the Qwen3-0.6B model...")
# print("Note: The first run will download the model (~1.2 GB). Please be patient.")
# model_name = "Qwen/Qwen3-0.6B"
# try:
# tokenizer = AutoTokenizer.from_pretrained(model_name)
# model = AutoModelForCausalLM.from_pretrained(
# model_name,
# torch_dtype="auto", # Automatically selects precision (e.g., float16)
# device_map="auto" # Automatically uses MPS (Mac GPU) if available
# )
# except Exception as e:
# print(f"❌ Failed to load the Qwen model. Error: {e}")
# print("Please ensure you have a stable internet connection and sufficient disk space.")
# sys.exit(1)
# # Inform the user which device is being used
# device = next(model.parameters()).device
# print(f"βœ… Qwen model loaded successfully on device: {str(device).upper()}")
# if "mps" in str(device):
# print(" (Running on Apple Silicon GPU)")
# # --- Run the Complete Process ---
# # Step 1: Scrape the article
# print(f"\nπŸ“° Step 1: Scraping article")
# article_content = scrape_article_text(article_url)
# if not article_content:
# print("❌ Failed to scrape article. Exiting.")
# sys.exit(1)
# # Step 2: Summarize the content
# print(f"\nπŸ€– Step 2: Generating summary")
# summary = summarize_with_qwen(article_content, model, tokenizer)
# # Step 3: Display the summary
# print("\n" + "="*60)
# print("✨ GENERATED SUMMARY ✨")
# print("="*60)
# print(summary)
# print("="*60)
# # Step 4: Ask if user wants TTS
# print(f"\n🎡 Step 3: Text-to-Speech")
# tts_choice = input("Would you like to hear the summary read aloud? (y/N): ").strip().lower()
# if tts_choice in ['y', 'yes']:
# # Let user select voice
# selected_voice = select_voice()
# # Generate speech
# audio_filename = speak_summary_with_kokoro(summary, voice=selected_voice)
# if audio_filename:
# print(f"\n🎧 Audio saved as: {audio_filename}")
# print("πŸ”Š You can now play this file to hear the summary!")
# # Optional: Try to play the audio automatically (macOS)
# try:
# import subprocess
# print("🎢 Attempting to play audio automatically...")
# subprocess.run(['afplay', audio_filename], check=True)
# print("βœ… Audio playback completed!")
# except (subprocess.CalledProcessError, FileNotFoundError):
# print("ℹ️ Auto-play not available. Please play the file manually.")
# else:
# print("❌ Failed to generate audio.")
# else:
# print("πŸ‘ Summary completed without audio generation.")
# print(f"\nπŸŽ‰ Process completed successfully!")
# print(f"πŸ“ Summary length: {len(summary)} characters")
# print(f"πŸ“Š Original article length: {len(article_content)} characters")
# print(f"πŸ“‰ Compression ratio: {len(summary)/len(article_content)*100:.1f}%")