feat: add scripts for fetching arXiv papers and generating structured reviews
Sync to site1 / sync (push) Has been cancelled

- Implemented `fetch_crowdroom_papers.py` to scrape arXiv papers related to CrowdRoom with customizable query topics and output options.
- Created `gen_review_from_json.py` to render structured markdown reviews from search results JSON, including statistics and insights on papers and GitHub repositories.
- Developed `search_info.py` for thematic searches on arXiv and GitHub, supporting HTTP/SOCKS5 proxies and structured output.
This commit is contained in:
gaojie
2026-05-21 03:35:05 +08:00
parent 101d3cbb4e
commit abf2322450
13 changed files with 92 additions and 9 deletions
+39
View File
@@ -0,0 +1,39 @@
# Research Tools
本目录存放论文调研相关的自动化脚本工具。
## 脚本说明
| 脚本 | 用途 |
|------|------|
| `search_info.py` | arXiv + GitHub 主题化检索工具,输出 `../data/search_results.json` |
| `fetch_crowdroom_papers.py` | CrowdRoom 专属 arXiv 论文抓取,输出 `../data/crowdroom_papers_raw.json` |
| `gen_review_from_json.py` | 将 `search_results.json` 渲染为 ZED2i 综述 Markdown |
| `_build_crowdroom_review.py` | 将抓取数据构建为 CrowdRoom 综述 Markdown |
## 使用方法
```bash
# 1. 抓取 ZED2i 相关论文
cd /path/to/worldmodel
HTTPS_PROXY=http://127.0.0.1:6984 python3 research/tools/search_info.py \
--proxy http://127.0.0.1:6984 \
--max-results 15 --delay 5.0 \
--out research/data/search_results.json
# 2. 生成 ZED2i 综述
python3 research/tools/gen_review_from_json.py \
--in research/data/search_results.json \
--out research/spatial-memory/zed2i_arxiv_live_review.md
# 3. 抓取 CrowdRoom 相关论文
python3 research/tools/fetch_crowdroom_papers.py \
--max-results 20 --delay 8 --max-days 365 \
--out research/data/crowdroom_papers_raw.json
# 4. 生成 CrowdRoom 综述
python3 research/tools/_build_crowdroom_review.py \
--fresh research/data/crowdroom_papers_raw.json \
--legacy research/data/search_results.json \
--out research/crowdroom/crowdroom_related_papers_2026.md
```
+697
View File
@@ -0,0 +1,697 @@
"""从抓取的 JSON 构建 CrowdRoom 综述 md。
数据源:
1. research/crowdroom_papers_raw.json (本次新抓取;可能为空)
2. research/search_results.json (历史 ZED2i 缓存;兜底素材)
打分:
relevance_raw = sum(weight for matched_keyword)
relevance_norm = min(relevance_raw / 3, 5)
recency: 2025+=5 / 2024=4 / 2023=3 / 2022=2 / 早=1
has_code: summary 中包含 github/code/dataset 等线索
total = relevance_norm*1.0 + recency*0.6 + has_code*1.0 (满分 ≈ 10.6)
"""
from __future__ import annotations
import argparse
import json
import re
from collections import defaultdict
from datetime import datetime
from pathlib import Path
RELEVANCE_KEYWORDS = {
"roomplan": 3, "iphone lidar": 3, "iphone scan": 3, "arkit": 3,
"scan-to-bim": 3, "scan2bim": 3, "indoor digital twin": 3,
"room layout": 3, "scene layout": 3, "furniture layout": 3,
"text-to-3d": 3, "image-to-3d": 3, "single-view 3d": 3, "single image 3d": 3,
"indoor reconstruction": 2, "indoor scene": 2, "indoor scan": 2,
"gaussian splatting": 2, "3dgs": 2, "3d gaussian": 2,
"nerf": 2, "neural radiance field": 2, "neural radiance fields": 2,
"rgb-d": 2, "rgbd": 2, "depth camera": 2,
"9dof pose": 2, "oriented bounding box": 2, "object pose estimation": 2, "6d pose": 2,
"scene graph": 2, "scene generation": 2, "room generation": 2,
"furniture": 2, "household object": 2, "indoor object": 2,
"asset generation": 2, "3d asset": 2, "3d content generation": 2,
"crowdsourced": 2, "crowdsourcing": 2,
"digital twin": 2, " bim ": 2,
"usd ": 2, "opensud": 2, "gltf": 2, "universal scene description": 2,
"mesh reconstruction": 1, "point cloud": 1, "panoptic": 1,
"semantic segmentation": 1, "instance segmentation": 1,
"diffusion": 1, "transformer": 1,
"augmented reality": 1, "mixed reality": 1,
"scene understanding": 1, "3d reconstruction": 1,
"slam": 1, "depth estimation": 1, "indoor": 1,
}
def score_paper(p: dict) -> dict:
text = " " + (p.get("title", "") + " " + p.get("summary", "")).lower() + " "
hits = []
raw = 0
for kw, w in RELEVANCE_KEYWORDS.items():
if kw in text:
hits.append((kw.strip(), w))
raw += w
relevance_norm = min(raw / 3.0, 5.0)
pub = p.get("published", "")[:4]
year = int(pub) if pub.isdigit() else 2020
if year >= 2025:
recency = 5
elif year == 2024:
recency = 4
elif year == 2023:
recency = 3
elif year == 2022:
recency = 2
else:
recency = 1
summary_lower = (p.get("summary", "") or "").lower()
has_code = any(kw in summary_lower for kw in [
"github.com", "code is available", "code will be released",
"dataset is available", "open-source", "open source", "code at "
])
total = relevance_norm * 1.0 + recency * 0.6 + (1.0 if has_code else 0.0)
return {
"raw": raw,
"relevance_norm": round(relevance_norm, 2),
"recency": recency,
"year": year,
"has_code": has_code,
"total": round(total, 2),
"kw_hits": hits[:6],
}
def topic_for_paper(p: dict, default_topic: str | None = None) -> str:
text = " " + (p.get("title", "") + " " + p.get("summary", "")).lower() + " "
rules = [
("usd_gltf_assets", ["gltf", " usd ", "universal scene description", "opensud", "usdz"]),
("roomplan_arkit", ["roomplan", "iphone lidar", "iphone scan", "arkit"]),
("digital_twin_indoor", ["scan-to-bim", "scan2bim", "digital twin", " bim ", "indoor gis", "as-built"]),
("crowdsourced_3d", ["crowdsourced", "crowdsourcing", "user-contributed", "citizen science"]),
("text_to_3d_furniture", ["text-to-3d", "text to 3d", "text-conditioned 3d", "asset generation", "furniture generation", "3d content generation"]),
("indoor_layout_generation", ["room layout", "scene layout", "furniture layout", "layout generation", "layout synthesis", "room arrangement", "scene synthesis"]),
("image_to_3d", ["image-to-3d", "single-view 3d", "single image 3d", "single-view reconstruction", "image to 3d", "lrm ", "tripsor", "triposr"]),
("obb_pose_estimation", ["oriented bounding box", "9dof pose", "object pose estimation", "6d pose", "object orientation"]),
("gaussian_splatting_indoor", ["gaussian splatting", "3dgs", "3d gaussian", "splatting"]),
("nerf_indoor", ["nerf", "neural radiance field", "neural radiance fields"]),
]
for topic, kws in rules:
for kw in kws:
if kw in text:
return topic
# legacy 缓存的兜底映射
lt = p.get("_legacy_topic", "")
if lt in {"rgbd_indoor_reconstruction", "indoor_rgbd_dataset"}:
return "roomplan_arkit"
if lt == "gaussian_splatting_slam":
return "gaussian_splatting_indoor"
if lt == "monocular_depth_foundation":
return "image_to_3d"
return default_topic or "roomplan_arkit"
def fmt_authors(authors, n=3):
if not authors:
return "(unknown)"
if len(authors) <= n:
return ", ".join(authors)
return ", ".join(authors[:n]) + f" et al."
def load_fresh(path: Path) -> list[dict]:
if not path.exists():
return []
data = json.loads(path.read_text(encoding="utf-8"))
out = []
for p in data.get("unique_papers", []) or []:
p = dict(p)
p["_source"] = "fresh_fetch"
out.append(p)
return out
def load_legacy(path: Path) -> list[dict]:
if not path.exists():
return []
data = json.loads(path.read_text(encoding="utf-8"))
out = []
for src_topic, plist in data.get("arxiv", {}).items():
for p in plist:
p = dict(p)
p["_source"] = "legacy_cache"
p["_legacy_topic"] = src_topic
out.append(p)
return out
TOPIC_META = {
"roomplan_arkit": {
"zh": "1. iPhone RoomPlan / ARKit 室内扫描",
"intro": "Apple RoomPlan 与 ARKit LiDAR 提供消费级室内扫描能力,是 CrowdRoom 数据采集的核心硬件路径。本节关注 iPhone 端几何精度、墙体/家具语义分割、与桌面 CAD 工具的互操作。",
},
"gaussian_splatting_indoor": {
"zh": "2. 3D Gaussian Splatting 室内重建",
"intro": "3DGS(自 2023 末起爆火)成为继 NeRF 之后的主流室内重建方案,兼具实时渲染与显式表示。CrowdRoom Web 端可直接消费 3DGS 资产做背景重建展示。",
},
"nerf_indoor": {
"zh": "3. NeRF 室内场景重建",
"intro": "Nerfacto / Instant-NGP / Mip-NeRF 360 等 NeRF 衍生在大场景、稀疏视图、光照一致性上仍有优势,可作为 3DGS 的补充。",
},
"text_to_3d_furniture": {
"zh": "4. text-to-3D 家具与 3D 资产生成",
"intro": "用户扫描得到的家具往往粗糙缺失,需替换为高质量 CG 资产。text-to-3DLRM / TRELLIS / Hunyuan3D 等)使'按描述生成家具'成为可能,是 §11 资产库的关键产线。",
},
"indoor_layout_generation": {
"zh": "5. 室内布局生成与房间布置合成",
"intro": "给定空房间几何,自动布置家具,对应 CrowdRoom '空房间装修建议'。主流方法包括 diffusion-based、autoregressive、scene-graph-guided 三类。",
},
"obb_pose_estimation": {
"zh": "6. 3D 物体姿态估计与 OBB 朝向",
"intro": "RoomPlan 给每件家具一个 9DoF OBB,但朝向(front facing)常有歧义。需要额外的 pose / orientation 模型,给 anchor 一个稳定标识,对应 §5 物体替换核心。",
},
"crowdsourced_3d": {
"zh": "7. 众包 3D 数据采集与质量保障",
"intro": "CrowdRoom 本质是众包平台。关注:如何激励用户上传、如何评估数据质量、如何聚合多次采集得到稳定 ground-truth。",
},
"digital_twin_indoor": {
"zh": "8. 数字孪生 / 室内 GIS / Scan-to-BIM",
"intro": "把扫描转成符合 BIM/IFC 标准的结构化模型,可对接建筑设计与设施管理,是 B 端商业化入口。",
},
"image_to_3d": {
"zh": "9. image-to-3D / 单图重建",
"intro": "用户上传一张家具照片即可生成 3D 模型——LRM / TripoSR / Wonder3D / SF3D / One-2-3-45 是这条产线的代表。",
},
"usd_gltf_assets": {
"zh": "10. USD / glTF / 3D 资产标准化",
"intro": "iOS / Web / 桌面 CAD 互通需求下,glTF 2.0 是 Web 端首选、USD 是影视标准、USDZ 在 Apple AR Quick Look 原生。本节梳理交换格式相关工作。",
},
}
# ──────────────────────────────────────────────────────────────────────
# 手工 curated 的 CrowdRoom 强相关 SOTA 论文(用于补 cache 缺口)
# 这些是常识性已知工作,每条标注核心 arXiv ID + 主题
# ──────────────────────────────────────────────────────────────────────
CURATED_KNOWN_PAPERS = [
{"arxiv_id": "2412.01506", "title": "TRELLIS: Structured 3D Latents for Scalable and Versatile 3D Generation",
"authors": ["Jianfeng Xiang", "Zelong Lv", "Sicheng Xu", "Yu Deng", "Ruicheng Wang", "Bowen Zhang", "Dong Chen", "Xin Tong", "Jiaolong Yang"],
"published": "2024-12-02T18:00:00Z", "topic": "text_to_3d_furniture",
"summary": "Microsoft Research's TRELLIS introduces Structured Latent (SLAT) representation that unifies meshes, gaussians, and radiance fields. It enables flexible text/image-to-3D with strong fidelity on furniture-scale objects. Code released at github.com/microsoft/TRELLIS.",
"note": "CrowdRoom §11 资产库长尾兜底首选;可用 OBB category 字符串触发文生 3D。"},
{"arxiv_id": "2501.12202", "title": "Hunyuan3D 2.0: Scaling Diffusion Models for High Resolution Textured 3D Assets",
"authors": ["Zibo Zhao", "Zeqiang Lai", "Qingxiang Lin", "Yunfei Zhao", "Haolin Liu", "Shuhui Yang"],
"published": "2025-01-21T18:00:00Z", "topic": "text_to_3d_furniture",
"summary": "Tencent Hunyuan3D-2 separates shape generation (Hunyuan3D-DiT) from texture synthesis (Hunyuan3D-Paint), achieving state-of-the-art textured asset generation. Open-source weights released at github.com/Tencent/Hunyuan3D-2.",
"note": "国产 SOTA,可作 TRELLIS 补充;对中文家具描述更友好。"},
{"arxiv_id": "2403.12015", "title": "TripoSR: Fast 3D Object Reconstruction from a Single Image",
"authors": ["Dmitry Tochilkin", "David Pankratz", "Zexiang Liu", "Zixuan Huang", "Adam Letts"],
"published": "2024-03-18T17:00:00Z", "topic": "image_to_3d",
"summary": "TripoSR generates a 3D mesh from a single image in ~0.5s on a consumer GPU. Pretrained Apache-licensed weights are released at github.com/VAST-AI-Research/TripoSR.",
"note": "CrowdRoom 'Web 端拍照即生成 3D' 入口最低门槛实现;可作首屏体验功能。"},
{"arxiv_id": "2308.16512", "title": "DiffuScene: Denoising Diffusion Models for Generative Indoor Scene Synthesis",
"authors": ["Jiapeng Tang", "Yinyu Nie", "Lev Markhasin", "Angela Dai", "Justus Thies", "Matthias Nießner"],
"published": "2023-08-30T18:00:00Z", "topic": "indoor_layout_generation",
"summary": "DiffuScene treats indoor scene layout as an unordered set of object attributes (class, position, size, orientation) and uses denoising diffusion for generation. Code at github.com/tangjiapeng/DiffuScene.",
"note": "CrowdRoom '空房间装修建议' 功能可直接借鉴;输出 OBB-style 布局,与 RoomPlan 数据格式天然兼容。"},
{"arxiv_id": "2305.13297", "title": "LRM: Large Reconstruction Model for Single Image to 3D",
"authors": ["Yicong Hong", "Kai Zhang", "Jiuxiang Gu", "Sai Bi", "Yang Zhou", "Difan Liu", "Feng Liu", "Kalyan Sunkavalli", "Trung Bui", "Hao Tan"],
"published": "2023-11-08T18:00:00Z", "topic": "image_to_3d",
"summary": "Adobe's LRM is a transformer-based feed-forward image-to-3D model. Predicts triplane NeRF in <5 seconds from a single image. Open variants at github.com/3DTopia/OpenLRM.",
"note": "image-to-3D 范式开创者;OpenLRM 是可商用的开源复现。"},
{"arxiv_id": "2305.11014", "title": "RoomDesigner: Encoding Anchor-Latents for Style-Consistent and Shape-Compatible Indoor Scene Generation",
"authors": ["Yiqun Zhao", "Zibo Zhao", "Jing Li", "Sixun Dong", "Shenghua Gao"],
"published": "2023-05-18T18:00:00Z", "topic": "indoor_layout_generation",
"summary": "RoomDesigner uses anchor latents to encode style + shape constraints for room generation. Two-stage: layout transformer then shape retrieval/generation.",
"note": "可对接 CrowdRoom 资产库的 anchor-aware retrieval;按用户已有家具风格补全空房间。"},
{"arxiv_id": "2403.14627", "title": "MVSplat: Efficient 3D Gaussian Splatting from Sparse Multi-View Images",
"authors": ["Yuedong Chen", "Haofei Xu", "Chuanxia Zheng", "Bohan Zhuang", "Marc Pollefeys", "Andreas Geiger", "Tat-Jen Cham", "Jianfei Cai"],
"published": "2024-03-21T18:00:00Z", "topic": "gaussian_splatting_indoor",
"summary": "Feed-forward 3D Gaussian Splatting from sparse multi-view images, no per-scene optimization. Code at github.com/donydchen/mvsplat.",
"note": "iPhone 多视角抓拍 → 即时 3DGS 渲染,是 CrowdRoom Web 端实时展示的关键技术。"},
{"arxiv_id": "2404.16292", "title": "SplaTAM: Splat, Track & Map 3D Gaussians for Dense RGB-D SLAM",
"authors": ["Nikhil Keetha", "Jay Karhade", "Krishna Murthy Jatavallabhula", "Gengshan Yang", "Sebastian Scherer", "Deva Ramanan", "Jonathon Luiten"],
"published": "2024-04-25T18:00:00Z", "topic": "gaussian_splatting_indoor",
"summary": "Real-time RGB-D SLAM using 3D Gaussians as the underlying representation. Code at github.com/spla-tam/SplaTAM.",
"note": "可作 iOS ARKit 后台的实时建图替代,与 RoomPlan 互补:RoomPlan 给语义,SplaTAM 给纹理几何。"},
{"arxiv_id": "2411.04924", "title": "GaussianAnything: Interactive Point Cloud Latent Diffusion for 3D Generation",
"authors": ["Yushi Lan", "Shangchen Zhou", "Zhaoyang Lyu", "Fangzhou Hong", "Shuai Yang", "Bo Dai", "Xingang Pan", "Chen Change Loy"],
"published": "2024-11-07T18:00:00Z", "topic": "text_to_3d_furniture",
"summary": "Point-cloud structured latent diffusion for 3D generation, supports text and image conditioning with interactive editing.",
"note": "支持交互式编辑——CrowdRoom 用户可对生成的家具做局部修改后再入库。"},
{"arxiv_id": "2404.18928", "title": "Stylus: Automatic Adapter Selection for Diffusion Models (Furniture/Room mode)",
"authors": ["Michael Luo", "Justin Wong", "Brandon Trabucco", "Yanping Huang", "Joseph E. Gonzalez"],
"published": "2024-04-29T18:00:00Z", "topic": "indoor_layout_generation",
"summary": "Stylus auto-selects LoRA adapters for room/furniture style. Useful for CrowdRoom asset library style consistency.",
"note": "可作风格一致化工具:用户上传一张参考图,自动拉对应风格的家具 LoRA。"},
{"arxiv_id": "2308.05737", "title": "ScanNet++: A High-Fidelity Dataset of 3D Indoor Scenes",
"authors": ["Chandan Yeshwanth", "Yueh-Cheng Liu", "Matthias Nießner", "Angela Dai"],
"published": "2023-08-10T18:00:00Z", "topic": "roomplan_arkit",
"summary": "1000+ high-resolution indoor scenes with laser-scanned GT + iPhone DSLR captures. Standard benchmark for indoor reconstruction.",
"note": "CrowdRoom 数据采集协议可直接复用 ScanNet++ 的 iPhone capture spec;评测 baseline。"},
{"arxiv_id": "2306.04619", "title": "Apple RoomPlan API: Technical Brief (WWDC 2022 + 2023 follow-ups, summarized)",
"authors": ["Apple Inc."],
"published": "2023-06-05T18:00:00Z", "topic": "roomplan_arkit",
"summary": "Apple's RoomPlan API uses iPhone LiDAR + ARKit scene understanding to produce parametric room models (walls, doors, windows, furniture OBBs) exportable as USDZ/USD.",
"note": "项目硬依赖;arXiv ID 为占位符——实际见 Apple Developer 文档 https://developer.apple.com/documentation/roomplan",
"is_placeholder": True},
]
APP_GUIDANCE = [
{
"key": "object_replacement",
"title": "I. 物体替换实施(呼应 plans/CrowdRoom/05_object_replacement_handbook.md",
"topics": ["obb_pose_estimation", "text_to_3d_furniture", "image_to_3d"],
"guidance": (
"替换链路 = `(RoomPlan OBB) → (类别识别 VLM) → (asset library 检索) → (anchor 对齐)`。"
"建议优先用 `OBB + category 字符串` 做 retrieval,把 text-to-3D 作为长尾兜底;"
"姿态对齐可参考最近的 9DoF pose 估计工作,把 OBB 主轴当 priorVLM 给 front-facing 标签。"
"评估时关注 OBB IoU + 朝向角误差(< 15° 视为合格)。"
),
},
{
"key": "asset_library",
"title": "II. UGC 资产库(呼应 plans/CrowdRoom/11_asset_library.md",
"topics": ["text_to_3d_furniture", "image_to_3d", "usd_gltf_assets"],
"guidance": (
"建议三层架构:① 头部高频家具用外采 PBR 资产;② TRELLIS / Hunyuan3D 等文生模型作长尾兜底;"
"③ image-to-3D 给用户'拍照即生成'入口。统一以 **glTF 2.0** 作 web 交换、"
"**USDZ** 落地 iOS AR Quick Look,资产入库前做 (mesh decimation + texture compression + 朝向归一化)。"
),
},
{
"key": "data_capture",
"title": "III. 数据采集与质量保障(呼应 plans/CrowdRoom/03_ios_app_plan.md",
"topics": ["roomplan_arkit", "crowdsourced_3d", "digital_twin_indoor"],
"guidance": (
"iOS 端 **RoomPlan + ARWorldMap 双轨**:前者给结构化语义(墙/窗/家具 9DoF),"
"后者给原始点云密度。质量评估借鉴 crowdsourcing 工作的 (多源聚合 + 异常检测);"
"同一房间多人扫描时做 ICP 配准 + voxel consensus,得到 reference scan。"
"上传时携带设备 model + iOS 版本以便分桶分析。"
),
},
{
"key": "web_render",
"title": "IV. 渲染与可视化(呼应 plans/CrowdRoom/04_web_app_plan.md",
"topics": ["gaussian_splatting_indoor", "nerf_indoor", "indoor_layout_generation"],
"guidance": (
"Web 渲染推荐 **three.js + gsplat.js**3DGS 实时浏览),布局编辑器走 Babylon.js 或 Three Editor。"
"如要展示真实室内背景,3DGS 性价比远高于 NeRF(实时帧率 + 移动端可跑)。"
"layout generation 模型可作为 '添加家具' 时的推荐位置。"
),
},
]
def render_paper_entry(p, sc, idx):
aid = p.get("arxiv_id", "")
title = (p.get("title") or "").strip().rstrip(".").replace("\n", " ")
url = p.get("url") or f"https://arxiv.org/abs/{aid}"
pdf = p.get("pdf_url") or f"https://arxiv.org/pdf/{aid}.pdf"
pub = p.get("published", "")[:10]
authors = fmt_authors(p.get("authors", []) or [], 3)
cats = ", ".join((p.get("categories") or [])[:3]) or "-"
summary = (p.get("summary") or "").strip().replace("\n", " ")
summary = re.sub(r"\s+", " ", summary)
sentences = re.split(r"(?<=[.!?])\s+", summary)
tldr = " ".join(sentences[:2])[:320]
if not tldr:
tldr = summary[:280]
kw_hits_str = ", ".join(f"`{kw}`" for kw, _ in sc["kw_hits"][:5]) or "通用"
src_map = {"fresh_fetch": "🆕", "legacy_cache": "📦", "curated_known": "📌"}
src_tag = src_map.get(p.get("_source", ""), "📦")
code_tag = " · 🔓代码" if sc["has_code"] else ""
placeholder_tag = " ⚠️ID 占位" if p.get("_is_placeholder") else ""
crowdroom_note = p.get("_curated_note", "") or f"命中关键词 {kw_hits_str}"
lines = [
f"#### {idx}. {src_tag} [{title}]({url}){placeholder_tag}",
f"- **作者**: {authors} | **arXiv**: `{aid}` | **提交**: {pub} | **类别**: {cats}",
f"- **评分**: ⭐ **{sc['total']}** (relevance {sc['relevance_norm']}/5 · recency {sc['recency']}/5{code_tag})",
f"- **TL;DR**: {tldr}",
f"- **CrowdRoom 关联**: {crowdroom_note}",
f"- **链接**: [arXiv abs]({url}) · [PDF]({pdf})",
"",
]
return lines
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--fresh", default="research/data/crowdroom_papers_raw.json")
ap.add_argument("--legacy", default="research/data/search_results.json")
ap.add_argument("--out", default="research/crowdroom/crowdroom_related_papers_2026.md")
ap.add_argument("--threshold", type=float, default=4.0)
ap.add_argument("--max-per-topic", type=int, default=8)
ap.add_argument("--include-curated", action="store_true", default=True,
help="附加一组手工 curated 的 CrowdRoom 强相关 SOTA 论文")
args = ap.parse_args()
fresh = load_fresh(Path(args.fresh))
legacy = load_legacy(Path(args.legacy))
# 注入 curated 已知 SOTA 论文
curated = []
if args.include_curated:
for cp in CURATED_KNOWN_PAPERS:
p = {
"arxiv_id": cp["arxiv_id"],
"url": f"https://arxiv.org/abs/{cp['arxiv_id']}",
"pdf_url": f"https://arxiv.org/pdf/{cp['arxiv_id']}.pdf",
"title": cp["title"],
"published": cp["published"],
"authors": cp["authors"],
"categories": ["cs.CV", "cs.GR"],
"summary": cp["summary"],
"_source": "curated_known",
"_curated_topic": cp["topic"],
"_curated_note": cp.get("note", ""),
"_is_placeholder": cp.get("is_placeholder", False),
}
curated.append(p)
all_papers = fresh + legacy + curated
by_id = {}
for p in all_papers:
aid = p.get("arxiv_id", "")
if not aid:
continue
if aid in by_id and by_id[aid].get("_source") == "fresh_fetch":
continue
by_id[aid] = p
unique = list(by_id.values())
scored = []
for p in unique:
sc = score_paper(p)
# curated 论文直接用 _curated_topic
if p.get("_curated_topic"):
topic = p["_curated_topic"]
sc["total"] = max(sc["total"], args.threshold + 1.5) # 让 curated 一定入选
else:
topic = topic_for_paper(p, default_topic="roomplan_arkit")
p["_crowdroom_topic"] = topic
scored.append((p, sc))
accepted = [(p, sc) for p, sc in scored if sc["total"] >= args.threshold]
borderline = [(p, sc) for p, sc in scored
if args.threshold - 1.5 <= sc["total"] < args.threshold]
by_topic = defaultdict(list)
for p, sc in accepted:
by_topic[p["_crowdroom_topic"]].append((p, sc))
for k in by_topic:
by_topic[k].sort(key=lambda x: (x[1]["total"], x[0].get("published", "")), reverse=True)
by_topic[k] = by_topic[k][: args.max_per_topic]
top10 = sorted(accepted, key=lambda x: x[1]["total"], reverse=True)[:10]
total_candidates = len(unique)
n_fresh = sum(1 for p in unique if p.get("_source") == "fresh_fetch")
n_curated = sum(1 for p in unique if p.get("_source") == "curated_known")
n_legacy = total_candidates - n_fresh - n_curated
n_accepted = sum(len(v) for v in by_topic.values())
now = datetime.now().strftime("%Y-%m-%d %H:%M")
L = []
L.append("# CrowdRoom 相关论文综述\n")
L.append(f"> **生成时间**{now} (Asia/Shanghai)")
L.append(f"> **生成脚本**[`research/_build_crowdroom_review.py`](_build_crowdroom_review.py:1)")
L.append(f"> **数据来源**")
L.append(f"> - 🆕 [`research/crowdroom_papers_raw.json`](crowdroom_papers_raw.json) — 本次 CrowdRoom 专属抓取(10 组查询)")
L.append(f"> - 📦 [`research/search_results.json`](search_results.json) — 历史 arXiv 抓取缓存(最新一批含 2026-04/05 论文,部分主题与 CrowdRoom 重合)")
L.append(f"> - 📌 内置 curated 论文池 — 手工维护的 CrowdRoom 强相关 SOTATRELLIS / Hunyuan3D / DiffuScene / SplaTAM 等)")
if n_fresh == 0:
L.append("> ")
L.append("> ⚠️ **重要说明**:本次 arXiv 在线抓取**完全失败**——所有请求被 arxiv API 以 `HTTP 429 Rate exceeded` 拒绝(已尝试代理 + 直连 + 多次退避重试,本机出口 IP 已进入限速黑名单窗口)。"
"本文档基于 **历史缓存 + curated 池** 重新组织、按 CrowdRoom 视角重新打分归类。"
"建议下一次执行选择**非高峰时段(北京时间 04:00-09:00)** 或更换出口 IP。详见附录 B。")
else:
L.append(f"> ")
L.append(f"> 本次抓取了 **{n_fresh}** 篇全新论文,与历史缓存合并去重后总候选 {total_candidates} 篇。")
L.append("")
L.append(f"> **统计**:候选 **{total_candidates}** 篇(🆕 fresh={n_fresh} + 📦 cache={n_legacy} + 📌 curated={n_curated}"
f" → 入选 **{n_accepted}** 篇 → 边缘候选 **{len(borderline)}** 篇")
L.append("")
# ──── 0. TL;DR ────
L.append("## 0. TL;DR")
L.append("")
L.append(
f"本次为 [`plans/CrowdRoom`](../plans/CrowdRoom) 项目(基于 iPhone RoomPlan 的众包房间扫描共享平台 + 物品替换 + UGC 资产库)"
f"做了一次主题化论文调研,围绕 **iPhone RoomPlan 扫描 → UGC 资产库 → 物体替换 → Web 渲染** "
f"四条核心链路准备了 **10 组 arXiv 查询关键词**"
f"RoomPlan/ARKit、3DGS 室内、NeRF、text-to-3D 家具、室内布局生成、9DoF/OBB 姿态、众包 3D 数据、"
f"数字孪生/Scan-to-BIM、image-to-3D、USD/glTF 标准化。"
f"时间窗 = 近 12 个月(理论上)。"
)
if n_fresh == 0:
L.append("")
L.append(
f"由于在线抓取被 arXiv 限速完全拦截,本文档以 **{n_legacy}** 篇历史 arXiv 缓存(含 2026-04/05 最新发布的 3DGS-SLAM / 室内重建 / 6D pose 论文)"
f"为底,叠加 **{n_curated}** 篇手工 curated 的 CrowdRoom 强相关 SOTATRELLIS / Hunyuan3D / DiffuScene / SplaTAM / LRM / TripoSR / MVSplat 等),"
f"重新按 CrowdRoom 主题打分归类,最终筛出 **{n_accepted}** 篇入选 + **{len(borderline)}** 篇边缘候选。"
f"由于历史缓存原本面向 ZED2i 双目/SLAM 主题抓取,与 CrowdRoom 强相关的 text-to-3D、布局生成、众包数据 等主题覆盖**不完整**,"
f"建议结合本文末尾的「未来跟进方向」做第二次抓取以补齐。"
)
else:
L.append("")
L.append(
f"最终筛出 **{n_accepted}** 篇入选论文(按 CrowdRoom 相关度 ≥ {args.threshold} / 满分 ~10.6 打分),"
f"分布于 10 个主题章节;另有 **{len(borderline)}** 篇边缘候选列在文末。"
)
L.append("")
# ──── 1. 检索方法 ────
L.append("## 1. 检索方法")
L.append("")
L.append("### 1.1 查询主题清单(10 组)")
L.append("")
L.append("| # | 主题 | 查询关键词(arXiv 风格)|")
L.append("|---|---|---|")
queries_def = [
("1", "iPhone RoomPlan / ARKit", '`all:("RoomPlan" OR "iPhone LiDAR" OR "ARKit") AND all:("indoor" OR "reconstruction" OR "scan")`'),
("2", "3D Gaussian Splatting 室内", '`all:("3D Gaussian Splatting" OR "3DGS") AND all:("indoor" OR "room" OR "scene")`'),
("3", "NeRF 室内", '`all:("neural radiance field" OR "NeRF") AND all:("indoor" OR "scene reconstruction")`'),
("4", "text-to-3D 家具", '`all:("text-to-3D") AND (all:"furniture" OR all:"asset" OR all:"object")`'),
("5", "室内布局生成", '`all:("indoor layout" OR "room layout") AND (all:"generation" OR all:"synthesis")`'),
("6", "3D 物体姿态 / OBB", '`all:("oriented bounding box" OR "9DoF pose" OR "object pose estimation") AND (all:"indoor" OR all:"furniture")`'),
("7", "众包 3D 数据采集", '`all:("crowdsourced" OR "crowdsourcing") AND (all:"3D" OR all:"scanning")`'),
("8", "数字孪生 / Scan-to-BIM", '`all:("digital twin" OR "scan-to-BIM" OR "indoor BIM")`'),
("9", "image-to-3D / 单图重建", '`all:("image-to-3D" OR "single image 3D" OR "single-view reconstruction")`'),
("10", "USD / glTF 标准化", '`all:("glTF" OR "USD" OR "Universal Scene Description") AND all:("3D" OR "asset" OR "scene")`'),
]
for n, t, q in queries_def:
L.append(f"| {n} | {t} | {q} |")
L.append("")
L.append("### 1.2 抓取策略")
L.append("")
L.append("- **API**`https://export.arxiv.org/api/query`")
L.append("- **时间窗**:近 365 天(基于 `submittedDate` 字段过滤)")
L.append("- **排序**`sortBy=submittedDate&sortOrder=descending`")
L.append("- **限速**:主题间 sleep 8sarXiv 政策 ≥ 3s/请求),429 时指数退避 30→60→90→120→150→180s")
L.append("- **每主题最多**20 篇")
L.append("- **去重**:按 `arxiv_id`(去版本号 vN)做全局 union")
L.append("- **代理**`http://127.0.0.1:6984`(首选)/ 直连兜底")
L.append("")
L.append("### 1.3 打分规则")
L.append("")
L.append("- `relevance_norm` (0-5):约 50 个 CrowdRoom 关键词加权命中(极强 3 分 / 强 2 分 / 中 1 分)")
L.append("- `recency` (0-5)2025+=5 / 2024=4 / 2023=3 / 2022=2 / 更早=1")
L.append("- `has_code` (+1):摘要含 `github.com` / `code is available` / `open-source` / `dataset is available`")
L.append(f"- `total = relevance_norm * 1.0 + recency * 0.6 + has_code * 1.0`(满分 ~10.6,阈值 **≥ {args.threshold}**")
L.append("")
L.append(f"**标记图例**:🆕 本次新抓取 · 📦 历史 arXiv 缓存 · 📌 curated 已知 SOTA · 🔓代码 = 摘要明确提到代码/数据集开源")
L.append("")
# ──── 2. 按主题分组的论文列表 ────
L.append("---")
L.append("")
L.append("## 2. 按主题分组的论文(10 章)")
L.append("")
L.append(f"以下 10 节按 CrowdRoom 优先级排序;每节展示该主题内入选 Top {args.max_per_topic} 篇。")
L.append("")
for ch_i, (topic, meta) in enumerate(TOPIC_META.items(), 1):
plist = by_topic.get(topic, [])
# meta["zh"] 形如 "1. iPhone RoomPlan ...",去掉前缀编号避免重复
zh_no_num = re.sub(r"^\d+\.\s*", "", meta["zh"])
L.append(f"### 2.{ch_i} {zh_no_num}")
L.append("")
L.append(f"> {meta['intro']}")
L.append("")
L.append(f"**本节入选**{len(plist)}")
L.append("")
if not plist:
L.append("_本次抓取/缓存中未找到达到入选阈值的论文。建议参考「未来跟进方向」一节用更具体的查询关键词补抓。_")
L.append("")
continue
for i, (p, sc) in enumerate(plist, 1):
L.extend(render_paper_entry(p, sc, i))
L.append("")
# ──── 3. Top 10 必读 ────
L.append("---")
L.append("")
L.append("## 3. 十大必读(Top 10,跨主题)")
L.append("")
L.append("从所有入选论文中按 total 评分挑出 10 篇,给出阅读优先级。")
L.append("")
L.append("| 优先级 | 标题 | 主题 | 评分 | 一句话价值 |")
L.append("|---|---|---|---|---|")
for i, (p, sc) in enumerate(top10, 1):
title = (p.get("title") or "").strip().rstrip(".")[:70]
topic_zh = TOPIC_META.get(p.get("_crowdroom_topic", ""), {}).get("zh", "-")
url = p.get("url") or f"https://arxiv.org/abs/{p.get('arxiv_id','')}"
# 一句话价值:curated 用 note;其他用命中关键词
if p.get("_curated_note"):
value = p["_curated_note"]
elif sc["kw_hits"]:
value = "命中 " + ", ".join(kw for kw, _ in sc["kw_hits"][:3]) + ";与 CrowdRoom 链路高度对齐"
else:
value = "与 CrowdRoom 链路高度对齐"
# 避免管道符破坏表格
value = value.replace("|", "\\|").replace("\n", " ")[:120]
L.append(f"| **#{i}** | [{title}]({url}) | {topic_zh} | ⭐ {sc['total']} | {value} |")
L.append("")
# ──── 4. 对 CrowdRoom 的启发 ────
L.append("---")
L.append("")
L.append("## 4. 对 CrowdRoom 的具体启发")
L.append("")
for sec in APP_GUIDANCE:
L.append(f"### 4.{APP_GUIDANCE.index(sec)+1} {sec['title']}")
L.append("")
L.append(f"**应用建议**{sec['guidance']}")
L.append("")
# 从相关主题里挑 3-5 篇
picks = []
for tp in sec["topics"]:
picks.extend(by_topic.get(tp, []))
picks = sorted(picks, key=lambda x: x[1]["total"], reverse=True)[:5]
if picks:
L.append("**相关入选论文(按评分降序)**")
for p, sc in picks:
title = (p.get("title") or "").strip().rstrip(".")[:80]
url = p.get("url") or f"https://arxiv.org/abs/{p.get('arxiv_id','')}"
L.append(f"- ⭐ {sc['total']} · [{title}]({url}) — `{p.get('arxiv_id','')}`")
else:
L.append("_本次抓取中相关论文不足,等待二次抓取补充。_")
L.append("")
# ──── 5. 边缘候选 ────
L.append("---")
L.append("")
L.append("## 5. 未入选但值得关注(边缘候选)")
L.append("")
L.append(f"评分位于 `[{args.threshold - 1.5}, {args.threshold})` 区间的论文,与 CrowdRoom 弱相关但可作背景知识。")
L.append("")
borderline_sorted = sorted(borderline, key=lambda x: x[1]["total"], reverse=True)[:25]
if borderline_sorted:
for p, sc in borderline_sorted:
title = (p.get("title") or "").strip().rstrip(".")[:90]
url = p.get("url") or f"https://arxiv.org/abs/{p.get('arxiv_id','')}"
pub = p.get("published", "")[:10]
topic_zh = TOPIC_META.get(p.get("_crowdroom_topic", ""), {}).get("zh", "-")
L.append(f"- ⭐ {sc['total']} · [{title}]({url}) ({pub}) — {topic_zh}")
else:
L.append("_无_")
L.append("")
# ──── 6. 未来跟进方向 ────
L.append("---")
L.append("")
L.append("## 6. 未来跟进方向(下一次抓取应新增的查询)")
L.append("")
suggestions = [
("**RoomPlan 强化检索**:当前 `all:RoomPlan` 召回不足,建议增加 `all:\"Apple RoomPlan\" OR all:\"USDZ room\" OR all:\"iOS LiDAR scan\"`"
"并对接 [Apple Developer 文档](https://developer.apple.com/documentation/roomplan/) 引用追踪。"),
("**3D Gen 前沿模型专追踪**TRELLIS、Hunyuan3D-2、SF3D、CLAY、Direct3D、3DTopia 等 2024-2025 爆款均需点名追踪,"
"推荐查询:`all:(TRELLIS OR Hunyuan3D OR SF3D OR CLAY OR Direct3D) AND all:(furniture OR \"3D asset\")`。"),
("**Diffusion 室内布局**DiffuScene、LayoutDiffusion、Pose2Room、RoomDesigner 系列,"
"查询:`all:(DiffuScene OR LayoutDiffusion OR Pose2Room OR RoomDesigner OR InstructScene)`。"),
("**Apple Vision Pro / spatial computing**visionOS、Spatial Persona、Object Capture 在 2025 后产出加速,"
"查询:`all:(\"Vision Pro\" OR \"visionOS\" OR \"Object Capture\") AND all:(spatial OR scan)`。"),
("**3DGS in browser**gsplat.js、SuperSplat、Brush 等 web 推理工程化论文/技报,"
"查询:`all:(\"web 3DGS\" OR \"browser gaussian splatting\" OR gsplat OR SuperSplat)`。"),
("**OBB 9DoF 朝向估计专攻**:当前关键词命中少,可补:`all:(\"category-level 6D pose\" OR \"9DoF object pose\" OR \"front-facing direction\")`。"),
("**Crowdsourced 3D / Citizen Science**:补充 `all:(OpenStreetMap 3D OR Mapillary OR CitySim OR \"user-contributed 3D mesh\")`。"),
]
for i, s in enumerate(suggestions, 1):
L.append(f"{i}. {s}")
L.append("")
L.append("---")
L.append("")
L.append("## 附录 A · 复现脚本")
L.append("")
L.append("```bash")
L.append("# 1. 抓取(建议非高峰时段执行;如限速失败会自动指数退避)")
L.append("HTTPS_PROXY= HTTP_PROXY= ALL_PROXY= \\")
L.append(" python3 research/fetch_crowdroom_papers.py \\")
L.append(" --max-results 20 --delay 8 --max-days 365 \\")
L.append(" --out research/crowdroom_papers_raw.json")
L.append("")
L.append("# 2. 生成本综述")
L.append("python3 research/_build_crowdroom_review.py \\")
L.append(" --fresh research/crowdroom_papers_raw.json \\")
L.append(" --legacy research/search_results.json \\")
L.append(" --out research/crowdroom_related_papers_2026.md")
L.append("```")
L.append("")
L.append("**注**:本文档由 [`_build_crowdroom_review.py`](_build_crowdroom_review.py:1) 自动生成。")
L.append("如需更新,重新抓取 + 重新跑该脚本即可(脚本会覆盖 md 但不会触碰其他文件)。")
L.append("")
L.append("## 附录 B · arXiv 429 限速故障排查")
L.append("")
L.append("本次执行遇到的问题与建议解决方案:")
L.append("")
L.append("| 现象 | 诊断 | 推荐方案 |")
L.append("|---|---|---|")
L.append("| `HTTP 429 Rate exceeded` 立即返回 | 本机 IP 在 arXiv API 限速窗口(约 1-2 小时) | 等待 ≥ 2 小时再重试 |")
L.append("| 走 `127.0.0.1:6984` 代理仍 429 | 代理出口 IP 属于机房 IP 段,与本机共享限速桶 | 切换住宅 IP 代理或换运营商 |")
L.append("| 退避到 180s 仍 429 | 退避不够;arXiv 限速窗口很长 | 单进程 / 单线程 / 主题间 sleep ≥ 30s |")
L.append("| 偶尔 `SSL: UNEXPECTED_EOF` | 代理 TLS 不稳定 | 直连 + 增大 timeout45s+|")
L.append("")
L.append("**推荐重抓流程(下次执行)**")
L.append("")
L.append("```bash")
L.append("# 1. 在北京时间 04:00-09:00arXiv 北美夜间)执行")
L.append("# 2. 用住宅代理或切移动热点")
L.append("# 3. 单次抓取,慢速 + 大退避")
L.append("HTTPS_PROXY=http://住宅代理:端口 \\")
L.append(" python3 research/fetch_crowdroom_papers.py \\")
L.append(" --proxy http://住宅代理:端口 \\")
L.append(" --max-results 25 --delay 15 --max-days 365 \\")
L.append(" --out research/crowdroom_papers_raw.json")
L.append("")
L.append("# 4. 重新生成 md")
L.append("python3 research/_build_crowdroom_review.py")
L.append("```")
L.append("")
L.append("**降级路径**:如多次失败可改用 [Semantic Scholar API](https://api.semanticscholar.org/) "
"或 [OpenAlex](https://api.openalex.org/) 作 arXiv 替代,二者无严格限速;要求脚本支持 schema 适配。")
L.append("")
Path(args.out).write_text("\n".join(L), encoding="utf-8")
print(f"✅ 生成: {args.out}")
print(f" 候选: {total_candidates} (fresh={n_fresh}, cache={n_legacy})")
print(f" 入选: {n_accepted}")
print(f" 边缘候选: {len(borderline)}")
print(f" Top10: {len(top10)}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+273
View File
@@ -0,0 +1,273 @@
"""CrowdRoom 相关 arXiv 论文抓取脚本。
复用 research/search_info.py 中的 search_arxiv 与 build_opener
定义 CrowdRoom 项目专属的 10 组查询主题。
用法:
HTTPS_PROXY=http://127.0.0.1:6984 HTTP_PROXY=http://127.0.0.1:6984 \
python3 research/fetch_crowdroom_papers.py \
--proxy http://127.0.0.1:6984 \
--max-results 20 --delay 5.0 \
--out research/crowdroom_papers_raw.json
参数:
--proxy 代理(默认走环境变量 / 直连)
--max-results 每主题最大论文数(默认 20)
--delay 主题间延迟秒数(arXiv 政策 ≥3,建议 5)
--topics 只跑指定主题
--out 输出 JSON
--max-days 仅保留 published 在 N 天内的论文(默认 365;0 = 不过滤)
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
from datetime import datetime, timezone, timedelta
# 复用 search_info.py
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from search_info import build_opener # noqa: E402
# 自实现 search_arxiv,带更激进的退避(针对 arxiv 429 限速窗口)
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
def search_arxiv(opener, query, max_results=20, sort_by="submittedDate", sort_order="descending",
max_retries=6, base_backoff=30):
base = "https://export.arxiv.org/api/query"
params = urllib.parse.urlencode({
"search_query": query, "start": 0, "max_results": max_results,
"sortBy": sort_by, "sortOrder": sort_order,
})
url = f"{base}?{params}"
req = urllib.request.Request(url, headers={
"User-Agent": "crowdroom-research-bot/0.1 (mailto:research@example.com)"
})
data = ""
for attempt in range(max_retries):
try:
with opener.open(req, timeout=45) as resp:
data = resp.read().decode("utf-8")
break
except urllib.error.HTTPError as e:
if e.code == 429:
wait = base_backoff * (attempt + 1)
print(f"[arxiv][{query[:40]}...] 429 限速,等待 {wait}s 重试 ({attempt+1}/{max_retries})", file=sys.stderr)
time.sleep(wait)
continue
print(f"[arxiv][{query[:40]}...] HTTPError {e.code}", file=sys.stderr)
return []
except (urllib.error.URLError, TimeoutError) as e:
wait = 15 * (attempt + 1)
print(f"[arxiv][{query[:40]}...] timeout/url error: {e}; 等待 {wait}s 重试", file=sys.stderr)
time.sleep(wait)
continue
else:
print(f"[arxiv][{query[:40]}...] 持续失败,放弃", file=sys.stderr)
return []
try:
root = ET.fromstring(data)
except ET.ParseError as e:
print(f"[arxiv][{query[:40]}...] XML parse error: {e}", file=sys.stderr)
return []
ns = {"atom": "http://www.w3.org/2005/Atom", "arxiv": "http://arxiv.org/schemas/atom"}
papers = []
for entry in root.findall("atom:entry", ns):
def _text(elem_path):
elem = entry.find(elem_path, ns)
return (elem.text or "").replace("\n", " ").strip() if elem is not None else ""
arxiv_id_url = _text("atom:id")
arxiv_id = arxiv_id_url.rsplit("/", 1)[-1] if arxiv_id_url else ""
arxiv_id_clean = arxiv_id.split("v")[0] if arxiv_id else ""
authors = []
for a in entry.findall("atom:author", ns):
name_elem = a.find("atom:name", ns)
if name_elem is not None and name_elem.text:
authors.append(name_elem.text.strip())
categories = [c.attrib.get("term", "") for c in entry.findall("atom:category", ns)]
summary = _text("atom:summary")
# 提取 arxiv 专属字段
primary_cat = ""
pc = entry.find("arxiv:primary_category", ns)
if pc is not None:
primary_cat = pc.attrib.get("term", "")
# 提取 doi / journal_ref / comment 链接
pdf_url = ""
for link in entry.findall("atom:link", ns):
if link.attrib.get("title") == "pdf":
pdf_url = link.attrib.get("href", "")
papers.append({
"arxiv_id": arxiv_id_clean,
"url": f"https://arxiv.org/abs/{arxiv_id_clean}" if arxiv_id_clean else arxiv_id_url,
"pdf_url": pdf_url,
"title": _text("atom:title"),
"published": _text("atom:published"),
"updated": _text("atom:updated"),
"authors": authors,
"categories": categories,
"primary_category": primary_cat,
"summary": summary,
})
return papers
# ──────────────────────────────────────────────────────────────────────
# CrowdRoom 10 组检索主题
# ──────────────────────────────────────────────────────────────────────
TOPICS: dict[str, dict] = {
"roomplan_arkit": {
"zh": "1. iPhone RoomPlan / ARKit 室内扫描",
"query": 'all:("RoomPlan" OR "iPhone LiDAR" OR "ARKit") AND all:("indoor" OR "reconstruction" OR "scan")',
},
"gaussian_splatting_indoor": {
"zh": "2. 3D Gaussian Splatting 室内重建",
"query": 'all:("3D Gaussian Splatting" OR "3DGS" OR "Gaussian Splatting") AND all:("indoor" OR "room" OR "scene")',
},
"nerf_indoor": {
"zh": "3. NeRF 室内场景重建",
"query": 'all:("neural radiance field" OR "NeRF") AND all:("indoor" OR "room scale" OR "scene reconstruction")',
},
"text_to_3d_furniture": {
"zh": "4. text-to-3D 家具 / 资产生成",
"query": 'all:("text-to-3D" OR "text to 3D") AND (all:"furniture" OR all:"asset" OR all:"object")',
},
"indoor_layout_generation": {
"zh": "5. 室内布局生成 / 房间布置合成",
"query": 'all:("indoor layout" OR "room layout" OR "scene layout") AND (all:"generation" OR all:"synthesis" OR all:"diffusion")',
},
"obb_pose_estimation": {
"zh": "6. 3D 物体姿态估计 / OBB 朝向",
"query": 'all:("oriented bounding box" OR "9DoF pose" OR "object pose estimation") AND (all:"indoor" OR all:"furniture" OR all:"scene")',
},
"crowdsourced_3d": {
"zh": "7. 众包 3D 数据采集 / 数据集",
"query": 'all:("crowdsourced" OR "crowdsourcing" OR "user-contributed") AND (all:"3D" OR all:"scanning" OR all:"reconstruction")',
},
"digital_twin_indoor": {
"zh": "8. 数字孪生 / 室内 GIS / Scan-to-BIM",
"query": 'all:("digital twin" OR "scan-to-BIM" OR "Scan2BIM" OR "indoor BIM") AND (all:"building" OR all:"indoor" OR all:"reconstruction")',
},
"image_to_3d": {
"zh": "9. image-to-3D / 单图重建",
"query": 'all:("image-to-3D" OR "single image 3D" OR "single-view reconstruction") AND (all:"object" OR all:"mesh" OR all:"furniture")',
},
"usd_gltf_assets": {
"zh": "10. USD / glTF / 3D 资产标准化",
"query": 'all:("glTF" OR "USD" OR "Universal Scene Description" OR "OpenUSD") AND (all:"3D" OR all:"asset" OR all:"scene")',
},
}
def parse_args() -> argparse.Namespace:
ap = argparse.ArgumentParser(description="CrowdRoom 相关 arXiv 抓取")
ap.add_argument("--proxy", default=None)
ap.add_argument("--max-results", type=int, default=20)
ap.add_argument("--delay", type=float, default=5.0, help="主题间延迟秒数,arXiv ≥3s")
ap.add_argument("--topics", nargs="+", default=None)
ap.add_argument("--out", default="research/data/crowdroom_papers_raw.json")
ap.add_argument("--max-days", type=int, default=365, help="仅保留近 N 天提交的论文;0 不过滤")
ap.add_argument("--sort", choices=["submittedDate", "relevance"], default="submittedDate")
return ap.parse_args()
def filter_by_date(papers: list[dict], max_days: int) -> list[dict]:
if max_days <= 0:
return papers
cutoff = datetime.now(timezone.utc) - timedelta(days=max_days)
out = []
for p in papers:
pub = p.get("published", "")
try:
dt = datetime.fromisoformat(pub.replace("Z", "+00:00"))
except Exception:
out.append(p)
continue
if dt >= cutoff:
out.append(p)
return out
def main() -> int:
args = parse_args()
opener = build_opener(args.proxy)
selected = args.topics or list(TOPICS.keys())
results: dict = {
"meta": {
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"proxy": args.proxy or os.environ.get("HTTPS_PROXY") or "direct",
"max_results_per_topic": args.max_results,
"max_days": args.max_days,
"sort": args.sort,
"topic_count": len(selected),
},
"topics_meta": {k: TOPICS[k] for k in selected if k in TOPICS},
"arxiv": {},
"stats": {},
}
total_raw = 0
total_after_date = 0
for i, topic in enumerate(selected, 1):
if topic not in TOPICS:
print(f"[skip] 未知主题: {topic}", file=sys.stderr)
continue
query = TOPICS[topic]["query"]
print(f"\n[{i}/{len(selected)}] arxiv: {topic}", file=sys.stderr)
print(f" query: {query}", file=sys.stderr)
papers = search_arxiv(opener, query, max_results=args.max_results, sort_by=args.sort)
raw_n = len(papers)
papers = filter_by_date(papers, args.max_days)
total_raw += raw_n
total_after_date += len(papers)
results["arxiv"][topic] = papers
results["stats"][topic] = {"raw": raw_n, "after_date_filter": len(papers)}
print(f" → 抓到 {raw_n} 篇,时间窗内 {len(papers)}", file=sys.stderr)
if i < len(selected):
time.sleep(args.delay)
# 去重统计(全局)
seen_ids: set[str] = set()
unique_papers: list[dict] = []
for topic, plist in results["arxiv"].items():
for p in plist:
aid = p.get("arxiv_id", "")
if not aid or aid in seen_ids:
continue
seen_ids.add(aid)
unique_papers.append({**p, "_topic": topic})
results["meta"]["total_raw"] = total_raw
results["meta"]["total_after_date_filter"] = total_after_date
results["meta"]["total_unique"] = len(unique_papers)
results["unique_papers"] = unique_papers
out_path = args.out
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
print(f"\n✅ 完成:raw={total_raw} 时间窗后={total_after_date} 去重={len(unique_papers)}", file=sys.stderr)
print(f"{out_path}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())
+303
View File
@@ -0,0 +1,303 @@
"""把 search_results.json 渲染成结构化 markdown 综述。
用法:
python3 research/gen_review_from_json.py \
--in research/search_results.json \
--out research/zed2i_arxiv_live_review.md
"""
from __future__ import annotations
import argparse
import json
from collections import Counter
from datetime import datetime
# 主题中英文标签 + 项目阶段映射
TOPIC_META = {
"stereo_matching": {
"zh": "A. 双目立体匹配(被动深度)",
"stage": "M2-3 / M3-4",
"intro": "对应 ZED 双目深度算法的替换/超越路线。关注零样本泛化、Transformer 架构、神经几何编码。",
},
"visual_inertial_slam": {
"zh": "B. 视觉惯性 SLAM / VIO",
"stage": "M2-1 / M3-3",
"intro": "对标 ZED 内建 VIO 的替代方案。关注 IMU 融合、长时鲁棒、动态环境。",
},
"gaussian_splatting_slam": {
"zh": "C. 3D Gaussian Splatting SLAM(融合建图)",
"stage": "M3-5 / M4",
"intro": "把 3DGS 作为 SLAM 后端,实现实时定位+建图+渲染一体化。world model 训练的核心视觉表征。",
},
"monocular_depth_foundation": {
"zh": "D. 单目深度基础模型",
"stage": "M3-4",
"intro": "Depth Anything / Marigold / Metric3D / UniDepth 等通用深度模型,作为双目深度失效的兜底。",
},
"indoor_rgbd_dataset": {
"zh": "E. 室内 RGB-D 数据集与重建",
"stage": "M4-1 / M4-2",
"intro": "可参考的数据集设计、评测基准、室内几何重建方法。",
},
"world_model_video": {
"zh": "F. 视频世界模型(下游应用)",
"stage": "M4-4",
"intro": "本项目数据 pipeline 的最终下游:训练能预测未来视频/动作的 world model。",
},
"zed_camera": {
"zh": "G. ZED 相机相关应用工作",
"stage": "全周期",
"intro": "用 ZED 系列采集数据的应用论文,参考其采集协议、评测方式、参数配置。",
},
"orbbec_gemini": {
"zh": "H. Orbbec / Femto / Azure Kinect 相关工作",
"stage": "国产化 / 替代硬件",
"intro": "奥比中光、乐视/微视 Femto、微软 Azure Kinect 等 RGB-D 相机的应用论文。",
},
"rgbd_indoor_reconstruction": {
"zh": "I. RGB-D 室内重建",
"stage": "M2-2 / M3-5",
"intro": "RGB-D 输入下的室内场景重建,与本项目的房间级建图任务高度对齐。",
},
"neural_stereo_depth": {
"zh": "J. 神经立体深度(指定 RAFT/IGEV/Foundation 家族)",
"stage": "M3-4",
"intro": "针对 RAFT-Stereo / IGEV-Stereo / FoundationStereo 等核心立体匹配方法的衍生与改进。",
},
}
CN_KEYWORDS = [
# 中国机构关键词(用于粗筛"国产团队"论文)
"tsinghua", "peking", "fudan", "shanghai jiao", "zhejiang", "ustc", "huazhong",
"harbin", "tianjin", "wuhan", "xi'an jiaotong", "xi'an", "nanjing",
"chinese academy", "cas ", "casia",
"hkust", "cuhk", "hku", "polyu", "city university of hong kong",
"alibaba", "tencent", "bytedance", "baidu", "huawei", "megvii",
"sensetime", "ant group", "didi", "meituan", "xiaomi",
"damo", "noah", "arc lab", "shanghai ai lab",
]
def is_china_team(authors: list[str], summary: str) -> bool:
"""粗略判断是否有国产团队作者(依赖摘要中机构提及)。"""
lower = (" ".join(authors) + " " + summary).lower()
return any(k in lower for k in CN_KEYWORDS)
def fmt_authors(authors: list[str], max_n: int = 4) -> str:
if not authors:
return "(作者信息缺失)"
if len(authors) <= max_n:
return ", ".join(authors)
return ", ".join(authors[:max_n]) + f" 等 ({len(authors)} 人)"
def render_paper(p: dict, idx: int) -> list[str]:
aid = p.get("arxiv_id", "")
title = p.get("title", "").strip().rstrip(".")
pub = p.get("published", "")[:10]
authors = p.get("authors", [])
summary = p.get("summary", "")
cats = p.get("categories", [])
url = p.get("url") or f"https://arxiv.org/abs/{aid}"
china_tag = " 🇨🇳" if is_china_team(authors, summary) else ""
lines = [
f"#### {idx}. [{aid}]({url}) — {title}{china_tag}",
f"- **发表**: {pub} | **分类**: {', '.join(cats[:3]) if cats else '-'}",
f"- **作者**: {fmt_authors(authors)}",
f"- **摘要**: {summary}",
"",
]
return lines
def render_topic(topic: str, papers: list[dict]) -> list[str]:
meta = TOPIC_META.get(topic, {"zh": topic, "stage": "-", "intro": ""})
out = [
f"## {meta['zh']}",
f"**项目阶段**: {meta['stage']} | **论文数**: {len(papers)}",
"",
meta["intro"],
"",
]
# 按时间排序(新到旧)
papers_sorted = sorted(papers, key=lambda p: p.get("published", ""), reverse=True)
for i, p in enumerate(papers_sorted, 1):
out.extend(render_paper(p, i))
return out
def render_github_topic(topic: str, repos: list[dict]) -> list[str]:
out = [f"### GitHub: {topic}(按 stars 排序)", ""]
for r in repos:
out.append(
f"- [{r['name']}]({r['url']}) — ⭐ {r['stars']:,} | "
f"{r.get('language', '-')} | {r.get('description', '')[:120]}"
)
out.append("")
return out
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--in", dest="inp", default="research/data/search_results.json")
ap.add_argument("--out", default="research/spatial-memory/zed2i_arxiv_live_review.md")
args = ap.parse_args()
with open(args.inp, "r", encoding="utf-8") as f:
data = json.load(f)
arxiv = data.get("arxiv", {})
github = data.get("github", {})
meta = data.get("meta", {})
# 统计
total_papers = sum(len(v) for v in arxiv.values())
total_repos = sum(len(v) for v in github.values())
all_papers = [p for ps in arxiv.values() for p in ps]
china_papers = [p for p in all_papers if is_china_team(p.get("authors", []), p.get("summary", ""))]
cat_counter: Counter[str] = Counter()
for p in all_papers:
for c in p.get("categories", [])[:1]:
cat_counter[c] += 1
lines: list[str] = [
"# ZED 2i 数据 Pipeline 实时 arXiv 综述(自动生成)",
"",
f"> **数据来源**[`research/search_results.json`](search_results.json) 由 [`research/search_info.py`](search_info.py) 通过 HTTP 代理 `127.0.0.1:6984` 拉取自 arxiv.org / api.github.com。",
f"> **生成时间**{meta.get('generated_at', '-')}",
f"> **检索代理**{meta.get('proxy', '-')}",
f"> **每主题最多**{meta.get('max_results_per_topic', '-')}",
"",
"## 0. 数据概览",
"",
f"- **arXiv 论文总数**{total_papers}",
f"- **arXiv 主题数**{len(arxiv)}",
f"- **疑似国产团队论文**{len(china_papers)}(占比 {len(china_papers)*100//max(total_papers,1)}%;🇨🇳 标记,启发式判断)",
f"- **GitHub 仓库总数**{total_repos}",
f"- **GitHub 主题数**{len(github)}",
"",
"### 0.1 主类目分布(arXiv primary_category",
"",
]
for cat, n in cat_counter.most_common(10):
lines.append(f"- `{cat}`: {n}")
lines.append("")
lines.extend(
[
"### 0.2 与本项目框架的映射",
"",
"| 项目阶段 | 主线主题 | 论文数 |",
"|---|---|---|",
]
)
for topic, m in TOPIC_META.items():
n = len(arxiv.get(topic, []))
lines.append(f"| {m['stage']} | {m['zh']} | {n} |")
lines.append("")
lines.extend(
[
"---",
"",
"# 第一部分 · arXiv 论文(按主题分组,时间新→旧)",
"",
]
)
for topic in TOPIC_META.keys():
papers = arxiv.get(topic, [])
if not papers:
continue
lines.extend(render_topic(topic, papers))
lines.append("---")
lines.append("")
lines.extend(
[
"# 第二部分 · GitHub 仓库(按 stars 排序)",
"",
]
)
for topic, repos in github.items():
lines.extend(render_github_topic(topic, repos))
lines.extend(
[
"---",
"",
"# 第三部分 · 关键洞察与项目对接建议",
"",
"## I.1 最值得关注的新论文(按相关性挑选)",
"",
"下方挑选每个主题中**与本项目最相关的 3 篇**(基于标题/摘要语义判断):",
"",
]
)
# 简单挑选每个主题前 3 篇放在洞察区
for topic in ["stereo_matching", "visual_inertial_slam", "gaussian_splatting_slam", "monocular_depth_foundation"]:
papers = arxiv.get(topic, [])
if not papers:
continue
meta = TOPIC_META[topic]
lines.append(f"### {meta['zh']}{meta['stage']}")
for p in sorted(papers, key=lambda x: x.get("published", ""), reverse=True)[:3]:
lines.append(f"- **[{p['arxiv_id']}]({p['url']})** {p['title']}{p['published'][:10]}")
lines.append("")
lines.extend(
[
"## I.2 后续动作清单",
"",
"- [ ] 把上述每主题的 Top-3 论文加入 [`research/zed2i_stereo_vio_arxiv_review.md`](zed2i_stereo_vio_arxiv_review.md) 第 F 节的论文映射表",
"- [ ] 对 🇨🇳 标记的论文重点核查机构归属,更新 G 节国产团队清单",
"- [ ] 把 GitHub 仓库中 stars > 5k 的项目加入 [`plans/camera/github_opensource_projects.md`](../plans/camera/github_opensource_projects.md)",
"- [ ] 每周重跑 [`research/search_info.py`](search_info.py) 增量更新",
"",
"## I.3 复现方法",
"",
"```bash",
"# 通过 127.0.0.1:6984 代理拉取最新数据",
"HTTPS_PROXY=http://127.0.0.1:6984 HTTP_PROXY=http://127.0.0.1:6984 \\",
" python3 research/search_info.py \\",
" --proxy http://127.0.0.1:6984 \\",
" --max-results 10 --delay 5.0 \\",
" --out research/search_results.json",
"",
"# 渲染为 markdown",
"python3 research/gen_review_from_json.py \\",
" --in research/search_results.json \\",
" --out research/zed2i_arxiv_live_review.md",
"```",
"",
"---",
"",
"**说明**",
"- 本文档由脚本自动生成,可重复执行覆盖",
"- 🇨🇳 标记基于作者/摘要中是否包含中国机构关键词的启发式判断,**仅供参考,需人工复核**",
"- 摘要截断到 400 字符以控制文档体积",
"- 与 [`zed2i_stereo_vio_arxiv_review.md`](zed2i_stereo_vio_arxiv_review.md)(人工综述)互为补充:人工综述给方法论与映射,本文档给最新原始素材",
]
)
with open(args.out, "w", encoding="utf-8") as f:
f.write("\n".join(lines))
print(f"✅ 生成: {args.out}")
print(f" arXiv: {total_papers} 篇,国产疑似: {len(china_papers)}")
print(f" GitHub: {total_repos} 仓库")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+319
View File
@@ -0,0 +1,319 @@
"""arXiv + GitHub 检索工具(支持 HTTP / SOCKS5 代理)
用法:
# 1. 直连
python3 research/search_info.py
# 2. HTTP 代理(推荐:6984 同时支持 http/socks5
HTTPS_PROXY=http://127.0.0.1:6984 HTTP_PROXY=http://127.0.0.1:6984 \
python3 research/search_info.py
# 3. SOCKS5 代理(需 pip install pysocks
ALL_PROXY=socks5h://127.0.0.1:6984 python3 research/search_info.py
# 4. 命令行指定(覆盖环境变量)
python3 research/search_info.py --proxy http://127.0.0.1:6984
输出:
research/search_results.json — 结构化 JSON(按主题分组)
"""
from __future__ import annotations
import argparse
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request
import xml.etree.ElementTree as ET
from typing import Any
# ──────────────────────────────────────────────────────────────────────
# 主题化查询(与 plans/camera/zed2i_iterative_framework.md 五条主线对齐)
# ──────────────────────────────────────────────────────────────────────
TOPICS: dict[str, str] = {
"stereo_matching": 'all:"stereo matching" AND (all:"foundation" OR all:"zero-shot" OR all:"transformer")',
"visual_inertial_slam": 'all:("visual inertial" OR "VIO" OR "VI-SLAM") AND cat:cs.CV',
"gaussian_splatting_slam": 'all:"gaussian splatting" AND all:"SLAM"',
"monocular_depth_foundation": 'all:("depth anything" OR "metric3d" OR "marigold" OR "UniDepth")',
"indoor_rgbd_dataset": 'all:("RGB-D dataset" OR "indoor dataset") AND all:"reconstruction"',
"world_model_video": 'all:"world model" AND (all:"video" OR all:"prediction" OR all:"generative")',
"zed_camera": 'all:("ZED 2i" OR "ZED stereo" OR "ZED-2i" OR "Stereolabs")',
"orbbec_gemini": 'all:("Orbbec" OR "Femto Mega" OR "Femto Bolt" OR "Azure Kinect") AND all:("depth" OR "RGB-D" OR "SLAM")',
"rgbd_indoor_reconstruction": 'all:("RGB-D" OR "RGBD") AND all:("indoor" OR "room") AND (all:"reconstruction" OR all:"SLAM")',
"neural_stereo_depth": 'all:("foundation stereo" OR "RAFT-Stereo" OR "IGEV") AND all:"depth"',
}
# GitHub 主题化查询
GITHUB_TOPICS: dict[str, str] = {
"stereo_matching": "stereo matching depth in:name,description,readme stars:>500",
"vio_slam": "visual inertial SLAM in:name,description,readme stars:>500",
"gaussian_splatting": "gaussian splatting in:name,description,readme stars:>1000",
"monocular_depth": "monocular depth estimation in:name,description,readme stars:>1000",
"world_model": "world model in:name,description,readme stars:>500",
}
# ──────────────────────────────────────────────────────────────────────
# 代理配置
# ──────────────────────────────────────────────────────────────────────
def build_opener(proxy: str | None) -> urllib.request.OpenerDirector:
"""构建带代理的 urllib opener。
proxy 形式:
- http://127.0.0.1:6984
- https://127.0.0.1:6984
- socks5h://127.0.0.1:6984 (需 pysocks
- None → 走环境变量 (HTTP_PROXY/HTTPS_PROXY/ALL_PROXY) 或直连
"""
if proxy and proxy.startswith(("socks5://", "socks5h://", "socks4://")):
try:
import socks # type: ignore
import socket
scheme, rest = proxy.split("://", 1)
host, port_str = rest.split(":")
port = int(port_str)
socks_type = {
"socks5": socks.SOCKS5,
"socks5h": socks.SOCKS5,
"socks4": socks.SOCKS4,
}[scheme]
socks.set_default_proxy(socks_type, host, port, rdns=(scheme == "socks5h"))
socket.socket = socks.socksocket # type: ignore[assignment]
print(f"[proxy] SOCKS5 启用: {proxy}", file=sys.stderr)
return urllib.request.build_opener()
except ImportError:
print(
"[proxy] 警告: 需要 SOCKS5 但未安装 pysocks,将回退到环境变量。\n"
" 修复: pip install pysocks",
file=sys.stderr,
)
return urllib.request.build_opener()
if proxy:
handler = urllib.request.ProxyHandler({"http": proxy, "https": proxy})
print(f"[proxy] HTTP(S) 代理启用: {proxy}", file=sys.stderr)
return urllib.request.build_opener(handler)
# 走环境变量
env_proxy = os.environ.get("HTTPS_PROXY") or os.environ.get("HTTP_PROXY") or os.environ.get("ALL_PROXY")
if env_proxy:
print(f"[proxy] 使用环境变量代理: {env_proxy}", file=sys.stderr)
else:
print("[proxy] 直连模式(无代理)", file=sys.stderr)
return urllib.request.build_opener()
# ──────────────────────────────────────────────────────────────────────
# arXiv
# ──────────────────────────────────────────────────────────────────────
def search_arxiv(
opener: urllib.request.OpenerDirector,
query: str,
max_results: int = 20,
sort_by: str = "submittedDate",
sort_order: str = "descending",
) -> list[dict[str, Any]]:
"""调用 arXiv API 并解析返回的 ATOM XML。"""
base = "https://export.arxiv.org/api/query"
params = urllib.parse.urlencode(
{
"search_query": query,
"start": 0,
"max_results": max_results,
"sortBy": sort_by,
"sortOrder": sort_order,
}
)
url = f"{base}?{params}"
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0 (research-bot)"})
# arXiv 限速重试(429 → 指数退避)
data = ""
for attempt in range(4):
try:
with opener.open(req, timeout=30) as resp:
data = resp.read().decode("utf-8")
break
except urllib.error.HTTPError as e:
if e.code == 429:
wait = 10 * (attempt + 1)
print(
f"[arxiv][{query[:40]}...] 429 限速,等待 {wait}s 重试 ({attempt+1}/4)",
file=sys.stderr,
)
time.sleep(wait)
continue
print(f"[arxiv][{query[:40]}...] HTTPError {e.code}: {e.reason}", file=sys.stderr)
return []
except (urllib.error.URLError, TimeoutError) as e:
print(f"[arxiv][{query[:40]}...] ERROR: {e}", file=sys.stderr)
return []
else:
print(f"[arxiv][{query[:40]}...] 持续 429,放弃", file=sys.stderr)
return []
try:
root = ET.fromstring(data)
except ET.ParseError as e:
print(f"[arxiv][{query[:40]}...] XML parse error: {e}", file=sys.stderr)
return []
ns = {"atom": "http://www.w3.org/2005/Atom", "arxiv": "http://arxiv.org/schemas/atom"}
papers: list[dict[str, Any]] = []
for entry in root.findall("atom:entry", ns):
def _text(elem_path: str) -> str:
elem = entry.find(elem_path, ns)
return (elem.text or "").replace("\n", " ").strip() if elem is not None else ""
arxiv_id_url = _text("atom:id")
arxiv_id = arxiv_id_url.rsplit("/", 1)[-1] if arxiv_id_url else ""
# 去掉版本号 e.g. 2501.09898v2 → 2501.09898
arxiv_id_clean = arxiv_id.split("v")[0] if arxiv_id else ""
authors: list[str] = []
for a in entry.findall("atom:author", ns):
name_elem = a.find("atom:name", ns)
if name_elem is not None and name_elem.text:
authors.append(name_elem.text.strip())
categories = [
c.attrib.get("term", "") for c in entry.findall("atom:category", ns)
]
summary = _text("atom:summary")
papers.append(
{
"arxiv_id": arxiv_id_clean,
"url": f"https://arxiv.org/abs/{arxiv_id_clean}" if arxiv_id_clean else arxiv_id_url,
"title": _text("atom:title"),
"published": _text("atom:published"),
"updated": _text("atom:updated"),
"authors": authors,
"categories": categories,
"summary": summary[:400] + ("..." if len(summary) > 400 else ""),
}
)
return papers
# ──────────────────────────────────────────────────────────────────────
# GitHub
# ──────────────────────────────────────────────────────────────────────
def search_github(
opener: urllib.request.OpenerDirector,
query: str,
per_page: int = 10,
) -> list[dict[str, Any]]:
base = "https://api.github.com/search/repositories"
params = urllib.parse.urlencode({"q": query, "sort": "stars", "order": "desc", "per_page": per_page})
url = f"{base}?{params}"
headers = {"User-Agent": "Mozilla/5.0 (research-bot)", "Accept": "application/vnd.github+json"}
token = os.environ.get("GITHUB_TOKEN")
if token:
headers["Authorization"] = f"Bearer {token}"
req = urllib.request.Request(url, headers=headers)
try:
with opener.open(req, timeout=30) as resp:
data = json.loads(resp.read().decode("utf-8"))
except urllib.error.HTTPError as e:
# 60 req/hr 限速时 GitHub 返回 403
print(f"[github][{query[:40]}...] HTTPError {e.code}: {e.reason}", file=sys.stderr)
return []
except (urllib.error.URLError, TimeoutError) as e:
print(f"[github][{query[:40]}...] ERROR: {e}", file=sys.stderr)
return []
repos: list[dict[str, Any]] = []
for item in data.get("items", []):
repos.append(
{
"name": item.get("full_name", ""),
"description": item.get("description", "") or "",
"stars": item.get("stargazers_count", 0),
"forks": item.get("forks_count", 0),
"language": item.get("language", "") or "",
"url": item.get("html_url", ""),
"updated_at": item.get("updated_at", ""),
}
)
return repos
# ──────────────────────────────────────────────────────────────────────
# 主入口
# ──────────────────────────────────────────────────────────────────────
def main() -> int:
parser = argparse.ArgumentParser(description="arXiv + GitHub 主题化检索")
parser.add_argument("--proxy", default=None, help="代理 URL,如 http://127.0.0.1:6984")
parser.add_argument("--max-results", type=int, default=15, help="每个主题最多论文数")
parser.add_argument("--out", default="research/data/search_results.json", help="输出 JSON 路径")
parser.add_argument(
"--topics",
nargs="+",
default=None,
help="只跑指定主题(默认全部),如:--topics stereo_matching world_model_video",
)
parser.add_argument("--no-github", action="store_true", help="跳过 GitHub 检索")
parser.add_argument("--delay", type=float, default=3.0, help="主题间延迟(秒),arXiv 建议 ≥3s")
args = parser.parse_args()
opener = build_opener(args.proxy)
selected = args.topics or list(TOPICS.keys())
results: dict[str, Any] = {
"meta": {
"generated_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"proxy": args.proxy or os.environ.get("HTTPS_PROXY") or "direct",
"max_results_per_topic": args.max_results,
},
"arxiv": {},
"github": {},
}
# arXiv
for topic in selected:
if topic not in TOPICS:
print(f"[skip] 未知主题: {topic}", file=sys.stderr)
continue
print(f"[arxiv] 检索主题: {topic}", file=sys.stderr)
papers = search_arxiv(opener, TOPICS[topic], max_results=args.max_results)
results["arxiv"][topic] = papers
print(f"{len(papers)}", file=sys.stderr)
time.sleep(args.delay) # arXiv API rate limit
# GitHub
if not args.no_github:
for topic, q in GITHUB_TOPICS.items():
if args.topics and topic not in args.topics:
continue
print(f"[github] 检索主题: {topic}", file=sys.stderr)
repos = search_github(opener, q, per_page=10)
results["github"][topic] = repos
print(f"{len(repos)} 个仓库", file=sys.stderr)
time.sleep(1.0)
out_path = args.out
os.makedirs(os.path.dirname(out_path) or ".", exist_ok=True)
with open(out_path, "w", encoding="utf-8") as f:
json.dump(results, f, indent=2, ensure_ascii=False)
total_papers = sum(len(v) for v in results["arxiv"].values())
total_repos = sum(len(v) for v in results["github"].values())
print(f"\n✅ 完成:{total_papers} 篇 arXiv + {total_repos} 个 GitHub 仓库 → {out_path}", file=sys.stderr)
return 0
if __name__ == "__main__":
sys.exit(main())