This commit is contained in:
2025-11-12 16:26:59 +01:00
parent fe3e502912
commit 804a751fdf
5 changed files with 234 additions and 1 deletions

View File

@@ -112,6 +112,9 @@ class OllamaClient:
'duration': time.time() - start_time
}
# Clean markdown formatting from summary
summary = self._clean_markdown(summary)
summary_word_count = len(summary.split())
return {
@@ -303,6 +306,35 @@ German headline:
return translation
def _clean_markdown(self, text):
"""Remove markdown formatting from text"""
import re
# Remove markdown headers (##, ###, etc.)
text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE)
# Remove bold/italic markers (**text**, *text*, __text__, _text_)
text = re.sub(r'\*\*([^\*]+)\*\*', r'\1', text)
text = re.sub(r'__([^_]+)__', r'\1', text)
text = re.sub(r'\*([^\*]+)\*', r'\1', text)
text = re.sub(r'_([^_]+)_', r'\1', text)
# Remove markdown links [text](url) -> text
text = re.sub(r'\[([^\]]+)\]\([^\)]+\)', r'\1', text)
# Remove inline code `text`
text = re.sub(r'`([^`]+)`', r'\1', text)
# Remove bullet points and list markers
text = re.sub(r'^\s*[-*+]\s+', '', text, flags=re.MULTILINE)
text = re.sub(r'^\s*\d+\.\s+', '', text, flags=re.MULTILINE)
# Clean up extra whitespace
text = re.sub(r'\n\s*\n', '\n\n', text)
text = text.strip()
return text
def _build_summarization_prompt(self, content, max_words):
"""Build prompt for article summarization"""
# Truncate content if too long (keep first 5000 words)
@@ -319,6 +351,8 @@ Write in the clear, engaging, and authoritative style of New York Times Magazine
- Focus on what matters to readers
- Even if the source is in German or another language, write your summary entirely in English
IMPORTANT: Write in plain text only. Do NOT use markdown formatting (no ##, **, *, bullets, etc.). Just write natural prose.
Article:
{content}

View File

@@ -40,6 +40,39 @@
<p style="margin: 15px 0 0 0; font-size: 15px; line-height: 1.6; color: #666666; text-align: justify;">
Here's what's happening in Munich today. We've summarized {{ article_count }} stories using AI so you can stay informed in under 5 minutes.
</p>
{% if weather and weather.success %}
<!-- Weather Widget -->
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="margin-top: 20px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 12px; overflow: hidden;">
<tr>
<td style="padding: 20px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0">
<tr>
<td style="width: 60%; vertical-align: middle;">
<p style="margin: 0 0 5px 0; font-size: 13px; color: rgba(255,255,255,0.9); font-weight: 600;">
TODAY'S WEATHER
</p>
<p style="margin: 0; font-size: 32px; color: #ffffff; font-weight: 700; line-height: 1;">
{{ weather.icon }} {{ weather.temperature }}°C
</p>
<p style="margin: 5px 0 0 0; font-size: 14px; color: rgba(255,255,255,0.9);">
{{ weather.condition }}
</p>
</td>
<td style="width: 40%; text-align: right; vertical-align: middle;">
<p style="margin: 0; font-size: 14px; color: rgba(255,255,255,0.9);">
High: <strong style="color: #ffffff;">{{ weather.high }}°C</strong>
</p>
<p style="margin: 5px 0 0 0; font-size: 14px; color: rgba(255,255,255,0.9);">
Low: <strong style="color: #ffffff;">{{ weather.low }}°C</strong>
</p>
</td>
</tr>
</table>
</td>
</tr>
</table>
{% endif %}
</td>
</tr>
@@ -256,6 +289,36 @@
</td>
</tr>
<!-- TL;DR Section -->
<tr>
<td style="padding: 30px 40px;">
<h2 style="margin: 0 0 20px 0; font-size: 22px; font-weight: 700; color: #1a1a1a;">
📋 TL;DR - Quick Summary
</h2>
<p style="margin: 0 0 15px 0; font-size: 14px; color: #666666;">
Here's everything in one sentence each:
</p>
{% set all_articles = (trending_articles or []) + (other_articles or []) %}
{% for article in all_articles %}
<div style="margin-bottom: 12px; padding-left: 20px; position: relative;">
<span style="position: absolute; left: 0; top: 0; color: #667eea; font-weight: 700;">{{ loop.index }}.</span>
<p style="margin: 0; font-size: 14px; line-height: 1.5; color: #333333;">
<strong>{{ article.title_en if article.title_en else article.title }}</strong>
{{ article.summary.split('.')[0] }}.
</p>
</div>
{% endfor %}
</td>
</tr>
<!-- Divider -->
<tr>
<td style="padding: 0 40px;">
<div style="height: 1px; background-color: #e0e0e0;"></div>
</td>
</tr>
<!-- Summary Box -->
<tr>
<td style="padding: 30px 40px;">

View File

@@ -4,3 +4,4 @@ Jinja2==3.1.2
beautifulsoup4==4.12.2
schedule==1.2.0
pytz==2023.3
requests==2.31.0

View File

@@ -242,6 +242,10 @@ def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=N
trending_articles = articles[:3] if len(articles) >= 3 else articles
other_articles = articles[3:] if len(articles) > 3 else []
# Get weather data
from weather_service import get_munich_weather
weather = get_munich_weather()
# Prepare template data
now = datetime.now()
template_data = {
@@ -252,7 +256,8 @@ def render_newsletter_html(articles, tracking_enabled=False, pixel_tracking_id=N
'other_articles': other_articles,
'unsubscribe_link': f'{Config.WEBSITE_URL}/unsubscribe',
'website_link': Config.WEBSITE_URL,
'tracking_enabled': tracking_enabled
'tracking_enabled': tracking_enabled,
'weather': weather
}
# Render HTML

View File

@@ -0,0 +1,130 @@
"""
Weather service for Munich using Open-Meteo API (free, no API key needed)
"""
import requests
from datetime import datetime
def get_munich_weather():
"""
Get current weather for Munich, Germany
Returns:
{
'success': bool,
'temperature': float, # Celsius
'condition': str, # e.g., "Partly cloudy"
'icon': str, # Weather emoji
'high': float, # Today's high
'low': float, # Today's low
'error': str or None
}
"""
try:
# Munich coordinates
lat = 48.1351
lon = 11.5820
# Open-Meteo API (free, no key needed)
url = f"https://api.open-meteo.com/v1/forecast"
params = {
'latitude': lat,
'longitude': lon,
'current': 'temperature_2m,weather_code',
'daily': 'temperature_2m_max,temperature_2m_min',
'timezone': 'Europe/Berlin',
'forecast_days': 1
}
response = requests.get(url, params=params, timeout=10)
response.raise_for_status()
data = response.json()
# Extract data
current = data.get('current', {})
daily = data.get('daily', {})
temperature = current.get('temperature_2m')
weather_code = current.get('weather_code', 0)
high = daily.get('temperature_2m_max', [None])[0]
low = daily.get('temperature_2m_min', [None])[0]
# Map weather code to condition and emoji
condition, icon = _get_weather_description(weather_code)
return {
'success': True,
'temperature': round(temperature, 1) if temperature else None,
'condition': condition,
'icon': icon,
'high': round(high, 1) if high else None,
'low': round(low, 1) if low else None,
'error': None
}
except requests.exceptions.Timeout:
return {
'success': False,
'temperature': None,
'condition': 'Unknown',
'icon': '🌡️',
'high': None,
'low': None,
'error': 'Weather service timeout'
}
except Exception as e:
return {
'success': False,
'temperature': None,
'condition': 'Unknown',
'icon': '🌡️',
'high': None,
'low': None,
'error': str(e)
}
def _get_weather_description(code):
"""
Map WMO weather code to description and emoji
https://open-meteo.com/en/docs
"""
weather_map = {
0: ('Clear sky', '☀️'),
1: ('Mainly clear', '🌤️'),
2: ('Partly cloudy', ''),
3: ('Overcast', '☁️'),
45: ('Foggy', '🌫️'),
48: ('Foggy', '🌫️'),
51: ('Light drizzle', '🌦️'),
53: ('Drizzle', '🌦️'),
55: ('Heavy drizzle', '🌧️'),
61: ('Light rain', '🌧️'),
63: ('Rain', '🌧️'),
65: ('Heavy rain', '⛈️'),
71: ('Light snow', '🌨️'),
73: ('Snow', '❄️'),
75: ('Heavy snow', '❄️'),
77: ('Snow grains', '🌨️'),
80: ('Light showers', '🌦️'),
81: ('Showers', '🌧️'),
82: ('Heavy showers', '⛈️'),
85: ('Light snow showers', '🌨️'),
86: ('Snow showers', '❄️'),
95: ('Thunderstorm', '⛈️'),
96: ('Thunderstorm with hail', '⛈️'),
99: ('Thunderstorm with hail', '⛈️'),
}
return weather_map.get(code, ('Unknown', '🌡️'))
if __name__ == '__main__':
# Test
weather = get_munich_weather()
print(f"Success: {weather['success']}")
print(f"Temperature: {weather['temperature']}°C")
print(f"Condition: {weather['icon']} {weather['condition']}")
print(f"High/Low: {weather['high']}°C / {weather['low']}°C")
if weather['error']:
print(f"Error: {weather['error']}")