Просмотр исходного кода

feat: 添加研究报告转播客脚本功能

- 新增 huggingface-hub 依赖以支持模型调用
- 在 SummaryState 和 SummaryStateOutput 中添加 podcast_script 字段
- 新增 script_writer_instructions 提示词指导脚本生成
- 实现 ScriptGenerationService 服务类,负责将研究报告转换为双人对话脚本
- 在 agent.py 中集成脚本生成流程,并在研究完成后自动调用
- 更新 API 响应结构,包含生成的播客脚本
- 添加测试文件 test_script_generator.py 验证脚本生成功能
JJSun 5 месяцев назад
Родитель
Сommit
8dcd5364ea

+ 1 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/pyproject.toml

@@ -18,6 +18,7 @@ dependencies = [
     "uvicorn[standard]>=0.32.0",
     "ddgs>=9.6.1",
     "loguru>=0.7.3",
+    "huggingface-hub>=1.3.3",
 ]
 
 [project.optional-dependencies]

+ 14 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/src/agent.py

@@ -16,12 +16,14 @@ from hello_agents.tools.builtin.note_tool import NoteTool
 from config import Configuration
 from prompts import (
     report_writer_instructions,
+    script_writer_instructions,
     task_summarizer_instructions,
     todo_planner_system_prompt,
 )
 from models import SummaryState, SummaryStateOutput, TodoItem
 from services.planner import PlanningService
 from services.reporter import ReportingService
+from services.script_generator import ScriptGenerationService
 from services.search import dispatch_search, prepare_research_context
 from services.summarizer import SummarizationService
 from services.tool_events import ToolCallTracker
@@ -72,6 +74,7 @@ class DeepResearchAgent:
         self.summarizer = SummarizationService(self._summarizer_factory, self.config)
         self.reporting = ReportingService(self.report_agent, self.config)
         self._last_search_notices: list[str] = []
+        self.script_generator = ScriptGenerationService(self.llm, self.config)
 
     # ------------------------------------------------------------------
     # Public API
@@ -281,6 +284,17 @@ class DeepResearchAgent:
             "note_id": state.report_note_id,
             "note_path": state.report_note_path,
         }
+
+        yield {"type": "status", "message": "正在生成播客脚本..."}
+        script = self.script_generator.generate_script(state)
+        for event in self._drain_tool_events(state):
+            yield event
+        state.podcast_script = PodcastScript(script=script)
+        yield {
+            "type": "podcast_script",
+            "script": script,
+        }
+
         yield {"type": "done"}
 
     # ------------------------------------------------------------------

+ 12 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/src/main.py

@@ -42,6 +42,10 @@ class ResearchRequest(BaseModel):
         description="Override the default search backend configured via env",
     )
 
+class PodcastScript(BaseModel):
+    """Model for podcast script content."""
+    script: str = Field(..., description="Generated podcast script content")
+
 
 class ResearchResponse(BaseModel):
     """HTTP response containing the generated report and structured tasks."""
@@ -53,6 +57,10 @@ class ResearchResponse(BaseModel):
         default_factory=list,
         description="Structured TODO items with summaries and sources",
     )
+    podcast_script: Optional[PodcastScript] = Field(
+        default=None,
+        description="Generated podcast script content",
+    )
 
 
 def _mask_secret(value: Optional[str], visible: int = 4) -> str:
@@ -141,9 +149,13 @@ def create_app() -> FastAPI:
             for item in result.todo_items
         ]
 
+        # 添加podcast_script字段到返回响应中
+        podcast_script = result.podcast_script or PodcastScript(script="")
+
         return ResearchResponse(
             report_markdown=(result.report_markdown or result.running_summary or ""),
             todo_items=todo_payload,
+            podcast_script=podcast_script,
         )
 
     @app.post("/research/stream")

+ 2 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/src/models.py

@@ -36,6 +36,7 @@ class SummaryState:
     structured_report: Optional[str] = field(default=None)
     report_note_id: Optional[str] = field(default=None)
     report_note_path: Optional[str] = field(default=None)
+    podcast_script: Optional[list] = field(default=None)
 
 
 @dataclass(kw_only=True)
@@ -48,4 +49,5 @@ class SummaryStateOutput:
     running_summary: str = field(default=None)  # Backward-compatible文本
     report_markdown: Optional[str] = field(default=None)
     todo_items: List[TodoItem] = field(default_factory=list)
+    podcast_script: Optional[list] = field(default=None)
 

+ 28 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/src/prompts.py

@@ -108,3 +108,31 @@ report_writer_instructions = """
 - 如需在报告层面沉淀结果,可创建新的 `conclusion` 类型笔记,例如:`[TOOL_CALL:note:{"action":"create","title":"研究报告:{研究主题}","note_type":"conclusion","tags":["deep_research","report"],"content":"...报告要点..."}]`。
 </NOTES>
 """
+
+script_writer_instructions = """
+你是一名专业的播客策划人,擅长将严肃的研究报告转化为生动、有趣且有深度的双人对话脚本。
+
+<ROLES>
+- **Host (Xiayu)**: 男声。好奇心强、幽默风趣、善于引导话题、负责提问和总结,代表普通听众的视角。
+- **Guest (Liwa)**: 女声。领域专家、知识渊博、理性客观、负责深入解释和提供见解,偶尔也会用幽默化解枯燥。
+</ROLES>
+
+<GOAL>
+根据提供的【研究报告】,创作一份高质量的播客对话脚本。对话应自然流畅,像两个老朋友在聊天,避免生硬的朗读感。
+
+<REQUIREMENTS>
+1. **开场**:Host 热情开场,引出话题,Guest 简短回应。
+2. **主体**:深入浅出地讨论报告中的核心观点、数据和案例。Host 负责追问“为什么”、“怎么做”或“这对普通人有什么影响”,Guest 负责解答。
+3. **结尾**:Host 总结核心收获,Guest 给出简短的未来展望或金句。Host 结束语。
+4. **风格**:轻松、口语化,适当使用感叹词(如“哇”、“原来如此”、“确实”),但不要过度。
+5. **长度**:对话回合数控制在 10-20 轮之间,确保内容充实但不拖沓。
+
+<FORMAT>
+请严格输出 JSON 格式的列表,不包含 Markdown 代码块标记(如 ```json ... ```),直接输出 JSON 数组:
+[
+  {"role": "Host", "content": "欢迎大家收听 DeepCast。今天我们要聊的话题很有意思..."},
+  {"role": "Guest", "content": "是的,这确实是一个非常前沿的领域..."},
+  ...
+]
+</FORMAT>
+"""

+ 73 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/src/services/script_generator.py

@@ -0,0 +1,73 @@
+"""Service that converts the research report into a podcast script."""
+
+from __future__ import annotations
+
+import json
+import logging
+import re
+from typing import Any, List
+
+from hello_agents import ToolAwareSimpleAgent
+
+from models import SummaryState
+from config import Configuration
+from prompts import script_writer_instructions
+from utils import strip_thinking_tokens
+
+logger = logging.getLogger(__name__)
+
+
+class ScriptGenerationService:
+    """Generates a dialogue script from the research report."""
+
+    def __init__(self, script_agent: ToolAwareSimpleAgent, config: Configuration) -> None:
+        self._agent = script_agent
+        self._config = config
+
+    def generate_script(self, state: SummaryState) -> List[dict[str, str]]:
+        """Generate a podcast script based on the structured report."""
+
+        if not state.structured_report:
+            logger.warning("No structured report available for script generation.")
+            return []
+
+        prompt = f"{script_writer_instructions}\n\n<RESEARCH_REPORT>\n{state.structured_report}\n</RESEARCH_REPORT>"
+
+        response = self._agent.run(prompt)
+        self._agent.clear_history()
+
+        if self._config.strip_thinking_tokens:
+            response = strip_thinking_tokens(response)
+        
+        cleaned_response = response.strip()
+        
+        # 1. Try to find markdown code block
+        code_block_pattern = re.compile(r"```(?:json)?\s*(.*?)```", re.DOTALL)
+        match = code_block_pattern.search(cleaned_response)
+        if match:
+            cleaned_response = match.group(1).strip()
+        else:
+            # 2. Try to find content between [ and ]
+            start = cleaned_response.find("[")
+            end = cleaned_response.rfind("]")
+            if start != -1 and end != -1 and end > start:
+                cleaned_response = cleaned_response[start:end+1]
+        
+        try:
+            script = json.loads(cleaned_response)
+            if not isinstance(script, list):
+                logger.error("Script generation output is not a list: %s", type(script))
+                return []
+            
+            # Validate script format
+            valid_script = []
+            for item in script:
+                if isinstance(item, dict) and "role" in item and "content" in item:
+                    valid_script.append(item)
+            
+            logger.info("Generated script with %d dialogue turns.", len(valid_script))
+            return valid_script
+
+        except json.JSONDecodeError:
+            logger.error("Failed to parse script generation output as JSON: %s", response[:500])
+            return []

+ 62 - 0
Co-creation-projects/JJason-DeepCastAgent/backend/tests/test_script_generator.py

@@ -0,0 +1,62 @@
+import unittest
+from unittest.mock import MagicMock
+import sys
+import os
+
+# Add src to path
+sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '../src')))
+
+from services.script_generator import ScriptGenerationService
+from models import SummaryState
+from config import Configuration
+
+class TestScriptGenerationService(unittest.TestCase):
+    def setUp(self):
+        self.mock_agent = MagicMock()
+        self.mock_config = MagicMock()
+        self.mock_config.strip_thinking_tokens = True
+        self.service = ScriptGenerationService(self.mock_agent, self.mock_config)
+
+    def test_generate_script_success(self):
+        state = SummaryState(research_topic="Test Topic")
+        state.structured_report = "# Test Report\nContent..."
+        
+        # Mock LLM response
+        mock_response = """
+        Thinking process...
+        
+        ```json
+        [
+            {"role": "Host", "content": "Hello"},
+            {"role": "Guest", "content": "Hi there"}
+        ]
+        ```
+        """
+        self.mock_agent.run.return_value = mock_response
+
+        script = self.service.generate_script(state)
+        
+        self.assertEqual(len(script), 2)
+        self.assertEqual(script[0]['role'], "Host")
+        self.assertEqual(script[0]['content'], "Hello")
+        self.assertEqual(script[1]['role'], "Guest")
+        self.assertEqual(script[1]['content'], "Hi there")
+
+    def test_generate_script_no_report(self):
+        state = SummaryState(research_topic="Test Topic")
+        state.structured_report = None
+        
+        script = self.service.generate_script(state)
+        self.assertEqual(script, [])
+
+    def test_generate_script_invalid_json(self):
+        state = SummaryState(research_topic="Test Topic")
+        state.structured_report = "Report"
+        
+        self.mock_agent.run.return_value = "Not JSON"
+        
+        script = self.service.generate_script(state)
+        self.assertEqual(script, [])
+
+if __name__ == '__main__':
+    unittest.main()

+ 144 - 43
Co-creation-projects/JJason-DeepCastAgent/backend/uv.lock

@@ -352,6 +352,54 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/ba/58/4b35f13d21a44681e71ee8b1bca5755db0f84017cb29593eb0375aaa01e0/ddgs-9.6.1-py3-none-any.whl", hash = "sha256:e7d7e0c4dbae3f287627b9f6e411278256d7859d017bbad45b8229c230bf5270", size = 41577, upload-time = "2025-10-12T18:36:32.505Z" },
 ]
 
+[[package]]
+name = "deepcast"
+version = "0.0.1"
+source = { editable = "." }
+dependencies = [
+    { name = "ddgs" },
+    { name = "fastapi" },
+    { name = "hello-agents" },
+    { name = "huggingface-hub" },
+    { name = "loguru" },
+    { name = "openai" },
+    { name = "python-dotenv" },
+    { name = "requests" },
+    { name = "tavily-python" },
+    { name = "uvicorn", extra = ["standard"] },
+]
+
+[package.optional-dependencies]
+dev = [
+    { name = "mypy" },
+    { name = "ruff" },
+]
+
+[package.dev-dependencies]
+dev = [
+    { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "ddgs", specifier = ">=9.6.1" },
+    { name = "fastapi", specifier = ">=0.115.0" },
+    { name = "hello-agents", specifier = ">=0.2.8" },
+    { name = "huggingface-hub", specifier = ">=1.3.3" },
+    { name = "loguru", specifier = ">=0.7.3" },
+    { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11.1" },
+    { name = "openai", specifier = ">=1.12.0" },
+    { name = "python-dotenv", specifier = "==1.0.1" },
+    { name = "requests", specifier = ">=2.31.0" },
+    { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.1" },
+    { name = "tavily-python", specifier = ">=0.5.0" },
+    { name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" },
+]
+provides-extras = ["dev"]
+
+[package.metadata.requires-dev]
+dev = [{ name = "ruff", specifier = ">=0.12.7" }]
+
 [[package]]
 name = "distro"
 version = "1.9.0"
@@ -387,6 +435,24 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/b1/26/e6d959b4ac959fdb3e9c4154656fc160794db6af8e64673d52759456bf07/fastapi-0.119.1-py3-none-any.whl", hash = "sha256:0b8c2a2cce853216e150e9bd4faaed88227f8eb37de21cb200771f491586a27f", size = 108123, upload-time = "2025-10-20T11:30:26.185Z" },
 ]
 
+[[package]]
+name = "filelock"
+version = "3.20.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" },
+]
+
+[[package]]
+name = "fsspec"
+version = "2026.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d5/7d/5df2650c57d47c57232af5ef4b4fdbff182070421e405e0d62c6cdbfaa87/fsspec-2026.1.0.tar.gz", hash = "sha256:e987cb0496a0d81bba3a9d1cee62922fb395e7d4c3b575e57f547953334fe07b", size = 310496, upload-time = "2026-01-09T15:21:35.562Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/01/c9/97cc5aae1648dcb851958a3ddf73ccd7dbe5650d95203ecb4d7720b4cdbf/fsspec-2026.1.0-py3-none-any.whl", hash = "sha256:cb76aa913c2285a3b49bdd5fc55b1d7c708d7208126b60f2eb8194fe1b4cbdcc", size = 201838, upload-time = "2026-01-09T15:21:34.041Z" },
+]
+
 [[package]]
 name = "h11"
 version = "0.16.0"
@@ -431,50 +497,33 @@ wheels = [
 ]
 
 [[package]]
-name = "helloagents-deep-researcher"
-version = "0.0.1"
-source = { editable = "." }
-dependencies = [
-    { name = "ddgs" },
-    { name = "fastapi" },
-    { name = "hello-agents" },
-    { name = "loguru" },
-    { name = "openai" },
-    { name = "python-dotenv" },
-    { name = "requests" },
-    { name = "tavily-python" },
-    { name = "uvicorn", extra = ["standard"] },
-]
-
-[package.optional-dependencies]
-dev = [
-    { name = "mypy" },
-    { name = "ruff" },
-]
-
-[package.dev-dependencies]
-dev = [
-    { name = "ruff" },
-]
-
-[package.metadata]
-requires-dist = [
-    { name = "ddgs", specifier = ">=9.6.1" },
-    { name = "fastapi", specifier = ">=0.115.0" },
-    { name = "hello-agents", specifier = ">=0.2.8" },
-    { name = "loguru", specifier = ">=0.7.3" },
-    { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.11.1" },
-    { name = "openai", specifier = ">=1.12.0" },
-    { name = "python-dotenv", specifier = "==1.0.1" },
-    { name = "requests", specifier = ">=2.31.0" },
-    { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6.1" },
-    { name = "tavily-python", specifier = ">=0.5.0" },
-    { name = "uvicorn", extras = ["standard"], specifier = ">=0.32.0" },
+name = "hf-xet"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" },
+    { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" },
+    { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" },
+    { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" },
+    { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" },
+    { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" },
+    { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" },
+    { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" },
+    { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" },
+    { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" },
 ]
-provides-extras = ["dev"]
-
-[package.metadata.requires-dev]
-dev = [{ name = "ruff", specifier = ">=0.12.7" }]
 
 [[package]]
 name = "hpack"
@@ -568,6 +617,27 @@ socks = [
     { name = "socksio" },
 ]
 
+[[package]]
+name = "huggingface-hub"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "filelock" },
+    { name = "fsspec" },
+    { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+    { name = "httpx" },
+    { name = "packaging" },
+    { name = "pyyaml" },
+    { name = "shellingham" },
+    { name = "tqdm" },
+    { name = "typer-slim" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/02/c3/544cd4cdd4b3c6de8591b56bb69efc3682e9ac81e36135c02e909dd98c5b/huggingface_hub-1.3.3.tar.gz", hash = "sha256:f8be6f468da4470db48351e8c77d6d8115dff9b3daeb30276e568767b1ff7574", size = 627649, upload-time = "2026-01-22T13:59:46.931Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/48/e8/0d032698916b9773b710c46e3b8e0154fc34cd017b151cc316c84c6c34fe/huggingface_hub-1.3.3-py3-none-any.whl", hash = "sha256:44af7b62380efc87c1c3bde7e1bf0661899b5bdfca1fc60975c61ee68410e10e", size = 536604, upload-time = "2026-01-22T13:59:45.391Z" },
+]
+
 [[package]]
 name = "hyperframe"
 version = "6.1.0"
@@ -1068,6 +1138,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/1d/2a/7dd3d207ec669cacc1f186fd856a0f61dbc255d24f6fdc1a6715d6051b0f/openai-1.109.1-py3-none-any.whl", hash = "sha256:6bcaf57086cf59159b8e27447e4e7dd019db5d29a438072fbd49c290c7e65315", size = 948627, upload-time = "2025-09-24T13:00:50.754Z" },
 ]
 
+[[package]]
+name = "packaging"
+version = "26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+]
+
 [[package]]
 name = "pathspec"
 version = "0.12.1"
@@ -1452,6 +1531,15 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/b8/81/4b6387be7014858d924b843530e1b2a8e531846807516e9bea2ee0936bf7/ruff-0.14.1-py3-none-win_arm64.whl", hash = "sha256:e3b443c4c9f16ae850906b8d0a707b2a4c16f8d2f0a7fe65c475c5886665ce44", size = 12436636, upload-time = "2025-10-16T18:05:38.995Z" },
 ]
 
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
 [[package]]
 name = "sniffio"
 version = "1.3.1"
@@ -1628,6 +1716,19 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
 ]
 
+[[package]]
+name = "typer-slim"
+version = "0.21.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "click" },
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/17/d4/064570dec6358aa9049d4708e4a10407d74c99258f8b2136bb8702303f1a/typer_slim-0.21.1.tar.gz", hash = "sha256:73495dd08c2d0940d611c5a8c04e91c2a0a98600cbd4ee19192255a233b6dbfd", size = 110478, upload-time = "2026-01-06T11:21:11.176Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c8/0a/4aca634faf693e33004796b6cee0ae2e1dba375a800c16ab8d3eff4bb800/typer_slim-0.21.1-py3-none-any.whl", hash = "sha256:6e6c31047f171ac93cc5a973c9e617dbc5ab2bddc4d0a3135dc161b4e2020e0d", size = 47444, upload-time = "2026-01-06T11:21:12.441Z" },
+]
+
 [[package]]
 name = "typing-extensions"
 version = "4.15.0"