Spaces:
Running
Running
| # app.py | |
| # ============================================================ | |
| # 类型:界面层(乙负责) | |
| # 功能:Gradio 界面 + 研究方向全景研报格式化 | |
| # 用法:python app.py → 浏览器打开 http://127.0.0.1:7860 | |
| # 公网:设置 NGROK_AUTH_TOKEN 环境变量后自动生成公网链接 | |
| # ============================================================ | |
| import sys | |
| import os | |
| import gradio as gr | |
| # 自动加载 .env 文件 | |
| _ENV_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), ".env") | |
| if os.path.isfile(_ENV_PATH): | |
| with open(_ENV_PATH, "r", encoding="utf-8") as _f: | |
| for _line in _f: | |
| _line = _line.strip() | |
| if _line and not _line.startswith("#") and "=" in _line: | |
| _key, _val = _line.split("=", 1) | |
| if _key not in os.environ: | |
| os.environ[_key] = _val.strip().strip('"').strip("'") | |
| try: | |
| from run import run | |
| except (ImportError, ModuleNotFoundError) as e: | |
| print(f"[错误] 无法导入 run.py,请确认甲已交付所有 Workflow 模块。") | |
| print(f" {e}") | |
| sys.exit(1) | |
| def diagnose(arxiv_url: str, progress: gr.Progress = gr.Progress()) -> str: | |
| """Gradio 回调:用户输入 arXiv URL → 返回 Markdown 研报""" | |
| url = arxiv_url.strip() | |
| if not url: | |
| return "## ⚠️ 请输入 arXiv 论文链接\n\n示例: `https://arxiv.org/abs/2011.08785`" | |
| try: | |
| result = run(url, progress=progress) | |
| except ValueError as e: | |
| return f"## ❌ 输入格式错误\n\n{str(e)}\n\n请确认输入的是有效的 arXiv 链接。" | |
| except RuntimeError as e: | |
| return f"## ❌ API 请求失败\n\n{str(e)}\n\n请检查网络连接或稍后重试。" | |
| except Exception as e: | |
| return f"## ❌ 运行出错\n\n```\n{type(e).__name__}: {str(e)}\n```\n\n请将以上错误信息反馈给开发者。" | |
| # 处理部分失败的情况 | |
| if result.get("error") and not result.get("repos"): | |
| paper = result.get("paper", {}) | |
| direction = result.get("direction", {}) | |
| title = paper.get("title", "未知") if paper else "未知" | |
| md = f"""# ResearchRadar 研究方向全景研报 | |
| --- | |
| ## 论文信息 | |
| **{title}** | |
| --- | |
| ## ⚠️ 未能完成完整评估 | |
| **原因**: {result['error']} | |
| """ | |
| if direction: | |
| md += f"""## 研究方向解析(部分结果) | |
| **子领域**: {direction.get('subfield', 'N/A')} | |
| **趋势**: {direction.get('subfield_trend', 'N/A')} | |
| """ | |
| md += """ | |
| --- | |
| > 该论文方向可能太新或太冷门,暂无高质量开源实现。这恰好帮助你判断——在这个方向开展研究需要从头实现。 | |
| """ | |
| return md | |
| return format_report(result) | |
| def format_report(result: dict) -> str: | |
| """将 run() 的结果格式化为"研究方向全景研报" Markdown。 | |
| 研报结构: | |
| 1. 论文信息 | |
| 2. 研究方向全景(子领域 + 趋势 + 方法族谱系 + 开源覆盖度) | |
| 3. 方法族开源覆盖度(哪些已有实现、哪些是空白) | |
| 4. 开源实现评估排名(六维度评分明细) | |
| 5. 对比实验推荐总结(按就绪度分组) | |
| """ | |
| paper = result.get("paper", {}) | |
| direction = result.get("direction", {}) | |
| repos = result.get("repos", []) | |
| # ===== 第一部分:论文信息 ===== | |
| arxiv_id = paper.get("arxiv_id", "") | |
| arxiv_url = f"https://arxiv.org/abs/{arxiv_id}" if arxiv_id else "" | |
| md = "# ResearchRadar 研究方向全景研报\n\n---\n\n## 一、论文信息\n\n" | |
| md += "| 项目 | 内容 |\n|------|------|\n" | |
| md += f"| **标题** | {paper.get('title', 'N/A')} |\n" | |
| if arxiv_url: | |
| md += f"| **arXiv** | [{arxiv_id}]({arxiv_url}) |\n" | |
| else: | |
| md += f"| **arXiv ID** | {arxiv_id or 'N/A'} |\n" | |
| md += f"| **作者** | {', '.join(paper.get('authors', ['N/A'])[:5])}" | |
| if len(paper.get("authors", [])) > 5: | |
| md += f" 等 {len(paper.get('authors', []))} 人" | |
| md += " |\n" | |
| published = paper.get("published", "") | |
| if published: | |
| md += f"| **发表时间** | {published[:10]} |\n" | |
| categories = paper.get("categories", []) | |
| if categories: | |
| md += f"| **分类** | {', '.join(categories)} |\n" | |
| # ===== 论文自身代码评估 ===== | |
| own_repo = None | |
| other_repos = [] | |
| for r in repos: | |
| if r.get("method_family") == "本文代码": | |
| own_repo = r | |
| else: | |
| other_repos.append(r) | |
| if own_repo is not None: | |
| ev = own_repo.get("evaluation", {}) | |
| md += "\n---\n\n## 二、本文代码复现评估\n\n" | |
| md += "> 该论文是否提供了官方代码?代码质量如何?能否直接复现论文实验结果?\n\n" | |
| md += f"| 仓库 | Stars | 综合评分 | 可复现性 | 对比实验适配度 |\n" | |
| md += f"|------|:----:|:----:|:----:|:----:|\n" | |
| verdict_icon = {"reproducible": "✅", "partially": "⚠️", "not_reproducible": "❌", "error": "💥"}.get(ev.get("verdict", ""), "❓") | |
| readiness_icon = {"ready": "🟢", "partial": "🟡", "not_ready": "🔴"}.get(ev.get("benchmark_readiness", ""), "⚪") | |
| md += f"| [{own_repo['full_name']}]({own_repo.get('html_url', '#')}) | {own_repo.get('stars', 0)} | **{ev.get('overall_score', 'N/A')}/100** | {verdict_icon} | {readiness_icon} |\n\n" | |
| md += f"### 📋 详细评估\n\n{ev.get('reasoning', 'N/A')}\n\n" | |
| risks = ev.get("risks", []) | |
| if risks: | |
| md += "### ⚠️ 风险点\n\n" | |
| for r_ in risks: | |
| md += f"- {r_}\n" | |
| md += "\n" | |
| md += f"### 💡 使用建议\n\n{ev.get('suggested_use', 'N/A')}\n\n" | |
| else: | |
| md += "\n---\n\n## 二、本文代码复现评估\n\n" | |
| md += "> 🔴 **未找到该论文的官方开源代码。**\n>\n" | |
| md += "> 这意味着复现该论文的实验结果需要从头实现,难度较高。建议在对比实验中使用下面列出的同一方法族的开源实现作为替代。\n\n" | |
| # ===== 第三部分:研究方向全景 ===== | |
| md += "\n---\n\n## 三、研究方向全景\n\n" | |
| md += f"### 🎯 子领域定位\n\n**{direction.get('subfield', '未知')}**\n\n" | |
| trend = direction.get('subfield_trend', '暂无分析') | |
| md += f"### 📈 技术趋势与前沿动态\n\n{trend}\n\n" | |
| md += "### 🏗️ 方法族谱系\n\n" | |
| md += "> 以下方法族基于 GitHub 上该领域实际开源仓库的分析归纳,反映了当前学术界的组织结构和开源生态。\n\n" | |
| families = direction.get("method_families", []) | |
| for i, mf in enumerate(families): | |
| family_name = mf.get("family_name", f"方法族 {i+1}") | |
| desc = mf.get("description", "暂无详细描述") | |
| rep = mf.get("representative_work", "暂无代表工作") | |
| matched = mf.get("matched_repos", []) | |
| queries = mf.get("search_queries", []) | |
| md += f"#### {i+1}. {family_name}\n\n" | |
| md += f"**核心特点**: {desc}\n\n" | |
| md += f"**代表工作**: {rep}\n\n" | |
| if matched: | |
| md += f"**开源覆盖**: 🟢 {len(matched)} 个仓库 — {', '.join(matched)}\n\n" | |
| else: | |
| md += "**开源覆盖**: 🔴 暂无开源实现(可能是研究空白)\n\n" | |
| if queries: | |
| md += f"**精准搜索词**: {', '.join(queries)}\n\n" | |
| if not families: | |
| md += "| - | 未能识别出明确的方法族 | - | - |\n" | |
| md += "\n" | |
| # ===== 第四部分:仓库评估排名 ===== | |
| md += "\n---\n\n## 四、开源实现评估排名\n\n" | |
| md += "> 评分逻辑:可复现性(80分)评估代码能否被他人成功跑通;对比实验适配度(20分)评估该仓库能否直接用于论文 Experiment section。\n\n" | |
| if not other_repos and not own_repo: | |
| md += "**未找到相关开源仓库。**\n\n" | |
| return md | |
| if not other_repos: | |
| md += "**未找到其他开源仓库(仅找到论文自身代码)。**\n\n" | |
| for i, repo in enumerate(other_repos): | |
| full_name = repo.get("full_name", f"仓库 {i+1}") | |
| html_url = repo.get("html_url", "") | |
| ev = repo.get("evaluation", {}) | |
| mf = repo.get("method_family", "") | |
| verdict_icon = { | |
| "reproducible": "✅", "partially": "⚠️", | |
| "not_reproducible": "❌", "error": "💥", | |
| }.get(ev.get("verdict", ""), "❓") | |
| readiness_icon = { | |
| "ready": "🟢", "partial": "🟡", "not_ready": "🔴", | |
| }.get(ev.get("benchmark_readiness", ""), "⚪") | |
| family_tag = f" `[{mf}]`" if mf else "" | |
| md += f"### {i+1}. [{full_name}]({html_url}){family_tag}\n\n" | |
| repro_score = ev.get("reproducibility_score", ev.get("overall_score", "N/A")) | |
| bench_score = ev.get("benchmark_fitness_score", ev.get("benchmark_score", "N/A")) | |
| overall = ev.get("overall_score", "N/A") | |
| # 评分概要 | |
| md += f"| 指标 | 评分 |\n" | |
| md += f"|------|:----:|\n" | |
| md += f"| {verdict_icon} 可复现性 | **{repro_score}/80** |\n" | |
| md += f"| {readiness_icon} 对比实验适配度 | **{bench_score}/20** |\n" | |
| md += f"| 📊 综合评分 | **{overall}/100** |\n\n" | |
| # 详细分析(核心内容,放在最显眼位置) | |
| reasoning = ev.get("reasoning", "N/A") | |
| md += f"### 📋 详细分析\n\n{reasoning}\n\n" | |
| # 六维度评分表 | |
| md += "<details>\n<summary>📊 六维度评分明细(点击展开)</summary>\n\n" | |
| md += "| 维度 | 得分 | 满分 | 评估标准 |\n" | |
| md += "|------|:----:|:----:|----------|\n" | |
| md += f"| 环境配置 | {_fmt(ev.get('env_score'))} | 15 | 依赖文件完整性 |\n" | |
| md += f"| 文档质量 | {_fmt(ev.get('doc_score'))} | 20 | 安装/训练/数据说明 |\n" | |
| md += f"| 代码可用性 | {_fmt(ev.get('code_score'))} | 20 | 训练+推理脚本 |\n" | |
| md += f"| 社区活跃度 | {_fmt(ev.get('community_score'))} | 10 | Star + 更新频率 |\n" | |
| md += f"| 依赖健康度 | {_fmt(ev.get('dep_score'))} | 15 | 依赖兼容性 |\n" | |
| md += f"| **对比实验** | **{_fmt(ev.get('benchmark_score'))}** | **20** | **benchmark + 预训练权重** |\n" | |
| md += "\n</details>\n\n" | |
| # 对比实验建议 | |
| md += f"### 💡 对比实验建议\n\n{ev.get('suggested_use', 'N/A')}\n\n" | |
| # 风险点 | |
| risks = ev.get("risks", []) | |
| if risks: | |
| md += "### ⚠️ 风险点\n\n" | |
| for r in risks: | |
| md += f"- {r}\n" | |
| md += "\n" | |
| md += f"⭐ Star: {repo.get('stars', 0)}" | |
| if repo.get("match_keyword"): | |
| md += f" | 匹配词: `{repo.get('match_keyword', '')}`" | |
| md += "\n\n---\n\n" | |
| # ===== 第五部分:对比实验推荐总结 ===== | |
| md += "## 五、对比实验推荐总结\n\n" | |
| all_evaluated = other_repos + ([own_repo] if own_repo else []) | |
| ready = [r for r in all_evaluated if r["evaluation"].get("benchmark_readiness") == "ready"] | |
| partial = [r for r in all_evaluated if r["evaluation"].get("benchmark_readiness") == "partial"] | |
| not_ready = [ | |
| r for r in all_evaluated | |
| if r["evaluation"].get("benchmark_readiness") not in ("ready", "partial") | |
| ] | |
| if ready: | |
| md += "### 🟢 可直接用于对比实验\n\n" | |
| for r in ready: | |
| url = r.get("html_url", "#") | |
| name = r.get("full_name", "未知") | |
| suggestion = r["evaluation"].get("suggested_use", "") | |
| md += f"- **[{name}]({url})** — {suggestion}\n" | |
| md += "\n" | |
| if partial: | |
| md += "### 🟡 需要少量修改后可用\n\n" | |
| for r in partial: | |
| url = r.get("html_url", "#") | |
| name = r.get("full_name", "未知") | |
| suggestion = r["evaluation"].get("suggested_use", "") | |
| md += f"- **[{name}]({url})** — {suggestion}\n" | |
| md += "\n" | |
| if not_ready: | |
| md += "### 🔴 仅适合参考代码实现\n\n" | |
| for r in not_ready: | |
| url = r.get("html_url", "#") | |
| name = r.get("full_name", "未知") | |
| reason = r["evaluation"].get("suggested_use") or r["evaluation"].get("reasoning", "") | |
| md += f"- **[{name}]({url})** — {reason}\n" | |
| md += "\n" | |
| # ===== 审核质量报告 ===== | |
| audit = result.get("audit", {}) | |
| if audit: | |
| md += "\n---\n\n## 六、质量审核报告\n\n" | |
| md += f"**综合质量评分**: {audit.get('overall_score', 'N/A')}/100\n\n" | |
| md += f"{audit.get('summary', '')}\n\n" | |
| if audit.get("actions"): | |
| md += "### 改进建议\n\n" | |
| for action in audit["actions"]: | |
| md += f"- {action}\n" | |
| md += "\n" | |
| src = audit.get("source_audit", {}) | |
| if src.get("unreliable_repos"): | |
| md += "### ⚠️ 来源可靠性提醒\n\n" | |
| md += "以下仓库来源可靠性较低,评估结果仅供参考:\n\n" | |
| for r in src["unreliable_repos"]: | |
| md += f"- `{r}`\n" | |
| md += "\n" | |
| md += "---\n\n> ResearchRadar · 论文研究方向全景 + 开源代码评估 + 对比实验推荐\n" | |
| return md | |
| def _fmt(value) -> str: | |
| """格式化数值,None 显示为 N/A""" | |
| if value is None: | |
| return "N/A" | |
| return str(value) | |
| # ============================================================ | |
| # Gradio 启动 | |
| # ============================================================ | |
| DEMO_EXAMPLES = [ | |
| ["https://arxiv.org/abs/2011.08785"], # PaDiM — 工业缺陷检测(制造工程) | |
| ["https://arxiv.org/abs/2301.06230"], # Swarm-SLAM — 多机器人协同SLAM(机器人工程) | |
| ["https://arxiv.org/abs/2304.07794"], # NDP-NMPC — 四旋翼非线性控制(航空航天) | |
| ["https://arxiv.org/abs/2306.05889"], # C(NN)FD — 涡轮机械CFD(机械工程) | |
| ] | |
| with gr.Blocks(title="ResearchRadar") as demo: | |
| gr.HTML(""" | |
| <div style="text-align: center; margin-bottom: 20px;"> | |
| <h1>🔬 ResearchRadar</h1> | |
| <h3>研究方向全景分析 + 开源代码评估 + 对比实验推荐</h3> | |
| <p style="color: #666; max-width: 700px; margin: 0 auto;"> | |
| 输入一篇工科论文的 arXiv 链接,系统自动完成:<br> | |
| ① <strong>摸清方向</strong> — 该领域有哪些方法族?各自什么特点?当前趋势是什么?<br> | |
| ② <strong>找到代码</strong> — GitHub 上哪些开源仓库实现了这些方法?覆盖度如何?<br> | |
| ③ <strong>评估可用性</strong> — 每个仓库能不能跑通?能不能直接用于论文对比实验? | |
| </p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| with gr.Column(scale=4): | |
| url_input = gr.Textbox( | |
| label="📄 论文 arXiv 链接", | |
| placeholder="https://arxiv.org/abs/2011.08785", | |
| lines=1, | |
| show_label=True, | |
| ) | |
| with gr.Column(scale=1): | |
| submit_btn = gr.Button("🔍 开始分析", variant="primary", size="lg") | |
| with gr.Accordion("📚 示例论文(点击展开,选择后自动填入)", open=True): | |
| gr.Examples( | |
| examples=DEMO_EXAMPLES, | |
| inputs=url_input, | |
| examples_per_page=4, | |
| ) | |
| with gr.Row(): | |
| report_output = gr.Markdown( | |
| label="研究方向全景研报", | |
| value=""" | |
| --- | |
| ## 等待输入 | |
| 请输入一篇工科论文的 **arXiv 链接**(如 `https://arxiv.org/abs/2011.08785`),点击「开始分析」。 | |
| 系统将在 3-5 分钟内生成一份完整的 **研究方向全景研报**,包含: | |
| | 章节 | 内容 | | |
| |------|------| | |
| | **一、论文信息** | 标题、作者、分类、发表时间 | | |
| | **二、研究方向全景** | 子领域定位、技术趋势、方法族谱系(每个方法族含核心特点/代表工作/开源覆盖度/精准搜索词) | | |
| | **三、开源实现评估排名** | 每个仓库的详细分析 + 六维度评分 + 风险点 | | |
| | **四、对比实验推荐总结** | 🟢可直接使用 / 🟡需少量修改 / 🔴仅适合参考 | | |
| --- | |
| """, | |
| ) | |
| submit_btn.click(fn=diagnose, inputs=url_input, outputs=report_output) | |
| url_input.submit(fn=diagnose, inputs=url_input, outputs=report_output) | |
| demo.queue(default_concurrency_limit=2, max_size=20, status_update_rate=2.0) | |
| if __name__ == "__main__": | |
| public_url = None | |
| # 尝试 ngrok 公网隧道(国内可用) | |
| ngrok_token = os.getenv("NGROK_AUTH_TOKEN", "") | |
| if ngrok_token: | |
| try: | |
| from pyngrok import ngrok, conf | |
| conf.get_default().auth_token = ngrok_token | |
| tunnel = ngrok.connect(7860, "http") | |
| public_url = tunnel.public_url | |
| print(f"\n{'='*60}") | |
| print(f" 公网链接: {public_url}") | |
| print(f" 任何人可以打开此链接使用 ResearchRadar") | |
| print(f"{'='*60}\n") | |
| except Exception as e: | |
| print(f" [WARN] ngrok 启动失败: {e}") | |
| print(f" 降级为仅本地访问") | |
| if not public_url: | |
| print("\n 未配置公网隧道,仅本地访问: http://127.0.0.1:7860") | |
| print(" 如需公网访问,请设置 NGROK_AUTH_TOKEN 环境变量") | |
| print(" 免费获取 token: https://dashboard.ngrok.com/get-started/your-authtoken\n") | |
| demo.launch(server_name="0.0.0.0", share=False, theme="soft", show_error=True) | |