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