-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathnews_summarizer_app.py
More file actions
280 lines (228 loc) · 10.3 KB
/
news_summarizer_app.py
File metadata and controls
280 lines (228 loc) · 10.3 KB
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
import threading
import time
import feedparser
import requests
import tkinter as tk
from tkinter import messagebox
import pyttsx3
import ttkbootstrap as tb
class NewsSummaryApp:
def __init__(self, root):
self.root = root
self.root.title("News Summary App")
self.theme_var = tk.BooleanVar(value=True) # True = dark mode
self.model_var = tk.StringVar()
self.stop_event = threading.Event()
self.tts_thread = None
self.generate_thread = None
self.tts_engine = pyttsx3.init()
self.set_tts_voice()
self.build_interface()
self.populate_models()
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
def set_tts_voice(self):
voices = self.tts_engine.getProperty('voices')
# Try to select a more natural voice if available
# On Windows, use female voice if available
for voice in voices:
if 'female' in voice.name.lower() or 'zira' in voice.name.lower():
self.tts_engine.setProperty('voice', voice.id)
return
# Otherwise, keep default voice
def build_interface(self):
frame = tb.Frame(self.root, padding=15)
frame.grid(row=0, column=0, sticky="nsew")
btn_width = 18
dropdown_width_chars = 30
# Theme toggle switch with moon and sun emoji at top right
theme_frame = tb.Frame(frame)
theme_frame.grid(row=0, column=2, sticky="e", pady=(0, 10))
moon_label = tb.Label(theme_frame, text="🌙", font=("Segoe UI Emoji", 14))
moon_label.pack(side="left", padx=(0, 5))
self.theme_switch = tb.Checkbutton(
theme_frame,
variable=self.theme_var,
bootstyle="success-round-toggle",
command=self.toggle_theme,
text=""
)
self.theme_switch.pack(side="left")
sun_label = tb.Label(theme_frame, text="☀️", font=("Segoe UI Emoji", 14))
sun_label.pack(side="left", padx=(5, 0))
# Model selection label and dropdown
lbl_model = tb.Label(frame, text="Select Ollama Model:", font=("Segoe UI", 11))
lbl_model.grid(row=0, column=0, sticky="w", padx=(0, 0), pady=(0, 10))
self.model_dropdown = tb.Combobox(
frame, textvariable=self.model_var,
state="readonly",
font=("Segoe UI", 10),
width=dropdown_width_chars
)
self.model_dropdown.grid(row=0, column=1, sticky="w", padx=(10, 10), pady=(0, 10))
# Buttons frame to group buttons horizontally
buttons_frame = tb.Frame(frame)
buttons_frame.grid(row=1, column=0, columnspan=3, sticky="ew", pady=(0, 10))
buttons_frame.columnconfigure((0, 1, 2), weight=1)
self.generate_button = tb.Button(buttons_frame, text="Generate Summary", width=20, command=self.start_generate)
self.generate_button.grid(row=0, column=0, padx=5)
self.stop_gen_button = tb.Button(buttons_frame, text="Stop Generating", width=20, command=self.stop_generating, state="disabled")
self.stop_gen_button.grid(row=0, column=1, padx=5)
self.listen_button = tb.Button(buttons_frame, text="Listen Summary", width=20, command=self.listen_summary)
self.listen_button.grid(row=0, column=2, padx=5)
self.stop_listen_button = tb.Button(buttons_frame, text="Stop Listening", width=20, command=self.stop_listening, state="disabled")
self.stop_listen_button.grid(row=0, column=3, padx=5)
# Progress bar
self.progress = tb.Progressbar(frame, maximum=3)
self.progress.grid(row=2, column=0, columnspan=3, sticky="ew", pady=(0, 10))
# Text area for summary output
self.text_area = tk.Text(frame, wrap="word", font=("Segoe UI", 11), state="normal", bg="#121212", fg="#d0d0d0")
self.text_area.grid(row=3, column=0, columnspan=3, sticky="nsew")
# Make text area expand with window
self.root.rowconfigure(0, weight=1)
self.root.columnconfigure(0, weight=1)
frame.rowconfigure(3, weight=1)
frame.columnconfigure(2, weight=1)
def toggle_theme(self):
if self.theme_var.get():
self.root.style.theme_use("darkly")
self.text_area.config(bg="#121212", fg="#d0d0d0")
else:
self.root.style.theme_use("flatly")
self.text_area.config(bg="white", fg="black")
def populate_models(self):
try:
response = requests.get("http://localhost:11434/api/tags", timeout=5)
response.raise_for_status()
tags = response.json().get("models", [])
model_names = [tag["name"] for tag in tags]
if not model_names:
messagebox.showerror("Error", "No models found in Ollama.")
self.model_dropdown['values'] = []
return
self.first_model = model_names[0]
model_names = model_names[1:] # hide first model if needed
self.model_dropdown['values'] = model_names
self.model_var.set(model_names[0] if model_names else self.first_model)
except requests.RequestException as e:
messagebox.showerror("Error", f"Failed to get models from Ollama:\n{e}")
self.model_dropdown['values'] = []
self.first_model = None
def start_generate(self):
if self.generate_thread and self.generate_thread.is_alive():
messagebox.showinfo("Info", "Already generating summary.")
return
self.stop_event.clear()
self.text_area.delete("1.0", "end")
self.progress["value"] = 0
self.generate_button.config(state="disabled")
self.stop_gen_button.config(state="enabled")
self.generate_thread = threading.Thread(target=self.generate_summary, daemon=True)
self.generate_thread.start()
def stop_generating(self):
self.stop_event.set()
self.generate_button.config(state="enabled")
self.stop_gen_button.config(state="disabled")
def generate_summary(self):
sections = {
"UK News": [
"http://feeds.bbci.co.uk/news/rss.xml",
"http://feeds.reuters.com/reuters/topNews"
],
"Greek Sports News": [
"https://www.sdna.gr/latest.xml",
"https://www.sport24.gr/rss.ashx"
],
"Greek General News": [
"https://thepressproject.gr/feed/"
]
}
model = self.first_model if not self.model_var.get() else self.model_var.get()
for idx, (section_name, feeds) in enumerate(sections.items(), start=1):
if self.stop_event.is_set():
break
self.text_area.insert("end", f"\n*** {section_name} Summary ***\n\n")
self.text_area.see("end")
articles = self.fetch_articles(feeds)
if not articles:
self.text_area.insert("end", f"No articles found for {section_name}.\n\n")
continue
summary = self.summarize_articles(articles, model, section_name)
if self.stop_event.is_set():
break
# Format summary: add bold titles, more spacing
formatted_summary = self.format_summary(summary)
self.text_area.insert("end", formatted_summary + "\n\n")
self.text_area.see("end")
self.progress["value"] = idx
time.sleep(1)
self.generate_button.config(state="enabled")
self.stop_gen_button.config(state="disabled")
def format_summary(self, text):
# Simple formatting: add extra newlines between paragraphs
# The text from Ollama is plain text; you can add spacing for readability
paragraphs = text.strip().split('\n')
return "\n\n".join(p.strip() for p in paragraphs)
def fetch_articles(self, feed_urls, limit_per_feed=5):
articles = []
for url in feed_urls:
feed = feedparser.parse(url)
for entry in feed.entries[:limit_per_feed]:
title = entry.title
summary = getattr(entry, "summary", "")
articles.append(f"{title}: {summary}")
return articles
def summarize_articles(self, articles, model, section_name):
combined_text = "\n\n".join(articles)
prompt = (
f"Summarize the following news from the section '{section_name}' into a short digest with highlights:\n\n"
f"{combined_text}"
)
payload = {
"model": model,
"prompt": prompt,
"stream": False,
"max_tokens": 512,
"temperature": 0.5
}
try:
response = requests.post("http://localhost:11434/api/generate", json=payload, timeout=60)
response.raise_for_status()
data = response.json()
return data.get("response", "").strip()
except requests.RequestException as e:
return f"Error summarizing {section_name}: {e}"
def listen_summary(self):
text = self.text_area.get("1.0", "end").strip()
if not text:
messagebox.showinfo("Info", "Nothing to listen to.")
return
if self.tts_thread and self.tts_thread.is_alive():
messagebox.showinfo("Info", "Already listening.")
return
self.stop_event.clear()
self.tts_thread = threading.Thread(target=self._tts_run, args=(text,), daemon=True)
self.tts_thread.start()
self.listen_button.config(state="disabled")
self.stop_listen_button.config(state="enabled")
def _tts_run(self, text):
self.tts_engine.say(text)
self.tts_engine.runAndWait()
self.listen_button.config(state="enabled")
self.stop_listen_button.config(state="disabled")
def stop_listening(self):
self.tts_engine.stop()
self.stop_event.set()
self.listen_button.config(state="enabled")
self.stop_listen_button.config(state="disabled")
def on_closing(self):
self.stop_event.set()
if self.tts_thread and self.tts_thread.is_alive():
self.tts_engine.stop()
self.root.destroy()
def main():
root = tb.Window(themename="darkly")
root.geometry("1800x1200")
app = NewsSummaryApp(root)
root.mainloop()
if __name__ == "__main__":
main()