# #!/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 ") # 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}%")