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