#!/usr/bin/env python3 import datetime as dt import json import re import urllib.parse import urllib.request import xml.etree.ElementTree as ET from pathlib import Path BASE = Path('/Users/guchen/.openclaw/workspace/org/cases/arxiv_digest') OUT_DIR = BASE / 'output' STATE_DIR = BASE / 'state' OUT_DIR.mkdir(parents=True, exist_ok=True) STATE_DIR.mkdir(parents=True, exist_ok=True) CATS = ['cs.AI', 'cs.LG', 'cs.CL', 'cs.CV', 'cs.RO', 'stat.ML'] MAX_RESULTS = 120 HOT_KEYWORDS = ['agent', 'reasoning', 'multimodal', 'alignment', 'rl', 'diffusion', 'transformer', 'benchmark'] def fetch(url, timeout=20): req = urllib.request.Request(url, headers={'User-Agent': 'Val-ArXiv-Digest/1.0'}) with urllib.request.urlopen(req, timeout=timeout) as r: return r.read().decode('utf-8', errors='ignore') def query_arxiv(): q = ' OR '.join([f'cat:{c}' for c in CATS]) params = { 'search_query': q, 'start': 0, 'max_results': MAX_RESULTS, 'sortBy': 'submittedDate', 'sortOrder': 'descending', } url = 'http://export.arxiv.org/api/query?' + urllib.parse.urlencode(params) xml = fetch(url) ns = {'a': 'http://www.w3.org/2005/Atom', 'arxiv': 'http://arxiv.org/schemas/atom'} root = ET.fromstring(xml) papers = [] for e in root.findall('a:entry', ns): pid = (e.findtext('a:id', default='', namespaces=ns) or '').strip() title = re.sub(r'\s+', ' ', (e.findtext('a:title', default='', namespaces=ns) or '').strip()) summary = re.sub(r'\s+', ' ', (e.findtext('a:summary', default='', namespaces=ns) or '').strip()) published = e.findtext('a:published', default='', namespaces=ns) updated = e.findtext('a:updated', default='', namespaces=ns) authors = [re.sub(r'\s+', ' ', (a.findtext('a:name', default='', namespaces=ns) or '').strip()) for a in e.findall('a:author', ns)] primary = '' pcat = e.find('arxiv:primary_category', ns) if pcat is not None: primary = pcat.attrib.get('term', '') all_cats = [c.attrib.get('term', '') for c in e.findall('a:category', ns)] papers.append({ 'id': pid, 'arxiv_id': pid.split('/abs/')[-1] if '/abs/' in pid else pid, 'title': title, 'summary': summary, 'published': published, 'updated': updated, 'authors': authors, 'primary': primary, 'categories': all_cats, }) return papers def hours_since(iso): try: t = dt.datetime.fromisoformat(iso.replace('Z', '+00:00')) now = dt.datetime.now(dt.timezone.utc) return max(0.0, (now - t).total_seconds() / 3600) except Exception: return 999.0 def hn_hits_for(paper): # lightweight buzz proxy q = paper['arxiv_id'] url = 'https://hn.algolia.com/api/v1/search?' + urllib.parse.urlencode({'query': q, 'tags': 'story'}) try: data = json.loads(fetch(url, timeout=3)) return int(data.get('nbHits', 0)) except Exception: return 0 def score(p): h = hours_since(p['published']) recency = max(0, 72 - h) / 72 * 60 kw = sum(1 for k in HOT_KEYWORDS if k in (p['title'] + ' ' + p['summary']).lower()) keyword_score = min(20, kw * 4) github_bonus = 8 if ('github.com' in p['summary'].lower() or 'code:' in p['summary'].lower()) else 0 hn = hn_hits_for(p) hn_score = min(12, hn * 2) cat_bonus = 5 if p['primary'] in ['cs.AI', 'cs.LG', 'cs.CL'] else 2 total = recency + keyword_score + github_bonus + hn_score + cat_bonus return round(total, 2), {'recency': round(recency,2), 'keywords': keyword_score, 'github': github_bonus, 'hn': hn_score, 'cat': cat_bonus} def one_liner(p): t = p['title'] if len(t) > 90: t = t[:87] + '...' return f"{t}({p['primary']})" def split_sentences(text): text = re.sub(r'\s+', ' ', text).strip() if not text: return [] parts = re.split(r'(?<=[\.!?])\s+', text) return [p.strip() for p in parts if p.strip()] def pick_sentence(sentences, cues): for s in sentences: low = s.lower() if any(c in low for c in cues): return s return sentences[0] if sentences else '' def zh_simplify(text): if not text: return '(摘要未提供)' m = { 'this paper': '本文', 'we propose': '提出了', 'we present': '提出了', 'we introduce': '引入了', 'our method': '该方法', 'results show': '结果显示', 'experiments show': '实验显示', 'state-of-the-art': 'SOTA', 'benchmark': '基准测试', 'model': '模型', 'models': '模型', 'dataset': '数据集', 'datasets': '数据集', 'training': '训练', 'inference': '推理', 'diffusion': '扩散', 'transformer': 'Transformer', 'vision-language-action': '视觉-语言-动作', 'large language model': '大语言模型', 'llm': 'LLM', 'video generation': '视频生成' } out = text for k, v in m.items(): out = re.sub(k, v, out, flags=re.IGNORECASE) if len(out) > 140: out = out[:137] + '...' return out def title_zh(title): t = title repl = { 'Benchmark': '基准', 'Accelerating': '加速', 'Generation': '生成', 'Controlling': '控制', 'Features': '特征', 'Vision-Language-Action': '视觉-语言-动作', 'Models': '模型', 'Unbiased': '无偏', 'Evaluation': '评估', 'medical': '医疗', 'neural network': '神经网络' } for k, v in repl.items(): t = re.sub(k, v, t, flags=re.IGNORECASE) return t def paper_brief(p): sents = split_sentences(p.get('summary', '')) problem = pick_sentence(sents, ['challenge', 'problem', 'critical', 'limited', 'suffer']) method = pick_sentence(sents, ['we propose', 'we present', 'we introduce', 'our method']) result = pick_sentence(sents, ['results show', 'experiments show', 'outperform', 'improve', 'achieve']) impact = pick_sentence(sents, ['enables', 'useful', 'applications', 'real-world', 'towards']) if not method: method = sents[0] if sents else '' if not result: result = '文中给出了实验结果来验证方法有效性。' if not impact: impact = '对相关方向的研究和落地应用有参考价值。' return { 'title_zh': title_zh(p['title']), 'problem': zh_simplify(problem), 'method': zh_simplify(method), 'result': zh_simplify(result), 'impact': zh_simplify(impact), } def build_digest(papers): # limit scoring set for speed/stability recent_pool = sorted(papers, key=lambda x: x['published'], reverse=True)[:40] scored = [] for p in recent_pool: s, detail = score(p) p2 = dict(p) p2['hot_score'] = s p2['score_detail'] = detail scored.append(p2) hot = sorted(scored, key=lambda x: x['hot_score'], reverse=True)[:5] latest = sorted(scored, key=lambda x: x['published'], reverse=True)[:10] return hot, latest, scored def to_md(hot, latest): today = dt.datetime.now().strftime('%Y-%m-%d') lines = [] lines.append(f"# ArXiv Daily Brief - {today}") lines.append('') lines.append('## 🧠 今日 Top 3(中文可读版)') for i, p in enumerate(hot[:3], 1): b = paper_brief(p) lines.append(f"{i}. **{p['title']}**") lines.append(f" - 中文题目(意译): {b['title_zh']}") lines.append(f" - 这篇在讲什么: {b['problem']}") lines.append(f" - 它怎么做: {b['method']}") lines.append(f" - 得出了什么结果: {b['result']}") lines.append(f" - 可能的影响: {b['impact']}") lines.append(f" - arXiv: {p['id']}") lines.append('') lines.append('## 🔥 今日热度 Top 5(新鲜度+关键词+HN提及+代码线索)') for i, p in enumerate(hot, 1): lines.append(f"{i}. **{p['title']}**") lines.append(f" - arXiv: {p['id']}") lines.append(f" - 类别: {p['primary']} | HotScore: {p['hot_score']} | 作者: {', '.join(p['authors'][:3])}") lines.append(f" - 速读: {one_liner(p)}") lines.append('') lines.append('## 🆕 最新上新 Top 10') for i, p in enumerate(latest, 1): lines.append(f"{i}. {p['title']} ({p['primary']}) - {p['id']}") lines.append('') lines.append('## Val 今日建议') lines.append('- 先读 Top 5 里的 1-2 篇,优先看是否有可直接复用的方法/代码。') lines.append('- 若你愿意,我下一步可对 Top 3 产出“中文三段式精读卡”(问题-方法-可落地点)。') return '\n'.join(lines) + '\n' def main(): papers = query_arxiv() hot, latest, scored = build_digest(papers) md = to_md(hot, latest) day = dt.datetime.now().strftime('%Y-%m-%d') out = OUT_DIR / f'{day}.md' out.write_text(md, encoding='utf-8') (OUT_DIR / 'latest.md').write_text(md, encoding='utf-8') state = { 'updatedAt': dt.datetime.now().isoformat(), 'date': day, 'papersFetched': len(papers), 'topHot': [ {'title': p['title'], 'arxiv_id': p['arxiv_id'], 'score': p['hot_score']} for p in hot ] } (STATE_DIR / 'latest_state.json').write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding='utf-8') print(str(out)) if __name__ == '__main__': main()