Files
inspiration-collector/scripts/summarize_news.py

210 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Use DeepSeek to summarize top news into categorized Chinese briefs (5 per category).
v2: back-fills pub_time from raw RSS data after AI summarization.
"""
import json, os, requests, re
from datetime import datetime, timezone, timedelta
from dateutil import parser as dtparser
API_KEY = "sk-bbca4a0380d549389f0d27cdea0b5228"
API_URL = "https://api.deepseek.com/v1/chat/completions"
MODEL = "deepseek-v4-flash"
NEWS_FILE = "/var/www/nav/data/news.json"
OUTPUT = "/var/www/nav/data/news_summary.json"
tz = timezone(timedelta(hours=8))
def load_raw_news():
"""Load raw news and build title->published lookup."""
if not os.path.exists(NEWS_FILE):
return []
with open(NEWS_FILE) as f:
data = json.load(f)
return data.get("items", [])
def build_time_lookup(raw_items):
"""Build lookup: title -> pub_time (Beijing time string like '06-15 10:30')."""
lookup = {}
for item in raw_items:
title = item.get("title", "").strip()
published = item.get("published", "").strip()
if not title or not published:
continue
try:
dt = dtparser.parse(published)
# Convert to Beijing time if not already
if dt.tzinfo:
dt = dt.astimezone(tz)
else:
dt = dt.replace(tzinfo=tz)
# Format: MM-DD HH:MM for today, MM-DD for older
now = datetime.now(tz)
if dt.date() == now.date():
time_str = dt.strftime("%H:%M")
else:
time_str = dt.strftime("%m-%d %H:%M")
lookup[title] = time_str
except Exception:
continue
return lookup
def match_pub_time(items, lookup):
"""Match AI output items back to raw RSS pub_time by title similarity."""
for item in items:
if "pub_time" in item:
continue
title = item.get("title", "").strip()
subtitle = item.get("subtitle", "").strip()
matched = None
# Exact match on original title first
if title in lookup:
matched = lookup[title]
else:
# Fuzzy: check if subtitle contains original title keywords
for raw_title, time_val in lookup.items():
# Try partial match
short = min(len(raw_title), len(title))
if short > 10 and raw_title[:short] in title or title[:short] in raw_title:
matched = time_val
break
# Check if first 20 chars match
if raw_title[:20].strip() == title[:20].strip():
matched = time_val
break
if matched:
item["pub_time"] = matched
def summarize(items):
news_text = ""
for i, item in enumerate(items[:200], 1):
title = item.get("title", "")
summary = item.get("summary", "")[:150].replace("<", "").replace(">", "")
source = item.get("source", "")
link = item.get("link", "")
news_text += f"{i}. [{source}] {title}\n {summary}\n url: {link}\n\n"
prompt = f"""你是一个资深的新闻编辑助手。你的任务是精选出当下最重要的新闻,按分类组织。
## 选稿标准(满足任一即可)
- 地缘政治新闻(战争、条约、制裁、外交、选举)
- 宏观经济决策(央行、通胀、贸易、就业)
- 科技重大突破AI、航天、医药、能源
- 重大自然灾害/公共卫生事件
- **中国官方媒体头条**(新华社、人民日报的置顶/头条报道——这些非常重要,必须收录)
- 中国国内引发广泛讨论的社会事件
- 任何具有长期影响的政策变化
## 分类(每个分类**至少15条最多30条**
- 国际要闻
- 科技前沿
- 财经动向
- 中国热点(必须包含新华社、人民日报的重要报道)
- 社会生活
## 输出格式
每条输出到 items 数组,包含以下字段:
- subtitle**15-30字完整标题**(不要缩写)
- title原标题必须与原始新闻标题完全一致这是时间匹配的关键
- source来源
- link原文链接如不可用则留空
- summary**300-500字完整新闻摘要**,用自然段落叙述,关键信息用**加粗**标出。不在前面写"核心事实""背景分析"等前缀,直接写内容,段落清晰流畅。
## 原始新闻
{news_text}
请输出JSON格式。注意宁多勿少确保每个分类**至少15条最多30条**。
JSON格式: {{"categories": [{{"name":"分类名","items":[{{"rank":1,"subtitle":"小标题","title":"原标题","source":"来源","link":"https://...","summary":"正文..."}}]}}]}}"""
resp = requests.post(
API_URL,
headers={"Authorization": f"Bearer {API_KEY}", "Content-Type": "application/json"},
json={
"model": MODEL,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.3,
"max_tokens": 40000,
},
timeout=180,
)
if resp.status_code != 200:
print(f"API error: {resp.status_code} {resp.text[:200]}")
return None
content = resp.json()["choices"][0]["message"]["content"]
content = content.strip()
if content.startswith("```"):
lines = content.split("\n")
content = "\n".join(lines[1:-1]) if lines[-1].strip() == "```" else "\n".join(lines[1:])
try:
result = json.loads(content)
except:
content = content.strip()
content = re.sub(r',\s*}', '}', content)
content = re.sub(r',\s*]', ']', content)
for attempt in range(3):
try:
result = json.loads(content)
break
except:
if attempt == 0:
idx = content.rfind('{"rank"')
if idx > 0:
content = content[:content.rfind('}', 0, idx)] + '}]}]}'
elif attempt == 1:
lines = content.split('\n')
fixed = []
for line in lines:
if line.strip().endswith(',') and not any(x in line for x in ['"', ']', '}']):
line = line.rstrip(',')
fixed.append(line)
content = '\n'.join(fixed)
else:
print(f"JSON parse failed, len={len(content)}")
print(f"First 800 chars: {content[:800]}")
return None
return result
if __name__ == "__main__":
now = datetime.now(tz).strftime("%Y-%m-%d %H:%M")
raw_items = load_raw_news()
if not raw_items:
print("No news data")
exit(0)
print(f"Loaded {len(raw_items)} raw news items. AI summarizing by category...")
data = summarize(raw_items)
if not data:
print("Summarization failed")
exit(1)
# Back-fill pub_time from raw RSS data
time_lookup = build_time_lookup(raw_items)
matched = 0
for cat in data.get("categories", []):
match_pub_time(cat["items"], time_lookup)
matched += sum(1 for item in cat["items"] if "pub_time" in item)
print(f"Matched pub_time for {matched} articles")
result = {
"updated_at": now,
"categories": data.get("categories", []),
}
total = sum(len(c["items"]) for c in result["categories"])
os.makedirs(os.path.dirname(OUTPUT), exist_ok=True)
with open(OUTPUT, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
print(f"Written {total} articles across {len(result['categories'])} categories")
for c in result["categories"]:
print(f" {c['name']}: {len(c['items'])} items")