Spaces:
Sleeping
Sleeping
File size: 10,452 Bytes
501847e | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 | # #!/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}%") |