1 line
546 KiB
JSON
1 line
546 KiB
JSON
{"repo": "bcefghj/noteking-pro", "files": {"api/models/schemas.py": {"46214e6fd67075dce84001d7626b048fcfc38392": {"t": 1785599222.9472225, "n": 1509, "h": "64ebde1d0772ddca", "c": "\"\"\"API request/response schemas.\"\"\"\n\nfrom __future__ import annotations\n\nfrom pydantic import BaseModel, Field\n\n\nclass VideoRequest(BaseModel):\n url: str = Field(..., description=\"Video URL or local file path\")\n template: str = Field(\"detailed\", description=\"Output template name\")\n custom_prompt: str = Field(\"\", description=\"Custom prompt for 'custom' template\")\n use_cache: bool = Field(True, description=\"Use cached results if available\")\n\n\nclass BatchRequest(BaseModel):\n url: str = Field(..., description=\"Playlist/collection URL\")\n template: str = Field(\"detailed\", description=\"Output template name\")\n merge: bool = Field(True, description=\"Merge all results into one document\")\n\n\nclass ConfigUpdate(BaseModel):\n llm_api_key: str | None = None\n llm_base_url: str | None = None\n llm_model: str | None = None\n proxy_enabled: bool | None = None\n proxy_url: str | None = None\n default_template: str | None = None\n bilibili_sessdata: str | None = None\n\n\nclass VideoResponse(BaseModel):\n title: str\n content: str\n template: str\n source: str\n platform: str\n url: str\n duration: float = 0\n output_file: str = \"\"\n\n\nclass BatchResponse(BaseModel):\n title: str\n content: str\n template: str\n total: int\n completed: int\n failed: list[str] = []\n output_file: str = \"\"\n\n\nclass TemplateInfo(BaseModel):\n name: str\n display_name: str\n description: str\n\n\nclass ErrorResponse(BaseModel):\n error: str\n detail: str = \"\"\n"}}, "web/postcss.config.js": {"12a703d900da8159c30e75acbd2c4d87ae177f62": {"t": 1785599223.2032545, "n": 83, "h": "81781ed39dc8e6d6", "c": "module.exports = {\n plugins: {\n tailwindcss: {},\n autoprefixer: {},\n },\n};\n"}}, "api/main.py": {"f834dfa74c3d72568a2bd31653f08c1fb79be7cd": {"t": 1785599223.2087014, "n": 3195, "h": "de113953774aac9e", "c": "\"\"\"FastAPI application entry point.\"\"\"\n\nfrom __future__ import annotations\n\nimport time\nfrom collections import defaultdict\nfrom datetime import date\n\nfrom fastapi import FastAPI, Request, Response\nfrom fastapi.middleware.cors import CORSMiddleware\nfrom fastapi.responses import JSONResponse\n\nfrom api.routes.video import router as video_router\nfrom api.routes.config import router as config_router\nfrom api.routes.recording import router as recording_router\n\nDAILY_LIMIT = 20\n_usage: dict[str, int] = defaultdict(int)\n_usage_date: str = \"\"\n\napp = FastAPI(\n title=\"NoteKing Pro API\",\n description=(\n \"The ultimate video/recording to learning notes API. \"\n \"Supports 30+ platforms, local recordings, speaker diarization, \"\n \"noise reduction, and 23 output templates.\"\n ),\n version=\"2.0.0\",\n docs_url=\"/docs\",\n redoc_url=\"/redoc\",\n)\n\napp.add_middleware(\n CORSMiddleware,\n allow_origins=[\"*\"],\n allow_credentials=True,\n allow_methods=[\"*\"],\n allow_headers=[\"*\"],\n expose_headers=[\"X-RateLimit-Remaining\", \"X-RateLimit-Limit\"],\n)\n\n\n@app.middleware(\"http\")\nasync def rate_limit_middleware(request: Request, call_next):\n global _usage, _usage_date\n\n today = date.today().isoformat()\n if _usage_date != today:\n _usage = defaultdict(int)\n _usage_date = today\n\n rate_limited_prefixes = (\"/api/v1/summarize\",)\n path = request.url.path\n is_limited = any(path.startswith(p) for p in rate_limited_prefixes)\n\n if is_limited and request.method == \"POST\":\n client_ip = request.headers.get(\"X-Forwarded-For\", \"\").split(\",\")[0].strip()\n if not client_ip:\n client_ip = request.client.host if request.client else \"unknown\"\n\n used = _usage[client_ip]\n remaining = max(0, DAILY_LIMIT - used)\n\n if used >= DAILY_LIMIT:\n return JSONResponse(\n status_code=429,\n content={\n \"detail\": f\"今日免费次数已用完({DAILY_LIMIT}次/天),明天再来吧~\",\n \"limit\": DAILY_LIMIT,\n \"remaining\": 0,\n },\n headers={\n \"X-RateLimit-Limit\": str(DAILY_LIMIT),\n \"X-RateLimit-Remaining\": \"0\",\n },\n )\n\n _usage[client_ip] += 1\n remaining = max(0, DAILY_LIMIT - _usage[client_ip])\n\n response: Response = await call_next(request)\n response.headers[\"X-RateLimit-Limit\"] = str(DAILY_LIMIT)\n response.headers[\"X-RateLimit-Remaining\"] = str(remaining)\n return response\n\n return await call_next(request)\n\n\napp.include_router(video_router)\napp.include_router(config_router)\napp.include_router(recording_router)\n\n\n@app.get(\"/\")\nasync def root():\n return {\n \"name\": \"NoteKing Pro API\",\n \"version\": \"2.0.0\",\n \"docs\": \"/docs\",\n \"templates\": \"/api/v1/templates\",\n \"recording\": \"/api/v1/recording\",\n \"scenes\": \"/api/v1/recording/scenes\",\n }\n\n\n@app.get(\"/health\")\nasync def health():\n return {\"status\": \"ok\"}\n\n\ndef start():\n import uvicorn\n uvicorn.run(\"api.main:app\", host=\"0.0.0.0\", port=8000, reload=True)\n\n\nif __name__ == \"__main__\":\n start()\n"}}, "web/tailwind.config.js": {"b142f18653ff59cd155f996124fccce98c5d9e5b": {"t": 1785599223.2623029, "n": 295, "h": "77b540f716252393", "c": "/** @type {import('tailwindcss').Config} */\nmodule.exports = {\n content: [\"./src/**/*.{js,ts,jsx,tsx,mdx}\"],\n darkMode: \"class\",\n theme: {\n extend: {\n colors: {\n primary: { 50: \"#eff6ff\", 500: \"#3b82f6\", 600: \"#2563eb\", 700: \"#1d4ed8\" },\n },\n },\n },\n plugins: [],\n};\n"}}, "api/routes/video.py": {"47919d109f37ce2b9e9100249996bf15c9fb9f68": {"t": 1785599223.2768612, "n": 8550, "h": "a39d12639e2b1fd6", "c": "\"\"\"Video processing API routes.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport os\nimport re\nimport tempfile\nfrom pathlib import Path\nfrom typing import Generator\n\nfrom fastapi import APIRouter, HTTPException\nfrom fastapi.responses import StreamingResponse\n\nfrom api.models.schemas import (\n VideoRequest, VideoResponse, BatchRequest, BatchResponse,\n TemplateInfo, ErrorResponse,\n)\nfrom core.config import AppConfig, LLMConfig\nfrom core import summarize\nfrom core.templates import TEMPLATE_LIST\n\nrouter = APIRouter(prefix=\"/api/v1\", tags=[\"video\"])\n\n\ndef _load_config() -> AppConfig:\n \"\"\"Load config from environment variables (called on each request).\"\"\"\n cfg = AppConfig()\n if os.environ.get(\"NOTEKING_LLM_API_KEY\"):\n cfg.llm.api_key = os.environ[\"NOTEKING_LLM_API_KEY\"]\n if os.environ.get(\"NOTEKING_LLM_BASE_URL\"):\n cfg.llm.base_url = os.environ[\"NOTEKING_LLM_BASE_URL\"]\n if os.environ.get(\"NOTEKING_LLM_MODEL\"):\n cfg.llm.model = os.environ[\"NOTEKING_LLM_MODEL\"]\n if os.environ.get(\"BILIBILI_SESSDATA\"):\n cfg.bilibili_sessdata = os.environ[\"BILIBILI_SESSDATA\"]\n if os.environ.get(\"NOTEKING_PROXY\"):\n cfg.proxy.enabled = True\n cfg.proxy.http = os.environ[\"NOTEKING_PROXY\"]\n cfg.proxy.https = os.environ[\"NOTEKING_PROXY\"]\n if not cfg.llm.api_key:\n raise RuntimeError(\n \"No LLM API key configured. Set it via config file, \"\n \"environment variable NOTEKING_LLM_API_KEY, or --api-key flag.\"\n )\n return cfg\n\n\ndef _sse_event(stage: str, **kwargs) -> str:\n data = {\"stage\": stage, **kwargs}\n return f\"data: {json.dumps(data, ensure_ascii=False)}\\n\\n\"\n\n\n@router.post(\"/summarize\", response_model=VideoResponse)\nasync def summarize_video(req: VideoRequest):\n \"\"\"Process a single video and generate notes.\"\"\"\n try:\n cfg = _load_config()\n result = summarize(\n url=req.url,\n template=req.template,\n config=cfg,\n custom_prompt=req.custom_prompt,\n use_cache=req.use_cache,\n )\n return VideoResponse(\n title=result.get(\"title\", \"\"),\n content=result.get(\"content\", \"\"),\n template=result.get(\"template\", req.template),\n source=result.get(\"source\", \"\"),\n platform=result.get(\"platform\", \"\"),\n url=result.get(\"url\", req.url),\n duration=result.get(\"duration\", 0),\n output_file=result.get(\"output_file\", \"\"),\n )\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n\n\n@router.post(\"/summarize/stream\")\nasync def summarize_video_stream(req: VideoRequest):\n \"\"\"Process a video with SSE streaming progress.\"\"\"\n\n def generate() -> Generator[str, None, None]:\n try:\n cfg = _load_config()\n\n yield _sse_event(\"info\", message=\"正在获取视频信息...\")\n from core.parser import parse_link, Platform, is_batch\n from core.downloader import get_video_info, VideoMeta\n from core.subtitle import extract_subtitles, SubtitleResult\n from core.llm import chat_stream\n from core.templates import get_template, TemplateContext\n from core.cache import Cache\n\n parsed = parse_link(req.url)\n meta = get_video_info(req.url, cfg)\n\n yield _sse_event(\"info\", message=f\"视频: {meta.title}\",\n title=meta.title, duration=meta.duration,\n platform=parsed.platform.value)\n\n cache = Cache(cfg)\n cached = cache.get(req.url, req.template)\n if cached and req.use_cache:\n content = cached.get(\"content\", \"\")\n content = re.sub(r\"<think>.*?</think>\", \"\", content, flags=re.DOTALL).strip()\n yield _sse_event(\"done\",\n title=cached.get(\"title\", \"\"),\n content=content,\n template=req.template,\n source=cached.get(\"source\", \"\"),\n platform=parsed.platform.value,\n duration=meta.duration)\n return\n\n yield _sse_event(\"subtitle\", message=\"正在提取字幕...\")\n work_dir = Path(tempfile.mkdtemp(prefix=\"noteking_\"))\n subs = extract_subtitles(parsed, work_dir, cfg, skip_asr=True)\n\n if not subs.segments and subs.source == \"visual\":\n subs = SubtitleResult(\n segments=[], source=\"visual\",\n raw_text=f\"标题:{meta.title}\\n简介:{meta.description[:800]}\",\n )\n\n yield _sse_event(\"subtitle\",\n message=f\"字幕来源: {subs.source}\",\n source=subs.source)\n\n yield _sse_event(\"generating\", message=\"AI 正在生成笔记...\")\n tmpl = get_template(req.template, user_prompt=req.custom_prompt)\n ctx = TemplateContext(\n meta=meta, subtitles=subs, config=cfg,\n extra={\"custom_prompt\": req.custom_prompt},\n )\n prompt = tmpl.build_prompt(ctx)\n system = tmpl.system_prompt(ctx)\n\n full_content = \"\"\n in_think = False\n for chunk in chat_stream(prompt, cfg, system=system):\n full_content += chunk\n if \"<think>\" in chunk:\n in_think = True\n if \"</think>\" in chunk:\n in_think = False\n continue\n if not in_think:\n yield _sse_event(\"generating\", content=chunk)\n\n content = re.sub(r\"<think>.*?</think>\", \"\", full_content, flags=re.DOTALL).strip()\n content = tmpl.post_process(content, ctx)\n\n result = {\n \"title\": meta.title,\n \"content\": content,\n \"template\": req.template,\n \"source\": subs.source,\n \"url\": req.url,\n \"platform\": parsed.platform.value,\n \"duration\": meta.duration,\n }\n cache.set(req.url, req.template, result)\n\n yield _sse_event(\"done\", **result)\n\n except Exception as e:\n yield _sse_event(\"error\", message=str(e))\n\n return StreamingResponse(\n generate(),\n media_type=\"text/event-stream\",\n headers={\"Cache-Control\": \"no-cache\", \"X-Accel-Buffering\": \"no\"},\n )\n\n\n@router.post(\"/batch\", response_model=BatchResponse)\nasync def batch_summarize(req: BatchRequest):\n \"\"\"Process a playlist/collection.\"\"\"\n try:\n cfg = _load_config()\n result = summarize(\n url=req.url,\n template=req.template,\n config=cfg,\n )\n return BatchResponse(\n title=result.get(\"title\", \"\"),\n content=result.get(\"content\", \"\"),\n template=result.get(\"template\", req.template),\n total=result.get(\"total\", 1),\n completed=result.get(\"completed\", 1),\n failed=result.get(\"failed\", []),\n output_file=result.get(\"output_file\", \"\"),\n )\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n\n\n@router.get(\"/templates\", response_model=list[TemplateInfo])\nasync def list_templates():\n \"\"\"List all available output templates.\"\"\"\n return [TemplateInfo(**t) for t in TEMPLATE_LIST]\n\n\n@router.get(\"/info\")\nasync def get_video_info_endpoint(url: str):\n \"\"\"Get video metadata without processing.\"\"\"\n from core.downloader import get_video_info as fetch_info\n try:\n cfg = _load_config()\n meta = fetch_info(url, cfg)\n return {\n \"title\": meta.title,\n \"uploader\": meta.uploader,\n \"duration\": meta.duration,\n \"thumbnail\": meta.thumbnail,\n \"has_subtitles\": meta.has_subtitles,\n \"is_playlist\": meta.is_playlist,\n \"entry_count\": meta.entry_count,\n \"chapters\": meta.chapters,\n }\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n\n\n@router.get(\"/transcript\")\nasync def get_transcript(url: str):\n \"\"\"Get only the transcript text.\"\"\"\n from core import get_transcript as fetch_transcript\n try:\n cfg = _load_config()\n text = fetch_transcript(url, cfg)\n return {\"url\": url, \"transcript\": text}\n except Exception as e:\n raise HTTPException(status_code=500, detail=str(e))\n"}}, "web/next.config.js": {"4f21436ea57b4393062e9f77203c9a586f2e928f": {"t": 1785599223.5650673, "n": 246, "h": "32e9093b0ef3811c", "c": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n async rewrites() {\n return [\n {\n source: \"/api/:path*\",\n destination: \"http://127.0.0.1:8000/api/:path*\",\n },\n ];\n },\n};\n\nmodule.exports = nextConfig;\n"}}, "core/config.py": {"e1fbf9fd7073a5158c4aa8608648353871ec1f63": {"t": 1785599224.0020485, "n": 4096, "h": "f0779dfea3d4b39f", "c": "\"\"\"Global configuration management for NoteKing.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass, field, asdict\nfrom pathlib import Path\nfrom typing import Any\n\nDEFAULT_CONFIG_DIR = Path.home() / \".noteking\"\nDEFAULT_CONFIG_FILE = DEFAULT_CONFIG_DIR / \"config.json\"\nDEFAULT_CACHE_DIR = DEFAULT_CONFIG_DIR / \"cache\"\nDEFAULT_OUTPUT_DIR = Path.cwd() / \"noteking_output\"\n\n\n@dataclass\nclass ProxyConfig:\n enabled: bool = False\n http: str = \"\"\n https: str = \"\"\n socks5: str = \"\"\n\n @property\n def for_requests(self) -> dict[str, str] | None:\n if not self.enabled:\n return None\n proxies: dict[str, str] = {}\n if self.http:\n proxies[\"http\"] = self.http\n if self.https:\n proxies[\"https\"] = self.https\n elif self.socks5:\n proxies[\"http\"] = self.socks5\n proxies[\"https\"] = self.socks5\n return proxies or None\n\n @property\n def for_ytdlp(self) -> str | None:\n if not self.enabled:\n return None\n return self.socks5 or self.https or self.http or None\n\n\n@dataclass\nclass ASRConfig:\n default_engine: str = \"auto\"\n faster_whisper_model: str = \"base\"\n groq_api_key: str = \"\"\n openai_api_key: str = \"\"\n volcengine_app_id: str = \"\"\n volcengine_token: str = \"\"\n deepgram_api_key: str = \"\"\n\n\n@dataclass\nclass LLMConfig:\n provider: str = \"openai\"\n api_key: str = \"\"\n base_url: str = \"\"\n model: str = \"gpt-4o-mini\"\n temperature: float = 0.3\n max_tokens: int = 16000\n language: str = \"zh-CN\"\n\n\n@dataclass\nclass RecordingConfig:\n \"\"\"Configuration for recording/meeting processing.\"\"\"\n denoise_level: int = 1 # 0=none, 1=light, 2=medium, 3=heavy\n num_speakers: int | None = None # None=auto-detect\n enable_diarization: bool = True\n default_scene: str = \"meeting\"\n default_output_formats: list[str] = field(default_factory=lambda: [\"markdown\", \"pdf\"])\n hotwords: list[str] = field(default_factory=list) # boost ASR accuracy\n context: str = \"\" # user-provided description\n\n\n@dataclass\nclass AppConfig:\n proxy: ProxyConfig = field(default_factory=ProxyConfig)\n asr: ASRConfig = field(default_factory=ASRConfig)\n llm: LLMConfig = field(default_factory=LLMConfig)\n recording: RecordingConfig = field(default_factory=RecordingConfig)\n cache_dir: str = str(DEFAULT_CACHE_DIR)\n output_dir: str = str(DEFAULT_OUTPUT_DIR)\n bilibili_sessdata: str = \"\"\n default_template: str = \"detailed\"\n max_concurrent_downloads: int = 3\n\n def save(self, path: Path | None = None) -> None:\n path = path or DEFAULT_CONFIG_FILE\n path.parent.mkdir(parents=True, exist_ok=True)\n path.write_text(json.dumps(asdict(self), indent=2, ensure_ascii=False))\n\n @classmethod\n def load(cls, path: Path | None = None) -> \"AppConfig\":\n path = path or DEFAULT_CONFIG_FILE\n if not path.exists():\n cfg = cls()\n cfg.save(path)\n return cfg\n try:\n data = json.loads(path.read_text())\n rec_data = data.get(\"recording\", {})\n rec_data.pop(\"default_output_formats\", None) # avoid type issues\n return cls(\n proxy=ProxyConfig(**data.get(\"proxy\", {})),\n asr=ASRConfig(**data.get(\"asr\", {})),\n llm=LLMConfig(**data.get(\"llm\", {})),\n recording=RecordingConfig(**{\n k: v for k, v in rec_data.items()\n if k in RecordingConfig.__dataclass_fields__\n }),\n cache_dir=data.get(\"cache_dir\", str(DEFAULT_CACHE_DIR)),\n output_dir=data.get(\"output_dir\", str(DEFAULT_OUTPUT_DIR)),\n bilibili_sessdata=data.get(\"bilibili_sessdata\", \"\"),\n default_template=data.get(\"default_template\", \"detailed\"),\n max_concurrent_downloads=data.get(\"max_concurrent_downloads\", 3),\n )\n except Exception:\n return cls()\n\n def to_dict(self) -> dict[str, Any]:\n return asdict(self)\n"}}, "cli/__main__.py": {"cde827b942faae243efa31295f3f872600185c19": {"t": 1785599224.3028784, "n": 15953, "h": "b92655cfd9e43273", "c": "\"\"\"NoteKing CLI: command-line interface for video/recording to notes conversion.\"\"\"\n\nfrom __future__ import annotations\n\nimport sys\nfrom pathlib import Path\n\nimport click\nfrom rich.console import Console\nfrom rich.panel import Panel\nfrom rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn, TaskProgressColumn\nfrom rich.table import Table\n\nconsole = Console()\n\nALL_TEMPLATES = [\n \"brief\", \"detailed\", \"mindmap\", \"flashcard\", \"quiz\", \"timeline\",\n \"exam\", \"tutorial\", \"news\", \"podcast\", \"xhs_note\", \"latex_pdf\", \"custom\",\n \"meeting_minutes\", \"lecture_notes\", \"interview\", \"brainstorm\",\n \"news_digest\", \"exam_prep\", \"cornell_notes\", \"podcast_shownotes\",\n \"entertainment\", \"smart_summary\",\n]\n\n\n@click.group(invoke_without_command=True)\n@click.pass_context\n@click.version_option(version=\"2.0.0\", prog_name=\"noteking\")\ndef main(ctx):\n \"\"\"NoteKing Pro - The ultimate video/recording to learning notes tool.\n\n Supports 30+ platforms, local video/audio files, meeting recordings,\n speaker diarization, noise reduction, and 23 output templates.\n \"\"\"\n if ctx.invoked_subcommand is None:\n click.echo(ctx.get_help())\n\n\n# ────────────────────────────────────────────────────────────────\n# Original commands\n# ────────────────────────────────────────────────────────────────\n\n@main.command()\n@click.argument(\"url\")\n@click.option(\"-t\", \"--template\", default=\"detailed\",\n help=\"Output template name\")\n@click.option(\"-o\", \"--output\", default=None, help=\"Output directory\")\n@click.option(\"--api-key\", default=None, help=\"LLM API key\")\n@click.option(\"--base-url\", default=None, help=\"LLM API base URL\")\n@click.option(\"--model\", default=None, help=\"LLM model name\")\n@click.option(\"--proxy\", default=None, help=\"Proxy URL\")\n@click.option(\"--custom-prompt\", default=\"\", help=\"Custom prompt for 'custom' template\")\n@click.option(\"--no-cache\", is_flag=True, help=\"Disable result caching\")\ndef run(url, template, output, api_key, base_url, model, proxy, custom_prompt, no_cache):\n \"\"\"Process a video/blog URL and generate notes.\n\n Examples:\n\n noteking run \"https://www.bilibili.com/video/BV1xx\" -t detailed\n\n noteking run \"https://youtu.be/xxx\" -t mindmap\n\n noteking run \"./lecture.mp4\" -t exam\n \"\"\"\n from core.config import AppConfig\n from core import summarize\n\n config = AppConfig.load()\n _apply_overrides(config, api_key, base_url, model, proxy)\n\n console.print(Panel(\n f\"[bold cyan]NoteKing[/bold cyan] - Video to Notes\\n\"\n f\"URL: {url}\\nTemplate: {template}\",\n title=\"Processing\",\n ))\n\n with Progress(SpinnerColumn(), TextColumn(\"[progress.description]{task.description}\"), console=console) as progress:\n task = progress.add_task(\"Fetching video info and subtitles...\", total=None)\n try:\n result = summarize(url=url, template=template, config=config,\n custom_prompt=custom_prompt, use_cache=not no_cache, output_dir=output)\n progress.update(task, description=\"Done!\")\n except Exception as e:\n progress.update(task, description=f\"[red]Error: {e}[/red]\")\n console.print(f\"\\n[red]Error:[/red] {e}\")\n sys.exit(1)\n\n _print_result(result)\n\n\n@main.command()\n@click.argument(\"url\")\n@click.option(\"-o\", \"--output\", default=None, help=\"Output directory\")\n@click.option(\"--proxy\", default=None, help=\"Proxy URL\")\ndef transcript(url, output, proxy):\n \"\"\"Extract only the transcript/subtitles from a video.\"\"\"\n from core.config import AppConfig\n from core import get_transcript\n\n config = AppConfig.load()\n if proxy:\n config.proxy.enabled = True\n config.proxy.https = proxy\n\n text = get_transcript(url, config)\n if output:\n Path(output).mkdir(parents=True, exist_ok=True)\n out_file = Path(output) / \"transcript.txt\"\n out_file.write_text(text, encoding=\"utf-8\")\n console.print(f\"[green]Saved to {out_file}[/green]\")\n else:\n console.print(text)\n\n\n# ────────────────────────────────────────────────────────────────\n# New recording processing commands\n# ────────────────────────────────────────────────────────────────\n\n@main.command()\n@click.argument(\"files\", nargs=-1, required=True, type=click.Path(exists=True))\n@click.option(\"-t\", \"--template\", default=\"meeting_minutes\", help=\"Output template name\")\n@click.option(\"-c\", \"--context\", default=None, help=\"Content description (e.g. 'AI开源项目圆桌讨论')\")\n@click.option(\"-s\", \"--scene\", default=None,\n type=click.Choice([\"meeting\", \"lecture\", \"interview\", \"brainstorm\",\n \"news\", \"exam\", \"entertainment\", \"custom\"]),\n help=\"Scene type\")\n@click.option(\"--speakers\", default=None, type=int, help=\"Number of speakers (auto-detect if not set)\")\n@click.option(\"--denoise\", default=1, type=click.IntRange(0, 3), help=\"Noise reduction level (0-3)\")\n@click.option(\"--format\", \"output_formats\", default=\"markdown\", help=\"Output formats, comma-separated (markdown,pdf,srt,vtt,transcript,json)\")\n@click.option(\"-o\", \"--output\", default=None, help=\"Output directory\")\n@click.option(\"--api-key\", default=None, help=\"LLM API key\")\n@click.option(\"--base-url\", default=None, help=\"LLM API base URL\")\n@click.option(\"--model\", default=None, help=\"LLM model name\")\ndef process(files, template, context, scene, speakers, denoise, output_formats, output, api_key, base_url, model):\n \"\"\"Process local recording/video files and generate notes.\n\n Supports video (MP4, MKV, AVI, MOV) and audio (MP3, WAV, FLAC, M4A) files.\n Multiple files will be merged in order.\n\n Examples:\n\n noteking process meeting.mp4 -t meeting_minutes -c \"产品周会\"\n\n noteking process lecture.mp3 -t lecture_notes --denoise 2\n\n noteking process part1.mp4 part2.mp4 -t meeting_minutes --speakers 4\n\n noteking process interview.wav -t interview --format markdown,srt\n \"\"\"\n from core.config import AppConfig\n from core import process_recording\n\n config = AppConfig.load()\n _apply_overrides(config, api_key, base_url, model, None)\n\n formats = [f.strip() for f in output_formats.split(\",\")]\n\n console.print(Panel(\n f\"[bold cyan]NoteKing Pro[/bold cyan] - Recording Processor\\n\"\n f\"Files: {', '.join(str(f) for f in files)}\\n\"\n f\"Template: {template}\\n\"\n f\"Scene: {scene or 'auto'}\\n\"\n f\"Denoise: Level {denoise}\\n\"\n f\"Speakers: {speakers or 'auto-detect'}\\n\"\n f\"Formats: {', '.join(formats)}\",\n title=\"Processing Recording\",\n ))\n\n with Progress(\n SpinnerColumn(),\n TextColumn(\"[progress.description]{task.description}\"),\n BarColumn(),\n TaskProgressColumn(),\n console=console,\n ) as progress:\n task = progress.add_task(\"Starting...\", total=100)\n\n def _progress(step: str, pct: float):\n step_names = {\n \"preprocess/validate\": \"Validating files...\",\n \"preprocess/merge\": \"Merging files...\",\n \"preprocess/extract_audio\": \"Extracting audio...\",\n \"preprocess/denoise\": \"Reducing noise...\",\n \"preprocess/done\": \"Preprocessing done\",\n \"preprocess\": \"Preprocessing...\",\n \"transcribe\": \"Transcribing audio...\",\n \"diarize\": \"Identifying speakers...\",\n \"generate\": \"Generating notes with AI...\",\n \"output\": \"Saving output files...\",\n \"done\": \"Complete!\",\n }\n desc = step_names.get(step, step)\n progress.update(task, completed=int(pct * 100), description=desc)\n\n try:\n result = process_recording(\n input_files=list(files),\n template=template,\n context=context,\n scene=scene,\n num_speakers=speakers,\n denoise_level=denoise,\n output_formats=formats,\n config=config,\n output_dir=output,\n progress_callback=_progress,\n )\n except Exception as e:\n console.print(f\"\\n[red]Error:[/red] {e}\")\n import traceback\n traceback.print_exc()\n sys.exit(1)\n\n console.print()\n _print_recording_result(result)\n\n\n@main.command()\n@click.argument(\"file\", type=click.Path(exists=True))\n@click.option(\"--lang\", default=None, help=\"Language code (auto-detect if not set)\")\n@click.option(\"-o\", \"--output\", default=None, help=\"Output directory\")\n@click.option(\"--api-key\", default=None, help=\"LLM API key\")\ndef transcribe(file, lang, output, api_key):\n \"\"\"Transcribe a local audio/video file to text.\n\n Example:\n\n noteking transcribe meeting.mp4\n\n noteking transcribe lecture.mp3 --lang en\n \"\"\"\n from core.config import AppConfig\n from core.preprocessor import preprocess\n from core.transcriber import transcribe as do_transcribe, detect_language\n\n config = AppConfig.load()\n if api_key:\n config.llm.api_key = api_key\n\n console.print(Panel(f\"[cyan]Transcribing:[/cyan] {file}\", title=\"ASR\"))\n\n with Progress(SpinnerColumn(), TextColumn(\"[progress.description]{task.description}\"), console=console) as progress:\n task = progress.add_task(\"Extracting audio...\", total=None)\n pre = preprocess([file], denoise_level=0)\n\n progress.update(task, description=\"Detecting language...\")\n language = lang or detect_language(pre.audio_path, config)\n\n progress.update(task, description=f\"Transcribing ({language})...\")\n result = do_transcribe(pre.audio_path, config, language=language)\n progress.update(task, description=\"Done!\")\n\n if output:\n out_dir = Path(output)\n out_dir.mkdir(parents=True, exist_ok=True)\n txt_path = out_dir / f\"{Path(file).stem}_transcript.txt\"\n srt_path = out_dir / f\"{Path(file).stem}.srt\"\n result.save_txt(txt_path)\n result.save_srt(srt_path)\n console.print(f\"[green]Saved:[/green] {txt_path}\")\n console.print(f\"[green]Saved:[/green] {srt_path}\")\n else:\n console.print(result.full_text)\n\n console.print(f\"\\nLanguage: {language}\")\n console.print(f\"Segments: {len(result.segments)}\")\n\n\n@main.command()\n@click.argument(\"file\", type=click.Path(exists=True))\n@click.option(\"--level\", default=2, type=click.IntRange(1, 3), help=\"Denoise level (1-3)\")\n@click.option(\"-o\", \"--output\", default=None, help=\"Output file path\")\ndef denoise(file, level, output):\n \"\"\"Apply noise reduction to an audio file.\n\n Example:\n\n noteking denoise noisy_recording.wav --level 2\n \"\"\"\n from core.preprocessor import extract_audio, denoise_audio\n\n import tempfile\n work_dir = Path(tempfile.mkdtemp(prefix=\"noteking_dn_\"))\n\n console.print(Panel(f\"[cyan]Denoising:[/cyan] {file}\\nLevel: {level}\", title=\"Noise Reduction\"))\n\n with Progress(SpinnerColumn(), TextColumn(\"[progress.description]{task.description}\"), console=console) as progress:\n task = progress.add_task(\"Extracting audio...\", total=None)\n audio_path = work_dir / \"audio.wav\"\n extract_audio(Path(file), audio_path)\n\n progress.update(task, description=f\"Applying noise reduction (level {level})...\")\n out_path = Path(output) if output else Path(file).parent / f\"{Path(file).stem}_denoised.wav\"\n result = denoise_audio(audio_path, out_path, level=level)\n progress.update(task, description=\"Done!\")\n\n console.print(f\"[green]Saved:[/green] {result}\")\n\n\n@main.command()\n@click.argument(\"files\", nargs=-1, required=True, type=click.Path(exists=True))\n@click.option(\"-o\", \"--output\", required=True, help=\"Output file path\")\ndef merge(files, output):\n \"\"\"Merge multiple audio/video files into one.\n\n Example:\n\n noteking merge part1.mp4 part2.mp4 -o merged.mp4\n \"\"\"\n from core.preprocessor import merge_files\n\n console.print(Panel(\n f\"[cyan]Merging {len(files)} files[/cyan]\",\n title=\"Merge\",\n ))\n\n with Progress(SpinnerColumn(), TextColumn(\"[progress.description]{task.description}\"), console=console) as progress:\n task = progress.add_task(\"Merging...\", total=None)\n paths = [Path(f) for f in files]\n merged, segments = merge_files(paths, Path(output))\n progress.update(task, description=\"Done!\")\n\n console.print(f\"[green]Merged:[/green] {merged}\")\n for seg in segments:\n console.print(f\" {seg.file_path} (offset: {seg.start_offset:.1f}s, duration: {seg.duration:.1f}s)\")\n\n\n# ────────────────────────────────────────────────────────────────\n# Utility commands\n# ────────────────────────────────────────────────────────────────\n\n@main.command()\ndef templates():\n \"\"\"List all available output templates.\"\"\"\n from core.templates import TEMPLATE_LIST\n\n table = Table(title=\"Available Templates (23)\")\n table.add_column(\"Name\", style=\"cyan\")\n table.add_column(\"Display Name\", style=\"green\")\n table.add_column(\"Description\")\n\n for t in TEMPLATE_LIST:\n table.add_row(t[\"name\"], t[\"display_name\"], t[\"description\"])\n\n console.print(table)\n\n\n@main.command()\n@click.option(\"--api-key\", prompt=\"LLM API Key\", help=\"Your LLM API key\")\n@click.option(\"--base-url\", default=\"\", help=\"LLM API base URL (for MiniMax/DeepSeek/Ollama)\")\n@click.option(\"--model\", default=\"gpt-4o-mini\", help=\"LLM model name\")\n@click.option(\"--proxy\", default=\"\", help=\"Proxy URL for YouTube access\")\ndef setup(api_key, base_url, model, proxy):\n \"\"\"Interactive setup wizard for first-time configuration.\"\"\"\n from core.config import AppConfig\n\n config = AppConfig.load()\n config.llm.api_key = api_key\n if base_url:\n config.llm.base_url = base_url\n config.llm.model = model\n if proxy:\n config.proxy.enabled = True\n if proxy.startswith(\"socks\"):\n config.proxy.socks5 = proxy\n else:\n config.proxy.https = proxy\n config.save()\n\n console.print(\"[green]Configuration saved![/green]\")\n console.print(f\"Config: ~/.noteking/config.json\")\n\n\n@main.command()\ndef cache_clear():\n \"\"\"Clear all cached results.\"\"\"\n from core.config import AppConfig\n from core.cache import Cache\n\n config = AppConfig.load()\n c = Cache(config)\n count = c.clear()\n console.print(f\"[green]Cleared {count} cached entries.[/green]\")\n\n\n# ────────────────────────────────────────────────────────────────\n# Helpers\n# ────────────────────────────────────────────────────────────────\n\ndef _apply_overrides(config, api_key, base_url, model, proxy):\n if api_key:\n config.llm.api_key = api_key\n if base_url:\n config.llm.base_url = base_url\n if model:\n config.llm.model = model\n if proxy:\n config.proxy.enabled = True\n if proxy.startswith(\"socks\"):\n config.proxy.socks5 = proxy\n else:\n config.proxy.https = proxy\n config.proxy.http = proxy\n\n\ndef _print_result(result):\n console.print(f\"\\n[green]Success![/green]\")\n console.print(f\"Title: {result.get('title', 'N/A')}\")\n console.print(f\"Platform: {result.get('platform', 'N/A')}\")\n console.print(f\"Source: {result.get('source', 'N/A')}\")\n console.print(f\"Output: {result.get('output_file', 'N/A')}\")\n\n\ndef _print_recording_result(result):\n console.print(Panel(\"[bold green]Processing Complete![/bold green]\", title=\"Result\"))\n console.print(f\"Title: {result.get('title', 'N/A')}\")\n console.print(f\"Template: {result.get('template', 'N/A')}\")\n console.print(f\"Language: {result.get('language', 'N/A')}\")\n console.print(f\"Duration: {result.get('duration', 0):.0f} seconds\")\n console.print(f\"Speakers: {result.get('num_speakers', 'N/A')}\")\n\n files = result.get(\"output_files\", {})\n if files:\n console.print(\"\\n[bold]Output Files:[/bold]\")\n for fmt, path in files.items():\n console.print(f\" [{fmt}] {path}\")\n\n\nif __name__ == \"__main__\":\n main()\n"}}, "core/diarizer.py": {"67f1e1eb428a4c901d7da65b05c05d541d19b41b": {"t": 1785599224.365412, "n": 7795, "h": "7087df9dba469665", "c": "\"\"\"Speaker diarization: identify who spoke when using pyannote-audio.\n\nSupports:\n- Automatic speaker count detection or user-specified count\n- WhisperX-style word-level timestamp alignment\n- Merging adjacent same-speaker segments to reduce fragmentation\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Optional\n\nfrom .subtitle import SubtitleSegment, SubtitleResult\n\nlogger = logging.getLogger(__name__)\n\n\n@dataclass\nclass DiarizedSegment:\n \"\"\"A transcript segment with speaker label.\"\"\"\n start: float\n end: float\n text: str\n speaker: str = \"\"\n confidence: float = 0.0\n\n @property\n def start_ts(self) -> str:\n return _format_ts(self.start)\n\n @property\n def end_ts(self) -> str:\n return _format_ts(self.end)\n\n\n@dataclass\nclass DiarizationResult:\n \"\"\"Result of speaker diarization.\"\"\"\n segments: list[DiarizedSegment]\n num_speakers: int = 0\n speakers: list[str] | None = None\n language: str = \"zh\"\n\n @property\n def full_text(self) -> str:\n lines = []\n for seg in self.segments:\n speaker = seg.speaker or \"Unknown\"\n lines.append(f\"[{speaker}] ({seg.start_ts}): {seg.text}\")\n return \"\\n\".join(lines)\n\n @property\n def transcript_with_speakers(self) -> str:\n \"\"\"Generate a formatted transcript with speaker labels.\"\"\"\n lines = []\n current_speaker = None\n for seg in self.segments:\n if seg.speaker != current_speaker:\n current_speaker = seg.speaker\n lines.append(f\"\\n**{current_speaker or 'Unknown'}** ({seg.start_ts}):\")\n lines.append(f\" {seg.text}\")\n return \"\\n\".join(lines)\n\n\ndef _format_ts(seconds: float) -> str:\n m, s = divmod(int(seconds), 60)\n h, m = divmod(m, 60)\n return f\"{h:02d}:{m:02d}:{s:02d}\"\n\n\ndef diarize(\n audio_path: Path,\n transcript: SubtitleResult | None = None,\n num_speakers: int | None = None,\n min_speakers: int | None = None,\n max_speakers: int | None = None,\n) -> DiarizationResult:\n \"\"\"Perform speaker diarization on an audio file.\n\n Uses pyannote-audio 3.x if available, falls back to simple\n energy-based segmentation.\n\n Args:\n audio_path: Path to WAV audio file\n transcript: Optional ASR transcript to align with speakers\n num_speakers: Exact number of speakers (None for auto-detect)\n min_speakers: Minimum speakers for auto-detect\n max_speakers: Maximum speakers for auto-detect\n\n Returns:\n DiarizationResult with speaker-labeled segments\n \"\"\"\n try:\n return _diarize_pyannote(\n audio_path, transcript, num_speakers, min_speakers, max_speakers\n )\n except ImportError:\n logger.warning(\"pyannote-audio not installed, using simple diarization\")\n return _diarize_simple(audio_path, transcript)\n except Exception as e:\n logger.warning(f\"pyannote diarization failed: {e}, using simple fallback\")\n return _diarize_simple(audio_path, transcript)\n\n\ndef _diarize_pyannote(\n audio_path: Path,\n transcript: SubtitleResult | None,\n num_speakers: int | None,\n min_speakers: int | None,\n max_speakers: int | None,\n) -> DiarizationResult:\n \"\"\"Diarize using pyannote-audio 3.x pipeline.\"\"\"\n from pyannote.audio import Pipeline\n import torch\n\n hf_token = _get_hf_token()\n device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n\n pipeline = Pipeline.from_pretrained(\n \"pyannote/speaker-diarization-3.1\",\n use_auth_token=hf_token,\n )\n pipeline.to(device)\n\n diarize_kwargs = {}\n if num_speakers is not None:\n diarize_kwargs[\"num_speakers\"] = num_speakers\n if min_speakers is not None:\n diarize_kwargs[\"min_speakers\"] = min_speakers\n if max_speakers is not None:\n diarize_kwargs[\"max_speakers\"] = max_speakers\n\n diarization = pipeline(str(audio_path), **diarize_kwargs)\n\n speaker_turns = []\n for turn, _, speaker in diarization.itertracks(yield_label=True):\n speaker_turns.append({\n \"start\": turn.start,\n \"end\": turn.end,\n \"speaker\": speaker,\n })\n\n speakers = sorted(set(t[\"speaker\"] for t in speaker_turns))\n speaker_map = {s: f\"Speaker {i+1}\" for i, s in enumerate(speakers)}\n\n if transcript and transcript.segments:\n segments = _align_transcript_with_speakers(\n transcript.segments, speaker_turns, speaker_map\n )\n else:\n segments = [\n DiarizedSegment(\n start=t[\"start\"],\n end=t[\"end\"],\n text=\"\",\n speaker=speaker_map.get(t[\"speaker\"], t[\"speaker\"]),\n )\n for t in speaker_turns\n ]\n\n segments = _merge_adjacent_speakers(segments)\n\n return DiarizationResult(\n segments=segments,\n num_speakers=len(speakers),\n speakers=list(speaker_map.values()),\n language=transcript.language if transcript else \"zh\",\n )\n\n\ndef _diarize_simple(\n audio_path: Path,\n transcript: SubtitleResult | None,\n) -> DiarizationResult:\n \"\"\"Simple fallback: assign all segments to Speaker 1.\n\n Provides structural compatibility without actual speaker separation.\n \"\"\"\n segments = []\n if transcript and transcript.segments:\n for seg in transcript.segments:\n segments.append(DiarizedSegment(\n start=seg.start,\n end=seg.end,\n text=seg.text,\n speaker=\"Speaker 1\",\n ))\n return DiarizationResult(\n segments=segments,\n num_speakers=1,\n speakers=[\"Speaker 1\"],\n language=transcript.language if transcript else \"zh\",\n )\n\n\ndef _align_transcript_with_speakers(\n transcript_segments: list[SubtitleSegment],\n speaker_turns: list[dict],\n speaker_map: dict[str, str],\n) -> list[DiarizedSegment]:\n \"\"\"Align ASR transcript segments with speaker diarization turns.\n\n For each transcript segment, find the speaker turn with maximum\n temporal overlap and assign that speaker.\n \"\"\"\n results = []\n for seg in transcript_segments:\n best_speaker = \"Unknown\"\n best_overlap = 0.0\n\n for turn in speaker_turns:\n overlap_start = max(seg.start, turn[\"start\"])\n overlap_end = min(seg.end, turn[\"end\"])\n overlap = max(0, overlap_end - overlap_start)\n if overlap > best_overlap:\n best_overlap = overlap\n best_speaker = speaker_map.get(turn[\"speaker\"], turn[\"speaker\"])\n\n results.append(DiarizedSegment(\n start=seg.start,\n end=seg.end,\n text=seg.text,\n speaker=best_speaker,\n ))\n\n return results\n\n\ndef _merge_adjacent_speakers(\n segments: list[DiarizedSegment],\n max_gap: float = 1.0,\n) -> list[DiarizedSegment]:\n \"\"\"Merge adjacent segments from the same speaker to reduce fragmentation.\"\"\"\n if not segments:\n return segments\n\n merged = [segments[0]]\n for seg in segments[1:]:\n prev = merged[-1]\n if (seg.speaker == prev.speaker\n and seg.start - prev.end <= max_gap\n and prev.text and seg.text):\n merged[-1] = DiarizedSegment(\n start=prev.start,\n end=seg.end,\n text=f\"{prev.text} {seg.text}\",\n speaker=prev.speaker,\n )\n else:\n merged.append(seg)\n\n return merged\n\n\ndef _get_hf_token() -> str | None:\n \"\"\"Get HuggingFace token from environment.\"\"\"\n import os\n return (\n os.environ.get(\"HF_TOKEN\")\n or os.environ.get(\"HUGGING_FACE_HUB_TOKEN\")\n or None\n )\n"}}, "core/downloader.py": {"417d9ef7999ddc10cabfa489c1c6d22d53ebf5c9": {"t": 1785599224.4146516, "n": 6467, "h": "cb4d06c6d20d9c50", "c": "\"\"\"Download engine: yt-dlp wrapper with proxy, cookie, and batch support.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nimport subprocess\nimport tempfile\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Generator\n\nfrom .config import AppConfig\n\n\n@dataclass\nclass VideoMeta:\n title: str = \"\"\n description: str = \"\"\n duration: float = 0.0\n uploader: str = \"\"\n upload_date: str = \"\"\n thumbnail: str = \"\"\n webpage_url: str = \"\"\n chapters: list[dict] = field(default_factory=list)\n subtitles: dict = field(default_factory=dict)\n entries: list[dict] = field(default_factory=list)\n extra: dict = field(default_factory=dict)\n\n @property\n def has_subtitles(self) -> bool:\n return bool(self.subtitles)\n\n @property\n def is_playlist(self) -> bool:\n return bool(self.entries)\n\n @property\n def entry_count(self) -> int:\n return len(self.entries) if self.entries else 1\n\n\nBILIBILI_COOKIES_FILE = Path(\"/app/bilibili_cookies.txt\")\n\n\ndef _base_cmd(config: AppConfig) -> list[str]:\n import os\n from urllib.parse import unquote\n\n cmd = [\"yt-dlp\", \"--no-warnings\"]\n\n # 代理:配置优先,其次读环境变量\n proxy = config.proxy.for_ytdlp or os.environ.get(\"NOTEKING_PROXY\") or os.environ.get(\"HTTP_PROXY\")\n if proxy:\n cmd += [\"--proxy\", proxy]\n\n # B站 cookies:文件优先,其次用 SESSDATA 生成临时文件\n if BILIBILI_COOKIES_FILE.exists():\n cmd += [\"--cookies\", str(BILIBILI_COOKIES_FILE)]\n else:\n sessdata = config.bilibili_sessdata or os.environ.get(\"BILIBILI_SESSDATA\", \"\")\n if sessdata:\n decoded = unquote(sessdata)\n tmp = Path(tempfile.mktemp(suffix=\"_bili_cookies.txt\"))\n tmp.write_text(\n f\"# Netscape HTTP Cookie File\\n\"\n f\".bilibili.com\\tTRUE\\t/\\tTRUE\\t9999999999\\tSESSDATA\\t{decoded}\\n\"\n )\n cmd += [\"--cookies\", str(tmp)]\n\n return cmd\n\n\ndef get_video_info(url: str, config: AppConfig) -> VideoMeta:\n \"\"\"Fetch metadata without downloading.\"\"\"\n cmd = _base_cmd(config) + [\n \"--dump-json\",\n \"--flat-playlist\",\n \"--no-download\",\n url,\n ]\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)\n if result.returncode != 0:\n raise RuntimeError(f\"yt-dlp info failed: {result.stderr[:500]}\")\n\n lines = [l for l in result.stdout.strip().split(\"\\n\") if l.strip()]\n if not lines:\n raise RuntimeError(\"yt-dlp returned no data\")\n\n entries = []\n first = json.loads(lines[0])\n\n if len(lines) > 1:\n for line in lines:\n try:\n entries.append(json.loads(line))\n except json.JSONDecodeError:\n continue\n\n return VideoMeta(\n title=first.get(\"title\", \"\"),\n description=first.get(\"description\", \"\"),\n duration=first.get(\"duration\", 0) or 0,\n uploader=first.get(\"uploader\", \"\"),\n upload_date=first.get(\"upload_date\", \"\"),\n thumbnail=first.get(\"thumbnail\", \"\"),\n webpage_url=first.get(\"webpage_url\", url),\n chapters=first.get(\"chapters\") or [],\n subtitles=first.get(\"subtitles\") or {},\n entries=entries if len(entries) > 1 else [],\n extra={\"id\": first.get(\"id\", \"\")},\n )\n\n\ndef download_subtitles(\n url: str,\n output_dir: Path,\n config: AppConfig,\n langs: str = \"zh-Hans,zh-CN,zh,ai-zh,en\",\n) -> list[Path]:\n \"\"\"Download subtitles using yt-dlp.\"\"\"\n output_dir.mkdir(parents=True, exist_ok=True)\n cmd = _base_cmd(config) + [\n \"--write-subs\",\n \"--write-auto-subs\",\n \"--sub-langs\", langs,\n \"--convert-subs\", \"srt\",\n \"--skip-download\",\n \"-o\", str(output_dir / \"%(title)s.%(ext)s\"),\n url,\n ]\n subprocess.run(cmd, capture_output=True, text=True, timeout=120)\n return list(output_dir.glob(\"*.srt\"))\n\n\ndef download_audio(\n url: str,\n output_dir: Path,\n config: AppConfig,\n) -> Path:\n \"\"\"Extract audio from video for ASR.\"\"\"\n output_dir.mkdir(parents=True, exist_ok=True)\n output_path = output_dir / \"audio.wav\"\n cmd = _base_cmd(config) + [\n \"-x\",\n \"--audio-format\", \"wav\",\n \"--audio-quality\", \"0\",\n \"-o\", str(output_path),\n url,\n ]\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)\n if result.returncode != 0:\n raise RuntimeError(f\"Audio download failed: {result.stderr[:500]}\")\n\n wavs = list(output_dir.glob(\"*.wav\"))\n if wavs:\n return wavs[0]\n raise FileNotFoundError(\"No WAV file produced\")\n\n\ndef download_video(\n url: str,\n output_dir: Path,\n config: AppConfig,\n quality: str = \"best\",\n) -> Path:\n \"\"\"Download video file.\"\"\"\n output_dir.mkdir(parents=True, exist_ok=True)\n cmd = _base_cmd(config) + [\n \"-f\", quality,\n \"-o\", str(output_dir / \"%(title)s.%(ext)s\"),\n url,\n ]\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=1800)\n if result.returncode != 0:\n raise RuntimeError(f\"Video download failed: {result.stderr[:500]}\")\n\n for ext in (\"mp4\", \"mkv\", \"webm\", \"flv\"):\n vids = list(output_dir.glob(f\"*.{ext}\"))\n if vids:\n return vids[0]\n raise FileNotFoundError(\"No video file produced\")\n\n\ndef download_thumbnail(\n url: str,\n output_dir: Path,\n config: AppConfig,\n) -> Path | None:\n \"\"\"Download video thumbnail/cover image.\"\"\"\n output_dir.mkdir(parents=True, exist_ok=True)\n cmd = _base_cmd(config) + [\n \"--write-thumbnail\",\n \"--skip-download\",\n \"--convert-thumbnails\", \"jpg\",\n \"-o\", str(output_dir / \"thumbnail\"),\n url,\n ]\n subprocess.run(cmd, capture_output=True, text=True, timeout=60)\n for ext in (\"jpg\", \"png\", \"webp\"):\n thumbs = list(output_dir.glob(f\"thumbnail*.{ext}\"))\n if thumbs:\n return thumbs[0]\n return None\n\n\ndef list_playlist_entries(url: str, config: AppConfig) -> list[dict]:\n \"\"\"List all entries in a playlist/collection.\"\"\"\n cmd = _base_cmd(config) + [\n \"--flat-playlist\",\n \"--dump-json\",\n url,\n ]\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)\n entries = []\n for line in result.stdout.strip().split(\"\\n\"):\n if line.strip():\n try:\n entries.append(json.loads(line))\n except json.JSONDecodeError:\n continue\n return entries\n"}}, "core/formatter.py": {"6e7ea691765469bd43bca60b84192361c8558928": {"t": 1785599224.4178097, "n": 5480, "h": "fcc1d40b534a18d1", "c": "\"\"\"Multi-format output engine: generate Markdown, SRT, VTT, mind map, flashcards, JSON.\n\nTakes a processing result and renders it in multiple output formats simultaneously.\n\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass, field, asdict\nfrom pathlib import Path\nfrom typing import Any\n\nfrom .diarizer import DiarizationResult, DiarizedSegment\n\n\n@dataclass\nclass OutputBundle:\n \"\"\"All generated output files from a single processing run.\"\"\"\n markdown_path: Path | None = None\n pdf_path: Path | None = None\n srt_path: Path | None = None\n vtt_path: Path | None = None\n mindmap_path: Path | None = None\n flashcard_path: Path | None = None\n transcript_path: Path | None = None\n action_items_path: Path | None = None\n json_path: Path | None = None\n all_files: dict[str, Path] = field(default_factory=dict)\n\n\ndef save_markdown(content: str, output_path: Path) -> Path:\n \"\"\"Save generated notes as Markdown.\"\"\"\n output_path.parent.mkdir(parents=True, exist_ok=True)\n output_path.write_text(content, encoding=\"utf-8\")\n return output_path\n\n\ndef save_srt(\n diarization: DiarizationResult,\n output_path: Path,\n include_speaker: bool = True,\n) -> Path:\n \"\"\"Save transcript as SRT subtitle file with optional speaker labels.\"\"\"\n output_path.parent.mkdir(parents=True, exist_ok=True)\n lines = []\n for i, seg in enumerate(diarization.segments, 1):\n lines.append(str(i))\n lines.append(f\"{_srt_ts(seg.start)} --> {_srt_ts(seg.end)}\")\n prefix = f\"[{seg.speaker}] \" if include_speaker and seg.speaker else \"\"\n lines.append(f\"{prefix}{seg.text}\")\n lines.append(\"\")\n output_path.write_text(\"\\n\".join(lines), encoding=\"utf-8\")\n return output_path\n\n\ndef save_vtt(\n diarization: DiarizationResult,\n output_path: Path,\n include_speaker: bool = True,\n) -> Path:\n \"\"\"Save transcript as WebVTT subtitle file.\"\"\"\n output_path.parent.mkdir(parents=True, exist_ok=True)\n lines = [\"WEBVTT\", \"\"]\n for i, seg in enumerate(diarization.segments, 1):\n lines.append(str(i))\n lines.append(f\"{_vtt_ts(seg.start)} --> {_vtt_ts(seg.end)}\")\n prefix = f\"<v {seg.speaker}>\" if include_speaker and seg.speaker else \"\"\n lines.append(f\"{prefix}{seg.text}\")\n lines.append(\"\")\n output_path.write_text(\"\\n\".join(lines), encoding=\"utf-8\")\n return output_path\n\n\ndef save_transcript(\n diarization: DiarizationResult,\n output_path: Path,\n) -> Path:\n \"\"\"Save full transcript with speaker labels and timestamps.\"\"\"\n output_path.parent.mkdir(parents=True, exist_ok=True)\n output_path.write_text(\n diarization.transcript_with_speakers, encoding=\"utf-8\"\n )\n return output_path\n\n\ndef save_json(\n data: dict[str, Any],\n output_path: Path,\n) -> Path:\n \"\"\"Save structured result as JSON.\"\"\"\n output_path.parent.mkdir(parents=True, exist_ok=True)\n output_path.write_text(\n json.dumps(data, ensure_ascii=False, indent=2),\n encoding=\"utf-8\",\n )\n return output_path\n\n\ndef generate_outputs(\n notes_content: str,\n diarization: DiarizationResult | None,\n output_dir: Path,\n base_name: str,\n formats: list[str],\n full_result: dict[str, Any] | None = None,\n) -> OutputBundle:\n \"\"\"Generate all requested output formats.\n\n Args:\n notes_content: Generated notes in Markdown\n diarization: Speaker-labeled transcript\n output_dir: Directory for output files\n base_name: Base filename (without extension)\n formats: List of formats: markdown, pdf, srt, vtt, transcript, json, mindmap, flashcard\n full_result: Complete result dict for JSON export\n \"\"\"\n output_dir.mkdir(parents=True, exist_ok=True)\n safe_name = _safe_filename(base_name)\n bundle = OutputBundle()\n\n if \"markdown\" in formats:\n path = output_dir / f\"{safe_name}.md\"\n save_markdown(notes_content, path)\n bundle.markdown_path = path\n bundle.all_files[\"markdown\"] = path\n\n if \"srt\" in formats and diarization:\n path = output_dir / f\"{safe_name}.srt\"\n save_srt(diarization, path)\n bundle.srt_path = path\n bundle.all_files[\"srt\"] = path\n\n if \"vtt\" in formats and diarization:\n path = output_dir / f\"{safe_name}.vtt\"\n save_vtt(diarization, path)\n bundle.vtt_path = path\n bundle.all_files[\"vtt\"] = path\n\n if \"transcript\" in formats and diarization:\n path = output_dir / f\"{safe_name}_transcript.txt\"\n save_transcript(diarization, path)\n bundle.transcript_path = path\n bundle.all_files[\"transcript\"] = path\n\n if \"json\" in formats and full_result:\n path = output_dir / f\"{safe_name}.json\"\n save_json(full_result, path)\n bundle.json_path = path\n bundle.all_files[\"json\"] = path\n\n return bundle\n\n\ndef _safe_filename(text: str, max_len: int = 80) -> str:\n return \"\".join(\n c if c.isalnum() or c in \" _-\" else \"_\" for c in text\n )[:max_len].strip()\n\n\ndef _srt_ts(seconds: float) -> str:\n h = int(seconds // 3600)\n m = int((seconds % 3600) // 60)\n s = int(seconds % 60)\n ms = int((seconds - int(seconds)) * 1000)\n return f\"{h:02d}:{m:02d}:{s:02d},{ms:03d}\"\n\n\ndef _vtt_ts(seconds: float) -> str:\n h = int(seconds // 3600)\n m = int((seconds % 3600) // 60)\n s = int(seconds % 60)\n ms = int((seconds - int(seconds)) * 1000)\n return f\"{h:02d}:{m:02d}:{s:02d}.{ms:03d}\"\n"}}, "web/tsconfig.json": {"fc2fbec484f343c2f7c3b108a4b1a7de5562c6f0": {"t": 1785599224.8488533, "n": 503, "h": "719cda3948fb97db", "c": "{\n \"compilerOptions\": {\n \"target\": \"es5\",\n \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n \"allowJs\": true,\n \"skipLibCheck\": true,\n \"strict\": true,\n \"noEmit\": true,\n \"esModuleInterop\": true,\n \"module\": \"esnext\",\n \"moduleResolution\": \"bundler\",\n \"resolveJsonModule\": true,\n \"isolatedModules\": true,\n \"jsx\": \"preserve\",\n \"incremental\": true,\n \"paths\": { \"@/*\": [\"./src/*\"] }\n },\n \"include\": [\"next-env.d.ts\", \"**/*.ts\", \"**/*.tsx\"],\n \"exclude\": [\"node_modules\"]\n}\n"}}, "core/__init__.py": {"4e63d61ae402ccdd9caed71d400d8ac7664eae7e": {"t": 1785599225.3105562, "n": 12891, "h": "6bc2cddddebc7499", "c": "\"\"\"NoteKing Core Engine: the ultimate video/blog/recording to notes tool.\n\nSupports:\n- Online video URLs (30+ platforms via yt-dlp)\n- Local video/audio files (MP4, MP3, WAV, etc.)\n- Meeting recordings with speaker diarization\n- Multi-file batch processing with auto-merge\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nimport tempfile\nfrom pathlib import Path\nfrom typing import Any, Callable\n\nfrom .config import AppConfig\nfrom .parser import parse_link, ParsedLink, Platform, LinkType, is_batch\nfrom .downloader import get_video_info, VideoMeta\nfrom .subtitle import extract_subtitles, SubtitleResult\nfrom .llm import chat\nfrom .cache import Cache\nfrom .templates import get_template, TEMPLATES, TEMPLATE_LIST, TemplateContext\nfrom .batch import get_batch_entries, process_batch, merge_batch_notes, BatchProgress\n\nlogger = logging.getLogger(__name__)\n\n__version__ = \"2.0.0\"\n\n\ndef summarize(\n url: str,\n template: str = \"detailed\",\n config: AppConfig | None = None,\n custom_prompt: str = \"\",\n use_cache: bool = True,\n output_dir: str | Path | None = None,\n) -> dict[str, Any]:\n \"\"\"Main entry point: convert a video/blog URL into notes.\n\n Args:\n url: Video URL or local file path\n template: Output template name (see TEMPLATE_LIST)\n config: Configuration (loads default if None)\n custom_prompt: Custom prompt for the 'custom' template\n use_cache: Whether to use cached results\n output_dir: Directory to save output files\n\n Returns:\n dict with keys: title, content, template, source, meta\n \"\"\"\n if config is None:\n config = AppConfig.load()\n\n cache = Cache(config)\n if use_cache:\n cached = cache.get(url, template)\n if cached:\n return cached\n\n out_dir = Path(output_dir) if output_dir else Path(config.output_dir)\n out_dir.mkdir(parents=True, exist_ok=True)\n\n parsed = parse_link(url)\n\n if is_batch(parsed):\n return _process_batch(parsed, template, config, custom_prompt, out_dir)\n\n return _process_single(\n parsed, template, config, custom_prompt, out_dir, cache, use_cache\n )\n\n\ndef _process_single(\n parsed: ParsedLink,\n template_name: str,\n config: AppConfig,\n custom_prompt: str,\n out_dir: Path,\n cache: Cache,\n use_cache: bool,\n) -> dict[str, Any]:\n \"\"\"Process a single video.\"\"\"\n import tempfile\n\n work_dir = Path(tempfile.mkdtemp(prefix=\"noteking_\"))\n\n # Step 1: Get video metadata\n if parsed.platform == Platform.LOCAL:\n meta = VideoMeta(title=Path(parsed.url).stem, webpage_url=parsed.url)\n else:\n meta = get_video_info(parsed.url, config)\n\n # Step 2: Extract subtitles (three-level fallback)\n subtitles = extract_subtitles(parsed, work_dir, config)\n\n if not subtitles.segments and subtitles.source == \"visual\":\n subtitles = SubtitleResult(\n segments=[],\n source=\"visual\",\n raw_text=f\"[视频无可用字幕,标题: {meta.title},描述: {meta.description[:500]}]\",\n )\n\n # Step 3: Generate notes using template\n tmpl = get_template(template_name, user_prompt=custom_prompt)\n ctx = TemplateContext(\n meta=meta,\n subtitles=subtitles,\n config=config,\n extra={\"custom_prompt\": custom_prompt},\n )\n content = tmpl.generate(ctx)\n\n # Step 4: Save output\n ext = tmpl.file_extension\n safe_title = \"\".join(c if c.isalnum() or c in \" _-\" else \"_\" for c in meta.title)[:80]\n output_file = out_dir / f\"{safe_title}_{template_name}{ext}\"\n output_file.write_text(content, encoding=\"utf-8\")\n\n # Save subtitle files\n if subtitles.segments:\n subtitles.save_srt(out_dir / f\"{safe_title}.srt\")\n subtitles.save_txt(out_dir / f\"{safe_title}_transcript.txt\")\n\n result = {\n \"title\": meta.title,\n \"content\": content,\n \"template\": template_name,\n \"source\": subtitles.source,\n \"output_file\": str(output_file),\n \"url\": parsed.url,\n \"platform\": parsed.platform.value,\n \"duration\": meta.duration,\n \"uploader\": meta.uploader,\n }\n\n if use_cache:\n cache.set(parsed.url, template_name, result)\n\n return result\n\n\ndef _process_batch(\n parsed: ParsedLink,\n template_name: str,\n config: AppConfig,\n custom_prompt: str,\n out_dir: Path,\n) -> dict[str, Any]:\n \"\"\"Process a batch (playlist/collection).\"\"\"\n entries = get_batch_entries(parsed, config)\n cache = Cache(config)\n\n all_results: list[dict] = []\n\n def process_one(entry_url: str, idx: int) -> dict:\n entry_parsed = parse_link(entry_url)\n r = _process_single(\n entry_parsed, template_name, config, custom_prompt,\n out_dir, cache, True,\n )\n all_results.append(r)\n return r\n\n progress = process_batch(entries, process_one)\n\n merged = merge_batch_notes(\n [{\"title\": r.get(\"title\", \"\"), \"content\": r.get(\"content\", \"\")}\n for r in all_results],\n title=f\"{parsed.video_id} 合集笔记\",\n )\n\n merged_file = out_dir / f\"batch_merged_{template_name}.md\"\n merged_file.write_text(merged, encoding=\"utf-8\")\n\n return {\n \"title\": f\"合集 ({len(all_results)} 个视频)\",\n \"content\": merged,\n \"template\": template_name,\n \"source\": \"batch\",\n \"output_file\": str(merged_file),\n \"url\": parsed.url,\n \"platform\": parsed.platform.value,\n \"total\": progress.total,\n \"completed\": progress.completed,\n \"failed\": progress.failed,\n \"individual_results\": all_results,\n }\n\n\ndef get_transcript(\n url: str,\n config: AppConfig | None = None,\n) -> str:\n \"\"\"Get just the transcript text for a video.\"\"\"\n if config is None:\n config = AppConfig.load()\n\n parsed = parse_link(url)\n work_dir = Path(tempfile.mkdtemp(prefix=\"noteking_\"))\n subtitles = extract_subtitles(parsed, work_dir, config)\n return subtitles.full_text\n\n\n# ────────────────────────────────────────────────────────────────\n# Recording Processing Pipeline (NEW in v2.0)\n# ────────────────────────────────────────────────────────────────\n\ndef process_recording(\n input_files: list[str | Path],\n template: str = \"meeting_minutes\",\n context: str | None = None,\n scene: str | None = None,\n num_speakers: int | None = None,\n denoise_level: int = 1,\n output_formats: list[str] | None = None,\n config: AppConfig | None = None,\n output_dir: str | Path | None = None,\n progress_callback: Callable[[str, float], None] | None = None,\n) -> dict[str, Any]:\n \"\"\"Process local recording/video files into structured notes.\n\n This is the main entry point for the recording processing pipeline:\n 1. Preprocess: validate, merge, extract audio, denoise\n 2. Transcribe: ASR with language auto-detection\n 3. Diarize: speaker identification and alignment\n 4. Generate: LLM-powered note generation with scene template\n 5. Output: multi-format export (Markdown, PDF, SRT, etc.)\n\n Args:\n input_files: List of local file paths (video or audio)\n template: Output template name (meeting_minutes, lecture_notes, etc.)\n context: User description (\"This is a meeting about AI open source projects\")\n scene: Scene type (meeting/lecture/interview/brainstorm/news/exam/entertainment)\n num_speakers: Expected number of speakers (None for auto-detect)\n denoise_level: 0=none, 1=light, 2=medium, 3=heavy\n output_formats: List of formats (markdown, pdf, srt, vtt, transcript, json)\n config: App configuration\n output_dir: Directory for output files\n progress_callback: Optional progress callback(step_name, progress_pct)\n\n Returns:\n dict with keys: title, content, template, transcript, speakers,\n duration, output_files, diarization\n \"\"\"\n from .preprocessor import preprocess, detect_media_type\n from .transcriber import transcribe, detect_language\n from .diarizer import diarize, DiarizationResult\n from .formatter import generate_outputs\n\n if config is None:\n config = AppConfig.load()\n\n if output_formats is None:\n output_formats = [\"markdown\"]\n\n out_dir = Path(output_dir) if output_dir else Path(config.output_dir)\n out_dir.mkdir(parents=True, exist_ok=True)\n work_dir = Path(tempfile.mkdtemp(prefix=\"noteking_rec_\"))\n\n def _progress(step: str, pct: float):\n logger.info(f\"[{step}] {pct*100:.0f}%\")\n if progress_callback:\n progress_callback(step, pct)\n\n # Step 1: Preprocess\n _progress(\"preprocess\", 0.0)\n pre_result = preprocess(\n input_files=input_files,\n work_dir=work_dir,\n denoise_level=denoise_level,\n progress_callback=lambda s, p: _progress(f\"preprocess/{s}\", p * 0.2),\n )\n _progress(\"preprocess\", 0.2)\n\n # Step 2: Language detection + ASR transcription\n _progress(\"transcribe\", 0.2)\n language = detect_language(pre_result.audio_path, config)\n transcript = transcribe(pre_result.audio_path, config, language=language)\n _progress(\"transcribe\", 0.5)\n\n # Step 3: Speaker diarization\n _progress(\"diarize\", 0.5)\n if config.recording.enable_diarization and len(transcript.segments) > 0:\n diar_result = diarize(\n audio_path=pre_result.audio_path,\n transcript=transcript,\n num_speakers=num_speakers or config.recording.num_speakers,\n )\n else:\n from .diarizer import DiarizationResult, DiarizedSegment\n diar_result = DiarizationResult(\n segments=[\n DiarizedSegment(s.start, s.end, s.text, \"Speaker 1\")\n for s in transcript.segments\n ],\n num_speakers=1,\n speakers=[\"Speaker 1\"],\n language=language,\n )\n _progress(\"diarize\", 0.6)\n\n # Step 4: Generate notes using template + LLM\n _progress(\"generate\", 0.6)\n\n title = _derive_title(input_files, context)\n transcript_for_llm = _build_transcript_for_llm(diar_result, context, scene)\n\n meta = VideoMeta(\n title=title,\n description=context or \"\",\n duration=pre_result.duration,\n webpage_url=str(input_files[0]),\n )\n\n tmpl = get_template(template)\n sub_result = SubtitleResult(\n segments=transcript.segments,\n source=\"asr\",\n language=language,\n raw_text=transcript_for_llm,\n )\n ctx = TemplateContext(\n meta=meta,\n subtitles=sub_result,\n config=config,\n extra={\n \"context\": context or \"\",\n \"scene\": scene or template,\n \"num_speakers\": diar_result.num_speakers,\n \"speakers\": diar_result.speakers,\n \"diarized_transcript\": diar_result.transcript_with_speakers,\n },\n )\n content = tmpl.generate(ctx)\n _progress(\"generate\", 0.85)\n\n # Step 5: Multi-format output\n _progress(\"output\", 0.85)\n base_name = _safe_title(title)\n bundle = generate_outputs(\n notes_content=content,\n diarization=diar_result,\n output_dir=out_dir,\n base_name=f\"{base_name}_{template}\",\n formats=output_formats,\n full_result={\n \"title\": title,\n \"template\": template,\n \"language\": language,\n \"duration\": pre_result.duration,\n \"num_speakers\": diar_result.num_speakers,\n \"speakers\": diar_result.speakers,\n \"content\": content,\n },\n )\n _progress(\"done\", 1.0)\n\n return {\n \"title\": title,\n \"content\": content,\n \"template\": template,\n \"source\": \"recording\",\n \"language\": language,\n \"duration\": pre_result.duration,\n \"num_speakers\": diar_result.num_speakers,\n \"speakers\": diar_result.speakers,\n \"output_files\": {k: str(v) for k, v in bundle.all_files.items()},\n \"transcript\": diar_result.full_text,\n \"diarized_transcript\": diar_result.transcript_with_speakers,\n }\n\n\ndef _derive_title(input_files: list[str | Path], context: str | None) -> str:\n \"\"\"Derive a title from filenames or user context.\"\"\"\n if context:\n return context[:80]\n names = [Path(f).stem for f in input_files]\n if len(names) == 1:\n return names[0]\n return f\"{names[0]} 等{len(names)}个文件\"\n\n\ndef _build_transcript_for_llm(\n diar: Any,\n context: str | None,\n scene: str | None,\n) -> str:\n \"\"\"Build a rich transcript string for LLM consumption.\"\"\"\n parts = []\n if context:\n parts.append(f\"[内容描述]: {context}\")\n if scene:\n parts.append(f\"[场景类型]: {scene}\")\n if diar.num_speakers > 1:\n parts.append(f\"[说话人数量]: {diar.num_speakers}\")\n parts.append(\"\")\n parts.append(diar.transcript_with_speakers)\n return \"\\n\".join(parts)\n\n\ndef _safe_title(text: str) -> str:\n return \"\".join(\n c if c.isalnum() or c in \" _-\" else \"_\" for c in text\n )[:80].strip()\n"}}, "core/frames.py": {"1b8ea6be1b03a012acc6a099e37824e511837644": {"t": 1785599225.4243178, "n": 7808, "h": "79ba0c738b22c3ef", "c": "\"\"\"Key frame extraction with smart scoring, hash dedup, and subtitle alignment.\n\nExtraction pipeline:\n 1. SceneDetect (content-aware scene boundaries) -> candidates\n 2. Uniform sampling fallback -> fill gaps\n 3. Info-density scoring (edge density + contrast + entropy)\n 4. Perceptual hash dedup (imagehash phash)\n 5. Progressive PPT detection (prefer final fully-revealed state)\n 6. Subtitle-frame alignment (time-based matching)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport subprocess\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom typing import Any\n\nfrom .subtitle import SubtitleResult\n\n\n@dataclass\nclass ExtractedFrame:\n path: Path\n timestamp: float\n scene_score: float = 0.0\n info_score: float = 0.0\n phash: str = \"\"\n description: str = \"\"\n\n @property\n def timestamp_str(self) -> str:\n h = int(self.timestamp // 3600)\n m = int((self.timestamp % 3600) // 60)\n s = int(self.timestamp % 60)\n return f\"{h:02d}:{m:02d}:{s:02d}\"\n\n @property\n def total_score(self) -> float:\n return self.scene_score * 0.4 + self.info_score * 0.6\n\n\ndef extract_keyframes(\n video_path: Path,\n output_dir: Path,\n max_frames: int = 20,\n interval_seconds: float = 0,\n threshold: float = 27.0,\n dedup: bool = True,\n score: bool = True,\n) -> list[ExtractedFrame]:\n \"\"\"Extract key frames with smart scoring and dedup.\n\n Pipeline: SceneDetect | uniform -> score -> dedup -> top-N\n \"\"\"\n output_dir.mkdir(parents=True, exist_ok=True)\n duration = _get_duration(video_path)\n\n # Stage 1: candidates via SceneDetect or uniform\n try:\n frames = _extract_with_scenedetect(video_path, output_dir, max_frames * 2, threshold)\n except (ImportError, Exception):\n frames = []\n\n # Fill gaps with uniform sampling\n if duration > 0:\n if interval_seconds <= 0:\n interval_seconds = max(duration / (max_frames * 1.5), 10)\n _fill_uniform(video_path, output_dir, frames, duration, interval_seconds)\n\n # Stage 2: info-density scoring\n if score:\n _score_info_density(frames)\n\n # Stage 3: perceptual hash dedup\n if dedup:\n frames = _dedup_by_hash(frames)\n\n # Stage 4: select top N by total score, then sort by time\n frames.sort(key=lambda f: f.total_score, reverse=True)\n frames = frames[:max_frames]\n frames.sort(key=lambda f: f.timestamp)\n\n return frames\n\n\ndef _extract_with_scenedetect(\n video_path: Path,\n output_dir: Path,\n max_frames: int,\n threshold: float,\n) -> list[ExtractedFrame]:\n from scenedetect import open_video, SceneManager\n from scenedetect.detectors import ContentDetector\n\n video = open_video(str(video_path))\n scene_manager = SceneManager()\n scene_manager.add_detector(ContentDetector(threshold=threshold))\n scene_manager.detect_scenes(video, show_progress=False)\n scene_list = scene_manager.get_scene_list()\n\n frames: list[ExtractedFrame] = []\n for i, (start, end) in enumerate(scene_list[:max_frames]):\n # Progressive PPT: prefer end-of-scene frame (fully revealed state)\n t = end.get_seconds() - 0.3\n if t < start.get_seconds():\n t = (start.get_seconds() + end.get_seconds()) / 2\n frame_path = output_dir / f\"scene_{i:04d}_{t:.1f}s.jpg\"\n\n _extract_frame_at(video_path, t, frame_path)\n\n if frame_path.exists() and frame_path.stat().st_size > 3000:\n frames.append(ExtractedFrame(\n path=frame_path,\n timestamp=t,\n scene_score=min(1.0, threshold / 30.0),\n ))\n\n return frames\n\n\ndef _fill_uniform(\n video_path: Path,\n output_dir: Path,\n existing: list[ExtractedFrame],\n duration: float,\n interval: float,\n) -> None:\n \"\"\"Add uniformly sampled frames to fill gaps.\"\"\"\n t = 3.0\n idx = len(existing)\n while t < duration - 3:\n if any(abs(f.timestamp - t) < 5.0 for f in existing):\n t += interval\n continue\n out = output_dir / f\"uniform_{idx:04d}_{t:.1f}s.jpg\"\n _extract_frame_at(video_path, t, out)\n if out.exists() and out.stat().st_size > 3000:\n existing.append(ExtractedFrame(path=out, timestamp=t, scene_score=0.3))\n idx += 1\n t += interval\n\n\ndef _score_info_density(frames: list[ExtractedFrame]) -> None:\n \"\"\"Score frames by visual information density (edges, contrast, entropy).\"\"\"\n try:\n from PIL import Image, ImageFilter\n import numpy as np\n except ImportError:\n for f in frames:\n f.info_score = 0.5\n return\n\n for frame in frames:\n try:\n img = Image.open(frame.path)\n gray = img.convert(\"L\")\n arr = np.array(gray, dtype=np.float32)\n\n # Edge density (sharpness and detail)\n edges = gray.filter(ImageFilter.FIND_EDGES)\n edge_score = min(1.0, np.std(np.array(edges, dtype=np.float32)) / 60.0)\n\n # Contrast\n contrast = min(1.0, np.std(arr) / 80.0)\n\n # Entropy (information content)\n hist, _ = np.histogram(arr.ravel(), bins=64, range=(0, 256))\n p = hist / hist.sum()\n p = p[p > 0]\n entropy_score = min(1.0, (-np.sum(p * np.log2(p))) / 6.0)\n\n # Color uniqueness\n unique_ratio = len(np.unique(arr.astype(np.uint8))) / 256.0\n\n frame.info_score = (\n edge_score * 0.35 + contrast * 0.25 +\n entropy_score * 0.25 + unique_ratio * 0.15\n )\n except Exception:\n frame.info_score = 0.3\n\n\ndef _dedup_by_hash(frames: list[ExtractedFrame], threshold: int = 8) -> list[ExtractedFrame]:\n \"\"\"Remove near-duplicate frames using perceptual hashing.\"\"\"\n try:\n import imagehash\n from PIL import Image\n except ImportError:\n return frames\n\n frames.sort(key=lambda f: f.timestamp)\n unique: list[ExtractedFrame] = []\n seen: list[Any] = []\n\n for frame in frames:\n try:\n h = imagehash.phash(Image.open(frame.path), hash_size=12)\n frame.phash = str(h)\n if not any(h - prev < threshold for prev in seen):\n seen.append(h)\n unique.append(frame)\n except Exception:\n unique.append(frame)\n\n return unique\n\n\ndef _extract_frame_at(video_path: Path, timestamp: float, output: Path) -> None:\n cmd = [\n \"ffmpeg\",\n \"-ss\", str(timestamp),\n \"-i\", str(video_path),\n \"-frames:v\", \"1\",\n \"-q:v\", \"2\",\n \"-vf\", \"scale=960:-1\",\n str(output),\n \"-y\", \"-loglevel\", \"error\",\n ]\n subprocess.run(cmd, capture_output=True, timeout=30)\n\n\ndef _get_duration(video_path: Path) -> float:\n cmd = [\n \"ffprobe\",\n \"-v\", \"error\",\n \"-show_entries\", \"format=duration\",\n \"-of\", \"default=noprint_wrappers=1:nokey=1\",\n str(video_path),\n ]\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)\n try:\n return float(result.stdout.strip())\n except (ValueError, AttributeError):\n return 0.0\n\n\ndef align_frames_to_subtitles(\n frames: list[ExtractedFrame],\n subtitles: SubtitleResult,\n tolerance: float = 10.0,\n) -> list[tuple[ExtractedFrame, str]]:\n \"\"\"Align extracted frames to the nearest subtitle text.\"\"\"\n aligned = []\n for frame in frames:\n best_text = \"\"\n best_dist = float(\"inf\")\n for seg in subtitles.segments:\n mid = (seg.start + seg.end) / 2\n dist = abs(frame.timestamp - mid)\n if dist < best_dist and dist <= tolerance + (seg.end - seg.start):\n best_dist = dist\n best_text = seg.text\n aligned.append((frame, best_text))\n return aligned\n"}}, "core/llm.py": {"2f082415823b371817b6cd12c6935680c61a97c8": {"t": 1785599225.6093156, "n": 2049, "h": "29e16c21f1d1ee76", "c": "\"\"\"LLM engine: unified interface supporting OpenAI-compatible providers.\"\"\"\n\nfrom __future__ import annotations\n\nfrom .config import AppConfig\n\n\ndef _build_client(config: AppConfig):\n from openai import OpenAI\n import httpx\n\n base_url = config.llm.base_url or None\n api_key = config.llm.api_key\n\n if not api_key:\n raise ValueError(\n \"No LLM API key configured. Set it via config file, \"\n \"environment variable NOTEKING_LLM_API_KEY, or --api-key flag.\"\n )\n\n return OpenAI(\n api_key=api_key,\n base_url=base_url,\n timeout=httpx.Timeout(300.0, connect=60.0),\n max_retries=3,\n )\n\n\ndef chat(\n prompt: str,\n config: AppConfig,\n system: str = \"\",\n temperature: float | None = None,\n max_tokens: int | None = None,\n) -> str:\n \"\"\"Send a chat completion request and return the text response.\"\"\"\n client = _build_client(config)\n\n messages = []\n if system:\n messages.append({\"role\": \"system\", \"content\": system})\n messages.append({\"role\": \"user\", \"content\": prompt})\n\n resp = client.chat.completions.create(\n model=config.llm.model,\n messages=messages,\n temperature=temperature if temperature is not None else config.llm.temperature,\n max_tokens=max_tokens or config.llm.max_tokens,\n )\n return resp.choices[0].message.content or \"\"\n\n\ndef chat_stream(\n prompt: str,\n config: AppConfig,\n system: str = \"\",\n):\n \"\"\"Stream a chat completion, yielding text chunks.\"\"\"\n client = _build_client(config)\n\n messages = []\n if system:\n messages.append({\"role\": \"system\", \"content\": system})\n messages.append({\"role\": \"user\", \"content\": prompt})\n\n stream = client.chat.completions.create(\n model=config.llm.model,\n messages=messages,\n temperature=config.llm.temperature,\n max_tokens=config.llm.max_tokens,\n stream=True,\n )\n for chunk in stream:\n delta = chunk.choices[0].delta\n if delta.content:\n yield delta.content\n"}}, "core/preprocessor.py": {"8b37e372380db6c30becb15cd63097bb9443fe37": {"t": 1785599225.842288, "n": 11584, "h": "e8f8a0baf6e35c05", "c": "\"\"\"Preprocessor: multi-file merge, audio extraction, noise reduction, VAD chunking.\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport subprocess\nimport tempfile\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Callable\n\nSUPPORTED_VIDEO_EXTS = {\".mp4\", \".mkv\", \".avi\", \".mov\", \".webm\", \".flv\", \".wmv\", \".ts\", \".m4v\"}\nSUPPORTED_AUDIO_EXTS = {\".mp3\", \".wav\", \".flac\", \".m4a\", \".ogg\", \".aac\", \".wma\", \".opus\"}\nALL_MEDIA_EXTS = SUPPORTED_VIDEO_EXTS | SUPPORTED_AUDIO_EXTS\n\n\n@dataclass\nclass MergeSegment:\n \"\"\"Metadata for one segment in a merged file.\"\"\"\n file_path: str\n start_offset: float # offset in the merged timeline\n duration: float\n\n\n@dataclass\nclass PreprocessResult:\n \"\"\"Result of preprocessing pipeline.\"\"\"\n audio_path: Path\n original_files: list[Path]\n merge_segments: list[MergeSegment] = field(default_factory=list)\n is_video: bool = False\n video_path: Path | None = None\n duration: float = 0.0\n sample_rate: int = 16000\n denoised: bool = False\n\n\ndef detect_media_type(file_path: Path) -> str:\n \"\"\"Detect whether a file is video, audio, or unsupported.\"\"\"\n ext = file_path.suffix.lower()\n if ext in SUPPORTED_VIDEO_EXTS:\n return \"video\"\n if ext in SUPPORTED_AUDIO_EXTS:\n return \"audio\"\n return \"unknown\"\n\n\ndef get_duration(file_path: Path) -> float:\n \"\"\"Get media file duration in seconds using ffprobe.\"\"\"\n try:\n result = subprocess.run(\n [\n \"ffprobe\", \"-v\", \"quiet\",\n \"-show_entries\", \"format=duration\",\n \"-of\", \"default=noprint_wrappers=1:nokey=1\",\n str(file_path),\n ],\n capture_output=True, text=True, timeout=30,\n )\n return float(result.stdout.strip())\n except Exception:\n return 0.0\n\n\ndef extract_audio(\n input_path: Path,\n output_path: Path,\n sample_rate: int = 16000,\n mono: bool = True,\n) -> Path:\n \"\"\"Extract audio from video or convert audio to ASR-friendly WAV format.\"\"\"\n output_path.parent.mkdir(parents=True, exist_ok=True)\n cmd = [\n \"ffmpeg\", \"-y\", \"-i\", str(input_path),\n \"-vn\",\n \"-acodec\", \"pcm_s16le\",\n \"-ar\", str(sample_rate),\n ]\n if mono:\n cmd += [\"-ac\", \"1\"]\n cmd.append(str(output_path))\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)\n if result.returncode != 0:\n raise RuntimeError(f\"Audio extraction failed: {result.stderr[:500]}\")\n if not output_path.exists():\n raise FileNotFoundError(f\"Output audio not created: {output_path}\")\n return output_path\n\n\ndef merge_files(\n file_paths: list[Path],\n output_path: Path,\n) -> tuple[Path, list[MergeSegment]]:\n \"\"\"Merge multiple media files into one using FFmpeg concat.\n\n Returns the merged file path and segment metadata for timestamp correction.\n \"\"\"\n if len(file_paths) == 1:\n seg = MergeSegment(\n file_path=str(file_paths[0]),\n start_offset=0.0,\n duration=get_duration(file_paths[0]),\n )\n return file_paths[0], [seg]\n\n output_path.parent.mkdir(parents=True, exist_ok=True)\n\n segments: list[MergeSegment] = []\n offset = 0.0\n\n first_ext = file_paths[0].suffix.lower()\n all_same_format = all(f.suffix.lower() == first_ext for f in file_paths)\n\n if all_same_format and first_ext in {\".mp4\", \".ts\", \".mp3\", \".wav\", \".flac\"}:\n merged, segments = _merge_concat_demuxer(file_paths, output_path)\n else:\n merged, segments = _merge_concat_filter(file_paths, output_path)\n\n return merged, segments\n\n\ndef _merge_concat_demuxer(\n file_paths: list[Path], output_path: Path\n) -> tuple[Path, list[MergeSegment]]:\n \"\"\"Fast merge using FFmpeg concat demuxer (same format, no re-encoding).\"\"\"\n list_file = output_path.parent / \"concat_list.txt\"\n segments: list[MergeSegment] = []\n offset = 0.0\n\n with open(list_file, \"w\") as f:\n for fp in file_paths:\n f.write(f\"file '{fp.resolve()}'\\n\")\n dur = get_duration(fp)\n segments.append(MergeSegment(str(fp), offset, dur))\n offset += dur\n\n cmd = [\n \"ffmpeg\", \"-y\", \"-f\", \"concat\", \"-safe\", \"0\",\n \"-i\", str(list_file), \"-c\", \"copy\", str(output_path),\n ]\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)\n list_file.unlink(missing_ok=True)\n\n if result.returncode != 0:\n raise RuntimeError(f\"Merge (demuxer) failed: {result.stderr[:500]}\")\n return output_path, segments\n\n\ndef _merge_concat_filter(\n file_paths: list[Path], output_path: Path\n) -> tuple[Path, list[MergeSegment]]:\n \"\"\"Flexible merge using FFmpeg concat filter (handles different formats).\"\"\"\n segments: list[MergeSegment] = []\n offset = 0.0\n\n inputs = []\n for fp in file_paths:\n inputs += [\"-i\", str(fp)]\n dur = get_duration(fp)\n segments.append(MergeSegment(str(fp), offset, dur))\n offset += dur\n\n n = len(file_paths)\n filter_parts = \"\".join(f\"[{i}:a]\" for i in range(n))\n filter_complex = f\"{filter_parts}concat=n={n}:v=0:a=1[outa]\"\n\n cmd = [\"ffmpeg\", \"-y\"] + inputs + [\n \"-filter_complex\", filter_complex,\n \"-map\", \"[outa]\",\n \"-acodec\", \"pcm_s16le\", \"-ar\", \"16000\", \"-ac\", \"1\",\n str(output_path),\n ]\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)\n if result.returncode != 0:\n raise RuntimeError(f\"Merge (filter) failed: {result.stderr[:500]}\")\n return output_path, segments\n\n\ndef denoise_audio(\n audio_path: Path,\n output_path: Path | None = None,\n level: int = 1,\n) -> Path:\n \"\"\"Apply noise reduction to audio.\n\n Args:\n audio_path: Input WAV file path\n output_path: Output path (defaults to overwriting input)\n level: 0=none, 1=light, 2=medium, 3=heavy (DeepFilterNet if available)\n \"\"\"\n if level == 0:\n return audio_path\n\n if output_path is None:\n output_path = audio_path.parent / f\"{audio_path.stem}_denoised.wav\"\n\n try:\n import noisereduce as nr\n import numpy as np\n\n data, sr = _load_wav(audio_path)\n\n if level == 1:\n reduced = nr.reduce_noise(\n y=data, sr=sr,\n prop_decrease=0.6,\n stationary=True,\n )\n elif level == 2:\n reduced = nr.reduce_noise(\n y=data, sr=sr,\n prop_decrease=0.8,\n stationary=False,\n )\n else:\n reduced = _try_deepfilternet(audio_path, output_path)\n if reduced is not None:\n return reduced\n reduced = nr.reduce_noise(\n y=data, sr=sr,\n prop_decrease=0.95,\n stationary=False,\n )\n\n _save_wav(output_path, reduced, sr)\n return output_path\n\n except ImportError:\n if level >= 3:\n result = _try_deepfilternet(audio_path, output_path)\n if result is not None:\n return result\n return _denoise_ffmpeg_fallback(audio_path, output_path, level)\n\n\ndef _try_deepfilternet(audio_path: Path, output_path: Path) -> Path | None:\n \"\"\"Try DeepFilterNet for heavy noise reduction.\"\"\"\n try:\n from df.enhance import enhance, init_df\n model, df_state, _ = init_df()\n import torchaudio\n audio, sr = torchaudio.load(str(audio_path))\n enhanced = enhance(model, df_state, audio)\n torchaudio.save(str(output_path), enhanced, sr)\n return output_path\n except (ImportError, Exception):\n return None\n\n\ndef _denoise_ffmpeg_fallback(\n audio_path: Path, output_path: Path, level: int\n) -> Path:\n \"\"\"FFmpeg-based noise reduction as fallback.\"\"\"\n if level == 1:\n af = \"highpass=f=80,lowpass=f=8000\"\n elif level == 2:\n af = \"highpass=f=100,lowpass=f=7000,anlmdn=s=7\"\n else:\n af = \"highpass=f=120,lowpass=f=6000,anlmdn=s=10,afftdn=nf=-20\"\n\n cmd = [\n \"ffmpeg\", \"-y\", \"-i\", str(audio_path),\n \"-af\", af,\n str(output_path),\n ]\n result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)\n if result.returncode != 0 or not output_path.exists():\n return audio_path\n return output_path\n\n\ndef _load_wav(path: Path) -> tuple:\n \"\"\"Load WAV file as numpy array.\"\"\"\n import numpy as np\n import wave\n\n with wave.open(str(path), \"rb\") as wf:\n sr = wf.getframerate()\n n_frames = wf.getnframes()\n data = wf.readframes(n_frames)\n dtype = np.int16 if wf.getsampwidth() == 2 else np.float32\n arr = np.frombuffer(data, dtype=dtype).astype(np.float32)\n if dtype == np.int16:\n arr = arr / 32768.0\n return arr, sr\n\n\ndef _save_wav(path: Path, data, sr: int):\n \"\"\"Save numpy array as WAV file.\"\"\"\n import numpy as np\n import wave\n\n path.parent.mkdir(parents=True, exist_ok=True)\n arr = (data * 32768.0).clip(-32768, 32767).astype(np.int16)\n with wave.open(str(path), \"wb\") as wf:\n wf.setnchannels(1)\n wf.setsampwidth(2)\n wf.setframerate(sr)\n wf.writeframes(arr.tobytes())\n\n\ndef preprocess(\n input_files: list[str | Path],\n work_dir: Path | None = None,\n denoise_level: int = 1,\n progress_callback: Callable[[str, float], None] | None = None,\n) -> PreprocessResult:\n \"\"\"Full preprocessing pipeline: validate -> merge -> extract audio -> denoise.\n\n Args:\n input_files: List of file paths (video or audio)\n work_dir: Working directory for temp files\n denoise_level: 0=none, 1=light, 2=medium, 3=heavy\n progress_callback: Optional callback(step_name, progress_0_to_1)\n \"\"\"\n if work_dir is None:\n work_dir = Path(tempfile.mkdtemp(prefix=\"noteking_pre_\"))\n work_dir.mkdir(parents=True, exist_ok=True)\n\n def _progress(step: str, pct: float):\n if progress_callback:\n progress_callback(step, pct)\n\n paths = [Path(f) for f in input_files]\n for p in paths:\n if not p.exists():\n raise FileNotFoundError(f\"File not found: {p}\")\n if detect_media_type(p) == \"unknown\":\n raise ValueError(f\"Unsupported file format: {p.suffix}\")\n\n _progress(\"validate\", 0.1)\n\n is_video = any(detect_media_type(p) == \"video\" for p in paths)\n video_path = None\n\n if len(paths) > 1:\n _progress(\"merge\", 0.2)\n merged_ext = \".wav\" if not is_video else paths[0].suffix\n merged_path = work_dir / f\"merged{merged_ext}\"\n merged_path, merge_segments = merge_files(paths, merged_path)\n else:\n merged_path = paths[0]\n merge_segments = [MergeSegment(str(paths[0]), 0.0, get_duration(paths[0]))]\n\n if is_video:\n video_path = merged_path\n\n _progress(\"extract_audio\", 0.4)\n audio_path = work_dir / \"audio_16k.wav\"\n extract_audio(merged_path, audio_path, sample_rate=16000, mono=True)\n\n _progress(\"denoise\", 0.6)\n if denoise_level > 0:\n denoised_path = work_dir / \"audio_denoised.wav\"\n audio_path = denoise_audio(audio_path, denoised_path, level=denoise_level)\n denoised = True\n else:\n denoised = False\n\n duration = get_duration(audio_path)\n _progress(\"done\", 1.0)\n\n return PreprocessResult(\n audio_path=audio_path,\n original_files=paths,\n merge_segments=merge_segments,\n is_video=is_video,\n video_path=video_path,\n duration=duration,\n sample_rate=16000,\n denoised=denoised,\n )\n"}}, "core/parser.py": {"d419e8dd5c48801d65d7cdd46c154942adab112c": {"t": 1785599225.9541283, "n": 4661, "h": "12284e2ac4a8c4d7", "c": "\"\"\"Link parser: platform detection, collection detection, short-link resolution.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom dataclasses import dataclass, field\nfrom enum import Enum\nfrom urllib.parse import urlparse, parse_qs\n\n\nclass Platform(str, Enum):\n BILIBILI = \"bilibili\"\n YOUTUBE = \"youtube\"\n DOUYIN = \"douyin\"\n XIAOHONGSHU = \"xiaohongshu\"\n KUAISHOU = \"kuaishou\"\n TIKTOK = \"tiktok\"\n TWITTER = \"twitter\"\n INSTAGRAM = \"instagram\"\n TWITCH = \"twitch\"\n VIMEO = \"vimeo\"\n FACEBOOK = \"facebook\"\n REDDIT = \"reddit\"\n PODCAST = \"podcast\"\n LOCAL = \"local\"\n UNKNOWN = \"unknown\"\n\n\nclass LinkType(str, Enum):\n SINGLE = \"single\"\n PLAYLIST = \"playlist\"\n SERIES = \"series\"\n COLLECTION = \"collection\"\n FAVORITES = \"favorites\"\n CHANNEL = \"channel\"\n MULTI_PART = \"multi_part\"\n LOCAL_FILE = \"local_file\"\n\n\n@dataclass\nclass ParsedLink:\n url: str\n platform: Platform\n link_type: LinkType\n video_id: str = \"\"\n playlist_id: str = \"\"\n title: str = \"\"\n extra: dict = field(default_factory=dict)\n\n\n# ---------- pattern tables ----------\n\n_BILIBILI_PATTERNS = [\n (r\"bilibili\\.com/video/(BV[\\w]+)\", LinkType.SINGLE),\n (r\"bilibili\\.com/video/av(\\d+)\", LinkType.SINGLE),\n (r\"b23\\.tv/([\\w]+)\", LinkType.SINGLE),\n (r\"bilibili\\.com/list/(\\d+)\", LinkType.PLAYLIST),\n (r\"space\\.bilibili\\.com/\\d+/channel/seriesdetail\\?sid=(\\d+)\", LinkType.SERIES),\n (r\"space\\.bilibili\\.com/\\d+/favlist\\?fid=(\\d+)\", LinkType.FAVORITES),\n (r\"bilibili\\.com/watchlater\", LinkType.COLLECTION),\n]\n\n_YOUTUBE_PATTERNS = [\n (r\"(?:youtube\\.com/watch\\?v=|youtu\\.be/)([\\w-]{11})\", LinkType.SINGLE),\n (r\"youtube\\.com/playlist\\?list=([\\w-]+)\", LinkType.PLAYLIST),\n (r\"youtube\\.com/(?:c/|channel/|@)([\\w-]+)\", LinkType.CHANNEL),\n]\n\n\ndef _detect_bilibili_multipart(url: str) -> bool:\n \"\"\"Check if a bilibili URL has ?p= indicating multi-part video.\"\"\"\n parsed = urlparse(url)\n qs = parse_qs(parsed.query)\n return \"p\" in qs\n\n\ndef parse_link(url: str) -> ParsedLink:\n \"\"\"Parse a URL or local file path into a structured ParsedLink.\"\"\"\n import os\n\n stripped = url.strip()\n\n if os.path.exists(stripped):\n return ParsedLink(\n url=stripped,\n platform=Platform.LOCAL,\n link_type=LinkType.LOCAL_FILE,\n video_id=os.path.basename(stripped),\n )\n\n for pattern, link_type in _BILIBILI_PATTERNS:\n m = re.search(pattern, stripped)\n if m:\n vid = m.group(1)\n lt = link_type\n if lt == LinkType.SINGLE and _detect_bilibili_multipart(stripped):\n lt = LinkType.MULTI_PART\n return ParsedLink(\n url=stripped,\n platform=Platform.BILIBILI,\n link_type=lt,\n video_id=vid,\n )\n\n for pattern, link_type in _YOUTUBE_PATTERNS:\n m = re.search(pattern, stripped)\n if m:\n vid = m.group(1)\n extra = {}\n if link_type == LinkType.SINGLE:\n parsed = urlparse(stripped)\n qs = parse_qs(parsed.query)\n if \"list\" in qs:\n extra[\"playlist_id\"] = qs[\"list\"][0]\n return ParsedLink(\n url=stripped,\n platform=Platform.YOUTUBE,\n link_type=link_type,\n video_id=vid,\n playlist_id=extra.get(\"playlist_id\", \"\"),\n extra=extra,\n )\n\n platform = _guess_platform(stripped)\n return ParsedLink(\n url=stripped,\n platform=platform,\n link_type=LinkType.SINGLE,\n )\n\n\ndef _guess_platform(url: str) -> Platform:\n host = urlparse(url).hostname or \"\"\n mapping = {\n \"douyin.com\": Platform.DOUYIN,\n \"iesdouyin.com\": Platform.DOUYIN,\n \"xiaohongshu.com\": Platform.XIAOHONGSHU,\n \"xhslink.com\": Platform.XIAOHONGSHU,\n \"kuaishou.com\": Platform.KUAISHOU,\n \"tiktok.com\": Platform.TIKTOK,\n \"twitter.com\": Platform.TWITTER,\n \"x.com\": Platform.TWITTER,\n \"instagram.com\": Platform.INSTAGRAM,\n \"twitch.tv\": Platform.TWITCH,\n \"vimeo.com\": Platform.VIMEO,\n \"facebook.com\": Platform.FACEBOOK,\n \"reddit.com\": Platform.REDDIT,\n }\n for domain, plat in mapping.items():\n if domain in host:\n return plat\n return Platform.UNKNOWN\n\n\ndef is_batch(parsed: ParsedLink) -> bool:\n return parsed.link_type in (\n LinkType.PLAYLIST,\n LinkType.SERIES,\n LinkType.COLLECTION,\n LinkType.FAVORITES,\n LinkType.CHANNEL,\n LinkType.MULTI_PART,\n )\n"}}, "core/templates/base.py": {"b13b68860f0299923bb61322c1cd95572fa7711a": {"t": 1785599226.1535199, "n": 2680, "h": "2a83ab4534a5ed97", "c": "\"\"\"Base template class for all output formats.\"\"\"\n\nfrom __future__ import annotations\n\nfrom abc import ABC, abstractmethod\nfrom dataclasses import dataclass, field\nfrom typing import Any\n\nfrom ..config import AppConfig\nfrom ..downloader import VideoMeta\nfrom ..subtitle import SubtitleResult\n\n\n@dataclass\nclass TemplateContext:\n meta: VideoMeta\n subtitles: SubtitleResult\n config: AppConfig\n extra: dict[str, Any] = field(default_factory=dict)\n\n @property\n def transcript(self) -> str:\n return self.subtitles.full_text\n\n @property\n def has_chapters(self) -> bool:\n return bool(self.meta.chapters)\n\n @property\n def chapter_texts(self) -> list[dict]:\n \"\"\"Split transcript by video chapters.\"\"\"\n if not self.meta.chapters or not self.subtitles.segments:\n return [{\"title\": self.meta.title, \"text\": self.transcript}]\n\n results = []\n for ch in self.meta.chapters:\n start = ch.get(\"start_time\", 0)\n end = ch.get(\"end_time\", float(\"inf\"))\n title = ch.get(\"title\", \"\")\n segs = [\n s.text for s in self.subtitles.segments\n if s.start >= start and s.start < end\n ]\n results.append({\"title\": title, \"text\": \"\\n\".join(segs)})\n return results\n\n\nclass BaseTemplate(ABC):\n \"\"\"All output templates inherit from this.\"\"\"\n\n name: str = \"base\"\n display_name: str = \"Base\"\n description: str = \"\"\n file_extension: str = \".md\"\n\n @abstractmethod\n def build_prompt(self, ctx: TemplateContext) -> str:\n \"\"\"Build the LLM prompt for this template.\"\"\"\n ...\n\n def system_prompt(self, ctx: TemplateContext) -> str:\n lang = ctx.config.llm.language\n lang_name = \"中文\" if \"zh\" in lang else \"English\"\n return (\n f\"You are NoteKing, an expert at converting video/audio content into \"\n f\"high-quality structured notes. Always respond in {lang_name}. \"\n f\"Be thorough, accurate, and well-organized.\"\n )\n\n def post_process(self, result: str, ctx: TemplateContext) -> str:\n \"\"\"Optional post-processing of LLM output.\"\"\"\n return result\n\n def generate(self, ctx: TemplateContext) -> str:\n from ..llm import chat\n\n prompt = self.build_prompt(ctx)\n system = self.system_prompt(ctx)\n raw = chat(prompt, ctx.config, system=system)\n return self.post_process(raw, ctx)\n\n\ndef _truncate_transcript(text: str, max_chars: int = 60000) -> str:\n if len(text) <= max_chars:\n return text\n half = max_chars // 2\n return text[:half] + \"\\n\\n[... 内容过长,中间部分已省略 ...]\\n\\n\" + text[-half:]\n"}}, "core/proxy.py": {"181e9362cfdd3d1c47a58f6364d9175e3fb39f96": {"t": 1785599226.201034, "n": 1590, "h": "44bb3d95a81b339a", "c": "\"\"\"Proxy management for YouTube access from China.\"\"\"\n\nfrom __future__ import annotations\n\nimport subprocess\nfrom .config import AppConfig\n\n\ndef test_youtube_access(config: AppConfig) -> bool:\n \"\"\"Test if YouTube is directly accessible.\"\"\"\n import httpx\n\n proxies = config.proxy.for_requests\n try:\n resp = httpx.get(\n \"https://www.youtube.com/\",\n timeout=10,\n follow_redirects=True,\n proxy=proxies.get(\"https\") if proxies else None,\n )\n return resp.status_code == 200\n except Exception:\n return False\n\n\ndef test_proxy(proxy_url: str) -> bool:\n \"\"\"Test if a proxy server is working.\"\"\"\n import httpx\n\n try:\n resp = httpx.get(\n \"https://www.google.com/\",\n timeout=10,\n proxy=proxy_url,\n )\n return resp.status_code == 200\n except Exception:\n return False\n\n\ndef get_ytdlp_proxy_args(config: AppConfig) -> list[str]:\n \"\"\"Get yt-dlp proxy command line arguments.\"\"\"\n proxy = config.proxy.for_ytdlp\n if proxy:\n return [\"--proxy\", proxy]\n return []\n\n\ndef get_transcript_api_proxy(config: AppConfig):\n \"\"\"Get proxy config for youtube-transcript-api.\"\"\"\n proxy_dict = config.proxy.for_requests\n if not proxy_dict:\n return None\n\n try:\n from youtube_transcript_api.proxies import GenericProxyConfig\n proxy_url = proxy_dict.get(\"https\") or proxy_dict.get(\"http\", \"\")\n if proxy_url:\n return GenericProxyConfig(proxy_url)\n except ImportError:\n pass\n return None\n"}}, ".env.example": {"6b710b44782373212e6349521cd55897a87a608e": {"t": 1785599226.424057, "n": 988, "h": "f9f9fd70778ecdd4", "c": "# NoteKing Pro Environment Configuration\n# Copy this file to .env and fill in your values\n\n# REQUIRED: LLM API Key (MiniMax / OpenAI / DeepSeek / etc.)\nNOTEKING_LLM_API_KEY=sk-your-api-key-here\n\n# OPTIONAL: Custom LLM provider\n# For MiniMax (recommended, 中国区用户):\n# NOTEKING_LLM_BASE_URL=https://api.minimax.chat/v1\n# NOTEKING_LLM_MODEL=MiniMax-M2.7\n# For MiniMax (international users):\n# NOTEKING_LLM_BASE_URL=https://api.minimax.io/v1\n# NOTEKING_LLM_MODEL=MiniMax-M2.7\n\n# For DeepSeek:\n# NOTEKING_LLM_BASE_URL=https://api.deepseek.com\n# NOTEKING_LLM_MODEL=deepseek-chat\n\n# For local Ollama:\n# NOTEKING_LLM_BASE_URL=http://localhost:11434/v1\n# NOTEKING_LLM_MODEL=qwen2.5\n\n# OPTIONAL: HuggingFace token for pyannote speaker diarization\n# Get yours at https://huggingface.co/settings/tokens\n# HF_TOKEN=hf_xxxxx\n\n# OPTIONAL: Proxy for YouTube access from China\n# NOTEKING_PROXY=socks5://127.0.0.1:7890\n\n# OPTIONAL: Bilibili login cookie for HD content\n# BILIBILI_SESSDATA=your_sessdata_here\n"}}, "core/templates/brainstorm.py": {"db4168d7eea9d41944f94249400bb2cd9e5cbc7c": {"t": 1785599226.617782, "n": 1231, "h": "dd416eb600338c33", "c": "\"\"\"Brainstorm template: idea capture with mind map and action suggestions.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass BrainstormTemplate(BaseTemplate):\n name = \"brainstorm\"\n display_name = \"灵感记录\"\n description = \"Idea capture with mind map structure and action suggestions\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n context_info = f\"\\n描述: {user_context}\" if user_context else \"\"\n\n return f\"\"\"请根据以下录音转录稿,提炼其中的灵感和想法,生成结构化的灵感记录。\n\n标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}\n\n录音转录稿:\n{transcript}\n\n请按以下格式输出:\n\n# 灵感记录: [主题]\n\n## 核心想法\n[用1-2句话概括最核心的想法]\n\n## 想法清单\n\n### 想法 1: [想法名称]\n- **描述**: [详细描述]\n- **价值**: [为什么重要]\n- **可行性**: ⭐⭐⭐⭐⭐ (1-5星评估)\n- **下一步**: [具体行动]\n\n### 想法 2: [想法名称]\n...\n\n## 思维导图\n\n```\n[主题]\n├── [分支1]\n│ ├── [子想法]\n│ └── [子想法]\n├── [分支2]\n│ ├── [子想法]\n│ └── [子想法]\n└── [分支3]\n └── [子想法]\n```\n\n## 想法关联\n[描述各想法之间的联系和组合可能]\n\n## 行动建议\n1. **立即执行**: [最紧急的行动]\n2. **本周内**: [短期行动]\n3. **后续跟进**: [长期规划]\n\n## 灵感原话\n> \"[值得记录的原话]\"\n\n要求:\n1. 提炼核心想法,不是逐字翻译\n2. 评估每个想法的可行性\n3. 给出具体可执行的行动建议\n4. 用中文输出\"\"\"\n"}}, "core/templates/brief.py": {"5136c0d9cfd9ed7c9c517b7a77e651047dadeea3": {"t": 1785599226.795601, "n": 617, "h": "ce81e2a6b7078953", "c": "\"\"\"Brief summary template: 3-5 sentence overview.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass BriefTemplate(BaseTemplate):\n name = \"brief\"\n display_name = \"简要总结\"\n description = \"3-5 sentence quick overview of the video content\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n return f\"\"\"请对以下视频内容生成一个简要总结。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n时长: {ctx.meta.duration:.0f} 秒\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 用 3-5 句话概括视频的核心内容\n2. 突出最重要的要点和结论\n3. 语言简洁精炼,适合快速浏览\n4. 格式为 Markdown\n\n请直接输出总结内容:\"\"\"\n"}}, "core/templates/cornell_notes.py": {"5897844ab3d65d715e891f96d8711da49386a154": {"t": 1785599226.8047907, "n": 1241, "h": "3c8c12a45bfdc74b", "c": "\"\"\"Cornell notes template: cue column, notes area, and summary section.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass CornellNotesTemplate(BaseTemplate):\n name = \"cornell_notes\"\n display_name = \"康奈尔笔记\"\n description = \"Cornell note-taking method with cue column, notes area, and summary\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n context_info = f\"\\n描述: {user_context}\" if user_context else \"\"\n\n return f\"\"\"请根据以下录音转录稿,使用康奈尔笔记法生成结构化笔记。\n\n标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}\n\n录音转录稿:\n{transcript}\n\n请按以下康奈尔笔记格式输出:\n\n# 康奈尔笔记: [标题]\n\n---\n\n## 📝 笔记区\n\n### [主题1]\n\n| 线索栏 (关键词/问题) | 笔记栏 (详细内容) |\n|---------------------|------------------|\n| [关键词1] | [对应的详细笔记内容] |\n| [关键问题1] | [对应的回答/内容] |\n| [关键概念1] | [解释和细节] |\n\n### [主题2]\n\n| 线索栏 | 笔记栏 |\n|--------|--------|\n| [关键词] | [详细内容] |\n\n### [主题3]\n...\n\n---\n\n## 📌 总结区\n[用自己的话概括全部内容的精华,3-5句话]\n\n---\n\n## 🔑 关键术语\n| 术语 | 定义 |\n|------|------|\n\n## ❓ 待解决问题\n1. [需要进一步了解的问题]\n2. [问题]\n\n要求:\n1. 严格遵循康奈尔笔记的三栏结构\n2. 线索栏用关键词和问题,便于复习时自测\n3. 笔记栏详细但精炼\n4. 总结区用自己的话概括,不是简单复制\n5. 用中文输出\"\"\"\n"}}, "core/templates/__init__.py": {"6b02f82d01feed6c7f05540656cdd2421c79c190": {"t": 1785599226.8494563, "n": 2673, "h": "f96b78e68df72601", "c": "\"\"\"Template registry: all output templates (original 13 + new recording templates).\"\"\"\n\nfrom __future__ import annotations\n\nfrom .base import BaseTemplate, TemplateContext\nfrom .brief import BriefTemplate\nfrom .detailed import DetailedTemplate\nfrom .mindmap import MindMapTemplate\nfrom .flashcard import FlashCardTemplate\nfrom .quiz import QuizTemplate\nfrom .timeline import TimelineTemplate\nfrom .exam import ExamTemplate\nfrom .tutorial import TutorialTemplate\nfrom .news import NewsTemplate\nfrom .podcast import PodcastTemplate\nfrom .xhs_note import XHSNoteTemplate\nfrom .latex_pdf import LaTeXPDFTemplate\nfrom .custom import CustomTemplate\n\n# New recording/meeting templates\nfrom .meeting_minutes import MeetingMinutesTemplate\nfrom .lecture_notes import LectureNotesTemplate\nfrom .interview_template import InterviewTemplate\nfrom .brainstorm import BrainstormTemplate\nfrom .news_digest import NewsDigestTemplate\nfrom .exam_prep import ExamPrepTemplate\nfrom .cornell_notes import CornellNotesTemplate\nfrom .podcast_shownotes import PodcastShowNotesTemplate\nfrom .entertainment import EntertainmentTemplate\nfrom .smart_summary import SmartSummaryTemplate\n\nTEMPLATES: dict[str, type[BaseTemplate]] = {\n # Original templates\n \"brief\": BriefTemplate,\n \"detailed\": DetailedTemplate,\n \"mindmap\": MindMapTemplate,\n \"flashcard\": FlashCardTemplate,\n \"quiz\": QuizTemplate,\n \"timeline\": TimelineTemplate,\n \"exam\": ExamTemplate,\n \"tutorial\": TutorialTemplate,\n \"news\": NewsTemplate,\n \"podcast\": PodcastTemplate,\n \"xhs_note\": XHSNoteTemplate,\n \"latex_pdf\": LaTeXPDFTemplate,\n \"custom\": CustomTemplate,\n # New recording/meeting templates\n \"meeting_minutes\": MeetingMinutesTemplate,\n \"lecture_notes\": LectureNotesTemplate,\n \"interview\": InterviewTemplate,\n \"brainstorm\": BrainstormTemplate,\n \"news_digest\": NewsDigestTemplate,\n \"exam_prep\": ExamPrepTemplate,\n \"cornell_notes\": CornellNotesTemplate,\n \"podcast_shownotes\": PodcastShowNotesTemplate,\n \"entertainment\": EntertainmentTemplate,\n \"smart_summary\": SmartSummaryTemplate,\n}\n\nTEMPLATE_LIST = [\n {\"name\": k, \"display_name\": v.display_name, \"description\": v.description}\n for k, v in TEMPLATES.items()\n if hasattr(v, \"display_name\")\n]\n\n\ndef get_template(name: str, **kwargs) -> BaseTemplate:\n cls = TEMPLATES.get(name)\n if cls is None:\n raise ValueError(\n f\"Unknown template '{name}'. Available: {', '.join(TEMPLATES.keys())}\"\n )\n if name == \"custom\":\n return cls(**kwargs)\n return cls()\n\n\n__all__ = [\n \"TEMPLATES\",\n \"TEMPLATE_LIST\",\n \"get_template\",\n \"BaseTemplate\",\n \"TemplateContext\",\n]\n"}}, "core/templates/detailed.py": {"ad9b8163e991437457c436cee03851e69693ffb3": {"t": 1785599227.1772523, "n": 1172, "h": "f7d2647e0fc67ef2", "c": "\"\"\"Detailed learning notes template with chapters and key points.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass DetailedTemplate(BaseTemplate):\n name = \"detailed\"\n display_name = \"详细学习笔记\"\n description = \"Structured learning notes with chapters, key points, and summaries\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n chapters_info = \"\"\n if ctx.has_chapters:\n chapters_info = \"\\n视频章节:\\n\"\n for ch in ctx.meta.chapters:\n t = ch.get(\"title\", \"\")\n s = ch.get(\"start_time\", 0)\n chapters_info += f\"- [{s:.0f}s] {t}\\n\"\n\n return f\"\"\"请根据以下视频内容,生成一份详细的结构化学习笔记。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n时长: {ctx.meta.duration:.0f} 秒\n{chapters_info}\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 使用 Markdown 格式,带清晰的层级结构 (## / ### / ####)\n2. 按教学逻辑组织内容,而非简单按时间顺序排列\n3. 每个主要章节包含:\n - 核心概念解释\n - 关键要点 (用列表形式)\n - 重要公式/代码 (如果有)\n - 实例说明\n4. 在笔记开头添加「📋 内容概览」列出所有章节\n5. 在笔记结尾添加「💡 核心收获」总结 3-5 个最重要的收获\n6. 在笔记结尾添加「🔗 延伸思考」提出 2-3 个值得深入探讨的问题\n7. 用 > 引用格式标注原始视频中的关键原话\n8. 标注重要概念为 **粗体**\n\n请直接输出完整的学习笔记:\"\"\"\n"}}, "core/subtitle.py": {"c2f065baaa18c551139d33e2e7211be96c2a171f": {"t": 1785599227.4654472, "n": 6595, "h": "468c252afd00d5a3", "c": "\"\"\"Subtitle extraction with three-level fallback: CC -> ASR -> Visual.\"\"\"\n\nfrom __future__ import annotations\n\nimport re\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Sequence\n\nfrom .config import AppConfig\nfrom .parser import ParsedLink, Platform\n\n\n@dataclass\nclass SubtitleSegment:\n start: float\n end: float\n text: str\n\n @property\n def start_ts(self) -> str:\n return _seconds_to_ts(self.start)\n\n @property\n def end_ts(self) -> str:\n return _seconds_to_ts(self.end)\n\n\n@dataclass\nclass SubtitleResult:\n segments: list[SubtitleSegment]\n source: str # \"cc\", \"asr\", \"visual\"\n language: str = \"zh\"\n raw_text: str = \"\"\n\n @property\n def full_text(self) -> str:\n if self.raw_text:\n return self.raw_text\n return \"\\n\".join(s.text for s in self.segments)\n\n @property\n def duration(self) -> float:\n if not self.segments:\n return 0\n return self.segments[-1].end\n\n @property\n def srt_content(self) -> str:\n lines = []\n for i, seg in enumerate(self.segments, 1):\n lines.append(str(i))\n lines.append(f\"{seg.start_ts} --> {seg.end_ts}\")\n lines.append(seg.text)\n lines.append(\"\")\n return \"\\n\".join(lines)\n\n def save_srt(self, path: Path) -> None:\n path.write_text(self.srt_content, encoding=\"utf-8\")\n\n def save_txt(self, path: Path) -> None:\n path.write_text(self.full_text, encoding=\"utf-8\")\n\n\ndef _seconds_to_ts(seconds: float) -> str:\n h = int(seconds // 3600)\n m = int((seconds % 3600) // 60)\n s = int(seconds % 60)\n ms = int((seconds - int(seconds)) * 1000)\n return f\"{h:02d}:{m:02d}:{s:02d},{ms:03d}\"\n\n\ndef parse_srt(srt_path: Path) -> list[SubtitleSegment]:\n \"\"\"Parse an SRT file into subtitle segments.\"\"\"\n content = srt_path.read_text(encoding=\"utf-8\", errors=\"replace\")\n content = content.replace(\"\\ufeff\", \"\") # Remove BOM\n\n segments: list[SubtitleSegment] = []\n blocks = re.split(r\"\\n\\s*\\n\", content.strip())\n\n for block in blocks:\n lines = block.strip().split(\"\\n\")\n if len(lines) < 2:\n continue\n\n ts_match = re.search(\n r\"(\\d{2}:\\d{2}:\\d{2}[,\\.]\\d{3})\\s*-->\\s*(\\d{2}:\\d{2}:\\d{2}[,\\.]\\d{3})\",\n block,\n )\n if not ts_match:\n continue\n\n start = _ts_to_seconds(ts_match.group(1))\n end = _ts_to_seconds(ts_match.group(2))\n\n ts_line_idx = next(\n i for i, l in enumerate(lines)\n if \"-->\" in l\n )\n text = \"\\n\".join(lines[ts_line_idx + 1:]).strip()\n text = re.sub(r\"<[^>]+>\", \"\", text) # Strip HTML tags\n\n if text:\n segments.append(SubtitleSegment(start=start, end=end, text=text))\n\n return segments\n\n\ndef _ts_to_seconds(ts: str) -> float:\n ts = ts.replace(\",\", \".\")\n parts = ts.split(\":\")\n return float(parts[0]) * 3600 + float(parts[1]) * 60 + float(parts[2])\n\n\n# ---------- extraction strategies ----------\n\ndef _try_youtube_transcript_api(\n parsed: ParsedLink, config: AppConfig\n) -> SubtitleResult | None:\n \"\"\"Try youtube-transcript-api for YouTube videos.\"\"\"\n if parsed.platform != Platform.YOUTUBE:\n return None\n try:\n from youtube_transcript_api import YouTubeTranscriptApi\n\n kwargs = {}\n proxy_dict = config.proxy.for_requests\n if proxy_dict:\n from youtube_transcript_api.proxies import GenericProxyConfig\n proxy_url = proxy_dict.get(\"https\") or proxy_dict.get(\"http\", \"\")\n if proxy_url:\n kwargs[\"proxy_config\"] = GenericProxyConfig(proxy_url)\n\n vid = parsed.video_id\n ytt = YouTubeTranscriptApi()\n fetched = ytt.fetch(vid, languages=[\"zh-Hans\", \"zh-CN\", \"zh\", \"en\"], **kwargs)\n segments = [\n SubtitleSegment(\n start=s.start,\n end=s.start + s.duration,\n text=s.text,\n )\n for s in fetched\n ]\n if segments:\n return SubtitleResult(segments=segments, source=\"cc\", language=\"zh\")\n except Exception:\n pass\n return None\n\n\ndef _try_ytdlp_subtitles(\n parsed: ParsedLink, work_dir: Path, config: AppConfig, timeout: int = 30\n) -> SubtitleResult | None:\n \"\"\"Try yt-dlp subtitle download with timeout.\"\"\"\n from .downloader import download_subtitles\n import signal\n\n def _handler(signum, frame):\n raise TimeoutError(\"subtitle download timeout\")\n\n try:\n old = signal.signal(signal.SIGALRM, _handler)\n signal.alarm(timeout)\n try:\n srt_files = download_subtitles(parsed.url, work_dir, config)\n finally:\n signal.alarm(0)\n signal.signal(signal.SIGALRM, old)\n if srt_files:\n segments = parse_srt(srt_files[0])\n if segments:\n return SubtitleResult(segments=segments, source=\"cc\")\n except Exception:\n pass\n return None\n\n\ndef _try_asr(\n parsed: ParsedLink, work_dir: Path, config: AppConfig\n) -> SubtitleResult | None:\n \"\"\"Fall back to ASR transcription.\"\"\"\n from .downloader import download_audio\n from .transcriber import transcribe\n\n try:\n audio_path = download_audio(parsed.url, work_dir, config)\n result = transcribe(audio_path, config)\n return result\n except Exception:\n pass\n return None\n\n\ndef extract_subtitles(\n parsed: ParsedLink,\n work_dir: Path,\n config: AppConfig,\n skip_asr: bool = True,\n) -> SubtitleResult:\n \"\"\"Three-level subtitle extraction fallback.\n\n Priority 1: Platform CC subtitles (API or yt-dlp)\n Priority 2: ASR speech-to-text (only if skip_asr=False)\n Priority 3: Empty result (visual mode deferred to LLM with metadata)\n \"\"\"\n if parsed.platform == Platform.LOCAL:\n if not skip_asr:\n result = _try_asr(parsed, work_dir, config)\n if result:\n return result\n return SubtitleResult(segments=[], source=\"visual\")\n\n # Level 1: CC subtitles via YouTube Transcript API\n result = _try_youtube_transcript_api(parsed, config)\n if result:\n return result\n\n # Level 1b: CC subtitles via yt-dlp\n result = _try_ytdlp_subtitles(parsed, work_dir, config)\n if result:\n return result\n\n # Level 2: ASR(默认跳过,避免长视频下载超时)\n if not skip_asr:\n result = _try_asr(parsed, work_dir, config)\n if result:\n return result\n\n # Level 3: Visual mode\n return SubtitleResult(segments=[], source=\"visual\")\n"}}, "core/cache.py": {"a799f0e26ca84e6c9804d6743d90fe27ede384c1": {"t": 1785599227.552393, "n": 1714, "h": "6aa2ad681a9582bc", "c": "\"\"\"Cache management to avoid reprocessing.\"\"\"\n\nfrom __future__ import annotations\n\nimport hashlib\nimport json\nfrom pathlib import Path\nfrom typing import Any\n\nfrom .config import AppConfig\n\n\nclass Cache:\n def __init__(self, config: AppConfig):\n self.cache_dir = Path(config.cache_dir)\n self.cache_dir.mkdir(parents=True, exist_ok=True)\n\n def _key(self, url: str, template: str = \"\") -> str:\n raw = f\"{url}:{template}\"\n return hashlib.sha256(raw.encode()).hexdigest()[:16]\n\n def _path(self, url: str, template: str = \"\") -> Path:\n return self.cache_dir / f\"{self._key(url, template)}.json\"\n\n def get(self, url: str, template: str = \"\") -> dict[str, Any] | None:\n p = self._path(url, template)\n if p.exists():\n try:\n return json.loads(p.read_text(encoding=\"utf-8\"))\n except Exception:\n return None\n return None\n\n def set(self, url: str, template: str, data: dict[str, Any]) -> None:\n p = self._path(url, template)\n p.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n\n def has(self, url: str, template: str = \"\") -> bool:\n return self._path(url, template).exists()\n\n def clear(self) -> int:\n count = 0\n for f in self.cache_dir.glob(\"*.json\"):\n f.unlink()\n count += 1\n return count\n\n def get_transcript(self, url: str) -> str | None:\n data = self.get(url, \"__transcript__\")\n if data:\n return data.get(\"transcript\")\n return None\n\n def set_transcript(self, url: str, transcript: str) -> None:\n self.set(url, \"__transcript__\", {\"transcript\": transcript})\n"}}, "core/templates/entertainment.py": {"32dd6f8875b9673b2184c309726a04b87e8f4f2a": {"t": 1785599227.5912616, "n": 1133, "h": "e031612546f4c3af", "c": "\"\"\"Entertainment template: highlights, quotes, and rating for casual content.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass EntertainmentTemplate(BaseTemplate):\n name = \"entertainment\"\n display_name = \"娱乐内容\"\n description = \"Entertainment highlights with quotes, key moments, and rating\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n context_info = f\"\\n描述: {user_context}\" if user_context else \"\"\n\n return f\"\"\"请根据以下娱乐/休闲内容的录音转录稿,生成一份精彩内容回顾。\n\n标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}\n\n录音转录稿:\n{transcript}\n\n请按以下格式输出:\n\n# 内容回顾: [标题]\n\n## 一句话推荐\n[用一句吸引人的话概括这个内容]\n\n## 推荐指数: ⭐⭐⭐⭐ (1-5星)\n\n## 适合人群\n[什么样的人会喜欢这个内容]\n\n## 内容概要\n[3-5句话介绍内容梗概,不含剧透]\n\n## 精彩时刻 🎬\n1. **[时间戳] [精彩瞬间标题]**: [描述]\n2. **[时间戳] [精彩瞬间标题]**: [描述]\n\n## 金句收集 💬\n- > \"[金句1]\"\n- > \"[金句2]\"\n- > \"[金句3]\"\n\n## 笑点/泪点/高能预警\n- [描述令人印象深刻的瞬间]\n\n## 关键信息\n[提取的有用信息或知识点,如果有]\n\n## 个人观后感\n[基于内容生成的简短评价]\n\n要求:\n1. 风格轻松活泼\n2. 突出精彩瞬间\n3. 保留最有趣的原话\n4. 不过度剧透\n5. 用中文输出\"\"\"\n"}}, "core/batch.py": {"2e17a94c3b5064d8cb6ced5c6c9f6fb25931797c": {"t": 1785599227.688303, "n": 4321, "h": "0ece84c6daa387e0", "c": "\"\"\"Batch processing: playlists, collections, and multi-part videos.\"\"\"\n\nfrom __future__ import annotations\n\nimport json\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Callable\n\nfrom .config import AppConfig\nfrom .downloader import list_playlist_entries, get_video_info, VideoMeta\nfrom .parser import ParsedLink, is_batch\n\n\n@dataclass\nclass BatchProgress:\n total: int = 0\n completed: int = 0\n current_title: str = \"\"\n failed: list[str] = field(default_factory=list)\n results: list[dict] = field(default_factory=list)\n\n @property\n def percent(self) -> float:\n if self.total == 0:\n return 0\n return (self.completed / self.total) * 100\n\n @property\n def status_line(self) -> str:\n return (\n f\"[{self.completed}/{self.total}] \"\n f\"({self.percent:.0f}%) {self.current_title}\"\n )\n\n\n@dataclass\nclass BatchResult:\n progress: BatchProgress\n output_dir: Path\n merged_output: str = \"\"\n\n @property\n def success_count(self) -> int:\n return self.progress.completed - len(self.progress.failed)\n\n\ndef get_batch_entries(parsed: ParsedLink, config: AppConfig) -> list[dict]:\n \"\"\"Get all entries from a playlist/collection.\"\"\"\n if not is_batch(parsed):\n return [{\"url\": parsed.url, \"title\": \"\"}]\n\n entries = list_playlist_entries(parsed.url, config)\n results = []\n for entry in entries:\n url = entry.get(\"url\") or entry.get(\"webpage_url\", \"\")\n if not url and entry.get(\"id\"):\n if \"bilibili\" in parsed.url:\n url = f\"https://www.bilibili.com/video/{entry['id']}\"\n elif \"youtube\" in parsed.url:\n url = f\"https://www.youtube.com/watch?v={entry['id']}\"\n if url:\n results.append({\n \"url\": url,\n \"title\": entry.get(\"title\", \"\"),\n \"duration\": entry.get(\"duration\", 0),\n })\n return results\n\n\ndef process_batch(\n entries: list[dict],\n process_fn: Callable[[str, int], dict],\n progress_callback: Callable[[BatchProgress], None] | None = None,\n) -> BatchProgress:\n \"\"\"Process a batch of entries, calling process_fn for each.\"\"\"\n progress = BatchProgress(total=len(entries))\n\n for i, entry in enumerate(entries):\n url = entry.get(\"url\", \"\")\n title = entry.get(\"title\", f\"Video {i + 1}\")\n progress.current_title = title\n\n if progress_callback:\n progress_callback(progress)\n\n try:\n result = process_fn(url, i)\n progress.results.append(result)\n except Exception as e:\n progress.failed.append(f\"{title}: {e}\")\n\n progress.completed = i + 1\n if progress_callback:\n progress_callback(progress)\n\n return progress\n\n\ndef merge_batch_notes(\n results: list[dict],\n title: str = \"课程笔记合集\",\n) -> str:\n \"\"\"Merge multiple note results into a single document.\"\"\"\n lines = [f\"# {title}\\n\"]\n lines.append(f\"共 {len(results)} 个视频\\n\")\n lines.append(\"---\\n\")\n\n lines.append(\"## 目录\\n\")\n for i, r in enumerate(results, 1):\n t = r.get(\"title\", f\"第 {i} 节\")\n lines.append(f\"{i}. [{t}](#{_slug(t)})\")\n lines.append(\"\\n---\\n\")\n\n for i, r in enumerate(results, 1):\n t = r.get(\"title\", f\"第 {i} 节\")\n content = r.get(\"content\", \"\")\n lines.append(f\"\\n## {i}. {t}\\n\")\n lines.append(content)\n lines.append(\"\\n---\\n\")\n\n return \"\\n\".join(lines)\n\n\ndef _slug(text: str) -> str:\n return text.lower().replace(\" \", \"-\").replace(\"/\", \"-\")[:50]\n\n\ndef save_batch_checkpoint(\n progress: BatchProgress,\n checkpoint_path: Path,\n) -> None:\n \"\"\"Save progress for resume capability.\"\"\"\n data = {\n \"total\": progress.total,\n \"completed\": progress.completed,\n \"failed\": progress.failed,\n \"results_count\": len(progress.results),\n }\n checkpoint_path.write_text(\n json.dumps(data, ensure_ascii=False, indent=2), encoding=\"utf-8\"\n )\n\n\ndef load_batch_checkpoint(checkpoint_path: Path) -> dict | None:\n \"\"\"Load a previous checkpoint for resuming.\"\"\"\n if not checkpoint_path.exists():\n return None\n try:\n return json.loads(checkpoint_path.read_text(encoding=\"utf-8\"))\n except Exception:\n return None\n"}}, "core/templates/exam.py": {"3d2b80ca4c650bcb74a9aecedbd2304d5f145bf1": {"t": 1785599227.9521425, "n": 861, "h": "74ba7573d90f2297", "c": "\"\"\"Exam review notes template: optimized for test preparation.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass ExamTemplate(BaseTemplate):\n name = \"exam\"\n display_name = \"考试复习笔记\"\n description = \"Exam-oriented review notes with key concepts and formulas\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n return f\"\"\"请根据以下视频内容,生成一份考试复习笔记。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 按考试复习的逻辑组织内容:\n\n## 📚 考试复习笔记: {ctx.meta.title}\n\n### 一、核心概念清单\n(列出所有需要掌握的核心概念,每个概念用一句话定义)\n\n### 二、重点公式/定理\n(列出所有重要公式,标注使用场景和注意事项)\n\n### 三、知识点详解\n(分主题详细展开,每个知识点标注重要程度 ⭐⭐⭐)\n\n### 四、易错点/易混淆点\n(列出常见的错误理解和混淆概念)\n\n### 五、速记口诀/记忆技巧\n(帮助快速记忆的口诀或联想方法)\n\n### 六、模拟考题\n(3-5 道典型考题及解答思路)\n\n2. 使用 ⭐ 标注重要程度 (⭐ 到 ⭐⭐⭐⭐⭐)\n3. 用高亮标注必考点\n4. 适合打印后快速复习\n\n请直接输出考试复习笔记:\"\"\"\n"}}, "core/pdf_engine.py": {"a1c1e265818aa36e90066e2444832c94ccc8572f": {"t": 1785599228.0903907, "n": 37158, "h": "ec2a62605859e09a", "c": "\"\"\"\nPDF Lecture Note Engine: video -> keyframes + subtitles -> LLM -> illustrated PDF\n\nFeatures:\n- Smart frame extraction (SceneDetect + info-density scoring + perceptual hash dedup)\n- Subtitle-frame alignment (timestamp + semantic matching)\n- Dual rendering: LaTeX (professional) / HTML+Chrome (zero-dependency fallback)\n- Concurrent batch processing for video collections\n\"\"\"\n\nfrom __future__ import annotations\n\nimport os\nimport re\nimport shutil\nimport subprocess\nimport time\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\nfrom typing import Any\n\nfrom .config import AppConfig\nfrom .frames import ExtractedFrame, _get_duration, _extract_frame_at\n\n\n# ── Data structures ────────────────────────────────────────────\n\n@dataclass\nclass ScoredFrame:\n \"\"\"A video frame with quality/info scores.\"\"\"\n path: Path\n timestamp: float\n scene_score: float = 0.0\n info_score: float = 0.0\n phash: str = \"\"\n subtitle_text: str = \"\"\n caption: str = \"\"\n\n @property\n def time_str(self) -> str:\n m, s = divmod(int(self.timestamp), 60)\n h, m = divmod(m, 60)\n return f\"{h:02d}:{m:02d}:{s:02d}\"\n\n @property\n def total_score(self) -> float:\n return self.scene_score * 0.4 + self.info_score * 0.6\n\n\n@dataclass\nclass EpisodeResult:\n \"\"\"Result of processing one video episode.\"\"\"\n episode: int\n title: str\n duration: float\n frames: list[ScoredFrame]\n subtitle_text: str\n notes_md: str = \"\"\n notes_tex: str = \"\"\n pdf_path: Path | None = None\n html_path: Path | None = None\n\n\n@dataclass\nclass CollectionResult:\n \"\"\"Result of processing a video collection.\"\"\"\n title: str\n episodes: list[EpisodeResult]\n merged_pdf: Path | None = None\n merged_html: Path | None = None\n\n\n# ── Smart Frame Extractor ──────────────────────────────────────\n\nclass SmartFrameExtractor:\n \"\"\"Extract keyframes with scene detection, info-density scoring, and hash dedup.\"\"\"\n\n def __init__(\n self,\n max_frames: int = 15,\n scene_threshold: float = 25.0,\n hash_threshold: int = 8,\n min_interval: float = 5.0,\n ):\n self.max_frames = max_frames\n self.scene_threshold = scene_threshold\n self.hash_threshold = hash_threshold\n self.min_interval = min_interval\n\n def extract(self, video_path: Path, output_dir: Path) -> list[ScoredFrame]:\n output_dir.mkdir(parents=True, exist_ok=True)\n duration = _get_duration(video_path)\n\n # Stage 1: dense candidate extraction\n candidates = self._extract_candidates(video_path, output_dir, duration)\n\n # Stage 2: compute info-density scores\n candidates = self._score_frames(candidates)\n\n # Stage 3: dedup with perceptual hash\n candidates = self._dedup_by_hash(candidates)\n\n # Stage 4: enforce minimum time interval\n candidates = self._enforce_interval(candidates)\n\n # Stage 5: pick top N by total score\n candidates.sort(key=lambda f: f.total_score, reverse=True)\n selected = candidates[:self.max_frames]\n selected.sort(key=lambda f: f.timestamp)\n\n return selected\n\n def _extract_candidates(\n self, video_path: Path, output_dir: Path, duration: float\n ) -> list[ScoredFrame]:\n frames: list[ScoredFrame] = []\n\n # Try SceneDetect first\n try:\n from scenedetect import open_video, SceneManager\n from scenedetect.detectors import ContentDetector\n\n video = open_video(str(video_path))\n sm = SceneManager()\n sm.add_detector(ContentDetector(threshold=self.scene_threshold))\n sm.detect_scenes(video, show_progress=False)\n scenes = sm.get_scene_list()\n\n for i, (start, end) in enumerate(scenes):\n # Prefer end of scene (progressive PPT: final state is most complete)\n t = end.get_seconds() - 0.5\n if t < 0:\n t = start.get_seconds() + 0.5\n out = output_dir / f\"scene_{i:03d}_{t:.1f}s.jpg\"\n _extract_frame_at(video_path, t, out)\n if out.exists() and out.stat().st_size > 3000:\n frames.append(ScoredFrame(\n path=out,\n timestamp=t,\n scene_score=min(1.0, self.scene_threshold / 30.0),\n ))\n except (ImportError, Exception):\n pass\n\n # Supplement with uniform sampling (every 20-30s)\n if duration > 0:\n interval = max(20, min(30, duration / 20))\n t = 3.0\n idx = len(frames)\n while t < duration - 3:\n # Skip if too close to an existing frame\n if any(abs(f.timestamp - t) < self.min_interval for f in frames):\n t += interval\n continue\n out = output_dir / f\"uniform_{idx:03d}_{t:.1f}s.jpg\"\n _extract_frame_at(video_path, t, out)\n if out.exists() and out.stat().st_size > 3000:\n frames.append(ScoredFrame(\n path=out, timestamp=t, scene_score=0.3,\n ))\n idx += 1\n t += interval\n\n return frames\n\n def _score_frames(self, frames: list[ScoredFrame]) -> list[ScoredFrame]:\n \"\"\"Score each frame based on visual information density.\"\"\"\n try:\n from PIL import Image\n import numpy as np\n except ImportError:\n for f in frames:\n f.info_score = 0.5\n return frames\n\n for frame in frames:\n try:\n img = Image.open(frame.path)\n arr = np.array(img.convert(\"L\"), dtype=np.float32)\n\n # Edge density (Laplacian variance) - measures sharpness and detail\n from PIL import ImageFilter\n edges = img.convert(\"L\").filter(ImageFilter.FIND_EDGES)\n edge_arr = np.array(edges, dtype=np.float32)\n edge_score = min(1.0, np.std(edge_arr) / 60.0)\n\n # Contrast score\n contrast = min(1.0, np.std(arr) / 80.0)\n\n # Entropy (information content)\n hist, _ = np.histogram(arr.ravel(), bins=64, range=(0, 256))\n hist = hist / hist.sum()\n hist = hist[hist > 0]\n entropy = -np.sum(hist * np.log2(hist))\n entropy_score = min(1.0, entropy / 6.0)\n\n # Non-uniformity: penalize frames that are mostly one color\n unique_ratio = len(np.unique(arr.astype(np.uint8))) / 256.0\n\n frame.info_score = (\n edge_score * 0.35 +\n contrast * 0.25 +\n entropy_score * 0.25 +\n unique_ratio * 0.15\n )\n except Exception:\n frame.info_score = 0.3\n\n return frames\n\n def _dedup_by_hash(self, frames: list[ScoredFrame]) -> list[ScoredFrame]:\n \"\"\"Remove near-duplicate frames using perceptual hashing.\"\"\"\n try:\n import imagehash\n from PIL import Image\n except ImportError:\n return frames\n\n unique: list[ScoredFrame] = []\n seen_hashes: list[Any] = []\n\n for frame in sorted(frames, key=lambda f: f.timestamp):\n try:\n img = Image.open(frame.path)\n h = imagehash.phash(img, hash_size=12)\n frame.phash = str(h)\n\n is_dup = False\n for prev_h in seen_hashes:\n if h - prev_h < self.hash_threshold:\n is_dup = True\n break\n\n if not is_dup:\n seen_hashes.append(h)\n unique.append(frame)\n except Exception:\n unique.append(frame)\n\n return unique\n\n def _enforce_interval(self, frames: list[ScoredFrame]) -> list[ScoredFrame]:\n \"\"\"Ensure minimum time gap between selected frames.\"\"\"\n frames.sort(key=lambda f: f.timestamp)\n filtered = []\n last_t = -999.0\n for f in frames:\n if f.timestamp - last_t >= self.min_interval:\n filtered.append(f)\n last_t = f.timestamp\n elif f.total_score > 0.7:\n filtered.append(f)\n last_t = f.timestamp\n return filtered\n\n\n# ── Subtitle-Frame Aligner ─────────────────────────────────────\n\nclass SubtitleFrameAligner:\n \"\"\"Align extracted frames to subtitle text by timestamp proximity.\"\"\"\n\n def __init__(self, tolerance: float = 10.0):\n self.tolerance = tolerance\n\n def align(\n self, frames: list[ScoredFrame], subtitle_segments: list\n ) -> list[ScoredFrame]:\n for frame in frames:\n best_text = \"\"\n best_dist = float(\"inf\")\n for seg in subtitle_segments:\n seg_start = seg.start if hasattr(seg, \"start\") else seg.get(\"start\", 0)\n seg_end = seg.end if hasattr(seg, \"end\") else seg.get(\"end\", 0)\n seg_text = seg.text if hasattr(seg, \"text\") else seg.get(\"text\", \"\")\n mid = (seg_start + seg_end) / 2\n dist = abs(frame.timestamp - mid)\n if dist < best_dist and dist <= self.tolerance:\n best_dist = dist\n best_text = seg_text\n frame.subtitle_text = best_text\n return frames\n\n\n# ── HTML Note Builder ──────────────────────────────────────────\n\nclass HTMLNoteBuilder:\n \"\"\"Build illustrated lecture notes as HTML -> PDF via Chrome headless.\"\"\"\n\n STYLE = \"\"\"\n<style>\n @page { size: A4; margin: 2cm; }\n body { font-family: \"PingFang SC\",\"Hiragino Sans GB\",\"Noto Sans CJK SC\",sans-serif;\n max-width: 900px; margin: 0 auto; padding: 2rem; line-height: 1.8; color: #2d2d2d; }\n h1 { color: #1a1a2e; border-bottom: 3px solid #e94560; padding-bottom: .5rem; font-size: 1.8rem; }\n h2 { color: #16213e; border-left: 4px solid #0f3460; padding-left: .8rem; margin-top: 2.5rem; }\n h3 { color: #0f3460; margin-top: 1.5rem; }\n code { background: #f0f0f0; padding: .15em .4em; border-radius: 3px; font-size: .88em;\n font-family: \"SF Mono\",Menlo,\"Fira Code\",monospace; }\n pre { background: #1e1e2e; color: #cdd6f4; padding: 1.2rem; border-radius: 8px;\n overflow-x: auto; font-size: .85em; line-height: 1.5; }\n pre code { background: none; color: inherit; padding: 0; }\n img { max-width: 100%; border-radius: 6px; box-shadow: 0 3px 12px rgba(0,0,0,.12);\n margin: 1rem auto; display: block; }\n .frame-caption { text-align: center; color: #666; font-size: .85em; margin-top: -.5rem;\n margin-bottom: 1.5rem; }\n blockquote { background: #e8f4fd; border-left: 4px solid #2196f3; padding: .8rem 1rem;\n margin: 1rem 0; border-radius: 0 8px 8px 0; }\n .important-box { background: #fff8e1; border-left: 4px solid #ffb300; padding: 1rem;\n border-radius: 0 8px 8px 0; margin: 1rem 0; }\n .important-box::before { content: \"\\\\2B50 \\\\91CD\\\\70B9\"; font-weight: bold; display: block;\n margin-bottom: .5rem; color: #e65100; }\n .knowledge-box { background: #e3f2fd; border-left: 4px solid #1565c0; padding: 1rem;\n border-radius: 0 8px 8px 0; margin: 1rem 0; }\n .knowledge-box::before { content: \"\\\\1F4D6 \\\\77E5\\\\8BC6\\\\8865\\\\5145\"; font-weight: bold;\n display: block; margin-bottom: .5rem; color: #0d47a1; }\n .warning-box { background: #fce4ec; border-left: 4px solid #c62828; padding: 1rem;\n border-radius: 0 8px 8px 0; margin: 1rem 0; }\n .warning-box::before { content: \"\\\\26A0\\\\FE0F \\\\6CE8\\\\610F\"; font-weight: bold;\n display: block; margin-bottom: .5rem; color: #b71c1c; }\n table { border-collapse: collapse; width: 100%; margin: 1rem 0; }\n th, td { border: 1px solid #ddd; padding: .6rem .8rem; text-align: left; }\n th { background: #0f3460; color: white; }\n tr:nth-child(even) { background: #f8f9fa; }\n .cover { text-align: center; padding: 3rem 0; page-break-after: always; }\n .cover h1 { border: none; font-size: 2.2rem; }\n .cover img { max-height: 300px; margin: 2rem auto; }\n .toc { page-break-after: always; }\n @media print { body { max-width: 100%; padding: 0; } pre { white-space: pre-wrap; } }\n</style>\"\"\"\n\n def build_html(\n self,\n notes_md: str,\n frames: list[ScoredFrame],\n title: str,\n meta: dict[str, str] | None = None,\n cover_path: Path | None = None,\n ) -> str:\n if meta is None:\n meta = {}\n\n # Replace {IMAGE:N} markers with actual images\n def _replace_img(match):\n n = int(match.group(1))\n if 1 <= n <= len(frames):\n fr = frames[n - 1]\n abs_path = str(fr.path.resolve())\n cap = fr.caption or fr.subtitle_text or f\"video {fr.time_str}\"\n return (\n f'\\n<img src=\"file://{abs_path}\" alt=\"frame {n}\">\\n'\n f'<div class=\"frame-caption\">Fig.{n} — {cap} ({fr.time_str})</div>\\n'\n )\n return f\"*(Fig.{n})*\"\n\n body = re.sub(r\"\\{IMAGE:(\\d+)\\}\", _replace_img, notes_md)\n\n # Convert highlight boxes\n body = re.sub(\n r\"\\{IMPORTANT\\}(.*?)\\{/IMPORTANT\\}\",\n r'<div class=\"important-box\">\\1</div>',\n body, flags=re.DOTALL,\n )\n body = re.sub(\n r\"\\{KNOWLEDGE\\}(.*?)\\{/KNOWLEDGE\\}\",\n r'<div class=\"knowledge-box\">\\1</div>',\n body, flags=re.DOTALL,\n )\n body = re.sub(\n r\"\\{WARNING\\}(.*?)\\{/WARNING\\}\",\n r'<div class=\"warning-box\">\\1</div>',\n body, flags=re.DOTALL,\n )\n\n # Build cover\n cover_html = \"\"\n if cover_path and cover_path.exists():\n cover_html = f'<img src=\"file://{cover_path.resolve()}\" alt=\"cover\">'\n meta_rows = \"\\n\".join(\n f\"<tr><td><strong>{k}</strong></td><td>{v}</td></tr>\"\n for k, v in meta.items()\n )\n\n html = f\"\"\"<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n<meta charset=\"utf-8\">\n<title>{title}</title>\n{self.STYLE}\n</head>\n<body>\n<div class=\"cover\">\n<h1>{title}</h1>\n{cover_html}\n<table style=\"max-width:500px;margin:2rem auto\">{meta_rows}</table>\n</div>\n{body}\n</body>\n</html>\"\"\"\n return html\n\n @staticmethod\n def html_to_pdf(html_path: Path, pdf_path: Path) -> bool:\n chrome = shutil.which(\"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\")\n if not chrome:\n chrome = shutil.which(\"chromium\") or shutil.which(\"google-chrome\")\n if not chrome:\n return False\n cmd = [\n chrome, \"--headless=new\",\n f\"--print-to-pdf={pdf_path}\",\n \"--print-to-pdf-no-header\", \"--no-sandbox\", \"--disable-gpu\",\n f\"file://{html_path.resolve()}\",\n ]\n r = subprocess.run(cmd, capture_output=True, timeout=60)\n return pdf_path.exists() and pdf_path.stat().st_size > 5000\n\n\n# ── LaTeX Note Builder ─────────────────────────────────────────\n\nclass LaTeXNoteBuilder:\n \"\"\"Build illustrated lecture notes as LaTeX -> xelatex -> PDF.\"\"\"\n\n def build_tex(\n self,\n notes_md: str,\n frames: list[ScoredFrame],\n title: str,\n meta: dict[str, str] | None = None,\n cover_path: Path | None = None,\n template_path: Path | None = None,\n ) -> str:\n if meta is None:\n meta = {}\n\n if template_path and template_path.exists():\n tex = template_path.read_text(encoding=\"utf-8\")\n else:\n tex = self._default_template()\n\n # Fill metadata\n tex = tex.replace(\"[TITLE]\", _tex_escape(title))\n tex = tex.replace(\"[DATE]\", time.strftime(\"%Y-%m-%d\"))\n tex = tex.replace(\"[CHANNEL]\", _tex_escape(meta.get(\"uploader\", \"\")))\n tex = tex.replace(\"[DURATION]\", meta.get(\"duration\", \"\"))\n tex = tex.replace(\"[URL]\", meta.get(\"url\", \"\"))\n\n if cover_path and cover_path.exists():\n tex = tex.replace(\"[COVER_PATH]\", str(cover_path.resolve()))\n else:\n tex = tex.replace(\"[COVER_PATH]\", \"\")\n\n # Build body from markdown-ish notes\n body = self._md_to_tex(notes_md, frames)\n\n tex = tex.replace(\"[BODY]\", body)\n return tex\n\n def compile_pdf(self, tex_path: Path, work_dir: Path) -> Path | None:\n xelatex = shutil.which(\"xelatex\")\n if not xelatex:\n return None\n cmd = [\n xelatex,\n \"-interaction=nonstopmode\",\n \"-output-directory\", str(work_dir),\n str(tex_path),\n ]\n for _ in range(2): # Two passes for TOC\n subprocess.run(cmd, capture_output=True, timeout=120, cwd=str(work_dir))\n pdf = tex_path.with_suffix(\".pdf\")\n if not pdf.exists():\n pdf = work_dir / tex_path.with_suffix(\".pdf\").name\n return pdf if pdf.exists() else None\n\n def _md_to_tex(self, md: str, frames: list[ScoredFrame]) -> str:\n \"\"\"Convert simplified markdown with {IMAGE:N} to LaTeX body.\"\"\"\n # Pre-process: resolve inline {TAG}...{/TAG} on the same line into multi-line\n BOX_TAGS = {\"IMPORTANT\": \"importantbox\", \"KNOWLEDGE\": \"knowledgebox\",\n \"WARNING\": \"warningbox\", \"DECISION\": \"decisionbox\", \"ACTION\": \"actionbox\"}\n for tag in BOX_TAGS:\n md = re.sub(\n rf\"\\{{{tag}\\}}(.*?)\\{{/{tag}\\}}\",\n rf\"{{{tag}}}\\n\\1\\n{{/{tag}}}\",\n md, flags=re.DOTALL,\n )\n\n lines = md.split(\"\\n\")\n tex_lines = []\n in_code = False\n in_table = False\n is_header = False\n code_lang = \"\"\n open_boxes: list[str] = []\n\n for line in lines:\n stripped = line.strip()\n\n # Code blocks\n if stripped.startswith(\"```\"):\n if in_code:\n tex_lines.append(\"\\\\end{lstlisting}\")\n in_code = False\n else:\n code_lang = stripped.replace(\"```\", \"\").strip() or \"python\"\n tex_lines.append(f\"\\\\begin{{lstlisting}}[language={code_lang}]\")\n in_code = True\n continue\n\n if in_code:\n tex_lines.append(line)\n continue\n\n # Image markers\n img_match = re.match(r\"\\s*\\{IMAGE:(\\d+)\\}\", stripped)\n if img_match:\n n = int(img_match.group(1))\n if 1 <= n <= len(frames):\n fr = frames[n - 1]\n cap = fr.caption or fr.subtitle_text or f\"video {fr.time_str}\"\n tex_lines.append(\"\\\\begin{figure}[H]\")\n tex_lines.append(\"\\\\centering\")\n tex_lines.append(\n f\"\\\\includegraphics[width=0.9\\\\textwidth]{{{fr.path.resolve()}}}\"\n )\n tex_lines.append(\n f\"\\\\caption{{{_tex_escape(cap)} \\\\protect\\\\footnotemark}}\"\n )\n tex_lines.append(\"\\\\end{figure}\")\n tex_lines.append(\n f\"\\\\footnotetext{{video time: {fr.time_str}}}\"\n )\n continue\n\n # Highlight box open/close tags\n box_handled = False\n for tag, env in BOX_TAGS.items():\n LABELS = {\"importantbox\": \"重点\", \"knowledgebox\": \"知识补充\",\n \"warningbox\": \"注意\", \"decisionbox\": \"决策\", \"actionbox\": \"行动项\"}\n if stripped == f\"{{{tag}}}\":\n tex_lines.append(f\"\\\\begin{{{env}}}{{{LABELS.get(env, tag)}}}\")\n open_boxes.append(env)\n box_handled = True\n break\n if stripped == f\"{{/{tag}}}\":\n tex_lines.append(f\"\\\\end{{{env}}}\")\n if open_boxes and open_boxes[-1] == env:\n open_boxes.pop()\n box_handled = True\n break\n if box_handled:\n continue\n\n # Headings\n if stripped.startswith(\"# \") and not stripped.startswith(\"## \"):\n tex_lines.append(f\"\\\\section*{{{_tex_escape(stripped[2:].strip())}}}\")\n continue\n if line.startswith(\"## \"):\n tex_lines.append(f\"\\\\section{{{_tex_escape(line[3:].strip())}}}\")\n continue\n if line.startswith(\"### \"):\n tex_lines.append(f\"\\\\subsection{{{_tex_escape(line[4:].strip())}}}\")\n continue\n if line.startswith(\"#### \"):\n tex_lines.append(f\"\\\\subsubsection{{{_tex_escape(line[5:].strip())}}}\")\n continue\n\n # Horizontal rules\n if stripped in (\"---\", \"***\", \"___\"):\n tex_lines.append(\"\\\\bigskip\\\\hrule\\\\bigskip\")\n continue\n\n # Blockquotes\n if stripped.startswith(\"> \"):\n tex_lines.append(f\"\\\\begin{{quote}}\\\\textit{{{_tex_escape(stripped[2:])}}}\\\\end{{quote}}\")\n continue\n\n # Tables: detect start/end and wrap in tabular\n if \"|\" in stripped and stripped.startswith(\"|\"):\n cells = [c.strip() for c in stripped.split(\"|\")[1:-1]]\n if all(set(c) <= set(\"-: \") for c in cells):\n continue # skip separator row\n if not in_table:\n ncols = len(cells)\n col_spec = \"|\" + \"l|\" * ncols\n tex_lines.append(\"\\\\begin{center}\")\n tex_lines.append(f\"\\\\begin{{tabular}}{{{col_spec}}}\")\n tex_lines.append(\"\\\\hline\")\n in_table = True\n is_header = True\n row = \" & \".join(_tex_escape(c) for c in cells)\n tex_lines.append(f\"{row} \\\\\\\\\")\n tex_lines.append(\"\\\\hline\")\n if is_header:\n is_header = False\n continue\n elif in_table:\n tex_lines.append(\"\\\\end{tabular}\")\n tex_lines.append(\"\\\\end{center}\")\n in_table = False\n\n # Bold/italic\n processed = re.sub(r\"\\*\\*(.+?)\\*\\*\", r\"\\\\textbf{\\1}\", line)\n processed = re.sub(r\"\\*(.+?)\\*\", r\"\\\\textit{\\1}\", processed)\n # Inline code\n processed = re.sub(r\"`([^`]+)`\", r\"\\\\texttt{\\1}\", processed)\n # Inline math\n processed = re.sub(r\"\\$(.+?)\\$\", r\"$\\1$\", processed)\n\n tex_lines.append(processed)\n\n # Close unclosed table\n if in_table:\n tex_lines.append(\"\\\\end{tabular}\")\n tex_lines.append(\"\\\\end{center}\")\n\n # Close any unclosed boxes\n for env in reversed(open_boxes):\n tex_lines.append(f\"\\\\end{{{env}}}\")\n\n return \"\\n\".join(tex_lines)\n\n @staticmethod\n def _default_template() -> str:\n return r\"\"\"\\documentclass[a4paper,11pt]{article}\n\\usepackage[fontset=fandol]{ctex}\n\\usepackage{amsmath,amssymb}\n\\usepackage{graphicx}\n\\usepackage[margin=2.5cm]{geometry}\n\\usepackage[most]{tcolorbox}\n\\usepackage{listings}\n\\usepackage{hyperref}\n\\usepackage{booktabs}\n\\usepackage{float}\n\\usepackage{fancyhdr}\n\\usepackage{tikz}\n\\usepackage{xcolor}\n\n\\newtcolorbox{knowledgebox}[1]{enhanced,colback=blue!5!white,colframe=blue!75!black,\n colbacktitle=blue!75!black,coltitle=white,fonttitle=\\bfseries,title=#1,\n attach boxed title to top left={yshift=-2mm,xshift=2mm},boxrule=1pt,sharp corners}\n\\newtcolorbox{importantbox}[1]{enhanced,colback=yellow!10!white,colframe=yellow!80!black,\n colbacktitle=yellow!80!black,coltitle=black,fonttitle=\\bfseries,title=#1,sharp corners}\n\\newtcolorbox{warningbox}[1]{enhanced,colback=red!5!white,colframe=red!75!black,\n colbacktitle=red!75!black,coltitle=white,fonttitle=\\bfseries,title=#1,sharp corners}\n\\newtcolorbox{decisionbox}[1]{enhanced,colback=green!5!white,colframe=green!60!black,\n colbacktitle=green!60!black,coltitle=white,fonttitle=\\bfseries,title=#1,sharp corners}\n\\newtcolorbox{actionbox}[1]{enhanced,colback=orange!5!white,colframe=orange!60!black,\n colbacktitle=orange!60!black,coltitle=white,fonttitle=\\bfseries,title=#1,sharp corners}\n\n\\definecolor{speaker1}{RGB}{31,119,180}\n\\definecolor{speaker2}{RGB}{255,127,14}\n\\definecolor{speaker3}{RGB}{44,160,44}\n\\definecolor{speaker4}{RGB}{214,39,40}\n\\definecolor{speaker5}{RGB}{148,103,189}\n\n\\lstset{language=Python,basicstyle=\\ttfamily\\small,keywordstyle=\\color{blue},\n stringstyle=\\color{red!60!black},commentstyle=\\color{green!60!black},\n breaklines=true,frame=single,numbers=left,numberstyle=\\tiny\\color{gray},captionpos=b}\n\n\\pagestyle{fancy}\n\\fancyhead[L]{\\small \\textcolor{gray}{[TITLE]}}\n\\fancyhead[R]{\\small \\textbf{NoteKing Pro} \\textcolor{gray}{$\\cdot$ \\href{https://github.com/bcefghj/noteking-pro}{\\texttt{github.com/bcefghj/noteking-pro}}}}\n\\fancyfoot[L]{\\small \\textcolor{gray}{小红书: \\textbf{@bcefghj}}}\n\\fancyfoot[C]{\\small \\thepage}\n\\fancyfoot[R]{\\small \\textcolor{gray}{\\href{https://github.com/bcefghj/noteking-pro}{⭐ NoteKing Pro}}}\n\\renewcommand{\\headrulewidth}{0.4pt}\n\\renewcommand{\\footrulewidth}{0.4pt}\n\n\\begin{document}\n\\begin{titlepage}\\centering\n\\vspace*{1.5cm}\n{\\large \\textcolor{gray}{👑 NoteKing Pro}\\par}\\vspace{0.5cm}\n{\\huge\\bfseries [TITLE]\\par}\\vspace{0.8cm}\n{\\large \\textcolor{gray}{Generated by NoteKing Pro}\\par}\\vspace{0.2cm}\n{\\large \\textcolor{gray}{[DATE]}\\par}\\vspace{2cm}\n\\begin{tcolorbox}[colback=gray!5,colframe=gray!40,width=0.75\\textwidth,boxrule=0.5pt]\n\\centering\n{\\small \\textbf{🔗 开源项目}}\\quad\n{\\small \\href{https://github.com/bcefghj/noteking-pro}{\\texttt{github.com/bcefghj/noteking-pro}}}\\\\[4pt]\n{\\small \\textbf{📕 小红书}}\\quad\n{\\small \\textbf{@bcefghj}}\n\\end{tcolorbox}\n\\vspace{1cm}\n\\end{titlepage}\n\\tableofcontents\\newpage\n[BODY]\n\\end{document}\n\"\"\"\n\n\n# ── PDF Pipeline ───────────────────────────────────────────────\n\nclass PDFPipeline:\n \"\"\"Full pipeline: download -> subtitles -> frames -> LLM notes -> render PDF.\"\"\"\n\n def __init__(\n self,\n api_key: str,\n base_url: str = \"https://api.minimax.chat/v1\",\n model: str = \"MiniMax-M2.7\",\n max_tokens: int = 5000,\n concurrency: int = 3,\n ):\n from openai import OpenAI\n self.client = OpenAI(api_key=api_key, base_url=base_url)\n self.model = model\n self.max_tokens = max_tokens\n self.concurrency = concurrency\n self.frame_extractor = SmartFrameExtractor()\n self.aligner = SubtitleFrameAligner()\n self.html_builder = HTMLNoteBuilder()\n self.latex_builder = LaTeXNoteBuilder()\n\n def process_episode(\n self,\n video_url: str,\n episode_num: int,\n title: str,\n work_dir: Path,\n output_dir: Path,\n total_episodes: int = 1,\n subtitle_text: str = \"\",\n ) -> EpisodeResult:\n \"\"\"Process one episode: download video -> extract frames -> LLM -> PDF.\"\"\"\n ep_work = work_dir / f\"ep{episode_num:02d}\"\n ep_work.mkdir(parents=True, exist_ok=True)\n frames_dir = ep_work / \"frames\"\n\n # Step 1: Download video\n video_path = ep_work / \"video.mp4\"\n if not (video_path.exists() and video_path.stat().st_size > 50000):\n self._download_video(video_url, video_path)\n\n duration = _get_duration(video_path) if video_path.exists() else 0\n\n # Step 2: Extract smart keyframes\n if video_path.exists():\n frames = self.frame_extractor.extract(video_path, frames_dir)\n else:\n frames = []\n\n # Step 3: Generate notes with LLM\n notes_md = self._generate_notes(\n episode_num, title, duration, subtitle_text, frames, total_episodes\n )\n\n # Step 4: Build HTML + PDF\n meta = {\n \"Episode\": f\"{episode_num}/{total_episodes}\",\n \"Title\": title,\n \"Duration\": f\"{int(duration//60)}m{int(duration%60)}s\",\n }\n\n html_content = self.html_builder.build_html(notes_md, frames, title, meta)\n html_path = output_dir / f\"ep{episode_num:02d}_{_safe(title)}.html\"\n html_path.write_text(html_content, encoding=\"utf-8\")\n\n pdf_path = html_path.with_suffix(\".pdf\")\n HTMLNoteBuilder.html_to_pdf(html_path, pdf_path)\n\n result = EpisodeResult(\n episode=episode_num,\n title=title,\n duration=duration,\n frames=frames,\n subtitle_text=subtitle_text,\n notes_md=notes_md,\n html_path=html_path,\n pdf_path=pdf_path if pdf_path.exists() else None,\n )\n return result\n\n def process_collection(\n self,\n episodes: list[dict],\n work_dir: Path,\n output_dir: Path,\n collection_title: str = \"Video Collection\",\n ) -> CollectionResult:\n \"\"\"Process multiple episodes, optionally concurrently.\"\"\"\n output_dir.mkdir(parents=True, exist_ok=True)\n total = len(episodes)\n results: list[EpisodeResult] = [None] * total # type: ignore\n\n def _do_one(idx: int) -> EpisodeResult:\n ep = episodes[idx]\n print(f\" [{idx+1}/{total}] {ep['title'][:40]}...\")\n r = self.process_episode(\n video_url=ep[\"url\"],\n episode_num=idx + 1,\n title=ep[\"title\"],\n work_dir=work_dir,\n output_dir=output_dir / \"episodes\",\n total_episodes=total,\n subtitle_text=ep.get(\"subtitle_text\", \"\"),\n )\n print(f\" [{idx+1}/{total}] done ({len(r.notes_md)} chars)\")\n return r\n\n # Concurrent execution\n with ThreadPoolExecutor(max_workers=self.concurrency) as pool:\n futures = {pool.submit(_do_one, i): i for i in range(total)}\n for future in as_completed(futures):\n idx = futures[future]\n try:\n results[idx] = future.result()\n except Exception as e:\n print(f\" [{idx+1}/{total}] FAILED: {e}\")\n results[idx] = EpisodeResult(\n episode=idx + 1,\n title=episodes[idx][\"title\"],\n duration=0,\n frames=[],\n subtitle_text=\"\",\n notes_md=f\"*Generation failed: {e}*\",\n )\n\n # Merge into one big HTML\n merged_html = self._merge_html(results, collection_title, output_dir)\n merged_pdf = merged_html.with_suffix(\".pdf\")\n HTMLNoteBuilder.html_to_pdf(merged_html, merged_pdf)\n\n return CollectionResult(\n title=collection_title,\n episodes=results,\n merged_pdf=merged_pdf if merged_pdf.exists() else None,\n merged_html=merged_html,\n )\n\n # ── Internal helpers ────────────────────────────────────────\n\n def _download_video(self, url: str, output: Path) -> None:\n cmd = (\n f'yt-dlp \"{url}\" '\n f'-f \"bestvideo[height<=720]+bestaudio/best[height<=720]\" '\n f'--merge-output-format mp4 '\n f'-o \"{output}\" --no-warnings --quiet'\n )\n subprocess.run(cmd, shell=True, capture_output=True, timeout=300)\n\n def _generate_notes(\n self,\n ep_num: int,\n title: str,\n duration: float,\n subtitle_text: str,\n frames: list[ScoredFrame],\n total: int,\n ) -> str:\n frames_desc = \"\"\n if frames:\n frames_desc = \"Available keyframes:\\n\" + \"\\n\".join(\n f\" Fig.{i+1} at {f.time_str}: {f.subtitle_text[:60] or '(no subtitle)'}\"\n for i, f in enumerate(frames)\n )\n\n mins = int(duration // 60)\n secs = int(duration % 60)\n\n prompt = f\"\"\"Generate detailed illustrated lecture notes for this video episode.\n\nEpisode: {ep_num}/{total} \"{title}\" ({mins}m{secs}s)\n\nTranscript:\n{subtitle_text[:6000] if subtitle_text else \"(No transcript; generate notes based on the title and topic)\"}\n\n{frames_desc}\n\nFORMAT REQUIREMENTS:\n1. Use ## for sections, ### for subsections\n2. Insert {{IMAGE:N}} where figure N should appear (e.g. {{IMAGE:1}}, {{IMAGE:2}})\n3. Use highlight boxes for key concepts:\n {{IMPORTANT}}Key concept text{{/IMPORTANT}}\n {{KNOWLEDGE}}Background knowledge{{/KNOWLEDGE}}\n {{WARNING}}Common pitfall{{/WARNING}}\n4. Include code blocks with ``` when code appears\n5. Use LaTeX math ($...$) for formulas\n6. End each section with a brief summary\n7. Write in Chinese\n\nCONTENT REQUIREMENTS:\n- Detailed, structured, suitable for serious study\n- Each figure reference must have a descriptive caption below it\n- Include code with detailed comments when relevant\n- Cover all major points from the transcript\n- Add a final summary section with key takeaways\"\"\"\n\n return self._call_llm(prompt)\n\n def _call_llm(self, prompt: str, retries: int = 3) -> str:\n system = \"You are a professional technical education expert. Output lecture notes directly in Markdown format with the specified markers. Do not wrap output in think tags.\"\n\n for attempt in range(retries):\n try:\n full = \"\"\n stream = self.client.chat.completions.create(\n model=self.model,\n messages=[\n {\"role\": \"system\", \"content\": system},\n {\"role\": \"user\", \"content\": prompt},\n ],\n max_tokens=self.max_tokens,\n stream=True,\n temperature=0.2,\n timeout=120,\n )\n for chunk in stream:\n delta = chunk.choices[0].delta.content or \"\"\n full += delta\n # Strip <think> blocks\n full = re.sub(r\"<think>.*?</think>\", \"\", full, flags=re.DOTALL).strip()\n if len(full) > 200:\n return full\n except Exception as e:\n if attempt < retries - 1:\n time.sleep((attempt + 1) * 8)\n else:\n return f\"*Note generation failed after {retries} attempts: {e}*\"\n return \"\"\n\n def _merge_html(\n self, episodes: list[EpisodeResult], title: str, output_dir: Path\n ) -> Path:\n \"\"\"Merge all episodes into one big HTML document.\"\"\"\n parts = [f\"\"\"<!DOCTYPE html>\n<html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><title>{title}</title>\n{HTMLNoteBuilder.STYLE}\n</head><body>\n<div class=\"cover\"><h1>{title}</h1>\n<p>Generated by NoteKing · {time.strftime('%Y-%m-%d')}</p>\n</div>\n<div class=\"toc\"><h2>Table of Contents</h2><ol>\"\"\"]\n\n for ep in episodes:\n if ep:\n parts.append(f'<li><a href=\"#ep{ep.episode}\">{ep.title}</a></li>')\n parts.append(\"</ol></div>\")\n\n for ep in episodes:\n if not ep:\n continue\n parts.append(f'<h1 id=\"ep{ep.episode}\">Ep.{ep.episode}: {ep.title}</h1>')\n\n body = ep.notes_md\n # Replace image markers with actual images\n def _rep(m, ep=ep):\n n = int(m.group(1))\n if 1 <= n <= len(ep.frames):\n fr = ep.frames[n - 1]\n return (\n f'\\n<img src=\"file://{fr.path.resolve()}\" alt=\"frame\">\\n'\n f'<div class=\"frame-caption\">Fig.{n} — {fr.time_str}</div>\\n'\n )\n return \"\"\n body = re.sub(r\"\\{IMAGE:(\\d+)\\}\", _rep, body)\n body = re.sub(r\"\\{IMPORTANT\\}(.*?)\\{/IMPORTANT\\}\",\n r'<div class=\"important-box\">\\1</div>', body, flags=re.DOTALL)\n body = re.sub(r\"\\{KNOWLEDGE\\}(.*?)\\{/KNOWLEDGE\\}\",\n r'<div class=\"knowledge-box\">\\1</div>', body, flags=re.DOTALL)\n body = re.sub(r\"\\{WARNING\\}(.*?)\\{/WARNING\\}\",\n r'<div class=\"warning-box\">\\1</div>', body, flags=re.DOTALL)\n parts.append(body)\n parts.append(\"<hr>\")\n\n parts.append(\"</body></html>\")\n\n out = output_dir / f\"{_safe(title)}_full.html\"\n out.write_text(\"\\n\".join(parts), encoding=\"utf-8\")\n return out\n\n\n# ── Helpers ────────────────────────────────────────────────────\n\ndef _safe(text: str, max_len: int = 40) -> str:\n return \"\".join(c if c.isalnum() or c in \"_- \" else \"_\" for c in text)[:max_len].strip()\n\n\ndef _tex_escape(text: str) -> str:\n specials = {\"&\": r\"\\&\", \"%\": r\"\\%\", \"$\": r\"\\$\", \"#\": r\"\\#\",\n \"_\": r\"\\_\", \"{\": r\"\\{\", \"}\": r\"\\}\", \"~\": r\"\\textasciitilde{}\",\n \"^\": r\"\\^{}\"}\n for k, v in specials.items():\n text = text.replace(k, v)\n return text\n"}}, "core/templates/meeting_minutes.py": {"9269bcd7b1f6eb8ce72f720a1e2be92f40178f8b": {"t": 1785599228.4500716, "n": 1579, "h": "c93f89cbf951f292", "c": "\"\"\"Meeting minutes template: structured meeting notes with action items.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass MeetingMinutesTemplate(BaseTemplate):\n name = \"meeting_minutes\"\n display_name = \"会议纪要\"\n description = \"Structured meeting minutes with attendees, topics, decisions, and action items\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n num_speakers = ctx.extra.get(\"num_speakers\", 0)\n speakers = ctx.extra.get(\"speakers\", [])\n\n context_info = f\"\\n内容描述: {user_context}\" if user_context else \"\"\n speaker_info = \"\"\n if speakers:\n speaker_info = f\"\\n检测到 {num_speakers} 位说话人: {', '.join(speakers)}\"\n\n return f\"\"\"请根据以下会议录音转录稿,生成一份专业的结构化会议纪要。\n\n会议标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}{speaker_info}\n\n会议录音转录稿(带说话人标签):\n{transcript}\n\n请按以下格式输出会议纪要:\n\n# 会议纪要: [会议标题]\n\n## 会议基本信息\n- **日期**: [从内容推断或标注\"待确认\"]\n- **时长**: {ctx.meta.duration:.0f} 秒\n- **参会人**: [从说话人标签和内容推断参会人身份及角色]\n\n## 会议摘要\n[2-3句话概括会议核心内容和成果]\n\n## 议题与讨论\n\n### 议题 1: [议题名称]\n- **讨论要点**:\n - [要点1,标注发言人]\n - [要点2]\n- **结论/决议**: [达成的共识或决定]\n\n### 议题 2: ...\n[继续列出所有议题]\n\n## 关键决议\n- [决议1]\n- [决议2]\n\n## 行动项\n| 序号 | 行动事项 | 负责人 | 截止日期 |\n|------|---------|--------|---------|\n| 1 | [事项描述] | [负责人] | [日期/待定] |\n\n## 重要引用\n> [会议中的关键原话]\n> —— [发言人]\n\n## 后续跟进\n- [需要跟进的事项]\n\n要求:\n1. 从对话内容中智能识别议题、决议和行动项\n2. 保持客观准确,不添加未讨论的内容\n3. 用中文输出\n4. 重要观点标注发言人\n5. 行动项要具体、可执行\"\"\"\n"}}, "core/templates/news.py": {"f218ad972edf728c9355875f5522104b306737c6": {"t": 1785599228.7491927, "n": 797, "h": "cd7610cd3a60359c", "c": "\"\"\"News/information summary template.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass NewsTemplate(BaseTemplate):\n name = \"news\"\n display_name = \"新闻速览\"\n description = \"News-style quick summary for information and current events\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n return f\"\"\"请根据以下视频内容,生成新闻速览格式的摘要。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n发布日期: {ctx.meta.upload_date}\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 格式为:\n\n## 📰 新闻速览\n\n**标题:** [一句话标题]\n**来源:** {ctx.meta.uploader} | {ctx.meta.upload_date}\n\n### 核心要点 (TL;DR)\n- 要点 1\n- 要点 2\n- 要点 3\n\n### 详细内容\n[分段展开核心内容,每段 2-3 句话]\n\n### 相关背景\n[补充理解所需的背景信息]\n\n### 影响分析\n[这条信息的意义和可能的影响]\n\n2. 语言风格: 客观、简洁、信息密度高\n3. 避免主观评价,侧重事实陈述\n\n请直接输出新闻速览:\"\"\"\n"}}, "core/templates/lecture_notes.py": {"44fcc3363401ca30e59123fadb37ef14e4a7c42e": {"t": 1785599228.8020613, "n": 1225, "h": "6c4f90ca353da954", "c": "\"\"\"Lecture notes template: structured knowledge with formulas and exercises.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass LectureNotesTemplate(BaseTemplate):\n name = \"lecture_notes\"\n display_name = \"课堂笔记\"\n description = \"Structured lecture notes with knowledge hierarchy, formulas, examples, and study tips\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n context_info = f\"\\n课程描述: {user_context}\" if user_context else \"\"\n\n return f\"\"\"请根据以下课程/讲座录音转录稿,生成一份专业的课堂学习笔记。\n\n课程标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}\n\n课程录音转录稿:\n{transcript}\n\n请按以下格式输出课堂笔记:\n\n# 课堂笔记: [课程标题]\n\n## 课程概览\n[简要概括本节课的主题和学习目标]\n\n## 知识点详解\n\n### 1. [知识点名称]\n\n**核心概念**: [详细解释]\n\n**重要公式/定义**:\n$$[LaTeX 公式,如果有]$$\n\n**示例**: [举例说明]\n\n**易错点**: [常见误区和注意事项]\n\n### 2. [知识点名称]\n[继续...]\n\n## 关键公式汇总\n| 公式 | 含义 | 适用场景 |\n|------|------|---------|\n\n## 课堂练习\n1. [从内容中提取或推导的练习题]\n2. [练习题]\n\n## 学习要点总结\n- [要点1]\n- [要点2]\n- [要点3]\n\n## 延伸学习建议\n- [推荐阅读/学习资源]\n\n要求:\n1. 按知识点逻辑组织,不按时间顺序\n2. 重要公式用 LaTeX 格式 ($..$ 或 $$..$$)\n3. 代码示例用代码块\n4. 标注难点和易错点\n5. 用中文输出\"\"\"\n"}}, "core/templates/news_digest.py": {"797abab87a6ee96581f9924c307ab201f8783e8e": {"t": 1785599229.001382, "n": 1180, "h": "af73a131cfd6b475", "c": "\"\"\"News digest template: 5W1H structured summary for news/podcast content.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass NewsDigestTemplate(BaseTemplate):\n name = \"news_digest\"\n display_name = \"新闻/播客摘要\"\n description = \"5W1H structured news digest with quotes and background\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n context_info = f\"\\n描述: {user_context}\" if user_context else \"\"\n\n return f\"\"\"请根据以下新闻/播客录音转录稿,生成一份结构化的新闻摘要。\n\n标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}\n\n录音转录稿:\n{transcript}\n\n请按以下格式输出:\n\n# 新闻摘要: [标题]\n\n## 一句话速览\n[一句话概括核心新闻]\n\n## 5W1H 结构分析\n- **Who (谁)**: [涉及的人物/组织]\n- **What (什么)**: [发生了什么]\n- **When (何时)**: [时间]\n- **Where (何地)**: [地点/场景]\n- **Why (为何)**: [原因/动机]\n- **How (如何)**: [过程/方式]\n\n## 关键事实\n1. [事实1]\n2. [事实2]\n3. [事实3]\n\n## 各方观点\n| 发言人/立场 | 核心观点 |\n|------------|---------|\n\n## 关键引用\n> \"[引用1]\" —— [发言人]\n> \"[引用2]\" —— [发言人]\n\n## 背景补充\n[相关背景知识和上下文]\n\n## 影响分析\n[对相关领域的潜在影响]\n\n要求:\n1. 区分事实和观点\n2. 保留关键引用原话\n3. 提供必要的背景信息\n4. 用中文输出\"\"\"\n"}}, "core/templates/podcast_shownotes.py": {"882f2828c086cddc3a2e2042e236565f28ce9532": {"t": 1785599229.2110171, "n": 1474, "h": "ef881e8944bc535b", "c": "\"\"\"Podcast show notes template: guest info, chapter timestamps, and key takeaways.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass PodcastShowNotesTemplate(BaseTemplate):\n name = \"podcast_shownotes\"\n display_name = \"播客节目笔记\"\n description = \"Podcast show notes with guest info, chapter timestamps, and takeaways\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n speakers = ctx.extra.get(\"speakers\", [])\n\n context_info = f\"\\n描述: {user_context}\" if user_context else \"\"\n speaker_info = f\"\\n嘉宾/主持人: {', '.join(speakers)}\" if speakers else \"\"\n\n return f\"\"\"请根据以下播客/对话节目录音转录稿,生成一份节目笔记。\n\n节目标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}{speaker_info}\n\n录音转录稿:\n{transcript}\n\n请按以下格式输出:\n\n# 节目笔记: [标题]\n\n## 节目信息\n- **主题**: [节目主题]\n- **主持人**: [主持人]\n- **嘉宾**: [嘉宾及身份]\n- **时长**: {ctx.meta.duration:.0f} 秒\n\n## 内容摘要\n[3-5句话概括节目核心内容]\n\n## 章节时间线\n| 时间 | 章节 | 要点 |\n|------|------|------|\n| [00:00] | [开场/引入] | [简述] |\n| [MM:SS] | [话题1] | [简述] |\n| [MM:SS] | [话题2] | [简述] |\n\n## 各章节详情\n\n### [话题1] (MM:SS - MM:SS)\n[详细内容]\n\n**嘉宾观点**:\n- [观点1]\n- [观点2]\n\n### [话题2]\n...\n\n## 关键收获 (Key Takeaways)\n1. [收获1]\n2. [收获2]\n3. [收获3]\n\n## 金句\n- > \"[金句1]\" —— [说话人]\n- > \"[金句2]\" —— [说话人]\n\n## 提到的资源/推荐\n- [书籍/工具/网站等]\n\n要求:\n1. 时间戳尽量准确\n2. 按话题组织章节\n3. 保留重要金句原话\n4. 列出提到的资源推荐\n5. 用中文输出\"\"\"\n"}}, "core/templates/quiz.py": {"18b1b26c659a1d2b4d635268e3a7a2e1104df0d1": {"t": 1785599229.5740616, "n": 820, "h": "a15c8e2b6f93b6a0", "c": "\"\"\"Quiz template: multiple-choice and short-answer questions.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass QuizTemplate(BaseTemplate):\n name = \"quiz\"\n display_name = \"测验题\"\n description = \"Multiple-choice and short-answer quiz questions\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n return f\"\"\"请根据以下视频内容,生成一套测验题。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 生成 10-15 道题目,包含:\n - 5-8 道选择题 (单选,4 个选项)\n - 3-5 道简答题\n - 2-3 道判断题 (对/错)\n2. 每道选择题格式:\n\n#### 第 N 题 (选择题)\n[题目内容]\n- A. [选项]\n- B. [选项]\n- C. [选项]\n- D. [选项]\n\n**答案:** [正确选项]\n**解析:** [为什么选这个答案]\n\n3. 每道简答题格式:\n\n#### 第 N 题 (简答题)\n[题目内容]\n\n**参考答案:**\n[详细解答]\n\n4. 题目难度分布: 简单 40%、中等 40%、困难 20%\n5. 覆盖视频的主要知识点\n\n请直接输出测验题:\"\"\"\n"}}, "core/templates/flashcard.py": {"709aff28d8abe180ea7c572fe4620eb929f7e352": {"t": 1785599229.7790642, "n": 835, "h": "85dfdd3268a3aa1b", "c": "\"\"\"Flashcard template: Anki-compatible Q&A pairs.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass FlashCardTemplate(BaseTemplate):\n name = \"flashcard\"\n display_name = \"闪卡 (Anki)\"\n description = \"Anki-compatible flashcards with Q&A pairs for spaced repetition\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n return f\"\"\"请根据以下视频内容,生成一组学习闪卡 (Flashcards)。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 生成 15-30 张闪卡\n2. 每张闪卡格式:\n\n### 卡片 N\n**Q:** [问题]\n**A:** [答案]\n\n3. 问题类型多样化:\n - 概念定义题: \"什么是 X?\"\n - 对比区分题: \"X 和 Y 的区别是什么?\"\n - 应用场景题: \"什么时候应该使用 X?\"\n - 原理机制题: \"X 是如何工作的?\"\n - 关键数据题: \"X 的关键参数/指标是什么?\"\n4. 答案简洁但完整,控制在 1-3 句话\n5. 遵循 Anki 闪卡的原子性原则: 每张卡只测试一个知识点\n6. 在文件开头注明视频来源信息\n\n请直接输出闪卡内容:\"\"\"\n"}}, "core/templates/mindmap.py": {"a4a396d3bc630ebba3611825b27b12c1f8a1078e": {"t": 1785599229.8228395, "n": 754, "h": "af3e91a08ee04ea2", "c": "\"\"\"Mind map template: Markmap-compatible markdown.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass MindMapTemplate(BaseTemplate):\n name = \"mindmap\"\n display_name = \"思维导图\"\n description = \"Markmap-compatible hierarchical mind map in Markdown\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n return f\"\"\"请根据以下视频内容,生成一份思维导图格式的笔记。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 使用 Markdown 标题层级表示思维导图结构 (# -> ## -> ### -> ####)\n2. 第一级 (#) 是视频主题\n3. 第二级 (##) 是主要分支 (3-7 个)\n4. 第三级 (###) 是子主题\n5. 第四级 (####) 是具体要点\n6. 每个叶子节点用简短的一句话或关键词\n7. 确保结构清晰、层次分明\n8. 适合用 Markmap 工具渲染为可视化思维导图\n\n请直接输出思维导图内容 (纯 Markdown 标题层级格式):\"\"\"\n"}}, "core/templates/timeline.py": {"ec67f75546524a148a74d1941e5b7dad2f9dfef7": {"t": 1785599229.9859533, "n": 986, "h": "d7a87520ad3dba44", "c": "\"\"\"Timeline template: timestamped notes with jump links.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass TimelineTemplate(BaseTemplate):\n name = \"timeline\"\n display_name = \"时间线笔记\"\n description = \"Timestamped notes aligned to video timeline\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n segments_text = \"\"\n for seg in ctx.subtitles.segments[:500]:\n segments_text += f\"[{seg.start_ts}] {seg.text}\\n\"\n\n if not segments_text:\n segments_text = _truncate_transcript(ctx.transcript)\n\n url = ctx.meta.webpage_url\n return f\"\"\"请根据以下带时间戳的视频内容,生成时间线笔记。\n\n视频标题: {ctx.meta.title}\n视频链接: {url}\n作者: {ctx.meta.uploader}\n时长: {ctx.meta.duration:.0f} 秒\n\n带时间戳的内容:\n{segments_text}\n\n要求:\n1. 按时间顺序组织,每个重要节点标注时间戳\n2. 格式为:\n\n## 🎬 视频时间线\n\n### [00:00:00] 开场/引言\n- 要点 1\n- 要点 2\n\n### [00:05:30] 第一个主题\n- 核心观点\n- 关键细节\n\n3. 合并相近的内容段落,不需要逐句翻译\n4. 每个时间节点提炼 2-5 个要点\n5. 标注转折点、高潮点、重要结论\n6. 在文件末尾生成一个快速跳转索引\n\n请直接输出时间线笔记:\"\"\"\n"}}, "core/templates/podcast.py": {"e9a443e9dbb6a6f96d7702d11d0381c2880d2bc6": {"t": 1785599230.0482266, "n": 907, "h": "48ef7dc5e77fc583", "c": "\"\"\"Podcast/interview summary template.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass PodcastTemplate(BaseTemplate):\n name = \"podcast\"\n display_name = \"播客/访谈摘要\"\n description = \"Structured summary for podcasts, interviews, and discussions\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n return f\"\"\"请根据以下播客/访谈内容,生成一份结构化摘要。\n\n标题: {ctx.meta.title}\n主持/嘉宾: {ctx.meta.uploader}\n时长: {ctx.meta.duration:.0f} 秒\n\n内容转录:\n{transcript}\n\n要求:\n1. 格式为:\n\n## 🎙️ 播客摘要: {ctx.meta.title}\n\n### 参与者\n- [识别并列出所有参与者及其身份/背景]\n\n### 主题概述\n[1-2 句话概括本期主题]\n\n### 核心观点\n1. **[观点 1 标题]**: 详细说明 (标注来自哪位嘉宾)\n2. **[观点 2 标题]**: 详细说明\n3. ...\n\n### 精彩语录\n> \"[原话摘录 1]\" —— [发言者]\n> \"[原话摘录 2]\" —— [发言者]\n\n### 讨论亮点\n- [值得关注的讨论点或交锋]\n\n### 提到的资源/推荐\n- [提到的书籍/工具/网站等]\n\n### 要点总结\n- 3-5 条核心收获\n\n2. 区分不同发言者的观点\n3. 保留有价值的原始语录\n4. 标注有争议或有趣的讨论点\n\n请直接输出播客摘要:\"\"\"\n"}}, "core/templates/custom.py": {"eac549cb9f174162f2f892f392a7b84718cf4318": {"t": 1785599230.4078875, "n": 769, "h": "4aac302fbc466fd0", "c": "\"\"\"Custom template: user-defined prompt.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass CustomTemplate(BaseTemplate):\n name = \"custom\"\n display_name = \"自定义模板\"\n description = \"User-defined prompt template for any output format\"\n\n def __init__(self, user_prompt: str = \"\"):\n self.user_prompt = user_prompt\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n custom = self.user_prompt or ctx.extra.get(\"custom_prompt\", \"\")\n\n if not custom:\n custom = \"请对这个视频内容进行全面分析和总结。\"\n\n return f\"\"\"视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n时长: {ctx.meta.duration:.0f} 秒\n\n视频内容转录:\n{transcript}\n\n用户自定义要求:\n{custom}\n\n请根据上述要求处理视频内容:\"\"\"\n"}}, "core/templates/tutorial.py": {"5c68397cdc3a46f4867fddbbb4afd414551b1389": {"t": 1785599230.5497675, "n": 822, "h": "913338af9f9a3ce8", "c": "\"\"\"Tutorial step-by-step template.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass TutorialTemplate(BaseTemplate):\n name = \"tutorial\"\n display_name = \"教程步骤\"\n description = \"Step-by-step tutorial notes extracted from how-to videos\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n return f\"\"\"请根据以下视频内容,提取并整理成步骤化教程笔记。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 格式为:\n\n## 🛠️ 教程: {ctx.meta.title}\n\n### 前置条件\n- 需要准备的工具/软件/知识\n\n### 步骤 1: [标题]\n**目标:** 这一步要达到什么效果\n**操作:**\n1. 具体操作 1\n2. 具体操作 2\n**代码/命令:** (如果有)\n```\n具体代码或命令\n```\n**注意事项:** 常见问题和解决方案\n\n### 步骤 2: [标题]\n...\n\n### 常见问题 FAQ\n- Q: 问题 1?\n A: 解答 1\n\n### 总结\n- 完成后应该达到的效果\n\n2. 提取所有代码片段、命令行操作\n3. 标注每个步骤的预期耗时\n4. 标注难度等级\n\n请直接输出教程笔记:\"\"\"\n"}}, "demos/run_minimind.py": {"ae29f1b47b40dc8476ef090979bc5c2d4add0cf7": {"t": 1785599231.0150998, "n": 16910, "h": "cbbd5f7dad5f94be", "c": "#!/usr/bin/env python3\n\"\"\"\nNoteKing Demo: Process all 26 episodes of MiniMind into illustrated PDF lecture notes.\n\nUsage: python3.11 run_minimind.py\n\"\"\"\n\nimport sys, os, time, re, json, subprocess\nfrom pathlib import Path\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\nsys.path.insert(0, str(Path(__file__).resolve().parent.parent))\n\nfrom openai import OpenAI\n\n# ── Config ──\nAPI_KEY = \"sk-cp-vX4T-YhmjkytkOexcwZ-uAdmALWR8ggXmtGOymuJQ1lfNLOR1phT0Ju09VggOTENL-y1pGe-KC4fTQppbzn_X_WPxVIApwG71PlvZHCGgfaIRH2zYoAI_RA\"\nBASE_URL = \"https://api.minimax.chat/v1\"\nMODEL = \"MiniMax-M2.7\"\nCONCURRENCY = 3\nMAX_TOKENS = 5000\n\nBASE_DIR = Path(__file__).resolve().parent / \"minimind\"\nVIDEOS_DIR = BASE_DIR / \"videos\"\nFRAMES_DIR = BASE_DIR / \"frames\"\nNOTES_DIR = BASE_DIR / \"notes\"\nPDF_DIR = BASE_DIR / \"pdf\"\n\nEPISODES = [\n (1, \"开篇\", 170, \"课程介绍与背景\"),\n (2, \"前言\", 268, \"大模型基础概念、MiniMind技术栈\"),\n (3, \"必看:前言补充\", 123, \"勘误与补充说明\"),\n (4, \"前置知识\", 437, \"PyTorch张量、矩阵乘法、注意力机制原理\"),\n (5, \"架构图解读\", 346, \"Transformer架构全图逐层解析\"),\n (6, \"初始化项目\", 227, \"项目结构设计、ModelArgs配置类\"),\n (7, \"理论:RMSNorm\", 246, \"RMSNorm原理与数学推导\"),\n (8, \"代码:RMSNorm\", 385, \"RMSNorm的PyTorch实现\"),\n (9, \"理论:RoPE&YaRN\", 831, \"旋转位置编码RoPE + YaRN外推原理\"),\n (10, \"代码:RoPE&YaRN\", 1040, \"RoPE实现、旋转矩阵计算、YaRN缩放\"),\n (11, \"理论:GQA\", 230, \"分组查询注意力 MHA/MQA/GQA对比\"),\n (12, \"代码:GQA 上\", 788, \"Q/K/V投影、repeat_kv\"),\n (13, \"代码:GQA 下\", 1162, \"注意力计算、因果掩码、输出投影\"),\n (14, \"理论:FFN\", 376, \"前馈网络、SwiGLU激活函数\"),\n (15, \"代码:FFN\", 339, \"FeedForward类的PyTorch实现\"),\n (16, \"拼接:Block\", 347, \"TransformerBlock组装\"),\n (17, \"组装:Model\", 703, \"Transformer完整实现\"),\n (18, \"封装:CausalLM\", 600, \"因果语言模型封装、generate推理\"),\n (19, \"回顾与知识检验\", 435, \"全架构回顾、核心概念测验\"),\n (20, \"必看:纠错补充\", 475, \"代码勘误、补充说明\"),\n (21, \"重制Dataset:理论\", 332, \"预训练数据格式、tokenizer\"),\n (22, \"重制Dataset:代码\", 484, \"PretrainDataset类实现\"),\n (23, \"重制Pretrain:理论\", 344, \"预训练目标、损失函数\"),\n (24, \"重制Pretrain:代码\", 580, \"训练循环、优化器、学习率调度\"),\n (25, \"训练,启动!\", 343, \"实际训练流程、日志分析\"),\n (26, \"Eval:完结!\", 497, \"模型评估、推理测试、展望\"),\n]\n\nclient = OpenAI(api_key=API_KEY, base_url=BASE_URL)\n\n\ndef strip_think(text):\n return re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()\n\n\ndef find_video(ep_num):\n \"\"\"Find downloaded video file for episode.\"\"\"\n patterns = [\n f\"p{ep_num:02d}_*.mp4\",\n f\"*p{ep_num:02d}*.mp4\",\n f\"*_p{ep_num:02d}_*.mp4\",\n ]\n for pat in patterns:\n matches = list(VIDEOS_DIR.glob(pat))\n if matches:\n return matches[0]\n return None\n\n\ndef extract_frames(video_path, ep_num, title):\n \"\"\"Extract keyframes for one episode.\"\"\"\n ep_frames_dir = FRAMES_DIR / f\"p{ep_num:02d}\"\n ep_frames_dir.mkdir(parents=True, exist_ok=True)\n\n existing = sorted(ep_frames_dir.glob(\"*.jpg\"))\n if len(existing) >= 3:\n return existing\n\n try:\n sys.path.insert(0, str(Path(__file__).resolve().parent.parent))\n from core.frames import extract_keyframes\n from core.frames import ExtractedFrame\n\n frames = extract_keyframes(\n video_path, ep_frames_dir,\n max_frames=12, threshold=25.0,\n dedup=True, score=True,\n )\n return [f.path for f in frames]\n except Exception as e:\n print(f\" SceneDetect/scoring failed ({e}), using ffmpeg uniform...\")\n\n # Fallback: uniform\n duration_cmd = f'ffprobe -v quiet -show_entries format=duration -of csv=p=0 \"{video_path}\"'\n r = subprocess.run(duration_cmd, shell=True, capture_output=True, text=True, timeout=30)\n duration = float(r.stdout.strip()) if r.stdout.strip() else 300\n\n interval = max(15, duration / 12)\n frame_paths = []\n t = 3.0\n idx = 0\n while t < duration - 3 and idx < 15:\n out = ep_frames_dir / f\"frame_{idx:03d}_{t:.0f}s.jpg\"\n cmd = f'ffmpeg -ss {t:.2f} -i \"{video_path}\" -vframes 1 -q:v 3 -vf scale=960:-1 \"{out}\" -y -loglevel error'\n subprocess.run(cmd, shell=True, capture_output=True, timeout=30)\n if out.exists() and out.stat().st_size > 3000:\n frame_paths.append(out)\n t += interval\n idx += 1\n\n return frame_paths\n\n\ndef generate_notes(ep_num, title, duration, content_hint, frame_paths):\n \"\"\"Generate illustrated lecture notes for one episode via LLM.\"\"\"\n mins = duration // 60\n secs = duration % 60\n\n frames_desc = \"\"\n if frame_paths:\n frames_desc = \"Available keyframes (insert {IMAGE:N} where appropriate):\\n\"\n for i, fp in enumerate(frame_paths):\n m = re.search(r'(\\d+\\.?\\d*)s', fp.name)\n ts = m.group(1) if m else str(i * 30)\n frames_desc += f\" Fig.{i+1} at {ts}s\\n\"\n\n prompt = f\"\"\"Generate detailed illustrated lecture notes in Chinese for this MiniMind episode.\n\nEpisode: {ep_num}/26 \"{title}\" ({int(mins)}m{int(secs)}s)\nTopic: {content_hint}\nCourse: MiniMind - PyTorch从零手敲大模型\n\n{frames_desc}\n\nREQUIRED FORMAT:\n- Use ## for sections, ### for subsections\n- Insert {{IMAGE:N}} markers for keyframe figures (e.g. {{IMAGE:1}})\n- Use highlight boxes:\n {{IMPORTANT}}核心概念{{/IMPORTANT}}\n {{KNOWLEDGE}}背景知识{{/KNOWLEDGE}}\n {{WARNING}}易错点{{/WARNING}}\n- Code blocks with ```python\n- Math with $...$ or $$...$$\n- Each section ends with brief summary\n\nCONTENT:\n- Write as a professional lecture note, 2000+ characters\n- Cover all knowledge points for this topic\n- Include code with comments when relevant\n- Add formulas with explanation\n- End with key takeaways and 1-2 thought questions\"\"\"\n\n for attempt in range(3):\n try:\n full = \"\"\n stream = client.chat.completions.create(\n model=MODEL,\n messages=[\n {\"role\": \"system\", \"content\": \"你是专业的AI技术讲义编写专家。直接输出Markdown格式讲义。\"},\n {\"role\": \"user\", \"content\": prompt}\n ],\n max_tokens=MAX_TOKENS, stream=True, temperature=0.2, timeout=120,\n )\n for chunk in stream:\n delta = chunk.choices[0].delta.content or \"\"\n full += delta\n return strip_think(full)\n except Exception as e:\n if attempt < 2:\n time.sleep((attempt + 1) * 8)\n else:\n return f\"*Episode {ep_num} generation failed: {e}*\"\n return \"\"\n\n\ndef build_html(notes_md, frame_paths, ep_num, title, duration):\n \"\"\"Build HTML lecture note with embedded images.\"\"\"\n def replace_img(match):\n n = int(match.group(1))\n if 1 <= n <= len(frame_paths):\n fp = frame_paths[n - 1]\n return f'\\n<img src=\"file://{fp.resolve()}\" alt=\"fig{n}\" style=\"max-width:100%;border-radius:6px;box-shadow:0 3px 12px rgba(0,0,0,.12);margin:1rem auto;display:block\">\\n<div style=\"text-align:center;color:#666;font-size:.85em;margin-bottom:1rem\">Fig.{n}</div>\\n'\n return f\"*(Fig.{n})*\"\n\n body = re.sub(r'\\{IMAGE:(\\d+)\\}', replace_img, notes_md)\n body = re.sub(r'\\{IMPORTANT\\}(.*?)\\{/IMPORTANT\\}',\n r'<div style=\"background:#fff8e1;border-left:4px solid #ffb300;padding:1rem;margin:1rem 0;border-radius:0 8px 8px 0\"><b>⭐ 重点</b><br>\\1</div>',\n body, flags=re.DOTALL)\n body = re.sub(r'\\{KNOWLEDGE\\}(.*?)\\{/KNOWLEDGE\\}',\n r'<div style=\"background:#e3f2fd;border-left:4px solid #1565c0;padding:1rem;margin:1rem 0;border-radius:0 8px 8px 0\"><b>📖 知识补充</b><br>\\1</div>',\n body, flags=re.DOTALL)\n body = re.sub(r'\\{WARNING\\}(.*?)\\{/WARNING\\}',\n r'<div style=\"background:#fce4ec;border-left:4px solid #c62828;padding:1rem;margin:1rem 0;border-radius:0 8px 8px 0\"><b>⚠️ 注意</b><br>\\1</div>',\n body, flags=re.DOTALL)\n\n style = \"\"\"<style>\nbody{font-family:\"PingFang SC\",\"Noto Sans CJK SC\",sans-serif;max-width:900px;margin:0 auto;padding:2rem;line-height:1.8;color:#2d2d2d}\nh1{color:#1a1a2e;border-bottom:3px solid #e94560;padding-bottom:.5rem}\nh2{color:#16213e;border-left:4px solid #0f3460;padding-left:.8rem;margin-top:2rem}\nh3{color:#0f3460}\ncode{background:#f0f0f0;padding:.15em .4em;border-radius:3px;font-size:.88em;font-family:\"SF Mono\",Menlo,monospace}\npre{background:#1e1e2e;color:#cdd6f4;padding:1.2rem;border-radius:8px;overflow-x:auto;font-size:.85em}\npre code{background:none;color:inherit;padding:0}\ntable{border-collapse:collapse;width:100%;margin:1rem 0}\nth,td{border:1px solid #ddd;padding:.6rem .8rem}\nth{background:#0f3460;color:white}\ntr:nth-child(even){background:#f8f9fa}\nblockquote{background:#e8f4fd;border-left:4px solid #2196f3;padding:.8rem 1rem;margin:1rem 0;border-radius:0 8px 8px 0}\n@media print{body{max-width:100%;padding:0}pre{white-space:pre-wrap}}\n</style>\"\"\"\n\n return f\"\"\"<!DOCTYPE html>\n<html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><title>Ep.{ep_num}: {title}</title>{style}</head>\n<body>\n<h1>第{ep_num}集: {title}</h1>\n<p style=\"color:#666\">MiniMind · PyTorch从零手敲大模型 | {int(duration//60)}分{int(duration%60)}秒 | NoteKing</p>\n<hr>\n{body}\n</body></html>\"\"\"\n\n\ndef html_to_pdf(html_path, pdf_path):\n chrome = \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"\n if not os.path.exists(chrome):\n return False\n cmd = [chrome, \"--headless=new\", f\"--print-to-pdf={pdf_path}\",\n \"--print-to-pdf-no-header\", \"--no-sandbox\", \"--disable-gpu\",\n f\"file://{html_path.resolve()}\"]\n subprocess.run(cmd, capture_output=True, timeout=60)\n return pdf_path.exists()\n\n\ndef process_one_episode(idx):\n \"\"\"Process a single episode end-to-end.\"\"\"\n ep_num, title, duration, content_hint = EPISODES[idx]\n\n # Find video\n video_path = find_video(ep_num)\n if not video_path or not video_path.exists():\n print(f\" [{ep_num:02d}/26] ⚠️ Video not found, generating notes without frames\")\n frame_paths = []\n else:\n print(f\" [{ep_num:02d}/26] Extracting frames from {video_path.name}...\")\n frame_paths = extract_frames(video_path, ep_num, title)\n print(f\" [{ep_num:02d}/26] Got {len(frame_paths)} frames\")\n\n # Generate notes\n print(f\" [{ep_num:02d}/26] Generating notes: {title}...\")\n notes = generate_notes(ep_num, title, duration, content_hint, frame_paths)\n print(f\" [{ep_num:02d}/26] Notes: {len(notes)} chars\")\n\n # Save markdown\n md_path = NOTES_DIR / f\"p{ep_num:02d}_{title.replace(':','_').replace(':','_')}.md\"\n md_path.write_text(f\"# 第{ep_num}集: {title}\\n\\n{notes}\", encoding=\"utf-8\")\n\n # Build HTML\n html_content = build_html(notes, frame_paths, ep_num, title, duration)\n html_path = PDF_DIR / f\"p{ep_num:02d}_{title.replace(':','_').replace(':','_')}.html\"\n html_path.write_text(html_content, encoding=\"utf-8\")\n\n # Convert to PDF\n pdf_path = html_path.with_suffix(\".pdf\")\n html_to_pdf(html_path, pdf_path)\n\n return {\n \"ep\": ep_num, \"title\": title,\n \"notes_len\": len(notes),\n \"frames\": len(frame_paths),\n \"pdf\": str(pdf_path) if pdf_path.exists() else None,\n \"html\": str(html_path),\n }\n\n\ndef build_merged_html(results):\n \"\"\"Merge all episodes into one HTML document.\"\"\"\n style = \"\"\"<style>\nbody{font-family:\"PingFang SC\",sans-serif;max-width:900px;margin:0 auto;padding:2rem;line-height:1.8;color:#2d2d2d}\nh1{color:#1a1a2e;border-bottom:3px solid #e94560;padding-bottom:.5rem}\nh2{color:#16213e;border-left:4px solid #0f3460;padding-left:.8rem;margin-top:2rem}\nh3{color:#0f3460}\ncode{background:#f0f0f0;padding:.15em .4em;border-radius:3px;font-size:.88em;font-family:\"SF Mono\",Menlo,monospace}\npre{background:#1e1e2e;color:#cdd6f4;padding:1.2rem;border-radius:8px;overflow-x:auto}\npre code{background:none;color:inherit;padding:0}\nimg{max-width:100%;border-radius:6px;box-shadow:0 3px 12px rgba(0,0,0,.12);margin:1rem auto;display:block}\ntable{border-collapse:collapse;width:100%;margin:1rem 0}\nth,td{border:1px solid #ddd;padding:.5rem .7rem}\nth{background:#0f3460;color:white}\ntr:nth-child(even){background:#f8f9fa}\nblockquote{background:#e8f4fd;border-left:4px solid #2196f3;padding:.8rem 1rem;margin:1rem 0;border-radius:0 8px 8px 0}\n.cover{text-align:center;padding:3rem 0;page-break-after:always}\nhr{margin:3rem 0}\n@media print{body{max-width:100%;padding:0}pre{white-space:pre-wrap}}\n</style>\"\"\"\n\n parts = [f\"\"\"<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\">\n<title>MiniMind 完整图文讲义</title>{style}</head><body>\n<div class=\"cover\">\n<h1 style=\"font-size:2.2rem;border:none\">MiniMind 完整图文讲义</h1>\n<p style=\"font-size:1.2rem;color:#666\">PyTorch从零手敲大模型 · 架构到训练全教程</p>\n<p>共 26 集 | NoteKing · {time.strftime('%Y-%m-%d')}</p>\n</div>\n<h2>目录</h2><ol>\"\"\"]\n\n for r in results:\n parts.append(f'<li>第{r[\"ep\"]}集: {r[\"title\"]}</li>')\n parts.append(\"</ol><hr>\")\n\n for r in results:\n md_path = NOTES_DIR / f\"p{r['ep']:02d}_{r['title'].replace(':','_').replace(':','_')}.md\"\n if md_path.exists():\n content = md_path.read_text(encoding=\"utf-8\")\n # Inline images from frames\n ep_frames = sorted((FRAMES_DIR / f\"p{r['ep']:02d}\").glob(\"*.jpg\")) if (FRAMES_DIR / f\"p{r['ep']:02d}\").exists() else []\n def _rep(m, fps=ep_frames):\n n = int(m.group(1))\n if 1 <= n <= len(fps):\n return f'\\n<img src=\"file://{fps[n-1].resolve()}\" alt=\"fig\">\\n'\n return \"\"\n content = re.sub(r'\\{IMAGE:(\\d+)\\}', _rep, content)\n content = re.sub(r'\\{IMPORTANT\\}(.*?)\\{/IMPORTANT\\}',\n r'<div style=\"background:#fff8e1;border-left:4px solid #ffb300;padding:.8rem;margin:.5rem 0;border-radius:0 6px 6px 0\"><b>重点</b>: \\1</div>',\n content, flags=re.DOTALL)\n content = re.sub(r'\\{KNOWLEDGE\\}(.*?)\\{/KNOWLEDGE\\}',\n r'<div style=\"background:#e3f2fd;border-left:4px solid #1565c0;padding:.8rem;margin:.5rem 0;border-radius:0 6px 6px 0\"><b>知识</b>: \\1</div>',\n content, flags=re.DOTALL)\n content = re.sub(r'\\{WARNING\\}(.*?)\\{/WARNING\\}',\n r'<div style=\"background:#fce4ec;border-left:4px solid #c62828;padding:.8rem;margin:.5rem 0;border-radius:0 6px 6px 0\"><b>注意</b>: \\1</div>',\n content, flags=re.DOTALL)\n parts.append(content)\n parts.append(\"<hr>\")\n\n parts.append(\"</body></html>\")\n return \"\\n\".join(parts)\n\n\ndef main():\n print(\"=\" * 65)\n print(\" NoteKing · MiniMind 全集图文讲义生成器\")\n print(f\" 共 {len(EPISODES)} 集 | 并发 {CONCURRENCY} 路 | MiniMax M2.7 Max\")\n print(\"=\" * 65)\n\n for d in [NOTES_DIR, PDF_DIR, FRAMES_DIR]:\n d.mkdir(parents=True, exist_ok=True)\n\n # Check video downloads\n video_count = len(list(VIDEOS_DIR.glob(\"*.mp4\"))) if VIDEOS_DIR.exists() else 0\n print(f\"\\n 已下载视频: {video_count}/26\")\n if video_count == 0:\n print(\" ⚠️ 视频尚未下载完成,将在无截图模式下生成笔记\")\n print(\" (视频下载完成后可重新运行以添加截图)\")\n\n # Process all episodes concurrently\n results = [None] * len(EPISODES)\n t0 = time.time()\n\n with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:\n futures = {pool.submit(process_one_episode, i): i for i in range(len(EPISODES))}\n for future in as_completed(futures):\n idx = futures[future]\n try:\n results[idx] = future.result()\n r = results[idx]\n print(f\" ✅ [{r['ep']:02d}/26] {r['title']} — {r['notes_len']} chars, {r['frames']} frames\")\n except Exception as e:\n ep = EPISODES[idx]\n print(f\" ❌ [{ep[0]:02d}/26] {ep[1]} — FAILED: {e}\")\n results[idx] = {\"ep\": ep[0], \"title\": ep[1], \"notes_len\": 0, \"frames\": 0, \"pdf\": None, \"html\": None}\n\n elapsed = time.time() - t0\n\n # Build merged HTML\n print(\"\\n Merging all episodes into one document...\")\n valid_results = [r for r in results if r]\n valid_results.sort(key=lambda r: r[\"ep\"])\n merged_html = build_merged_html(valid_results)\n merged_path = PDF_DIR / \"MiniMind_全集_图文讲义.html\"\n merged_path.write_text(merged_html, encoding=\"utf-8\")\n\n # Convert merged to PDF\n merged_pdf = merged_path.with_suffix(\".pdf\")\n html_to_pdf(merged_path, merged_pdf)\n\n # Summary\n print(\"\\n\" + \"=\" * 65)\n print(f\" ✅ 全部完成! 耗时 {elapsed:.0f}秒 ({elapsed/60:.1f}分钟)\")\n success = sum(1 for r in valid_results if r[\"notes_len\"] > 200)\n total_chars = sum(r[\"notes_len\"] for r in valid_results)\n total_frames = sum(r[\"frames\"] for r in valid_results)\n print(f\" 📊 成功: {success}/26 集\")\n print(f\" 📝 总字数: {total_chars:,}\")\n print(f\" 🖼️ 总截图: {total_frames}\")\n print(f\" 📄 合并HTML: {merged_path}\")\n if merged_pdf.exists():\n print(f\" 📕 合并PDF: {merged_pdf} ({merged_pdf.stat().st_size//1024}KB)\")\n print(\"=\" * 65)\n\n\nif __name__ == \"__main__\":\n main()\n"}, "df7bbcb625176a1e4515720e4a3c63c37e670401": {"t": 1785611528.3194098, "n": 16910, "h": "cbbd5f7dad5f94be", "c": "#!/usr/bin/env python3\n\"\"\"\nNoteKing Demo: Process all 26 episodes of MiniMind into illustrated PDF lecture notes.\n\nUsage: python3.11 run_minimind.py\n\"\"\"\n\nimport sys, os, time, re, json, subprocess\nfrom pathlib import Path\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\nsys.path.insert(0, str(Path(__file__).resolve().parent.parent))\n\nfrom openai import OpenAI\n\n# ── Config ──\nAPI_KEY = \"sk-cp-vX4T-YhmjkytkOexcwZ-uAdmALWR8ggXmtGOymuJQ1lfNLOR1phT0Ju09VggOTENL-y1pGe-KC4fTQppbzn_X_WPxVIApwG71PlvZHCGgfaIRH2zYoAI_RA\"\nBASE_URL = \"https://api.minimax.chat/v1\"\nMODEL = \"MiniMax-M2.7\"\nCONCURRENCY = 3\nMAX_TOKENS = 5000\n\nBASE_DIR = Path(__file__).resolve().parent / \"minimind\"\nVIDEOS_DIR = BASE_DIR / \"videos\"\nFRAMES_DIR = BASE_DIR / \"frames\"\nNOTES_DIR = BASE_DIR / \"notes\"\nPDF_DIR = BASE_DIR / \"pdf\"\n\nEPISODES = [\n (1, \"开篇\", 170, \"课程介绍与背景\"),\n (2, \"前言\", 268, \"大模型基础概念、MiniMind技术栈\"),\n (3, \"必看:前言补充\", 123, \"勘误与补充说明\"),\n (4, \"前置知识\", 437, \"PyTorch张量、矩阵乘法、注意力机制原理\"),\n (5, \"架构图解读\", 346, \"Transformer架构全图逐层解析\"),\n (6, \"初始化项目\", 227, \"项目结构设计、ModelArgs配置类\"),\n (7, \"理论:RMSNorm\", 246, \"RMSNorm原理与数学推导\"),\n (8, \"代码:RMSNorm\", 385, \"RMSNorm的PyTorch实现\"),\n (9, \"理论:RoPE&YaRN\", 831, \"旋转位置编码RoPE + YaRN外推原理\"),\n (10, \"代码:RoPE&YaRN\", 1040, \"RoPE实现、旋转矩阵计算、YaRN缩放\"),\n (11, \"理论:GQA\", 230, \"分组查询注意力 MHA/MQA/GQA对比\"),\n (12, \"代码:GQA 上\", 788, \"Q/K/V投影、repeat_kv\"),\n (13, \"代码:GQA 下\", 1162, \"注意力计算、因果掩码、输出投影\"),\n (14, \"理论:FFN\", 376, \"前馈网络、SwiGLU激活函数\"),\n (15, \"代码:FFN\", 339, \"FeedForward类的PyTorch实现\"),\n (16, \"拼接:Block\", 347, \"TransformerBlock组装\"),\n (17, \"组装:Model\", 703, \"Transformer完整实现\"),\n (18, \"封装:CausalLM\", 600, \"因果语言模型封装、generate推理\"),\n (19, \"回顾与知识检验\", 435, \"全架构回顾、核心概念测验\"),\n (20, \"必看:纠错补充\", 475, \"代码勘误、补充说明\"),\n (21, \"重制Dataset:理论\", 332, \"预训练数据格式、tokenizer\"),\n (22, \"重制Dataset:代码\", 484, \"PretrainDataset类实现\"),\n (23, \"重制Pretrain:理论\", 344, \"预训练目标、损失函数\"),\n (24, \"重制Pretrain:代码\", 580, \"训练循环、优化器、学习率调度\"),\n (25, \"训练,启动!\", 343, \"实际训练流程、日志分析\"),\n (26, \"Eval:完结!\", 497, \"模型评估、推理测试、展望\"),\n]\n\nclient = OpenAI(api_key=API_KEY, base_url=BASE_URL)\n\n\ndef strip_think(text):\n return re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()\n\n\ndef find_video(ep_num):\n \"\"\"Find downloaded video file for episode.\"\"\"\n patterns = [\n f\"p{ep_num:02d}_*.mp4\",\n f\"*p{ep_num:02d}*.mp4\",\n f\"*_p{ep_num:02d}_*.mp4\",\n ]\n for pat in patterns:\n matches = list(VIDEOS_DIR.glob(pat))\n if matches:\n return matches[0]\n return None\n\n\ndef extract_frames(video_path, ep_num, title):\n \"\"\"Extract keyframes for one episode.\"\"\"\n ep_frames_dir = FRAMES_DIR / f\"p{ep_num:02d}\"\n ep_frames_dir.mkdir(parents=True, exist_ok=True)\n\n existing = sorted(ep_frames_dir.glob(\"*.jpg\"))\n if len(existing) >= 3:\n return existing\n\n try:\n sys.path.insert(0, str(Path(__file__).resolve().parent.parent))\n from core.frames import extract_keyframes\n from core.frames import ExtractedFrame\n\n frames = extract_keyframes(\n video_path, ep_frames_dir,\n max_frames=12, threshold=25.0,\n dedup=True, score=True,\n )\n return [f.path for f in frames]\n except Exception as e:\n print(f\" SceneDetect/scoring failed ({e}), using ffmpeg uniform...\")\n\n # Fallback: uniform\n duration_cmd = f'ffprobe -v quiet -show_entries format=duration -of csv=p=0 \"{video_path}\"'\n r = subprocess.run(duration_cmd, shell=True, capture_output=True, text=True, timeout=30)\n duration = float(r.stdout.strip()) if r.stdout.strip() else 300\n\n interval = max(15, duration / 12)\n frame_paths = []\n t = 3.0\n idx = 0\n while t < duration - 3 and idx < 15:\n out = ep_frames_dir / f\"frame_{idx:03d}_{t:.0f}s.jpg\"\n cmd = f'ffmpeg -ss {t:.2f} -i \"{video_path}\" -vframes 1 -q:v 3 -vf scale=960:-1 \"{out}\" -y -loglevel error'\n subprocess.run(cmd, shell=True, capture_output=True, timeout=30)\n if out.exists() and out.stat().st_size > 3000:\n frame_paths.append(out)\n t += interval\n idx += 1\n\n return frame_paths\n\n\ndef generate_notes(ep_num, title, duration, content_hint, frame_paths):\n \"\"\"Generate illustrated lecture notes for one episode via LLM.\"\"\"\n mins = duration // 60\n secs = duration % 60\n\n frames_desc = \"\"\n if frame_paths:\n frames_desc = \"Available keyframes (insert {IMAGE:N} where appropriate):\\n\"\n for i, fp in enumerate(frame_paths):\n m = re.search(r'(\\d+\\.?\\d*)s', fp.name)\n ts = m.group(1) if m else str(i * 30)\n frames_desc += f\" Fig.{i+1} at {ts}s\\n\"\n\n prompt = f\"\"\"Generate detailed illustrated lecture notes in Chinese for this MiniMind episode.\n\nEpisode: {ep_num}/26 \"{title}\" ({int(mins)}m{int(secs)}s)\nTopic: {content_hint}\nCourse: MiniMind - PyTorch从零手敲大模型\n\n{frames_desc}\n\nREQUIRED FORMAT:\n- Use ## for sections, ### for subsections\n- Insert {{IMAGE:N}} markers for keyframe figures (e.g. {{IMAGE:1}})\n- Use highlight boxes:\n {{IMPORTANT}}核心概念{{/IMPORTANT}}\n {{KNOWLEDGE}}背景知识{{/KNOWLEDGE}}\n {{WARNING}}易错点{{/WARNING}}\n- Code blocks with ```python\n- Math with $...$ or $$...$$\n- Each section ends with brief summary\n\nCONTENT:\n- Write as a professional lecture note, 2000+ characters\n- Cover all knowledge points for this topic\n- Include code with comments when relevant\n- Add formulas with explanation\n- End with key takeaways and 1-2 thought questions\"\"\"\n\n for attempt in range(3):\n try:\n full = \"\"\n stream = client.chat.completions.create(\n model=MODEL,\n messages=[\n {\"role\": \"system\", \"content\": \"你是专业的AI技术讲义编写专家。直接输出Markdown格式讲义。\"},\n {\"role\": \"user\", \"content\": prompt}\n ],\n max_tokens=MAX_TOKENS, stream=True, temperature=0.2, timeout=120,\n )\n for chunk in stream:\n delta = chunk.choices[0].delta.content or \"\"\n full += delta\n return strip_think(full)\n except Exception as e:\n if attempt < 2:\n time.sleep((attempt + 1) * 8)\n else:\n return f\"*Episode {ep_num} generation failed: {e}*\"\n return \"\"\n\n\ndef build_html(notes_md, frame_paths, ep_num, title, duration):\n \"\"\"Build HTML lecture note with embedded images.\"\"\"\n def replace_img(match):\n n = int(match.group(1))\n if 1 <= n <= len(frame_paths):\n fp = frame_paths[n - 1]\n return f'\\n<img src=\"file://{fp.resolve()}\" alt=\"fig{n}\" style=\"max-width:100%;border-radius:6px;box-shadow:0 3px 12px rgba(0,0,0,.12);margin:1rem auto;display:block\">\\n<div style=\"text-align:center;color:#666;font-size:.85em;margin-bottom:1rem\">Fig.{n}</div>\\n'\n return f\"*(Fig.{n})*\"\n\n body = re.sub(r'\\{IMAGE:(\\d+)\\}', replace_img, notes_md)\n body = re.sub(r'\\{IMPORTANT\\}(.*?)\\{/IMPORTANT\\}',\n r'<div style=\"background:#fff8e1;border-left:4px solid #ffb300;padding:1rem;margin:1rem 0;border-radius:0 8px 8px 0\"><b>⭐ 重点</b><br>\\1</div>',\n body, flags=re.DOTALL)\n body = re.sub(r'\\{KNOWLEDGE\\}(.*?)\\{/KNOWLEDGE\\}',\n r'<div style=\"background:#e3f2fd;border-left:4px solid #1565c0;padding:1rem;margin:1rem 0;border-radius:0 8px 8px 0\"><b>📖 知识补充</b><br>\\1</div>',\n body, flags=re.DOTALL)\n body = re.sub(r'\\{WARNING\\}(.*?)\\{/WARNING\\}',\n r'<div style=\"background:#fce4ec;border-left:4px solid #c62828;padding:1rem;margin:1rem 0;border-radius:0 8px 8px 0\"><b>⚠️ 注意</b><br>\\1</div>',\n body, flags=re.DOTALL)\n\n style = \"\"\"<style>\nbody{font-family:\"PingFang SC\",\"Noto Sans CJK SC\",sans-serif;max-width:900px;margin:0 auto;padding:2rem;line-height:1.8;color:#2d2d2d}\nh1{color:#1a1a2e;border-bottom:3px solid #e94560;padding-bottom:.5rem}\nh2{color:#16213e;border-left:4px solid #0f3460;padding-left:.8rem;margin-top:2rem}\nh3{color:#0f3460}\ncode{background:#f0f0f0;padding:.15em .4em;border-radius:3px;font-size:.88em;font-family:\"SF Mono\",Menlo,monospace}\npre{background:#1e1e2e;color:#cdd6f4;padding:1.2rem;border-radius:8px;overflow-x:auto;font-size:.85em}\npre code{background:none;color:inherit;padding:0}\ntable{border-collapse:collapse;width:100%;margin:1rem 0}\nth,td{border:1px solid #ddd;padding:.6rem .8rem}\nth{background:#0f3460;color:white}\ntr:nth-child(even){background:#f8f9fa}\nblockquote{background:#e8f4fd;border-left:4px solid #2196f3;padding:.8rem 1rem;margin:1rem 0;border-radius:0 8px 8px 0}\n@media print{body{max-width:100%;padding:0}pre{white-space:pre-wrap}}\n</style>\"\"\"\n\n return f\"\"\"<!DOCTYPE html>\n<html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><title>Ep.{ep_num}: {title}</title>{style}</head>\n<body>\n<h1>第{ep_num}集: {title}</h1>\n<p style=\"color:#666\">MiniMind · PyTorch从零手敲大模型 | {int(duration//60)}分{int(duration%60)}秒 | NoteKing</p>\n<hr>\n{body}\n</body></html>\"\"\"\n\n\ndef html_to_pdf(html_path, pdf_path):\n chrome = \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"\n if not os.path.exists(chrome):\n return False\n cmd = [chrome, \"--headless=new\", f\"--print-to-pdf={pdf_path}\",\n \"--print-to-pdf-no-header\", \"--no-sandbox\", \"--disable-gpu\",\n f\"file://{html_path.resolve()}\"]\n subprocess.run(cmd, capture_output=True, timeout=60)\n return pdf_path.exists()\n\n\ndef process_one_episode(idx):\n \"\"\"Process a single episode end-to-end.\"\"\"\n ep_num, title, duration, content_hint = EPISODES[idx]\n\n # Find video\n video_path = find_video(ep_num)\n if not video_path or not video_path.exists():\n print(f\" [{ep_num:02d}/26] ⚠️ Video not found, generating notes without frames\")\n frame_paths = []\n else:\n print(f\" [{ep_num:02d}/26] Extracting frames from {video_path.name}...\")\n frame_paths = extract_frames(video_path, ep_num, title)\n print(f\" [{ep_num:02d}/26] Got {len(frame_paths)} frames\")\n\n # Generate notes\n print(f\" [{ep_num:02d}/26] Generating notes: {title}...\")\n notes = generate_notes(ep_num, title, duration, content_hint, frame_paths)\n print(f\" [{ep_num:02d}/26] Notes: {len(notes)} chars\")\n\n # Save markdown\n md_path = NOTES_DIR / f\"p{ep_num:02d}_{title.replace(':','_').replace(':','_')}.md\"\n md_path.write_text(f\"# 第{ep_num}集: {title}\\n\\n{notes}\", encoding=\"utf-8\")\n\n # Build HTML\n html_content = build_html(notes, frame_paths, ep_num, title, duration)\n html_path = PDF_DIR / f\"p{ep_num:02d}_{title.replace(':','_').replace(':','_')}.html\"\n html_path.write_text(html_content, encoding=\"utf-8\")\n\n # Convert to PDF\n pdf_path = html_path.with_suffix(\".pdf\")\n html_to_pdf(html_path, pdf_path)\n\n return {\n \"ep\": ep_num, \"title\": title,\n \"notes_len\": len(notes),\n \"frames\": len(frame_paths),\n \"pdf\": str(pdf_path) if pdf_path.exists() else None,\n \"html\": str(html_path),\n }\n\n\ndef build_merged_html(results):\n \"\"\"Merge all episodes into one HTML document.\"\"\"\n style = \"\"\"<style>\nbody{font-family:\"PingFang SC\",sans-serif;max-width:900px;margin:0 auto;padding:2rem;line-height:1.8;color:#2d2d2d}\nh1{color:#1a1a2e;border-bottom:3px solid #e94560;padding-bottom:.5rem}\nh2{color:#16213e;border-left:4px solid #0f3460;padding-left:.8rem;margin-top:2rem}\nh3{color:#0f3460}\ncode{background:#f0f0f0;padding:.15em .4em;border-radius:3px;font-size:.88em;font-family:\"SF Mono\",Menlo,monospace}\npre{background:#1e1e2e;color:#cdd6f4;padding:1.2rem;border-radius:8px;overflow-x:auto}\npre code{background:none;color:inherit;padding:0}\nimg{max-width:100%;border-radius:6px;box-shadow:0 3px 12px rgba(0,0,0,.12);margin:1rem auto;display:block}\ntable{border-collapse:collapse;width:100%;margin:1rem 0}\nth,td{border:1px solid #ddd;padding:.5rem .7rem}\nth{background:#0f3460;color:white}\ntr:nth-child(even){background:#f8f9fa}\nblockquote{background:#e8f4fd;border-left:4px solid #2196f3;padding:.8rem 1rem;margin:1rem 0;border-radius:0 8px 8px 0}\n.cover{text-align:center;padding:3rem 0;page-break-after:always}\nhr{margin:3rem 0}\n@media print{body{max-width:100%;padding:0}pre{white-space:pre-wrap}}\n</style>\"\"\"\n\n parts = [f\"\"\"<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\">\n<title>MiniMind 完整图文讲义</title>{style}</head><body>\n<div class=\"cover\">\n<h1 style=\"font-size:2.2rem;border:none\">MiniMind 完整图文讲义</h1>\n<p style=\"font-size:1.2rem;color:#666\">PyTorch从零手敲大模型 · 架构到训练全教程</p>\n<p>共 26 集 | NoteKing · {time.strftime('%Y-%m-%d')}</p>\n</div>\n<h2>目录</h2><ol>\"\"\"]\n\n for r in results:\n parts.append(f'<li>第{r[\"ep\"]}集: {r[\"title\"]}</li>')\n parts.append(\"</ol><hr>\")\n\n for r in results:\n md_path = NOTES_DIR / f\"p{r['ep']:02d}_{r['title'].replace(':','_').replace(':','_')}.md\"\n if md_path.exists():\n content = md_path.read_text(encoding=\"utf-8\")\n # Inline images from frames\n ep_frames = sorted((FRAMES_DIR / f\"p{r['ep']:02d}\").glob(\"*.jpg\")) if (FRAMES_DIR / f\"p{r['ep']:02d}\").exists() else []\n def _rep(m, fps=ep_frames):\n n = int(m.group(1))\n if 1 <= n <= len(fps):\n return f'\\n<img src=\"file://{fps[n-1].resolve()}\" alt=\"fig\">\\n'\n return \"\"\n content = re.sub(r'\\{IMAGE:(\\d+)\\}', _rep, content)\n content = re.sub(r'\\{IMPORTANT\\}(.*?)\\{/IMPORTANT\\}',\n r'<div style=\"background:#fff8e1;border-left:4px solid #ffb300;padding:.8rem;margin:.5rem 0;border-radius:0 6px 6px 0\"><b>重点</b>: \\1</div>',\n content, flags=re.DOTALL)\n content = re.sub(r'\\{KNOWLEDGE\\}(.*?)\\{/KNOWLEDGE\\}',\n r'<div style=\"background:#e3f2fd;border-left:4px solid #1565c0;padding:.8rem;margin:.5rem 0;border-radius:0 6px 6px 0\"><b>知识</b>: \\1</div>',\n content, flags=re.DOTALL)\n content = re.sub(r'\\{WARNING\\}(.*?)\\{/WARNING\\}',\n r'<div style=\"background:#fce4ec;border-left:4px solid #c62828;padding:.8rem;margin:.5rem 0;border-radius:0 6px 6px 0\"><b>注意</b>: \\1</div>',\n content, flags=re.DOTALL)\n parts.append(content)\n parts.append(\"<hr>\")\n\n parts.append(\"</body></html>\")\n return \"\\n\".join(parts)\n\n\ndef main():\n print(\"=\" * 65)\n print(\" NoteKing · MiniMind 全集图文讲义生成器\")\n print(f\" 共 {len(EPISODES)} 集 | 并发 {CONCURRENCY} 路 | MiniMax M2.7 Max\")\n print(\"=\" * 65)\n\n for d in [NOTES_DIR, PDF_DIR, FRAMES_DIR]:\n d.mkdir(parents=True, exist_ok=True)\n\n # Check video downloads\n video_count = len(list(VIDEOS_DIR.glob(\"*.mp4\"))) if VIDEOS_DIR.exists() else 0\n print(f\"\\n 已下载视频: {video_count}/26\")\n if video_count == 0:\n print(\" ⚠️ 视频尚未下载完成,将在无截图模式下生成笔记\")\n print(\" (视频下载完成后可重新运行以添加截图)\")\n\n # Process all episodes concurrently\n results = [None] * len(EPISODES)\n t0 = time.time()\n\n with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:\n futures = {pool.submit(process_one_episode, i): i for i in range(len(EPISODES))}\n for future in as_completed(futures):\n idx = futures[future]\n try:\n results[idx] = future.result()\n r = results[idx]\n print(f\" ✅ [{r['ep']:02d}/26] {r['title']} — {r['notes_len']} chars, {r['frames']} frames\")\n except Exception as e:\n ep = EPISODES[idx]\n print(f\" ❌ [{ep[0]:02d}/26] {ep[1]} — FAILED: {e}\")\n results[idx] = {\"ep\": ep[0], \"title\": ep[1], \"notes_len\": 0, \"frames\": 0, \"pdf\": None, \"html\": None}\n\n elapsed = time.time() - t0\n\n # Build merged HTML\n print(\"\\n Merging all episodes into one document...\")\n valid_results = [r for r in results if r]\n valid_results.sort(key=lambda r: r[\"ep\"])\n merged_html = build_merged_html(valid_results)\n merged_path = PDF_DIR / \"MiniMind_全集_图文讲义.html\"\n merged_path.write_text(merged_html, encoding=\"utf-8\")\n\n # Convert merged to PDF\n merged_pdf = merged_path.with_suffix(\".pdf\")\n html_to_pdf(merged_path, merged_pdf)\n\n # Summary\n print(\"\\n\" + \"=\" * 65)\n print(f\" ✅ 全部完成! 耗时 {elapsed:.0f}秒 ({elapsed/60:.1f}分钟)\")\n success = sum(1 for r in valid_results if r[\"notes_len\"] > 200)\n total_chars = sum(r[\"notes_len\"] for r in valid_results)\n total_frames = sum(r[\"frames\"] for r in valid_results)\n print(f\" 📊 成功: {success}/26 集\")\n print(f\" 📝 总字数: {total_chars:,}\")\n print(f\" 🖼️ 总截图: {total_frames}\")\n print(f\" 📄 合并HTML: {merged_path}\")\n if merged_pdf.exists():\n print(f\" 📕 合并PDF: {merged_pdf} ({merged_pdf.stat().st_size//1024}KB)\")\n print(\"=\" * 65)\n\n\nif __name__ == \"__main__\":\n main()\n"}, "fd366b09224e247ae64e886a34c2b45bf178a580": {"t": 1785674015.0162547, "n": 16910, "h": "cbbd5f7dad5f94be", "c": "#!/usr/bin/env python3\n\"\"\"\nNoteKing Demo: Process all 26 episodes of MiniMind into illustrated PDF lecture notes.\n\nUsage: python3.11 run_minimind.py\n\"\"\"\n\nimport sys, os, time, re, json, subprocess\nfrom pathlib import Path\nfrom concurrent.futures import ThreadPoolExecutor, as_completed\n\nsys.path.insert(0, str(Path(__file__).resolve().parent.parent))\n\nfrom openai import OpenAI\n\n# ── Config ──\nAPI_KEY = \"sk-cp-vX4T-YhmjkytkOexcwZ-uAdmALWR8ggXmtGOymuJQ1lfNLOR1phT0Ju09VggOTENL-y1pGe-KC4fTQppbzn_X_WPxVIApwG71PlvZHCGgfaIRH2zYoAI_RA\"\nBASE_URL = \"https://api.minimax.chat/v1\"\nMODEL = \"MiniMax-M2.7\"\nCONCURRENCY = 3\nMAX_TOKENS = 5000\n\nBASE_DIR = Path(__file__).resolve().parent / \"minimind\"\nVIDEOS_DIR = BASE_DIR / \"videos\"\nFRAMES_DIR = BASE_DIR / \"frames\"\nNOTES_DIR = BASE_DIR / \"notes\"\nPDF_DIR = BASE_DIR / \"pdf\"\n\nEPISODES = [\n (1, \"开篇\", 170, \"课程介绍与背景\"),\n (2, \"前言\", 268, \"大模型基础概念、MiniMind技术栈\"),\n (3, \"必看:前言补充\", 123, \"勘误与补充说明\"),\n (4, \"前置知识\", 437, \"PyTorch张量、矩阵乘法、注意力机制原理\"),\n (5, \"架构图解读\", 346, \"Transformer架构全图逐层解析\"),\n (6, \"初始化项目\", 227, \"项目结构设计、ModelArgs配置类\"),\n (7, \"理论:RMSNorm\", 246, \"RMSNorm原理与数学推导\"),\n (8, \"代码:RMSNorm\", 385, \"RMSNorm的PyTorch实现\"),\n (9, \"理论:RoPE&YaRN\", 831, \"旋转位置编码RoPE + YaRN外推原理\"),\n (10, \"代码:RoPE&YaRN\", 1040, \"RoPE实现、旋转矩阵计算、YaRN缩放\"),\n (11, \"理论:GQA\", 230, \"分组查询注意力 MHA/MQA/GQA对比\"),\n (12, \"代码:GQA 上\", 788, \"Q/K/V投影、repeat_kv\"),\n (13, \"代码:GQA 下\", 1162, \"注意力计算、因果掩码、输出投影\"),\n (14, \"理论:FFN\", 376, \"前馈网络、SwiGLU激活函数\"),\n (15, \"代码:FFN\", 339, \"FeedForward类的PyTorch实现\"),\n (16, \"拼接:Block\", 347, \"TransformerBlock组装\"),\n (17, \"组装:Model\", 703, \"Transformer完整实现\"),\n (18, \"封装:CausalLM\", 600, \"因果语言模型封装、generate推理\"),\n (19, \"回顾与知识检验\", 435, \"全架构回顾、核心概念测验\"),\n (20, \"必看:纠错补充\", 475, \"代码勘误、补充说明\"),\n (21, \"重制Dataset:理论\", 332, \"预训练数据格式、tokenizer\"),\n (22, \"重制Dataset:代码\", 484, \"PretrainDataset类实现\"),\n (23, \"重制Pretrain:理论\", 344, \"预训练目标、损失函数\"),\n (24, \"重制Pretrain:代码\", 580, \"训练循环、优化器、学习率调度\"),\n (25, \"训练,启动!\", 343, \"实际训练流程、日志分析\"),\n (26, \"Eval:完结!\", 497, \"模型评估、推理测试、展望\"),\n]\n\nclient = OpenAI(api_key=API_KEY, base_url=BASE_URL)\n\n\ndef strip_think(text):\n return re.sub(r'<think>.*?</think>', '', text, flags=re.DOTALL).strip()\n\n\ndef find_video(ep_num):\n \"\"\"Find downloaded video file for episode.\"\"\"\n patterns = [\n f\"p{ep_num:02d}_*.mp4\",\n f\"*p{ep_num:02d}*.mp4\",\n f\"*_p{ep_num:02d}_*.mp4\",\n ]\n for pat in patterns:\n matches = list(VIDEOS_DIR.glob(pat))\n if matches:\n return matches[0]\n return None\n\n\ndef extract_frames(video_path, ep_num, title):\n \"\"\"Extract keyframes for one episode.\"\"\"\n ep_frames_dir = FRAMES_DIR / f\"p{ep_num:02d}\"\n ep_frames_dir.mkdir(parents=True, exist_ok=True)\n\n existing = sorted(ep_frames_dir.glob(\"*.jpg\"))\n if len(existing) >= 3:\n return existing\n\n try:\n sys.path.insert(0, str(Path(__file__).resolve().parent.parent))\n from core.frames import extract_keyframes\n from core.frames import ExtractedFrame\n\n frames = extract_keyframes(\n video_path, ep_frames_dir,\n max_frames=12, threshold=25.0,\n dedup=True, score=True,\n )\n return [f.path for f in frames]\n except Exception as e:\n print(f\" SceneDetect/scoring failed ({e}), using ffmpeg uniform...\")\n\n # Fallback: uniform\n duration_cmd = f'ffprobe -v quiet -show_entries format=duration -of csv=p=0 \"{video_path}\"'\n r = subprocess.run(duration_cmd, shell=True, capture_output=True, text=True, timeout=30)\n duration = float(r.stdout.strip()) if r.stdout.strip() else 300\n\n interval = max(15, duration / 12)\n frame_paths = []\n t = 3.0\n idx = 0\n while t < duration - 3 and idx < 15:\n out = ep_frames_dir / f\"frame_{idx:03d}_{t:.0f}s.jpg\"\n cmd = f'ffmpeg -ss {t:.2f} -i \"{video_path}\" -vframes 1 -q:v 3 -vf scale=960:-1 \"{out}\" -y -loglevel error'\n subprocess.run(cmd, shell=True, capture_output=True, timeout=30)\n if out.exists() and out.stat().st_size > 3000:\n frame_paths.append(out)\n t += interval\n idx += 1\n\n return frame_paths\n\n\ndef generate_notes(ep_num, title, duration, content_hint, frame_paths):\n \"\"\"Generate illustrated lecture notes for one episode via LLM.\"\"\"\n mins = duration // 60\n secs = duration % 60\n\n frames_desc = \"\"\n if frame_paths:\n frames_desc = \"Available keyframes (insert {IMAGE:N} where appropriate):\\n\"\n for i, fp in enumerate(frame_paths):\n m = re.search(r'(\\d+\\.?\\d*)s', fp.name)\n ts = m.group(1) if m else str(i * 30)\n frames_desc += f\" Fig.{i+1} at {ts}s\\n\"\n\n prompt = f\"\"\"Generate detailed illustrated lecture notes in Chinese for this MiniMind episode.\n\nEpisode: {ep_num}/26 \"{title}\" ({int(mins)}m{int(secs)}s)\nTopic: {content_hint}\nCourse: MiniMind - PyTorch从零手敲大模型\n\n{frames_desc}\n\nREQUIRED FORMAT:\n- Use ## for sections, ### for subsections\n- Insert {{IMAGE:N}} markers for keyframe figures (e.g. {{IMAGE:1}})\n- Use highlight boxes:\n {{IMPORTANT}}核心概念{{/IMPORTANT}}\n {{KNOWLEDGE}}背景知识{{/KNOWLEDGE}}\n {{WARNING}}易错点{{/WARNING}}\n- Code blocks with ```python\n- Math with $...$ or $$...$$\n- Each section ends with brief summary\n\nCONTENT:\n- Write as a professional lecture note, 2000+ characters\n- Cover all knowledge points for this topic\n- Include code with comments when relevant\n- Add formulas with explanation\n- End with key takeaways and 1-2 thought questions\"\"\"\n\n for attempt in range(3):\n try:\n full = \"\"\n stream = client.chat.completions.create(\n model=MODEL,\n messages=[\n {\"role\": \"system\", \"content\": \"你是专业的AI技术讲义编写专家。直接输出Markdown格式讲义。\"},\n {\"role\": \"user\", \"content\": prompt}\n ],\n max_tokens=MAX_TOKENS, stream=True, temperature=0.2, timeout=120,\n )\n for chunk in stream:\n delta = chunk.choices[0].delta.content or \"\"\n full += delta\n return strip_think(full)\n except Exception as e:\n if attempt < 2:\n time.sleep((attempt + 1) * 8)\n else:\n return f\"*Episode {ep_num} generation failed: {e}*\"\n return \"\"\n\n\ndef build_html(notes_md, frame_paths, ep_num, title, duration):\n \"\"\"Build HTML lecture note with embedded images.\"\"\"\n def replace_img(match):\n n = int(match.group(1))\n if 1 <= n <= len(frame_paths):\n fp = frame_paths[n - 1]\n return f'\\n<img src=\"file://{fp.resolve()}\" alt=\"fig{n}\" style=\"max-width:100%;border-radius:6px;box-shadow:0 3px 12px rgba(0,0,0,.12);margin:1rem auto;display:block\">\\n<div style=\"text-align:center;color:#666;font-size:.85em;margin-bottom:1rem\">Fig.{n}</div>\\n'\n return f\"*(Fig.{n})*\"\n\n body = re.sub(r'\\{IMAGE:(\\d+)\\}', replace_img, notes_md)\n body = re.sub(r'\\{IMPORTANT\\}(.*?)\\{/IMPORTANT\\}',\n r'<div style=\"background:#fff8e1;border-left:4px solid #ffb300;padding:1rem;margin:1rem 0;border-radius:0 8px 8px 0\"><b>⭐ 重点</b><br>\\1</div>',\n body, flags=re.DOTALL)\n body = re.sub(r'\\{KNOWLEDGE\\}(.*?)\\{/KNOWLEDGE\\}',\n r'<div style=\"background:#e3f2fd;border-left:4px solid #1565c0;padding:1rem;margin:1rem 0;border-radius:0 8px 8px 0\"><b>📖 知识补充</b><br>\\1</div>',\n body, flags=re.DOTALL)\n body = re.sub(r'\\{WARNING\\}(.*?)\\{/WARNING\\}',\n r'<div style=\"background:#fce4ec;border-left:4px solid #c62828;padding:1rem;margin:1rem 0;border-radius:0 8px 8px 0\"><b>⚠️ 注意</b><br>\\1</div>',\n body, flags=re.DOTALL)\n\n style = \"\"\"<style>\nbody{font-family:\"PingFang SC\",\"Noto Sans CJK SC\",sans-serif;max-width:900px;margin:0 auto;padding:2rem;line-height:1.8;color:#2d2d2d}\nh1{color:#1a1a2e;border-bottom:3px solid #e94560;padding-bottom:.5rem}\nh2{color:#16213e;border-left:4px solid #0f3460;padding-left:.8rem;margin-top:2rem}\nh3{color:#0f3460}\ncode{background:#f0f0f0;padding:.15em .4em;border-radius:3px;font-size:.88em;font-family:\"SF Mono\",Menlo,monospace}\npre{background:#1e1e2e;color:#cdd6f4;padding:1.2rem;border-radius:8px;overflow-x:auto;font-size:.85em}\npre code{background:none;color:inherit;padding:0}\ntable{border-collapse:collapse;width:100%;margin:1rem 0}\nth,td{border:1px solid #ddd;padding:.6rem .8rem}\nth{background:#0f3460;color:white}\ntr:nth-child(even){background:#f8f9fa}\nblockquote{background:#e8f4fd;border-left:4px solid #2196f3;padding:.8rem 1rem;margin:1rem 0;border-radius:0 8px 8px 0}\n@media print{body{max-width:100%;padding:0}pre{white-space:pre-wrap}}\n</style>\"\"\"\n\n return f\"\"\"<!DOCTYPE html>\n<html lang=\"zh-CN\"><head><meta charset=\"utf-8\"><title>Ep.{ep_num}: {title}</title>{style}</head>\n<body>\n<h1>第{ep_num}集: {title}</h1>\n<p style=\"color:#666\">MiniMind · PyTorch从零手敲大模型 | {int(duration//60)}分{int(duration%60)}秒 | NoteKing</p>\n<hr>\n{body}\n</body></html>\"\"\"\n\n\ndef html_to_pdf(html_path, pdf_path):\n chrome = \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\"\n if not os.path.exists(chrome):\n return False\n cmd = [chrome, \"--headless=new\", f\"--print-to-pdf={pdf_path}\",\n \"--print-to-pdf-no-header\", \"--no-sandbox\", \"--disable-gpu\",\n f\"file://{html_path.resolve()}\"]\n subprocess.run(cmd, capture_output=True, timeout=60)\n return pdf_path.exists()\n\n\ndef process_one_episode(idx):\n \"\"\"Process a single episode end-to-end.\"\"\"\n ep_num, title, duration, content_hint = EPISODES[idx]\n\n # Find video\n video_path = find_video(ep_num)\n if not video_path or not video_path.exists():\n print(f\" [{ep_num:02d}/26] ⚠️ Video not found, generating notes without frames\")\n frame_paths = []\n else:\n print(f\" [{ep_num:02d}/26] Extracting frames from {video_path.name}...\")\n frame_paths = extract_frames(video_path, ep_num, title)\n print(f\" [{ep_num:02d}/26] Got {len(frame_paths)} frames\")\n\n # Generate notes\n print(f\" [{ep_num:02d}/26] Generating notes: {title}...\")\n notes = generate_notes(ep_num, title, duration, content_hint, frame_paths)\n print(f\" [{ep_num:02d}/26] Notes: {len(notes)} chars\")\n\n # Save markdown\n md_path = NOTES_DIR / f\"p{ep_num:02d}_{title.replace(':','_').replace(':','_')}.md\"\n md_path.write_text(f\"# 第{ep_num}集: {title}\\n\\n{notes}\", encoding=\"utf-8\")\n\n # Build HTML\n html_content = build_html(notes, frame_paths, ep_num, title, duration)\n html_path = PDF_DIR / f\"p{ep_num:02d}_{title.replace(':','_').replace(':','_')}.html\"\n html_path.write_text(html_content, encoding=\"utf-8\")\n\n # Convert to PDF\n pdf_path = html_path.with_suffix(\".pdf\")\n html_to_pdf(html_path, pdf_path)\n\n return {\n \"ep\": ep_num, \"title\": title,\n \"notes_len\": len(notes),\n \"frames\": len(frame_paths),\n \"pdf\": str(pdf_path) if pdf_path.exists() else None,\n \"html\": str(html_path),\n }\n\n\ndef build_merged_html(results):\n \"\"\"Merge all episodes into one HTML document.\"\"\"\n style = \"\"\"<style>\nbody{font-family:\"PingFang SC\",sans-serif;max-width:900px;margin:0 auto;padding:2rem;line-height:1.8;color:#2d2d2d}\nh1{color:#1a1a2e;border-bottom:3px solid #e94560;padding-bottom:.5rem}\nh2{color:#16213e;border-left:4px solid #0f3460;padding-left:.8rem;margin-top:2rem}\nh3{color:#0f3460}\ncode{background:#f0f0f0;padding:.15em .4em;border-radius:3px;font-size:.88em;font-family:\"SF Mono\",Menlo,monospace}\npre{background:#1e1e2e;color:#cdd6f4;padding:1.2rem;border-radius:8px;overflow-x:auto}\npre code{background:none;color:inherit;padding:0}\nimg{max-width:100%;border-radius:6px;box-shadow:0 3px 12px rgba(0,0,0,.12);margin:1rem auto;display:block}\ntable{border-collapse:collapse;width:100%;margin:1rem 0}\nth,td{border:1px solid #ddd;padding:.5rem .7rem}\nth{background:#0f3460;color:white}\ntr:nth-child(even){background:#f8f9fa}\nblockquote{background:#e8f4fd;border-left:4px solid #2196f3;padding:.8rem 1rem;margin:1rem 0;border-radius:0 8px 8px 0}\n.cover{text-align:center;padding:3rem 0;page-break-after:always}\nhr{margin:3rem 0}\n@media print{body{max-width:100%;padding:0}pre{white-space:pre-wrap}}\n</style>\"\"\"\n\n parts = [f\"\"\"<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"utf-8\">\n<title>MiniMind 完整图文讲义</title>{style}</head><body>\n<div class=\"cover\">\n<h1 style=\"font-size:2.2rem;border:none\">MiniMind 完整图文讲义</h1>\n<p style=\"font-size:1.2rem;color:#666\">PyTorch从零手敲大模型 · 架构到训练全教程</p>\n<p>共 26 集 | NoteKing · {time.strftime('%Y-%m-%d')}</p>\n</div>\n<h2>目录</h2><ol>\"\"\"]\n\n for r in results:\n parts.append(f'<li>第{r[\"ep\"]}集: {r[\"title\"]}</li>')\n parts.append(\"</ol><hr>\")\n\n for r in results:\n md_path = NOTES_DIR / f\"p{r['ep']:02d}_{r['title'].replace(':','_').replace(':','_')}.md\"\n if md_path.exists():\n content = md_path.read_text(encoding=\"utf-8\")\n # Inline images from frames\n ep_frames = sorted((FRAMES_DIR / f\"p{r['ep']:02d}\").glob(\"*.jpg\")) if (FRAMES_DIR / f\"p{r['ep']:02d}\").exists() else []\n def _rep(m, fps=ep_frames):\n n = int(m.group(1))\n if 1 <= n <= len(fps):\n return f'\\n<img src=\"file://{fps[n-1].resolve()}\" alt=\"fig\">\\n'\n return \"\"\n content = re.sub(r'\\{IMAGE:(\\d+)\\}', _rep, content)\n content = re.sub(r'\\{IMPORTANT\\}(.*?)\\{/IMPORTANT\\}',\n r'<div style=\"background:#fff8e1;border-left:4px solid #ffb300;padding:.8rem;margin:.5rem 0;border-radius:0 6px 6px 0\"><b>重点</b>: \\1</div>',\n content, flags=re.DOTALL)\n content = re.sub(r'\\{KNOWLEDGE\\}(.*?)\\{/KNOWLEDGE\\}',\n r'<div style=\"background:#e3f2fd;border-left:4px solid #1565c0;padding:.8rem;margin:.5rem 0;border-radius:0 6px 6px 0\"><b>知识</b>: \\1</div>',\n content, flags=re.DOTALL)\n content = re.sub(r'\\{WARNING\\}(.*?)\\{/WARNING\\}',\n r'<div style=\"background:#fce4ec;border-left:4px solid #c62828;padding:.8rem;margin:.5rem 0;border-radius:0 6px 6px 0\"><b>注意</b>: \\1</div>',\n content, flags=re.DOTALL)\n parts.append(content)\n parts.append(\"<hr>\")\n\n parts.append(\"</body></html>\")\n return \"\\n\".join(parts)\n\n\ndef main():\n print(\"=\" * 65)\n print(\" NoteKing · MiniMind 全集图文讲义生成器\")\n print(f\" 共 {len(EPISODES)} 集 | 并发 {CONCURRENCY} 路 | MiniMax M2.7 Max\")\n print(\"=\" * 65)\n\n for d in [NOTES_DIR, PDF_DIR, FRAMES_DIR]:\n d.mkdir(parents=True, exist_ok=True)\n\n # Check video downloads\n video_count = len(list(VIDEOS_DIR.glob(\"*.mp4\"))) if VIDEOS_DIR.exists() else 0\n print(f\"\\n 已下载视频: {video_count}/26\")\n if video_count == 0:\n print(\" ⚠️ 视频尚未下载完成,将在无截图模式下生成笔记\")\n print(\" (视频下载完成后可重新运行以添加截图)\")\n\n # Process all episodes concurrently\n results = [None] * len(EPISODES)\n t0 = time.time()\n\n with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:\n futures = {pool.submit(process_one_episode, i): i for i in range(len(EPISODES))}\n for future in as_completed(futures):\n idx = futures[future]\n try:\n results[idx] = future.result()\n r = results[idx]\n print(f\" ✅ [{r['ep']:02d}/26] {r['title']} — {r['notes_len']} chars, {r['frames']} frames\")\n except Exception as e:\n ep = EPISODES[idx]\n print(f\" ❌ [{ep[0]:02d}/26] {ep[1]} — FAILED: {e}\")\n results[idx] = {\"ep\": ep[0], \"title\": ep[1], \"notes_len\": 0, \"frames\": 0, \"pdf\": None, \"html\": None}\n\n elapsed = time.time() - t0\n\n # Build merged HTML\n print(\"\\n Merging all episodes into one document...\")\n valid_results = [r for r in results if r]\n valid_results.sort(key=lambda r: r[\"ep\"])\n merged_html = build_merged_html(valid_results)\n merged_path = PDF_DIR / \"MiniMind_全集_图文讲义.html\"\n merged_path.write_text(merged_html, encoding=\"utf-8\")\n\n # Convert merged to PDF\n merged_pdf = merged_path.with_suffix(\".pdf\")\n html_to_pdf(merged_path, merged_pdf)\n\n # Summary\n print(\"\\n\" + \"=\" * 65)\n print(f\" ✅ 全部完成! 耗时 {elapsed:.0f}秒 ({elapsed/60:.1f}分钟)\")\n success = sum(1 for r in valid_results if r[\"notes_len\"] > 200)\n total_chars = sum(r[\"notes_len\"] for r in valid_results)\n total_frames = sum(r[\"frames\"] for r in valid_results)\n print(f\" 📊 成功: {success}/26 集\")\n print(f\" 📝 总字数: {total_chars:,}\")\n print(f\" 🖼️ 总截图: {total_frames}\")\n print(f\" 📄 合并HTML: {merged_path}\")\n if merged_pdf.exists():\n print(f\" 📕 合并PDF: {merged_pdf} ({merged_pdf.stat().st_size//1024}KB)\")\n print(\"=\" * 65)\n\n\nif __name__ == \"__main__\":\n main()\n"}}, "desktop/src-tauri/tauri.conf.json": {"306884011b1412fecacfe702873262b0535696ea": {"t": 1785599231.1765406, "n": 849, "h": "1d1bcdd54d0024c5", "c": "{\n \"$schema\": \"https://raw.githubusercontent.com/nicegui/nicegui/main/tauri.conf.schema.json\",\n \"productName\": \"NoteKing Pro\",\n \"version\": \"2.0.0\",\n \"identifier\": \"com.noteking.app\",\n \"build\": {\n \"frontendDist\": \"../src\",\n \"devUrl\": \"http://localhost:3000\",\n \"beforeDevCommand\": \"\",\n \"beforeBuildCommand\": \"\"\n },\n \"app\": {\n \"title\": \"NoteKing Pro - 全网最强视频/录音处理工具\",\n \"windows\": [\n {\n \"title\": \"NoteKing Pro\",\n \"width\": 1280,\n \"height\": 860,\n \"resizable\": true,\n \"fullscreen\": false,\n \"decorations\": true\n }\n ],\n \"security\": {\n \"csp\": null\n }\n },\n \"bundle\": {\n \"active\": true,\n \"targets\": \"all\",\n \"icon\": [\n \"icons/32x32.png\",\n \"icons/128x128.png\",\n \"icons/128x128@2x.png\",\n \"icons/icon.icns\",\n \"icons/icon.ico\"\n ]\n }\n}\n"}}, "docker-compose.gpu.yml": {"86f1a72f4c61289e6be277a680defa66ef553c8a": {"t": 1785599231.255304, "n": 837, "h": "760707755b8e6f72", "c": "version: \"3.9\"\n\nservices:\n web:\n build:\n context: .\n dockerfile: Dockerfile.gpu\n ports:\n - \"8000:8000\"\n environment:\n - NOTEKING_LLM_API_KEY=${NOTEKING_LLM_API_KEY}\n - NOTEKING_LLM_BASE_URL=${NOTEKING_LLM_BASE_URL:-}\n - NOTEKING_LLM_MODEL=${NOTEKING_LLM_MODEL:-gpt-4o-mini}\n - NOTEKING_PROXY=${NOTEKING_PROXY:-}\n - BILIBILI_SESSDATA=${BILIBILI_SESSDATA:-}\n - HF_TOKEN=${HF_TOKEN:-}\n volumes:\n - noteking_output:/app/noteking_output\n - noteking_cache:/root/.noteking\n - huggingface_cache:/root/.cache/huggingface\n deploy:\n resources:\n reservations:\n devices:\n - driver: nvidia\n count: 1\n capabilities: [gpu]\n restart: unless-stopped\n\nvolumes:\n noteking_output:\n noteking_cache:\n huggingface_cache:\n"}}, "desktop/src-tauri/Cargo.toml": {"8e170ff7bf607625fbc5c1ebb4132370036b7f83": {"t": 1785599231.4028912, "n": 322, "h": "6bdfbfbb7670eac3", "c": "[package]\nname = \"noteking-desktop\"\nversion = \"0.1.0\"\nedition = \"2021\"\ndescription = \"NoteKing Desktop - Video to Learning Notes\"\n\n[dependencies]\ntauri = { version = \"2\", features = [] }\nserde = { version = \"1\", features = [\"derive\"] }\nserde_json = \"1\"\n\n[build-dependencies]\ntauri-build = { version = \"2\", features = [] }\n"}}, "core/templates/interview_template.py": {"e2804953e46861fe00c40a68253872d514410216": {"t": 1785599231.810766, "n": 1366, "h": "a2401a564960a8b9", "c": "\"\"\"Interview template: Q&A structure with key insights and positions.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass InterviewTemplate(BaseTemplate):\n name = \"interview\"\n display_name = \"访谈记录\"\n description = \"Structured interview notes with Q&A, key insights, and speaker positions\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n speakers = ctx.extra.get(\"speakers\", [])\n\n context_info = f\"\\n访谈描述: {user_context}\" if user_context else \"\"\n speaker_info = f\"\\n参与者: {', '.join(speakers)}\" if speakers else \"\"\n\n return f\"\"\"请根据以下访谈/对话录音转录稿,生成一份结构化的访谈记录。\n\n访谈标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}{speaker_info}\n\n访谈录音转录稿:\n{transcript}\n\n请按以下格式输出:\n\n# 访谈记录: [标题]\n\n## 访谈概要\n- **主题**: [访谈主题]\n- **参与者**: [采访者和受访者的身份]\n- **核心观点**: [一句话概括]\n\n## 人物介绍\n[每位参与者的简要背景介绍]\n\n## Q&A 结构化整理\n\n### Q1: [问题概括]\n**[受访者]**: [完整回答要点]\n> \"[原话引用]\"\n\n### Q2: [问题概括]\n...\n\n## 关键观点提取\n1. **[观点1]**: [详细说明] —— [发言人]\n2. **[观点2]**: [详细说明] —— [发言人]\n\n## 金句收集\n- > \"[金句1]\" —— [发言人]\n- > \"[金句2]\" —— [发言人]\n\n## 人物立场分析\n| 参与者 | 核心立场 | 支撑论据 |\n|--------|---------|---------|\n\n## 访谈总结\n[总结访谈的核心收获和启发]\n\n要求:\n1. 按Q&A逻辑重组,不必严格按时间顺序\n2. 保留重要原话引用\n3. 分析各方立场和观点\n4. 用中文输出\"\"\"\n"}}, "core/templates/latex_pdf.py": {"598475a5f95ae827a48a737cd0e749929f2ee648": {"t": 1785599231.8841708, "n": 1794, "h": "f227c42692f62c2d", "c": "\"\"\"LaTeX PDF lecture notes template (inspired by wdkns-skills).\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass LaTeXPDFTemplate(BaseTemplate):\n name = \"latex_pdf\"\n display_name = \"LaTeX PDF 讲义\"\n description = \"Professional LaTeX lecture notes compiled to PDF\"\n file_extension = \".tex\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n chapters_info = \"\"\n if ctx.has_chapters:\n chapters_info = \"\\n视频章节:\\n\"\n for ch in ctx.meta.chapters:\n t = ch.get(\"title\", \"\")\n s = ch.get(\"start_time\", 0)\n chapters_info += f\"- [{s:.0f}s] {t}\\n\"\n\n return f\"\"\"请根据以下视频内容,生成一份完整的 LaTeX 讲义文档。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n时长: {ctx.meta.duration:.0f} 秒\n{chapters_info}\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 生成完整的 .tex 文档,从 \\\\documentclass 到 \\\\end{{document}}\n2. 使用 ctexart 文档类(支持中文)\n3. 按教学逻辑重组内容,而非简单按字幕时间顺序排列\n4. 每个主要章节结构:\n - 动机: 为什么要学这个\n - 核心概念: 主要思想\n - 机制/方法: 如何工作\n - 示例: 具体案例\n - 小结: 本节要点\n5. 公式使用 $$...$$ 或 \\\\begin{{equation}},并解释每个符号\n6. 代码使用 \\\\begin{{lstlisting}} 环境\n7. 重要概念用 \\\\textbf{{}} 强调\n8. 每个主要 section 结尾加 \\\\subsection{{本章小结}}\n9. 文档最后加 \\\\section{{总结与延伸}}\n\n⚠️ 严格禁止:\n- 绝对不要使用 \\\\includegraphics 命令\n- 绝对不要使用 figure 环境 (\\\\begin{{figure}})\n- 绝对不要使用 graphicx 宏包\n- 绝对不要生成任何\"图 1\"、\"图 2\"、\"关键帧\"、\"截图\"等虚假图片引用或描述\n- 不要出现任何形式的图片占位符或图片说明文字\n- 这是纯文本讲义,只使用文字、公式、列表、表格和代码环境\n\nLaTeX 模板头部:\n\\\\documentclass[12pt,a4paper]{{ctexart}}\n\\\\usepackage{{amsmath,amssymb,listings,xcolor,geometry,hyperref}}\n\\\\geometry{{margin=2.5cm}}\n\\\\lstset{{basicstyle=\\\\ttfamily\\\\small,breaklines=true,frame=single}}\n\\\\title{{{ctx.meta.title}}}\n\\\\author{{笔记整理自 {ctx.meta.uploader} 的视频}}\n\\\\date{{}}\n\n请直接输出完整的 .tex 文档(纯文字,不含任何图片):\"\"\"\n"}}, "core/templates/smart_summary.py": {"6b9fb70a19747abeceb7f36bef3ec80728a4210d": {"t": 1785599231.9761176, "n": 1297, "h": "1f3a5661c1e7ebe2", "c": "\"\"\"Smart summary template: adaptive-length summary based on content complexity.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass SmartSummaryTemplate(BaseTemplate):\n name = \"smart_summary\"\n display_name = \"智能摘要\"\n description = \"Adaptive-length summary that adjusts based on content complexity\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n num_speakers = ctx.extra.get(\"num_speakers\", 0)\n\n context_info = f\"\\n描述: {user_context}\" if user_context else \"\"\n speaker_info = f\"\\n说话人数量: {num_speakers}\" if num_speakers else \"\"\n\n return f\"\"\"请根据以下录音转录稿,生成一份智能摘要。根据内容的复杂度和信息密度自动调节摘要的长度和深度。\n\n标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}{speaker_info}\n\n录音转录稿:\n{transcript}\n\n请按以下三个层次输出摘要:\n\n# 智能摘要: [标题]\n\n## 一句话概括\n[用一句精炼的话概括全部内容]\n\n## 要点速览 (30秒阅读)\n- [要点1]\n- [要点2]\n- [要点3]\n- [要点4]\n- [要点5]\n\n## 详细摘要 (3分钟阅读)\n\n### 背景\n[内容的背景和上下文]\n\n### 核心内容\n[详细的内容摘要,按逻辑组织]\n\n### 关键观点\n1. **[观点1]**: [阐述]\n2. **[观点2]**: [阐述]\n3. **[观点3]**: [阐述]\n\n### 结论/成果\n[最终结论或成果]\n\n## 关键数据/事实\n- [提取的关键数据或事实]\n\n## 值得记住的话\n> \"[引用]\" —— [发言人]\n\n要求:\n1. 三个层次从简到详,满足不同阅读需求\n2. 不遗漏重要信息\n3. 区分事实和观点\n4. 用中文输出\"\"\"\n"}}, "install.sh": {"d03f4fddb09b98c26f61acb834a491c59c85a8d3": {"t": 1785599232.367804, "n": 2032, "h": "0cacb40e2ca6e89c", "c": "#!/bin/bash\n# NoteKing One-Click Install Script\n# Usage: curl -sSL https://raw.githubusercontent.com/bcefghj/noteking/main/install.sh | bash\n\nset -e\n\nINSTALL_DIR=\"/opt/noteking\"\nREPO_URL=\"https://github.com/bcefghj/noteking.git\"\n\necho \"========================================\"\necho \" NoteKing - One-Click Install\"\necho \" Video/Blog to Learning Notes Tool\"\necho \"========================================\"\necho \"\"\n\n# Check if running as root\nif [ \"$EUID\" -ne 0 ]; then\n echo \"Please run as root: sudo bash install.sh\"\n exit 1\nfi\n\n# Install Docker if not present\nif ! command -v docker &> /dev/null; then\n echo \"[1/4] Installing Docker...\"\n curl -fsSL https://get.docker.com | sh\n systemctl start docker\n systemctl enable docker\n echo \"Docker installed successfully.\"\nelse\n echo \"[1/4] Docker already installed.\"\nfi\n\n# Install Docker Compose if not present\nif ! command -v docker compose &> /dev/null; then\n echo \"[2/4] Installing Docker Compose...\"\n apt-get update && apt-get install -y docker-compose-plugin 2>/dev/null || true\nfi\necho \"[2/4] Docker Compose ready.\"\n\n# Clone NoteKing\necho \"[3/4] Downloading NoteKing...\"\nif [ -d \"$INSTALL_DIR\" ]; then\n cd \"$INSTALL_DIR\"\n git pull\nelse\n git clone \"$REPO_URL\" \"$INSTALL_DIR\"\n cd \"$INSTALL_DIR\"\nfi\n\n# Create .env if not exists\nif [ ! -f \"$INSTALL_DIR/.env\" ]; then\n cp \"$INSTALL_DIR/.env.example\" \"$INSTALL_DIR/.env\"\n echo \"\"\n echo \"[IMPORTANT] Please edit $INSTALL_DIR/.env and set your LLM API Key!\"\n echo \"Run: nano $INSTALL_DIR/.env\"\nfi\n\n# Start services\necho \"[4/4] Starting NoteKing...\"\ncd \"$INSTALL_DIR\"\ndocker compose up -d --build\n\necho \"\"\necho \"========================================\"\necho \" NoteKing installed successfully!\"\necho \"\"\necho \" Web UI: http://$(hostname -I | awk '{print $1}'):3000\"\necho \" API: http://$(hostname -I | awk '{print $1}'):8000/docs\"\necho \"\"\necho \" Config: $INSTALL_DIR/.env\"\necho \" Logs: cd $INSTALL_DIR && docker compose logs -f\"\necho \"========================================\"\n"}}, "mcp/src/index.ts": {"462c6f15252d62dcf13f0defaa51e73087a46ef3": {"t": 1785599232.4231305, "n": 10303, "h": "525bcae6c6434420", "c": "#!/usr/bin/env node\n/**\n * NoteKing Pro MCP Server\n *\n * Provides 14+ tools for video/recording/meeting to notes conversion.\n * Supports STDIO and HTTP/SSE transport modes.\n * Compatible with Cursor, Claude Desktop, OpenClaw, Codex.\n */\n\nimport { McpServer } from \"@modelcontextprotocol/sdk/server/mcp.js\";\nimport { StdioServerTransport } from \"@modelcontextprotocol/sdk/server/stdio.js\";\nimport { z } from \"zod\";\nimport { execSync } from \"child_process\";\n\nconst server = new McpServer({\n name: \"noteking\",\n version: \"2.0.0\",\n});\n\nfunction callCore(args: string[]): string {\n try {\n const result = execSync(\n `python -m cli ${args.map((a) => `\"${a}\"`).join(\" \")}`,\n {\n encoding: \"utf-8\",\n timeout: 600_000,\n cwd: process.env.NOTEKING_DIR || \".\",\n maxBuffer: 50 * 1024 * 1024,\n }\n );\n return result;\n } catch (err: any) {\n return `Error: ${err.message || err}`;\n }\n}\n\nfunction callAPI(endpoint: string, body: Record<string, any>): string {\n const apiBase = process.env.NOTEKING_API || \"http://127.0.0.1:8000\";\n try {\n const result = execSync(\n `curl -s -X POST \"${apiBase}${endpoint}\" -H \"Content-Type: application/json\" -d '${JSON.stringify(body)}'`,\n { encoding: \"utf-8\", timeout: 600_000 }\n );\n return result;\n } catch (err: any) {\n return `Error: ${err.message || err}`;\n }\n}\n\n// ---------- Template list ----------\n\nconst TEMPLATES = [\n \"brief\", \"detailed\", \"mindmap\", \"flashcard\", \"quiz\", \"timeline\",\n \"exam\", \"tutorial\", \"news\", \"podcast\", \"xhs_note\", \"latex_pdf\", \"custom\",\n \"meeting_minutes\", \"lecture_notes\", \"interview\", \"brainstorm\",\n \"news_digest\", \"exam_prep\", \"cornell_notes\", \"podcast_shownotes\",\n \"entertainment\", \"smart_summary\",\n];\n\nconst SCENES = [\n \"meeting\", \"lecture\", \"interview\", \"brainstorm\",\n \"news\", \"exam\", \"entertainment\", \"custom\",\n];\n\n// ---------- Original Tools ----------\n\nserver.tool(\n \"summarize_video\",\n \"Convert a video/blog URL into structured notes using the specified template\",\n {\n url: z.string().describe(\"Video URL or local file path\"),\n template: z.enum(TEMPLATES as [string, ...string[]]).default(\"detailed\").describe(\"Output template\"),\n custom_prompt: z.string().optional().describe(\"Custom prompt for 'custom' template\"),\n },\n async ({ url, template, custom_prompt }) => {\n const body: Record<string, any> = { url, template };\n if (custom_prompt) body.custom_prompt = custom_prompt;\n const result = callAPI(\"/api/v1/summarize\", body);\n try {\n const parsed = JSON.parse(result);\n return {\n content: [{ type: \"text\" as const, text: `# ${parsed.title || \"Video Notes\"}\\n\\n${parsed.content || result}` }],\n };\n } catch {\n return { content: [{ type: \"text\" as const, text: result }] };\n }\n }\n);\n\nserver.tool(\n \"batch_summarize\",\n \"Process a playlist/collection/series and generate notes for all videos\",\n {\n url: z.string().describe(\"Playlist or collection URL\"),\n template: z.enum(TEMPLATES as [string, ...string[]]).default(\"detailed\").describe(\"Output template\"),\n },\n async ({ url, template }) => {\n const result = callAPI(\"/api/v1/batch\", { url, template });\n try {\n const parsed = JSON.parse(result);\n return {\n content: [{ type: \"text\" as const, text: `# Batch Results (${parsed.completed}/${parsed.total})\\n\\n${parsed.content || result}` }],\n };\n } catch {\n return { content: [{ type: \"text\" as const, text: result }] };\n }\n }\n);\n\nserver.tool(\n \"get_transcript\",\n \"Extract subtitles/transcript text from a video\",\n { url: z.string().describe(\"Video URL\") },\n async ({ url }) => {\n const result = callAPI(`/api/v1/transcript?url=${encodeURIComponent(url)}`, {});\n try {\n const parsed = JSON.parse(result);\n return { content: [{ type: \"text\" as const, text: parsed.transcript || result }] };\n } catch {\n return { content: [{ type: \"text\" as const, text: result }] };\n }\n }\n);\n\nserver.tool(\n \"get_video_info\",\n \"Get video metadata (title, duration, chapters, subtitle availability)\",\n { url: z.string().describe(\"Video URL\") },\n async ({ url }) => {\n try {\n const result = execSync(\n `curl -s \"${process.env.NOTEKING_API || \"http://127.0.0.1:8000\"}/api/v1/info?url=${encodeURIComponent(url)}\"`,\n { encoding: \"utf-8\", timeout: 60_000 }\n );\n return { content: [{ type: \"text\" as const, text: result }] };\n } catch (err: any) {\n return { content: [{ type: \"text\" as const, text: `Error: ${err.message}` }] };\n }\n }\n);\n\nserver.tool(\n \"search_in_transcript\",\n \"Search for specific text within a video transcript\",\n {\n url: z.string().describe(\"Video URL\"),\n query: z.string().describe(\"Search query\"),\n },\n async ({ url, query }) => {\n const transcriptResult = callAPI(`/api/v1/transcript?url=${encodeURIComponent(url)}`, {});\n try {\n const parsed = JSON.parse(transcriptResult);\n const text = parsed.transcript || \"\";\n const lines = text.split(\"\\n\");\n const matches = lines.filter((l: string) => l.toLowerCase().includes(query.toLowerCase()));\n return {\n content: [{\n type: \"text\" as const,\n text: matches.length ? `Found ${matches.length} matches:\\n\\n${matches.join(\"\\n\")}` : `No matches found for \"${query}\"`,\n }],\n };\n } catch {\n return { content: [{ type: \"text\" as const, text: transcriptResult }] };\n }\n }\n);\n\nserver.tool(\n \"answer_from_video\",\n \"Answer a question based on video content\",\n {\n url: z.string().describe(\"Video URL\"),\n question: z.string().describe(\"Question to answer\"),\n },\n async ({ url, question }) => {\n const result = callAPI(\"/api/v1/summarize\", {\n url, template: \"custom\",\n custom_prompt: `请根据视频内容回答以下问题:\\n\\n${question}\\n\\n要求: 引用视频中的具体内容来支持你的回答。`,\n });\n try {\n const parsed = JSON.parse(result);\n return { content: [{ type: \"text\" as const, text: parsed.content || result }] };\n } catch {\n return { content: [{ type: \"text\" as const, text: result }] };\n }\n }\n);\n\n// ---------- New Recording Tools ----------\n\nserver.tool(\n \"process_recording\",\n \"Process a local audio/video recording into structured notes with speaker diarization, noise reduction, and scene-specific templates. Supports meeting minutes, lecture notes, interview records, and more.\",\n {\n file_path: z.string().describe(\"Path to the local audio/video file\"),\n template: z.enum(TEMPLATES as [string, ...string[]]).default(\"meeting_minutes\").describe(\"Output template\"),\n context: z.string().optional().describe(\"Content description (e.g. 'AI开源圆桌会议')\"),\n scene: z.enum(SCENES as [string, ...string[]]).optional().describe(\"Scene type\"),\n num_speakers: z.number().optional().describe(\"Number of speakers (auto-detect if not set)\"),\n denoise_level: z.number().min(0).max(3).default(1).describe(\"Noise reduction level (0=none, 1=light, 2=medium, 3=heavy)\"),\n output_formats: z.string().default(\"markdown\").describe(\"Comma-separated output formats: markdown,pdf,srt,vtt,transcript,json\"),\n },\n async ({ file_path, template, context, scene, num_speakers, denoise_level, output_formats }) => {\n const args = [\"process\", file_path, \"-t\", template, \"--denoise\", String(denoise_level), \"--format\", output_formats];\n if (context) args.push(\"-c\", context);\n if (scene) args.push(\"-s\", scene);\n if (num_speakers) args.push(\"--speakers\", String(num_speakers));\n const result = callCore(args);\n return { content: [{ type: \"text\" as const, text: result }] };\n }\n);\n\nserver.tool(\n \"transcribe_file\",\n \"Transcribe a local audio/video file to text using the best available ASR engine\",\n {\n file_path: z.string().describe(\"Path to the local audio/video file\"),\n language: z.string().optional().describe(\"Language code (auto-detect if not set)\"),\n },\n async ({ file_path, language }) => {\n const args = [\"transcribe\", file_path];\n if (language) args.push(\"--lang\", language);\n const result = callCore(args);\n return { content: [{ type: \"text\" as const, text: result }] };\n }\n);\n\nserver.tool(\n \"denoise_audio\",\n \"Apply noise reduction to an audio file\",\n {\n file_path: z.string().describe(\"Path to the audio file\"),\n level: z.number().min(1).max(3).default(2).describe(\"Noise reduction level (1=light, 2=medium, 3=heavy)\"),\n },\n async ({ file_path, level }) => {\n const result = callCore([\"denoise\", file_path, \"--level\", String(level)]);\n return { content: [{ type: \"text\" as const, text: result }] };\n }\n);\n\nserver.tool(\n \"merge_media_files\",\n \"Merge multiple audio/video files into one\",\n {\n file_paths: z.array(z.string()).describe(\"Array of file paths to merge\"),\n output_path: z.string().describe(\"Output file path\"),\n },\n async ({ file_paths, output_path }) => {\n const args = [\"merge\", ...file_paths, \"-o\", output_path];\n const result = callCore(args);\n return { content: [{ type: \"text\" as const, text: result }] };\n }\n);\n\nserver.tool(\n \"list_templates\",\n \"List all available output templates with descriptions\",\n {},\n async () => {\n const templateList = TEMPLATES.map((t) => `- **${t}**`).join(\"\\n\");\n return {\n content: [{\n type: \"text\" as const,\n text: `# Available Templates (${TEMPLATES.length})\\n\\n${templateList}\\n\\nUse with summarize_video or process_recording tool's template parameter.`,\n }],\n };\n }\n);\n\nserver.tool(\n \"list_scenes\",\n \"List available scene types for recording processing\",\n {},\n async () => {\n const sceneDescs: Record<string, string> = {\n meeting: \"会议 — 参会人、议题、决议、行动项\",\n lecture: \"课堂/讲座 — 知识点、公式、习题\",\n interview: \"访谈 — Q&A、观点、立场分析\",\n brainstorm: \"灵感记录 — 想法、思维导图、行动建议\",\n news: \"新闻/播客 — 5W1H、引用、背景\",\n exam: \"考试复习 — 闪卡、模拟题、要点清单\",\n entertainment: \"娱乐 — 高光、金句、推荐指数\",\n custom: \"自定义 — 用户提供prompt\",\n };\n const text = Object.entries(sceneDescs).map(([k, v]) => `- **${k}**: ${v}`).join(\"\\n\");\n return { content: [{ type: \"text\" as const, text: `# Available Scenes\\n\\n${text}` }] };\n }\n);\n\n// ---------- Start ----------\n\nasync function main() {\n const transport = new StdioServerTransport();\n await server.connect(transport);\n console.error(\"NoteKing Pro MCP Server v2.0.0 running on STDIO\");\n}\n\nmain().catch(console.error);\n"}}, "core/transcriber.py": {"b43f3471c328822ed13193cbb617a504774817cd": {"t": 1785599232.4482305, "n": 12306, "h": "683d3037b6239b58", "c": "\"\"\"ASR engine pool: multiple transcription engines with factory pattern.\n\nSupports:\n- FunASR Paraformer-zh (best for Chinese, 12x faster, WER 8.4%)\n- faster-whisper (best for multilingual/English)\n- SenseVoice (50+ languages, 15x faster than Whisper)\n- Groq Whisper API (cloud fallback)\n- OpenAI Whisper API (cloud fallback)\n\"\"\"\n\nfrom __future__ import annotations\n\nimport logging\nfrom abc import ABC, abstractmethod\nfrom pathlib import Path\n\nfrom .config import AppConfig\nfrom .subtitle import SubtitleResult, SubtitleSegment\n\nlogger = logging.getLogger(__name__)\n\n\nclass ASREngine(ABC):\n \"\"\"Abstract base for all ASR engines.\"\"\"\n\n name: str = \"base\"\n\n @abstractmethod\n def transcribe(self, audio_path: Path, language: str = \"zh\") -> SubtitleResult:\n ...\n\n @classmethod\n def is_available(cls, config: AppConfig) -> bool:\n return True\n\n\nclass FunASREngine(ASREngine):\n \"\"\"Alibaba FunASR Paraformer — best Chinese ASR (WER 8.4%, 12x faster).\n\n Supports Chinese, English, and code-switching natively.\n Built-in VAD, punctuation restoration, and timestamps.\n \"\"\"\n\n name = \"funasr\"\n\n def __init__(self, model: str = \"paraformer-zh\"):\n self.model_name = model\n self._pipeline = None\n\n def _get_pipeline(self):\n if self._pipeline is None:\n from funasr import AutoModel\n self._pipeline = AutoModel(\n model=self.model_name,\n vad_model=\"fsmn-vad\",\n punc_model=\"ct-punc\",\n )\n return self._pipeline\n\n def transcribe(self, audio_path: Path, language: str = \"zh\") -> SubtitleResult:\n pipeline = self._get_pipeline()\n result = pipeline.generate(\n input=str(audio_path),\n batch_size_s=300,\n )\n\n segments = []\n if result and len(result) > 0:\n item = result[0]\n text = item.get(\"text\", \"\")\n timestamps = item.get(\"timestamp\", [])\n sentence_info = item.get(\"sentence_info\", [])\n\n if sentence_info:\n for sent in sentence_info:\n seg_text = sent.get(\"text\", \"\")\n start_ms = sent.get(\"start\", 0)\n end_ms = sent.get(\"end\", 0)\n segments.append(SubtitleSegment(\n start=start_ms / 1000.0,\n end=end_ms / 1000.0,\n text=seg_text.strip(),\n ))\n elif timestamps:\n for ts in timestamps:\n if len(ts) >= 2:\n start_ms = ts[0]\n end_ms = ts[1]\n segments.append(SubtitleSegment(\n start=start_ms / 1000.0,\n end=end_ms / 1000.0,\n text=\"\",\n ))\n if text and not any(s.text for s in segments):\n if segments:\n segments[0] = SubtitleSegment(\n start=segments[0].start,\n end=segments[-1].end,\n text=text.strip(),\n )\n segments = [segments[0]]\n elif text:\n segments.append(SubtitleSegment(start=0, end=0, text=text.strip()))\n\n return SubtitleResult(segments=segments, source=\"asr\", language=language)\n\n @classmethod\n def is_available(cls, config: AppConfig) -> bool:\n try:\n import funasr\n return True\n except ImportError:\n return False\n\n\nclass SenseVoiceEngine(ASREngine):\n \"\"\"FunAudioLLM SenseVoice — 50+ languages, 15x faster than Whisper-Large.\"\"\"\n\n name = \"sensevoice\"\n\n def __init__(self):\n self._model = None\n\n def _get_model(self):\n if self._model is None:\n from funasr import AutoModel\n self._model = AutoModel(model=\"iic/SenseVoiceSmall\")\n return self._model\n\n def transcribe(self, audio_path: Path, language: str = \"zh\") -> SubtitleResult:\n model = self._get_model()\n result = model.generate(\n input=str(audio_path),\n language=\"auto\",\n use_itn=True,\n )\n\n segments = []\n if result and len(result) > 0:\n text = result[0].get(\"text\", \"\")\n if text:\n segments.append(SubtitleSegment(start=0, end=0, text=text.strip()))\n\n return SubtitleResult(segments=segments, source=\"asr\", language=language)\n\n @classmethod\n def is_available(cls, config: AppConfig) -> bool:\n try:\n import funasr\n return True\n except ImportError:\n return False\n\n\nclass FasterWhisperEngine(ASREngine):\n \"\"\"Local faster-whisper engine (free, GPU-accelerated).\"\"\"\n\n name = \"faster_whisper\"\n\n def __init__(self, model_size: str = \"base\"):\n self.model_size = model_size\n self._model = None\n\n def _get_model(self):\n if self._model is None:\n from faster_whisper import WhisperModel\n self._model = WhisperModel(\n self.model_size, device=\"auto\", compute_type=\"auto\"\n )\n return self._model\n\n def transcribe(self, audio_path: Path, language: str = \"zh\") -> SubtitleResult:\n model = self._get_model()\n segs, info = model.transcribe(\n str(audio_path), language=language, vad_filter=True\n )\n segments = []\n for seg in segs:\n segments.append(SubtitleSegment(\n start=seg.start, end=seg.end, text=seg.text.strip()\n ))\n return SubtitleResult(\n segments=segments, source=\"asr\", language=info.language\n )\n\n @classmethod\n def is_available(cls, config: AppConfig) -> bool:\n try:\n import faster_whisper\n return True\n except ImportError:\n return False\n\n\nclass GroqWhisperEngine(ASREngine):\n \"\"\"Groq cloud Whisper API (free tier available).\"\"\"\n\n name = \"groq\"\n\n def __init__(self, api_key: str):\n self.api_key = api_key\n\n def transcribe(self, audio_path: Path, language: str = \"zh\") -> SubtitleResult:\n import httpx\n\n with open(audio_path, \"rb\") as f:\n resp = httpx.post(\n \"https://api.groq.com/openai/v1/audio/transcriptions\",\n headers={\"Authorization\": f\"Bearer {self.api_key}\"},\n files={\"file\": (audio_path.name, f, \"audio/wav\")},\n data={\n \"model\": \"whisper-large-v3\",\n \"response_format\": \"verbose_json\",\n \"language\": language,\n },\n timeout=300,\n )\n resp.raise_for_status()\n data = resp.json()\n segments = []\n for seg in data.get(\"segments\", []):\n segments.append(SubtitleSegment(\n start=seg[\"start\"], end=seg[\"end\"], text=seg[\"text\"].strip()\n ))\n if not segments and data.get(\"text\"):\n segments.append(SubtitleSegment(start=0, end=0, text=data[\"text\"]))\n return SubtitleResult(segments=segments, source=\"asr\", language=language)\n\n @classmethod\n def is_available(cls, config: AppConfig) -> bool:\n return bool(config.asr.groq_api_key)\n\n\nclass OpenAIWhisperEngine(ASREngine):\n \"\"\"OpenAI Whisper API.\"\"\"\n\n name = \"openai_whisper\"\n\n def __init__(self, api_key: str):\n self.api_key = api_key\n\n def transcribe(self, audio_path: Path, language: str = \"zh\") -> SubtitleResult:\n from openai import OpenAI\n\n client = OpenAI(api_key=self.api_key)\n with open(audio_path, \"rb\") as f:\n transcript = client.audio.transcriptions.create(\n model=\"whisper-1\",\n file=f,\n response_format=\"verbose_json\",\n language=language,\n )\n\n segments = []\n for seg in getattr(transcript, \"segments\", []) or []:\n segments.append(SubtitleSegment(\n start=seg[\"start\"], end=seg[\"end\"], text=seg[\"text\"].strip()\n ))\n if not segments and transcript.text:\n segments.append(SubtitleSegment(start=0, end=0, text=transcript.text))\n return SubtitleResult(segments=segments, source=\"asr\", language=language)\n\n @classmethod\n def is_available(cls, config: AppConfig) -> bool:\n if config.asr.openai_api_key:\n return True\n # Only use LLM API key if it's an actual OpenAI endpoint\n if config.llm.api_key and (\n not config.llm.base_url\n or \"openai\" in config.llm.base_url.lower()\n ):\n return True\n return False\n\n\n# ---------- language detection ----------\n\ndef detect_language(audio_path: Path, config: AppConfig) -> str:\n \"\"\"Detect the primary language of an audio file.\n\n Strategy:\n 1. Try faster-whisper's language detection (fast, ~1 second)\n 2. Fall back to default 'zh'\n\n Returns ISO 639-1 code (e.g. 'zh', 'en', 'ja')\n \"\"\"\n try:\n from faster_whisper import WhisperModel\n model = WhisperModel(\"tiny\", device=\"auto\", compute_type=\"auto\")\n _, info = model.transcribe(str(audio_path), language=None, vad_filter=True)\n detected = info.language\n prob = info.language_probability\n logger.info(f\"Detected language: {detected} (confidence: {prob:.2f})\")\n if prob > 0.5:\n return detected\n except (ImportError, Exception) as e:\n logger.debug(f\"Language detection failed: {e}\")\n\n return \"zh\"\n\n\ndef is_chinese_dominant(lang: str) -> bool:\n \"\"\"Check if the detected language is primarily Chinese.\"\"\"\n return lang in (\"zh\", \"yue\", \"wuu\", \"nan\", \"hak\")\n\n\n# ---------- factory ----------\n\n_ENGINES: list[type[ASREngine]] = [\n FunASREngine,\n FasterWhisperEngine,\n SenseVoiceEngine,\n GroqWhisperEngine,\n OpenAIWhisperEngine,\n]\n\n\ndef _create_engine(config: AppConfig, language: str = \"zh\") -> ASREngine:\n \"\"\"Create the best available ASR engine based on config and language.\"\"\"\n pref = config.asr.default_engine\n\n # Explicit engine preference\n if pref == \"funasr\" and FunASREngine.is_available(config):\n return FunASREngine()\n if pref == \"sensevoice\" and SenseVoiceEngine.is_available(config):\n return SenseVoiceEngine()\n if pref == \"faster_whisper\" and FasterWhisperEngine.is_available(config):\n return FasterWhisperEngine(config.asr.faster_whisper_model)\n if pref == \"groq\" and GroqWhisperEngine.is_available(config):\n return GroqWhisperEngine(config.asr.groq_api_key)\n if pref == \"openai\" and OpenAIWhisperEngine.is_available(config):\n key = config.asr.openai_api_key or config.llm.api_key\n return OpenAIWhisperEngine(key)\n\n # Auto mode: select by language\n if pref == \"auto\":\n if is_chinese_dominant(language):\n if FunASREngine.is_available(config):\n return FunASREngine()\n if FasterWhisperEngine.is_available(config):\n return FasterWhisperEngine(config.asr.faster_whisper_model)\n if SenseVoiceEngine.is_available(config):\n return SenseVoiceEngine()\n\n # Cloud fallback\n if GroqWhisperEngine.is_available(config):\n return GroqWhisperEngine(config.asr.groq_api_key)\n if OpenAIWhisperEngine.is_available(config):\n key = config.asr.openai_api_key or config.llm.api_key\n return OpenAIWhisperEngine(key)\n if FasterWhisperEngine.is_available(config):\n return FasterWhisperEngine(config.asr.faster_whisper_model)\n\n raise RuntimeError(\n \"No ASR engine available. Install funasr, faster-whisper, or provide \"\n \"an API key for Groq/OpenAI in the config.\"\n )\n\n\ndef transcribe(\n audio_path: Path,\n config: AppConfig,\n language: str | None = None,\n) -> SubtitleResult:\n \"\"\"Transcribe audio using the best available ASR engine.\n\n If language is None, auto-detects the language first.\n \"\"\"\n if language is None:\n language = detect_language(audio_path, config)\n\n engine = _create_engine(config, language)\n logger.info(f\"Using ASR engine: {engine.name} (language: {language})\")\n return engine.transcribe(audio_path, language)\n"}}, "pyproject.toml": {"9ec5dcf98053842bf0837a4c3adcab87a8779f2f": {"t": 1785599232.476777, "n": 1994, "h": "5719c82abee98cec", "c": "[build-system]\nrequires = [\"setuptools>=68.0\", \"wheel\"]\nbuild-backend = \"setuptools.build_meta\"\n\n[project]\nname = \"noteking\"\nversion = \"2.0.0\"\ndescription = \"The ultimate video/recording to learning notes tool. Supports 30+ platforms, local video/audio files, meeting recordings with speaker diarization, noise reduction, and 23 output templates including LaTeX PDF.\"\nreadme = \"README.md\"\nlicense = {text = \"MIT\"}\nrequires-python = \">=3.11\"\nauthors = [{name = \"NoteKing Contributors\"}]\nkeywords = [\"video\", \"notes\", \"summary\", \"meeting\", \"recording\", \"transcription\", \"diarization\", \"bilibili\", \"youtube\", \"ai\", \"mcp\"]\nclassifiers = [\n \"Development Status :: 4 - Beta\",\n \"Intended Audience :: Education\",\n \"License :: OSI Approved :: MIT License\",\n \"Programming Language :: Python :: 3.11\",\n \"Programming Language :: Python :: 3.12\",\n \"Topic :: Education\",\n \"Topic :: Multimedia :: Video\",\n \"Topic :: Multimedia :: Sound/Audio :: Speech\",\n]\ndependencies = [\n \"yt-dlp>=2024.0\",\n \"youtube-transcript-api>=1.0.0\",\n \"openai>=1.0.0\",\n \"httpx>=0.27.0\",\n \"pydantic>=2.0\",\n \"pydantic-settings>=2.0\",\n \"rich>=13.0\",\n \"click>=8.0\",\n \"aiofiles>=24.0\",\n \"pysrt>=1.1.2\",\n]\n\n[project.optional-dependencies]\nasr = [\"faster-whisper>=1.0.0\"]\nfunasr = [\"funasr>=1.0.0\", \"modelscope\"]\nmeeting = [\n \"noisereduce>=3.0.0\",\n \"numpy>=1.24.0\",\n]\ndiarize = [\"pyannote.audio>=3.1.0\", \"torch>=2.0.0\"]\napi = [\"fastapi>=0.115.0\", \"uvicorn[standard]>=0.30.0\", \"websockets>=13.0\"]\nframes = [\"scenedetect[opencv]>=0.6\"]\npdf = [\"weasyprint>=62.0\"]\nall = [\"noteking[asr,funasr,meeting,diarize,api,frames,pdf]\"]\n\n[project.scripts]\nnoteking = \"cli.__main__:main\"\n\n[project.urls]\nHomepage = \"https://github.com/bcefghj/noteking\"\nRepository = \"https://github.com/bcefghj/noteking\"\nDocumentation = \"https://github.com/bcefghj/noteking/tree/main/docs\"\nIssues = \"https://github.com/bcefghj/noteking/issues\"\n\n[tool.setuptools.packages.find]\ninclude = [\"core*\", \"api*\", \"cli*\"]\n"}}, "core/templates/exam_prep.py": {"053eb406e693111e345a9433c689c0823dc0605c": {"t": 1785599232.8357372, "n": 1250, "h": "04a581a5d6a35428", "c": "\"\"\"Exam preparation template: flashcards, mock questions, and study checklist.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass ExamPrepTemplate(BaseTemplate):\n name = \"exam_prep\"\n display_name = \"考试复习\"\n description = \"Exam prep with flashcards, mock questions, key concepts, and study checklist\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = ctx.extra.get(\"diarized_transcript\", \"\") or _truncate_transcript(ctx.transcript)\n user_context = ctx.extra.get(\"context\", \"\")\n context_info = f\"\\n描述: {user_context}\" if user_context else \"\"\n\n return f\"\"\"请根据以下课程/讲座录音转录稿,生成考试复习材料。\n\n标题: {ctx.meta.title}\n时长: {ctx.meta.duration:.0f} 秒{context_info}\n\n录音转录稿:\n{transcript}\n\n请按以下格式输出:\n\n# 考试复习: [课程主题]\n\n## 核心知识点清单\n- [ ] [知识点1]\n- [ ] [知识点2]\n- [ ] [知识点3]\n\n## 知识卡片\n\n### 卡片 1\n- **问题**: [问题]\n- **答案**: [答案]\n- **难度**: ⭐⭐ (1-5)\n\n### 卡片 2\n...\n\n## 模拟选择题\n\n**1. [题目]**\n- A. [选项]\n- B. [选项]\n- C. [选项]\n- D. [选项]\n\n**答案**: [正确答案及解析]\n\n**2. [题目]**\n...\n\n## 模拟简答题\n\n**1. [题目]**\n**参考答案**: [答案要点]\n\n## 重点公式/概念\n| 概念 | 定义/公式 | 备注 |\n|------|----------|------|\n\n## 易错点提醒\n1. ⚠️ [易错点1]\n2. ⚠️ [易错点2]\n\n## 复习建议\n[学习策略和重点复习方向]\n\n要求:\n1. 知识点要全面覆盖\n2. 题目难度适中,涵盖选择题和简答题\n3. 公式用 LaTeX 格式\n4. 标注易错点和重难点\n5. 用中文输出\"\"\"\n"}}, "demos/openclaw_meeting/transcript.json": {"701a851ee2e0df1862fb583890fdc35047564f0a": {"t": 1785599233.0543842, "n": 130788, "h": "85229ab457ee1d2d", "c": "{\n \"full_text\": \"我們今天邀請到各位重磅的嘉賓\\n然後也覆蓋了其實不同的層面\\n從模型層到底層的算力層到上面的Agent層\\n那就很高興今天能跟大家一起來探討一下\\n然後可能最主要的關鍵時事這個開源\\n然後還有Agent\\n那我們就先從第一個問題開始\\n這個問題是給所有人\\n就是我們現在也是最流行的這個Open Cloud\\n然後大家自己就常使用Open Cloud或者類似的產品\\n有什麼覺得最有想像力或者印象深刻的是什麼\\n然後從技術的角度來看\\n如何看待今天Open Cloud和相關的Agent的眼鏡\\n我們要不從這個張宏這邊開始\\n好 現在感謝這裡邀請\\n然後也感謝主辦方給你們機會跟大家來交流\\n確實很早我就開始自己來玩Open Cloud\\n當時還不叫Open Cloud\\n最早叫Cloudboard\\n然後就像這些事情自己動手來搞\\n畢竟也是程序員出身\\n玩這些東西還是有一些自己的體驗\\n我覺得這個事情他可能給大家帶來的最大的一個突破點\\n或者說新鮮的一個點就在於\\n可能這件事情不再是程序員\\n或者說即刻們的專利\\n普通人也可以比較方便的來使用\\n頂尖的這些模型的這個能力\\n尤其是在編程和最能體方面的這些能力\\n所以我到現在為止\\n可能跟大家在交流的過程當中\\n更願意把Open Cloud這個事情稱作一個叫\\n稱作叫角手架\\n它提供的是一種可能性\\n在模型的技術上\\n搭起來一個很牢固的很方便的\\n但是有很靈活的這樣的一個角手架\\n大家可以按照自己的意願\\n去使用很多的底層的模型\\n提供的這個很新奇的一些東西\\n原來自己的一些想法\\n受限於自己不會寫大碼\\n或者不會這樣的一些其他的一些技能\\n那今天終於可以通過很簡單的交流\\n就可以把它完成\\n所以這個事情就對我來說\\n是一個非常大的一個衝擊吧\\n或者說讓我重新認識了這樣的一件事情\\n對 大概是這樣\\n對 那個其實我最開始用Open Cloud的時候\\n是不太適應的\\n因為我習慣於這種跟大魔形容聊天的\\n這種交流方式\\n然後我發現 Open Cloud感覺很像\\n反而很慢這種感覺\\n但是後來我一手一個問題\\n就是它和之前的這種聊天器\\n人有一個很大的不一樣\\n就是它其實應該是一個\\n能夠幫我完成一個大型任務的一個人\\n所以我其實後面開始給它提交\\n一些更複雜的任務的時候\\n我也發現其實它能夠做得很好\\n然後這件事情對我來說\\n有一個很大的感觸\\n就是其實在模型從最開始\\n按照Token去聊天\\n到現在能夠變成一個Agent\\n變成一個龍蝦\\n能夠幫你去完成任務\\n它對於我們整個的AI的想像力空間\\n已經做得一個很大的提升\\n但同時它對於整個系統的能力的要求\\n也變得很大\\n這也是為什麼我一開始用文化的\\n會覺得它有點卡著這個原因\\n對 那所以呢\\n我作為這樣的一個技術設施存的一個廠商\\n我其實看到的是\\n可到對於整個AI後面的一個\\n整個大型的系統和生態都帶來的\\n共多的機率和挑戰\\n因為我們現在的這個\\n所有的能夠用到的資源\\n想要支撐起這樣一個快速增長的時代\\n是不夠的\\n比如說就拿我們公司來說\\n我們公司在\\n月底開始基本上沒兩週\\n我們的Token量就翻一翻\\n到現在基本上翻了10倍\\n對 上次見到這個速度\\n還是那個當時三制一的時候\\n手機流量的那種感覺\\n對 所以我現在有種感覺\\n就是就是現在的這個Token用量\\n就跟當年我們有100兆的\\n為100兆每個月的這個手機流量一樣\\n對那個時代那這種情況下\\n其實我們所有的資源是需要進行\\n更好的優化 進行更好的整合\\n然後讓我們的每一個人\\n每一個不是這個\\n不僅僅在AI領域\\n而是在整個社會上\\n就是每一個先活的人\\n都能夠去把這個\\nOwn Club把這樣的AI能力的用起來\\n所以我作為技術設施的玩家\\n其實對這個時代是非常激動和感觸的\\n而且認為這裡面有很多的優化空間\\n是我們仍然應該去探索\\n應該去嘗試的\\n好 謝謝\\n我自己是把Own Club當作Agent的\\n框架的一個非常革命性\\n和顛覆性的一個事件來看待的\\n雖然我知道就是其實我身邊\\n所有在進行非常深度扣定的人\\n可能它的第一選擇還是靠扣的\\n但是我相信只有英國Own Club\\n人會獨特地感受到這個框架\\n它其實有很多在Agent框架上的設計性\\n是你先與靠扣的\\n包括最近靠扣的有很多最新的更新\\n其實都是像在Own Club去靠近\\n我自己去使用Own Club的話\\n我感覺這個框架給我自己帶來\\n來更多是一個想像力的隨時隨地的一個擴展\\n就靠扣的可能最開始我只能在我的桌面上\\n然後去延長我的創意\\n但是在Own Club可以隨時隨地去延長我的創意\\n我後面發現其實Own Club其實它帶的\\n主要核心的價值在於兩個\\n第一個是它是開源\\n開源其實是一個非常有利於\\n整個社區去深入去參與去\\n重視改進去投入到Agent框架這個事兒\\n一個非常重要的一個前世條件\\n像Own Club像靠扣的這樣的一些Agent框架\\n我認為它很大了一個價值\\n是在把國內的可能沒有非常接近幣元\\n模型但是水平還是在四幣元模型的\\n這樣一個賽道上的模型的上線給拿到非常高\\n在絕大部分的場景其實我們能發現\\n它的任務的完成度已經非常接近Klaude的最新的模型\\n然後同時它又把下線給保證得非常好\\n因為它可以靠一套哈利斯系統\\n或者說Klaude的體系等等很多的速度設計\\n保證它的任務的完成度和準確率\\n所以我覺得Own Club其實給我自己帶的\\n我認為從一個基礎大模型的角度來說\\n它是保證了基礎大模型的下線\\n然後拉伸了它的上線\\n然後此外我自己認為Own Club它其實\\n給整個社區帶的價值是它更多去點燃\\n大家發現原來在大模型外的這一層\\n更重要的AZN的這一層\\n它有非常多的想像力和空間可以做\\n然後這是我看到最近社區在\\n有更多除了研究原因外人在去參與到\\nAGA的變革當中\\n然後也有更多人去接觸到更強的AZN框架\\n哈利斯或SK4的等等\\n然後去移民程度上說是在去替代自己的工作\\n然後一定程度上是在釋放自己的時間\\n然後去做給我想像力的事情\\n對\\n我感覺首先可能從交互模式上來講\\n為什麼Own Club這次會爆火\\n我感覺首先可能第一個就是\\n給了大家一種更有活人感的一種感覺\\n因為比如說我們其實做AZN也有一兩年\\n但之前包括像Cursor,Crop Code這些AZN\\n其實大家給人感覺更強的一種有種工具感\\n對\\n然後我覺得Own Club首先第一次它的交互模式\\n以這種IM軟件簽入的方式\\n可以讓大家可能更有一種活人感\\n更有一點更接近於自己想像中的\\n個人價為私障的概念\\n所以我覺得這可能是交互模式上的\\n然後另外一個我覺得它帶給整個\\n大家的一個啟發的話是說\\n可能從當他們架構AZN loop這種非常簡單\\n但高效的一種框架其實也是再次被證明\\n然後還有就是它可能整體來說\\n就會讓我們從一思考就是說我們是否需要一個\\nOwn 1非常強大的智能體\\n然後能夠幫我們做很多事情\\n還是需要一個比較好的一個像中歷史\\n一個輕量級操作系統或是角左架\\n一樣的一個小管架\\n我覺得它帶來的是說可以通過一個小的\\n這樣一個OpenCrow\\n或者是這樣一個龍蝦的一個操績\\n一個生態可以去把整個的這種包括\\n讓大家都我覺得讓整個社區\\n大家更有玩起來的心態\\n然後是跳動整個生態裡面所有的工具\\n包括隨著像Skills\\n或者Harlene是這些\\n大家也越來越多的人可以去設計\\n更面向於像OpenCrow\\n這樣的一些系統裡面的一些應用\\n然後去負冷於各種各業\\n我覺得它這個其實和天然就和整個的開源\\n生態結合的非常緊密\\n所以我覺得這兩點其實相對來說\\n我覺得是帶給我們最大的一個啟發\\n那順著這個剛才一直在討論OpenCrow\\n然後看想問一下\\n張鵬就是看到最近智庫\\n也發布了新的GROM5 Turbo模型\\n然後我理解就是對這個Agent能力\\n其實也做了很大的增強\\n所以能不能給大家介紹一下\\n這個新模型和其他模型的不同之處\\n以及就是說我們也觀察到\\n有一個我們提架的這個策略\\n然後這個反應了什麼樣的一個市場的信號\\n好\\n這是一個很好的問題\\n就是前兩天確實我們也這個緊急的更新的一波\\n當然這個是其實是在我們這個\\n整個的發展目標當中的其中的一個階段\\n我們提前可能把它放出來\\n那這個事情其實最主要的目的還是要\\n主打一個從原來的簡單的對話到幹活\\n也是剛才各位講的非常讚同的一點\\n就是OpenCrow真的讓大家覺得\\n大陸型不再是簡單的能夠聊天\\n真的能幫我幹活\\n但這個幹活的背後隱含了這個能力的需求\\n是非常高的\\n就是它需要自己去長程的做任務的規劃\\n不斷的重視\\n不斷的去壓縮自己的上下文\\n然後第八個等等\\n還要可能涉及到多麼太細細的處理\\n所以這個對於模型本身的能力的要求\\n其實是有跟傳統的面向對話的\\n其實人的這種通用翻用模型\\n其實是有一些不一樣的\\n所以監務特朗是在這方面\\n有一些專門的加強\\n尤其是你剛才你提到的\\n比如說我們要讓他幹活長程的\\n七十二次要是如何不停地自己去\\n露譜這個事兒\\n這裡面是做了很多的一些工作\\n另外就是大家也提到關於\\n偷肯消耗量的問題\\n所以你要讓他這個聰明的模型\\n讓他乾重複雜的任務\\n其實偷肯的消耗量是非常巨大的\\n可能一般人可能體會不出來\\n但是只會看到自己的帳單上的那個錢\\n再不停推完他票\\n所以在這方面我們也做了一些優化\\n在面臨複雜任務的時候\\n他可能會用更高效的偷肯效力\\n去完成這些事情\\n所以這個是主要在這幾個方面\\n你說了一些優化\\n但本質上的很模型的架構\\n還是一種多任務協同的這樣一種\\n通用的模型的這個架構\\n只是在能力上有一些偏向性的加強\\n那麼提價這個事情\\n其實也很順暢的能夠跟大家解釋\\n這方面也提到了\\n我們現在不再是簡單的問一個問題\\n他回答\\n他背後有思考的這個戀路是很長的\\n然後包括很多任務\\n他通過寫在馬的方式\\n跟底層的這些去技術設施進行打交道\\n還要去第八個\\n隨時的是改正自己的錯\\n其實這個消耗量非常非常的大\\n完成一個任務可能他需要的偷肯量\\n是原來的回答一個簡單的問題\\n偷肯量的可能十倍甚至百倍\\n所以這個價格上\\n是需要成本上是有一定提高的\\n所以模型也變得更大了\\n然後他的推理的成本也相應的提高了\\n所以我們一般他回歸到一個正常的商業價值上來\\n因為長期地靠低價的競爭\\n也不利於整個行業的發展\\n所以這是我們的一個考量\\n也讓我們能夠持續地在商業化的路徑上\\n有一個很好的良性的必須\\n不斷地去優化模型的能力\\n能夠跟持續地給大家提供\\n更好的這樣的模型和相應的這些偷肯的服務\\n非常好的這個分享\\n那其實因為現在有這個開源模型\\n然後跟這個推理算力\\n我感覺現在也開始形成一個深海\\n然後各種各樣的開源模型\\n可以在各種各樣的推理算力上\\n然後呢去給用戶提供更多的價值\\n然後隨著這個偷肯量的報價\\n現在也可能逐漸從訓練時代變成了這個推理時代\\n所以想請教一下歷史也就是說\\n從Infra的層面這個推理時代\\n對於這個文藍說他意味著什麼\\n感謝指令人對於確實\\n因為我們是一個誕生在AI時代的一個技術設施的廠商\\n然後我們現在也為Kimi為支撲\\n包括也在跟Mimo去做合作\\n然後讓大家能夠把我們這樣一個偷肯工廠\\n更高效的一個用起來\\n然後當然我們也在跟很多高橋開源所野的合作\\n所以我們其實一直都在思考一件事情\\n就是AGM時代需要的技術設施它會是什麼樣子的\\n然後我們怎麼能夠一步步的去\\n在這個過程中去實現它和推演它\\n那我們現在呢也是做好了充分的準備\\n也看到了就是短期中期和長期不同階段\\n我們需要解決的問題\\n那當前腳下的一個問題\\n其實就是剛剛大家也聊到的\\n現在整個這個像渴到帶起來的整個偷肯量的報征\\n對於我們的系統效率其實帶來的\\n更大的優化的需求\\n包括其實價格的增長\\n也是大家在這個需求下的一種解決方式\\n那我們呢其實一直以來都是\\n從這種軟硬件打通的方式\\n來去做布局和解決的\\n包括我們其實揭露了\\n我們能夠看到的幾乎所有種類的這種計算芯片\\n我們把國內的其實實際種類的這種芯片\\n和幾十個不同的算力集群都給同意的連接起來\\n這樣我們能夠去解決\\n就是AI的這樣一個系統中\\n對於算力四元的一個確的問題\\n因為我們當資源不足的時候\\n最好的辦法就是第一\\n把能夠用的資源都用起來\\n第二讓每一個算力都用在刀耳上\\n讓每一個資源都發揮出最大的\\n這樣一個轉化效率價值\\n所以在這個時代\\n我們在當前腳下要解決\\n就是怎麼能夠去進一步的打造一個\\n更高效的偷肯工廠\\n對\\n那這裡邊我們做了很多的優化\\n包括我們讓這個模型和硬件上的各種\\n這種顯存各種各樣的這些技術進行\\n一個最優質的失配\\n包括我們也在看\\n會不會在最新的模型結構和硬件結構下\\n能夠進行一些更深度的化學反應\\n對\\n那解決這個腳下的效率問題\\n其實我們還只是打造了一個\\n偷肯的一個標準化的一個偷肯工廠\\n但是面向 agent 時代我們認為\\n這還是不夠的\\n因為就像剛剛說的\\nagent 它更像是一個人\\n就是我們能夠交給它一個任務\\n那我認為我其實很堅定的認為\\n當前的很多的雲計算時代的技術設施\\n其實是為服務一個程序\\n服務一個人類工程師所設計的\\n而不是為 AI 設計\\n那有點像什麼\\n我們做了一個技術設施\\n然後它上面有一個接口\\n這個接口是為人類工程師做的\\n然後在上面要再保一層\\n再去接入 agent\\n那這種方式其實是用\\n人的操作的一些能力限制了\\nagent 發揮空間\\n我舉個例子比如說\\nagent 它能夠做到苗到毫秒級別\\n去思考和發起任務\\n那這件事情其實在我們之前的\\n這些比如說\\n幾層的KBIOS這些能力上\\n其實是沒有做好這個準備的\\n因為人類大概發起一個任務\\n是分中級別的\\n那我們這樣的一些功能\\n其實是需要進一步的能力\\n我們將它稱作\\nAgentic 的英文\\n就是我們叫做\\n我們需要打造一個\\n更智慧化的一個偷肯工廠\\n那這個是我們\\n我已經從現在正在做的事情\\n然後從更長遠的未來\\n就是真正AGI的實在帶來的時候\\n那我們認為連基礎設施\\n都應該是一個智能體\\n就是我們認為我們自己\\n做打造的這套工廠本身\\n也應該是一個能夠自我進化\\n自我跌代的\\n它能夠形成一個自主的組織\\n然後相當於它有一個 CEO\\n這個 CEO是一個 agent\\n它可能是一個 curl\\n在管理整個基礎設施\\n然後根據它的 AI 客戶的需求\\n自己去提需求跌代自己的基礎設施\\n然後這兩樣 AI 和 AI 之間\\n才能夠去更好的形成我和\\n所以我們也在做一些比如說\\n我們能夠讓 agent和 agent的之間\\n更好通信\\n然後做catch to catch\\n這樣一些複製的能力\\n所以我們其實一直說在思考\\n就是基礎設施和 AI 的發展\\n不應該是一個隔離的狀態\\n說我接受一個需求\\n我就去製作\\n而是應該是產生非常豐富的化學反應\\n那這件事情才是我認為\\n真正的做到所謂的軟硬\\n協同做到這種算法\\n和基礎設施的協同\\n這時候供應行程一直想實現的\\n一個自己的使命\\n謝謝\\n接下來想問問福利\\n小米最近也是通過發布性的模型\\n包括開源那些背後的技術\\n我覺得對這個社區做出了\\n很大的貢獻\\n所以想問一下\\n小米在做大模型方面\\n你覺得會有什麼獨特的優勢\\n我覺得我們先拋開\\n小米在做大模型方面\\n沒有什麼獨特優勢\\n這個話題\\n就是我更想談一下\\n就是其實\\n中國的做大模型的團隊\\n在做大模型上的一個優勢\\n我覺得這個話題\\n更具備\\n更廣泛的一個價值\\n其實在\\n大概兩年前\\n我看到中國的基礎大模型團隊\\n已經在開始了一個\\n非常好的突破\\n這個突破叫做\\n我們在\\n怎麼在有限的\\n算理尤其是\\n在一些\\nV-Link互聯帶寬送線的\\n一些順利的情況下\\n怎麼去突破這些\\n一端算理的線紙\\n然後去做一些\\n看似是\\n為了效率妥協的一些\\n模型結構的創新\\n相對於V-203系列的\\n系度M-O-E和M-A這也等等\\n但是其實我們\\n後面能看到\\n有這樣一些\\n創新引發的\\n是一個變革\\n這個變革就是\\n我們怎麼在\\n算理一定的情況下\\n然後怎麼去\\n發揮一定算理的\\n最高的紙紋水平\\n我覺得這個是\\n有Dipsy可帶來\\n給所有古來的基礎大模型團隊的\\n一個勇氣\\n一個信心\\n在今天\\n我們其實\\n我們自己的國產芯片也好\\n尤其是推理芯片\\n有我們的訓練芯片\\n其實是\\n不再受這個線紙\\n但是我們能看到\\n在這樣的線紙的情況下\\n催生了我們對於\\n更高的訓練效率\\n更低的推理效率\\n的模型結構的全新的探索\\n就像最近的\\nhybrid or sparse\\n或是拎熱推薦的結構\\nDipsy and S.A\\n然後Kimi\\n小秘也有新的\\n面向下一代結構的\\nhy sparse的結構\\n這是區域與Mimovir\\n這一代結構\\n我們面向於\\nAzine的這個時代\\n去思考的\\n如何在Azine的這個時代\\n我們去做\\n更好的模型結構的創新\\n我為什麼覺得\\n結構的創新\\n是如此的重要\\n是因為我們\\n其實剛剛探討到\\nOpenColo這個話題\\nOpenColo\\n其實\\n大家如果真實的\\n去用會發現\\n你越用越好\\n用越用越聰明\\n那麽它的一個\\n前提是你的\\n推理的contest\\n只是當contest\\n是一個\\n我們談論了\\n很久的一個話題\\n但是其實真正\\n現在你發現\\n能有一個模型\\n它在當contest的情況下\\n表現\\n非常好\\n性能\\n非常的強勁\\n推理成本的\\n非常地低\\n其實很多模型\\n它不是做不到\\n比如說\\n遺照或使照的contest\\n而是因為它去推\\n遺照使照\\n它的成本\\n太貴了\\n速度太慢了\\n那麽怎麽\\n你能做到\\n在遺照或使照\\n這個contest\\n的情況下\\n推了成本\\n夠低速度夠快\\n然後\\n在這樣的情況下\\n才會有真正的\\n高生產力價值的\\n任務交給這個模型\\n從而去激發\\n在這種\\n囊抗隨的情況下\\n它去完成\\n更高複雜度的任務\\n可能我們\\n才能在這樣\\n使照\\n甚至一百照康徹\\n的情況下\\n實現\\n模型的資疊帶\\n所以模型的資疊\\n大概就是它可以\\n再一個複雜的環境\\n一邊\\n依靠於超長的contest\\n然後完成對自我\\n的這個進化\\n這個進化\\n有可能是對\\n一種框架本身的\\n也有可能是對\\n模型參數本身的\\n因為我們認為\\nNuncaus本身\\n其實就是對參數的\\n一個進化\\n所以\\n其實\\n怎麽實現\\n一個Nuncaus infusion的\\n架構\\n以及怎麽在推理\\n測做到Nuncaus infusion\\n其實\\n它是一個\\n進化的進程\\n除了我剛剛提到的\\n在預訓練\\n做好一個\\nNuncaus infusion的架構\\n這個可能是我們一年前\\n就去探索\\n一個問題\\n然後而當今\\n你怎麽做到\\nNuncaus\\n真實的這種\\n長\\n長成距離\\n的任務上的\\n一個穩定性\\n和非常\\n高上線的一個效果\\n就是我們\\n現在後續練\\n再去疊帶的\\n創新的一個\\n範世\\n我們再想\\n怎麽去構造\\n更有效的學習算法\\n然後怎麽去採集到\\n真實的這種\\n在於照\\n十兆一百糟上下\\n為你們\\n真的都具有長去\\n依賴的這種\\n這種紋本\\n以及結合複雜的\\n環境產生的\\n這個\\n純潔和Terry\\n這是我們\\n現在在後續練\\n正在經歷的事情\\n但是我能看到\\n更長期的事情\\n是由於\\n大模型\\n本身\\n在飛速的進步\\n由於我們\\n有Agent的框架\\n更好的\\n一個加持\\n我們能看到\\n張麗雪說的\\n我們推理需求\\n我相信在今年\\n已經發生的事情\\n是在過去一段時間\\n已經進實\\n被一個增長\\n那麽今年整個\\n偷奮的增長\\n會不會到一百倍\\n那麽這裡\\n其實我們又到了\\n另外一個\\n圍堵的一個競爭\\n競爭就是\\n算你\\n或說是\\n推理芯片\\n甚至嚇到\\n能源\\n所以我\\n我認為\\n我們\\n如果大家\\n去思考這個問題\\n然後\\n我\\n我認為可能\\n我\\n我會從\\n大家身上\\n學到更多\\n對\\nOK\\n非常有印象\\n的分享\\n下面想問一下\\n黃超\\n因為你也開發了\\n一些非常有影響力的\\n這個\\nAgent的\\n像\\n包括像\\n這個\\nNanobot\\n也有很多\\n社區的粉絲\\n然後想問一下\\n就是你自己覺得說\\n從Agent的\\nHarney\\n或者說\\n應用的這個層面\\n接下來\\n會有什麼技術上\\n方向是\\n你覺得比較\\n比較重要\\n可能大家需要去關注\\nOK\\n感謝志靈\\n我覺得首先\\n就是從\\n如果我們是\\n把Agent的\\n一些技術粗向\\n出來\\n它關鍵幾個點\\n比如說\\nPlanning\\n然後還有\\n就是\\nMemory\\n和To Use\\n這幾個模塊的話\\n比如說\\nPlanning來講話\\n我覺得\\n現在的問題\\n就是\\n還是面向\\n一些長程的任務\\n或是非常复杂的\\n我覺得它\\n不一定能夠\\n去做\\n很好的一個Planning\\n因為我覺得它\\n本質上可能\\n不具備\\n這樣的一個\\n隱形的知識\\n有特別是\\n帶一些\\n很多一些\\n复杂的一些\\n隨著領域\\n我覺得這塊可能\\nPlanning\\n我覺得未來可能\\n是需要\\n去對於\\n很多\\n把以有的\\n各種一些\\n比多复杂\\n任務的知識\\n把它固化到\\n模型裡面去做\\n這塊我覺得可能會\\n是一個點\\n當然\\n比如說\\n高質量的\\nSkill它\\n本質上\\n也是在蓋的模型\\n去完成一些\\n比較難的一些\\nTask\\n這個是針對於Planning\\n然後\\n的話\\n就是\\nmemory的話\\n我覺得\\n就是\\nmemory\\n肯定\\n我感覺\\nmemory帶給我們那種感受\\n就是好像它\\n永遠都會存在著\\n一個\\n比如說\\n信息壓縮不準確\\n包括縮不準\\n這樣的一些問題\\n我覺得\\n包括像各類的\\n農家們\\n其實大家都採用的是\\n最簡單的一些\\n像文學系統\\n這樣\\nMakadang\\n格式的一些memory\\n通過我們是否\\n去共享一些文件\\n這樣的方式\\n來去做\\n我覺得這一點其實\\n可能未來memory\\n應該是\\n做可能類似於\\n會走向一些\\n分層的設計\\n包括\\n如何去更加的\\n去讓memory\\n做得更通用\\n因為其實\\n老師講\\n現在我覺得\\n整個memory\\n機制\\n其實很難做到\\n很通用\\n如何去對它\\n這些memory\\n已經很好的一個\\n解鎖\\n然後更高效的\\n我覺得這個\\n永遠是在做一個\\n吹到\\n就是可能是\\n我覺得這一點\\n是整個memory的\\n然後當另外的一點\\n就是說\\n現在其實\\n農家其實\\n讓大家對於\\n創建一件的這件事情\\n的門檻\\n大幅降低之後\\n其實未來\\n可能不止一個農家\\n我看到\\nKimi其實\\n也有\\n一件事我們這樣\\n那些\\n一些整個的機制出來\\n其實如果覺得\\n抱著我覺得這個\\n大家可以想像\\n會非常的大\\n這個也是帶給\\nmemory的一個壓力\\n就如何在\\n我覺得現在\\n其實還沒有一套\\n很好的一套機制\\n如何去管理\\n這種一群\\n弄一下帶來的\\n一直在上下文\\n因為這個我們感覺\\n特別是對於\\n複雜的\\n扣定\\n可言發現這種\\n我覺得這個\\n其實帶一個\\n不管是模型\\n還是整個的這種\\n就是\\nAgent的架構\\n其實都是\\n壓力挺大的\\n然後對於整個\\n我覺得\\nMCP本身存在的\\n問題其實現在\\nSkill其實還是會存在\\n比如說MCP當時\\n是有\\n質量不保障\\n然後包括\\n它可能會存在\\n一些安全問題\\n然後現在Skill的話\\n我覺得存在的首先\\n就是\\n現在看是確實\\n會有很多Skill\\n但是高質量的\\nSkill我們覺得\\n是比較少的\\n然後這樣的話\\n其實\\n低質量Skill會\\n很影響整個Agent的\\n完成任務的\\n一個完成度\\n然後另外的話\\n還有就是\\nSkill這個\\n我們覺得\\n很難\\n就很多時候\\n也會存在一些\\n我們覺得從\\nTo Use這塊的話\\n我覺得可能得需要靠整個社區\\n然後來去把\\n整個Skill發展得\\n更好\\n甚至讓如何去通過這種\\n在執行過程中\\n去進化出來一些\\nSkill\\n所以我覺得這個可能是\\n整個不管是\\nFrom Planning\\nMemory Haskill\\n裡面我們覺得\\n當下Agent的可能存在的\\n一些痛點\\n和未來可能會\\n潛在的一些方向\\n可以看到\\n剛剛兩位嘉賓\\n也是從不同的\\n其實四角\\n然後去討論\\n一個問題\\n就是說\\n任務複雜度的增加\\n你的上下文\\n會爆炸\\n那可能從模型層面\\n是可以去提升你的\\n原生的上下文長度\\n然後可能從Agent\\nHarness層面\\n可能是像剛剛說的\\n這種\\nPlanning memory\\nBokem Multi-Agent的\\nHarness實際上\\n也是\\n能讓你可能\\n在一個\\n特定的模型的能力下\\n能夠去支持更複雜的任務\\n我覺得\\n可能這兩個方向\\n接下來也會\\n有更多的這種\\n產生化學反應\\n然後能夠\\n提升去\\n完成這個\\n任務的這個\\n這個難度\\n那最後\\n我們來一個\\n開放式的展望\\n所以\\n想請各位\\n用一個\\n詩\\n來描述一下\\n接下來\\n十二個月\\n大模型發展的\\n趨勢以及你的期望\\n那這次\\n我們先從這個\\n黃超這邊開始\\n我感覺\\n十二個月\\n在Agent看起來\\n一個好遙遠\\n都不知道十二個月之後\\n會發展的是什麼樣子\\n裡面有來寫的是五年\\n我給了\\n對\\n對\\n首先我感覺\\n從我這邊的話\\n我感覺一個詞應該\\n要生態吧\\n我覺得未來\\n其實現在\\n龍蝦讓大家\\n這麼火躍了\\n但是我覺得未來\\nAgent真的是\\n要去\\n從真正的個人助手\\n特別是\\n轉換完打工人\\n我覺得還是很重要的\\n因為現在可能\\n大家很多時候玩龍蝦\\n也是一個新鮮感\\n然後可能是覺得好玩\\n但是我覺得\\n未來可能真正\\n得讓龍蝦們\\n真的成電下來\\n真的\\n應該是成為\\n大家的一個\\n班章的工具\\n或者是\\n成為真正的\\n一個Kowalker\\n包括整個\\n我就看完這款\\n其實就是一個\\n很重要的\\n把所有的相關的一些\\n一些技術探索\\n包括整個模型的\\n很多器物都看出\\n就需要整個生態\\n大家一起來供建\\n不管是對於這種\\n模型的疊帶\\n還是對於這種\\nSkills平台的疊帶\\n還有各種工具的\\n我覺得這款\\n都需要去\\n更好的去面向\\n龍蝦去創造\\n更好的生態\\n就從我自己感覺\\n比較明顯的就是\\n我覺得未來的\\n很多軟件\\n包括現在有了龍蝦\\n然後大家又會再考慮一個問題\\n就是\\n還是會給人用的嗎\\n我們相信可能\\n未來大部分很多的\\n軟件都不一定會\\n順便向人\\n面向冷淚了\\n因為人類是需要\\nG-I當了可能\\n順便向Agent的\\n原生的去使用的\\n這樣我感覺\\n一個比較有趣的就是\\n人質會去使用\\n讓自己快樂的G-I\\n有可能會有這樣一個感受\\n所以我覺得\\n現在整個的生態\\n又從不管是G-I\\n還SMP又轉到了\\nC-I這樣的一個模式\\n所以我覺得\\n這個就是需要一個整個生態\\n就需要生態\\n去把不管是軟件系統\\n還是我們的整個\\n這種數據\\n還是整個的這種\\n就是各種一些\\n各種技術\\n其實都需要把它\\n變成Agent\\n原生化\\nAgent Lative的模式\\n這樣可能能夠\\n才能讓整個的這種\\nAgent的發展\\n可能會更加豐富\\n對我覺得\\n把這個問題\\n縮小到一年\\n我覺得非常有意義\\n因為我覺得五年\\n從我心目中對於\\nAGD而言\\n我覺得已經實現了\\n所以\\n所以就是\\n如果說要用一個\\n子來描述\\n接下來一年\\n我覺得AGD\\n成立的最關鍵的一個事情的話\\n我認為會是\\n自徑化\\n雖然這個詞\\n它是一個非常\\n有點懸患的一個概念\\n我覺得在過去一年\\n大家也多次提到\\n但是我\\n我也是最近\\n才對這個詞\\n有了\\n一個更深的一個體會\\n或說\\n去體自徑化\\n這個事兒怎麼做\\n會有一個\\n更務實的\\n一個\\n十操可行的方案\\n對\\n然後\\n這裡面的原因\\n是確實\\n是藉助於很強大的模型\\n其實我們之前在\\n插頭那個方式下\\n根本沒有發揮出來\\n這個預訓練模型\\n它的上線\\n然後這個上線線\\n是被AGD的框架\\n給機會到了\\n我們現在\\n觸到了它可能\\n當它執行\\n更長時間的任務的時候\\n然後我們發現這個模型\\n它可以\\n自己去學習和進化\\n很簡單一個嘗試\\n就是說\\n當你在線了的\\nAGD的框架一邊\\n給它鐵架一個\\n可以\\nverify的\\n這樣的一個\\n條件限制\\n然後再給它設置一個路譜\\n然後讓這個模型\\n就不停下來\\n持續去跌代\\n有化這個目標\\n我們就能發現這個模型\\n會持續\\n拿出\\n更好的方案\\n如果這樣的\\n一種自徑化\\n能持續\\n現在其實\\n已經能跑\\n一兩天了\\n其實國內的模型\\n即將能跑一兩天了\\n當然跟任務的\\n難度有關\\n我們發現做\\n它在一些科學研究上\\n比如說去探索\\n一個更好的模型結構\\n然後因為模型結構\\n它有評估標準\\n比如說我們看\\n更低的PPL\\n這是一個評估標準\\n然後在這種\\n很確定的任務上\\n我們發現它已經\\n能自主的\\n運化和執行兩三天了\\n所以從我的角度上來看\\n我覺得\\n資金化是一個\\n唯一可以\\n創造一個新的東西\\n的地方\\n它不是去踢帶\\n我們現有的人的生產力\\n而是說\\n它是像頂尖的科學家一樣\\n去探索出來\\n這個事情上\\n沒有的一個東西\\n然後\\n其實在\\n一年前我會覺得\\n這個時間歷程\\n會拿到三到五年\\n但是也就是\\n在近期我會覺得\\n這個時間歷程\\n確實我們應該\\n說想到\\n最近一到兩年\\n可能我們就可以\\n讓\\n大模型\\n跌加一個非常強的\\n資金化的\\n一個一種框架\\n實現對於\\n科學研究的一個\\n至少\\n成\\n指出幾個家屬\\n因為我最近已經發現\\n我們組內\\n做大模型研究的同學\\n基本上\\n其實\\n它的workflow\\n是非常不確定的\\n是高度創造力的\\n然後其實\\n我們發現\\n介入\\ncloud code\\n加\\n加\\n基本上\\n已經\\n能夠加速\\n我們自己的研究效率\\n已經使用了\\n所以\\n我很期待\\n這樣的一種\\n範式\\n輔射到\\n更廣的學科領域\\n我覺得\\n自營化\\n是非常重要的\\n我的關鍵詞\\n叫\\n可持續頭看\\n因為\\n我也看到的\\n現在\\n整個A的發展\\n還在一個\\n長期持續的過程中\\n我們也希望\\n它是能夠\\n有長久的生命力的\\n這裡面\\n我們作為\\n即時設施繳\\n就看到的一個\\n很大的問題\\n就是\\n我們的資源中\\n就是有限的\\n就像我們\\n當年在\\n講可持續發展一下\\n我們現在\\n作為一個頭看工廠\\n能否\\n給大家提供\\n持續穩定\\n然後\\n大規模\\n能夠用起來的這些\\n就是我們看到\\n一個很重要的問題\\n所以我們現在\\n也是需要的\\n就相當於\\n把整個的視角\\n放寬到\\n剛剛說到的整個生態\\n從最早的\\n能源到轉化\\n到算力\\n然後在\\n轉化到頭看\\n最終在轉換成\\nGDP\\n這樣一個練路\\n能夠去進行\\n持續的\\n經濟化的跌帶\\n包括我們\\n其實不只是\\n在跑國內的\\n各種算力用起來\\n也在把這些能力\\n輸出到海外\\n讓全球的\\n資源都能夠進行\\n打通和整合\\n所以我也認為\\n持續也是\\n其實也是想把\\n就是我們中國特色的\\n這個\\n頭看經濟學給做起來\\n對\\n因為其實我們在\\n之前的時代\\n我們是叫\\nMade in China\\n我們會發現\\n我們能夠把中國的\\n低價的這種\\n製造能力\\n然後變成好的商品\\n輸出到全球\\n那我們現在\\n想做的\\n就是\\n有點像AI\\nMade in China\\n就是我們能夠把\\n中國的這些\\n能源上的優勢\\n然後\\n通過這些\\n頭看工廠\\n可持續的\\n在輸出到全球\\n成為一個\\n世界的這個\\n頭看工廠\\n這是我想要在\\n今年看到的\\n整個中國\\n給世界的人工\\n只能帶來的價值\\n我就簡短一點\\n大家都可能\\n都在養望信空\\n我就落地一點\\n我覺得關鍵詞\\n讓我說\\n未來12個月\\n面臨的最大的問題\\n可能就是算力\\n因為剛才說到\\n所有的這個技術\\n包括\\n智能體框架\\n讓很多人\\n有很好的創調力\\n效率提升10倍\\n技條件就是\\n大家能夠用得起來\\n你不能因為說\\n算力不夠\\n一個問題提出去\\n讓他思考半天\\n也不給我答案\\n這個肯定是不行的\\n然後\\n對\\n也是\\n因為這樣的一些原因\\n可能\\n甚至連我們\\n在這個\\n研究的進展\\n包括\\n想要做的一些事情\\n其實都受阻了\\n前兩年\\n我記得是亞行院\\n是應該也是在\\n中央村論壇談了一間\\n說了一句話\\n叫\\n美卡美感情\\n談談傷感情\\n我就今天又到了這個地步了\\n但\\n情況又不一樣了\\n就是你剛才講的\\n可能我們又到了\\n推理的這個階段\\n轉向推理階段\\n是因為需求\\n正在爆發\\n10倍百倍的爆發\\n那你也說過去10倍\\n其實需求是100倍\\n還有很大量的需求\\n沒有滿足\\n怎麼辦\\n我們大家可能\\n一起來想想辦法\\n好謝謝\\n好\\n感謝各位的精采分享\\n謝謝大家\",\n \"segments\": [\n {\n \"start\": 0.18,\n \"end\": 4.78,\n \"text\": \"我們今天邀請到各位重磅的嘉賓\"\n },\n {\n \"start\": 4.78,\n \"end\": 8.38,\n \"text\": \"然後也覆蓋了其實不同的層面\"\n },\n {\n \"start\": 8.38,\n \"end\": 13.18,\n \"text\": \"從模型層到底層的算力層到上面的Agent層\"\n },\n {\n \"start\": 13.18,\n \"end\": 17.98,\n \"text\": \"那就很高興今天能跟大家一起來探討一下\"\n },\n {\n \"start\": 17.98,\n \"end\": 21.38,\n \"text\": \"然後可能最主要的關鍵時事這個開源\"\n },\n {\n \"start\": 21.38,\n \"end\": 22.38,\n \"text\": \"然後還有Agent\"\n },\n {\n \"start\": 22.38,\n \"end\": 24.58,\n \"text\": \"那我們就先從第一個問題開始\"\n },\n {\n \"start\": 24.58,\n \"end\": 26.38,\n \"text\": \"這個問題是給所有人\"\n },\n {\n \"start\": 26.38,\n \"end\": 29.98,\n \"text\": \"就是我們現在也是最流行的這個Open Cloud\"\n },\n {\n \"start\": 29.98,\n \"end\": 35.18,\n \"text\": \"然後大家自己就常使用Open Cloud或者類似的產品\"\n },\n {\n \"start\": 35.18,\n \"end\": 39.98,\n \"text\": \"有什麼覺得最有想像力或者印象深刻的是什麼\"\n },\n {\n \"start\": 39.98,\n \"end\": 42.38,\n \"text\": \"然後從技術的角度來看\"\n },\n {\n \"start\": 42.38,\n \"end\": 46.98,\n \"text\": \"如何看待今天Open Cloud和相關的Agent的眼鏡\"\n },\n {\n \"start\": 46.98,\n \"end\": 49.98,\n \"text\": \"我們要不從這個張宏這邊開始\"\n },\n {\n \"start\": 49.98,\n \"end\": 51.98,\n \"text\": \"好 現在感謝這裡邀請\"\n },\n {\n \"start\": 51.98,\n \"end\": 55.58,\n \"text\": \"然後也感謝主辦方給你們機會跟大家來交流\"\n },\n {\n \"start\": 55.58,\n \"end\": 59.98,\n \"text\": \"確實很早我就開始自己來玩Open Cloud\"\n },\n {\n \"start\": 59.98,\n \"end\": 61.38,\n \"text\": \"當時還不叫Open Cloud\"\n },\n {\n \"start\": 61.38,\n \"end\": 62.78,\n \"text\": \"最早叫Cloudboard\"\n },\n {\n \"start\": 62.78,\n \"end\": 65.58,\n \"text\": \"然後就像這些事情自己動手來搞\"\n },\n {\n \"start\": 65.58,\n \"end\": 67.18,\n \"text\": \"畢竟也是程序員出身\"\n },\n {\n \"start\": 67.18,\n \"end\": 69.38,\n \"text\": \"玩這些東西還是有一些自己的體驗\"\n },\n {\n \"start\": 69.38,\n \"end\": 73.58,\n \"text\": \"我覺得這個事情他可能給大家帶來的最大的一個突破點\"\n },\n {\n \"start\": 73.58,\n \"end\": 75.98,\n \"text\": \"或者說新鮮的一個點就在於\"\n },\n {\n \"start\": 75.98,\n \"end\": 77.98,\n \"text\": \"可能這件事情不再是程序員\"\n },\n {\n \"start\": 77.98,\n \"end\": 82.58,\n \"text\": \"或者說即刻們的專利\"\n },\n {\n \"start\": 82.58,\n \"end\": 85.38,\n \"text\": \"普通人也可以比較方便的來使用\"\n },\n {\n \"start\": 85.38,\n \"end\": 87.38,\n \"text\": \"頂尖的這些模型的這個能力\"\n },\n {\n \"start\": 87.38,\n \"end\": 90.38,\n \"text\": \"尤其是在編程和最能體方面的這些能力\"\n },\n {\n \"start\": 90.38,\n \"end\": 93.18,\n \"text\": \"所以我到現在為止\"\n },\n {\n \"start\": 93.18,\n \"end\": 94.78,\n \"text\": \"可能跟大家在交流的過程當中\"\n },\n {\n \"start\": 94.78,\n \"end\": 97.78,\n \"text\": \"更願意把Open Cloud這個事情稱作一個叫\"\n },\n {\n \"start\": 97.78,\n \"end\": 99.78,\n \"text\": \"稱作叫角手架\"\n },\n {\n \"start\": 99.78,\n \"end\": 101.58,\n \"text\": \"它提供的是一種可能性\"\n },\n {\n \"start\": 101.58,\n \"end\": 102.78,\n \"text\": \"在模型的技術上\"\n },\n {\n \"start\": 102.78,\n \"end\": 105.78,\n \"text\": \"搭起來一個很牢固的很方便的\"\n },\n {\n \"start\": 105.78,\n \"end\": 108.18,\n \"text\": \"但是有很靈活的這樣的一個角手架\"\n },\n {\n \"start\": 108.18,\n \"end\": 109.58,\n \"text\": \"大家可以按照自己的意願\"\n },\n {\n \"start\": 109.58,\n \"end\": 112.18,\n \"text\": \"去使用很多的底層的模型\"\n },\n {\n \"start\": 112.18,\n \"end\": 113.98,\n \"text\": \"提供的這個很新奇的一些東西\"\n },\n {\n \"start\": 113.98,\n \"end\": 115.38,\n \"text\": \"原來自己的一些想法\"\n },\n {\n \"start\": 115.38,\n \"end\": 117.18,\n \"text\": \"受限於自己不會寫大碼\"\n },\n {\n \"start\": 117.18,\n \"end\": 119.58,\n \"text\": \"或者不會這樣的一些其他的一些技能\"\n },\n {\n \"start\": 119.58,\n \"end\": 122.18,\n \"text\": \"那今天終於可以通過很簡單的交流\"\n },\n {\n \"start\": 122.18,\n \"end\": 124.18,\n \"text\": \"就可以把它完成\"\n },\n {\n \"start\": 124.18,\n \"end\": 125.98,\n \"text\": \"所以這個事情就對我來說\"\n },\n {\n \"start\": 125.98,\n \"end\": 130.38,\n \"text\": \"是一個非常大的一個衝擊吧\"\n },\n {\n \"start\": 130.38,\n \"end\": 133.18,\n \"text\": \"或者說讓我重新認識了這樣的一件事情\"\n },\n {\n \"start\": 133.18,\n \"end\": 136.54,\n \"text\": \"對 大概是這樣\"\n },\n {\n \"start\": 136.54,\n \"end\": 139.34,\n \"text\": \"對 那個其實我最開始用Open Cloud的時候\"\n },\n {\n \"start\": 139.34,\n \"end\": 140.34,\n \"text\": \"是不太適應的\"\n },\n {\n \"start\": 140.34,\n \"end\": 144.34,\n \"text\": \"因為我習慣於這種跟大魔形容聊天的\"\n },\n {\n \"start\": 144.34,\n \"end\": 145.14,\n \"text\": \"這種交流方式\"\n },\n {\n \"start\": 145.14,\n \"end\": 146.94,\n \"text\": \"然後我發現 Open Cloud感覺很像\"\n },\n {\n \"start\": 146.94,\n \"end\": 148.54,\n \"text\": \"反而很慢這種感覺\"\n },\n {\n \"start\": 148.54,\n \"end\": 149.74,\n \"text\": \"但是後來我一手一個問題\"\n },\n {\n \"start\": 149.74,\n \"end\": 152.14,\n \"text\": \"就是它和之前的這種聊天器\"\n },\n {\n \"start\": 152.14,\n \"end\": 153.94,\n \"text\": \"人有一個很大的不一樣\"\n },\n {\n \"start\": 153.94,\n \"end\": 155.74,\n \"text\": \"就是它其實應該是一個\"\n },\n {\n \"start\": 155.74,\n \"end\": 158.34,\n \"text\": \"能夠幫我完成一個大型任務的一個人\"\n },\n {\n \"start\": 158.34,\n \"end\": 160.34,\n \"text\": \"所以我其實後面開始給它提交\"\n },\n {\n \"start\": 160.34,\n \"end\": 161.94,\n \"text\": \"一些更複雜的任務的時候\"\n },\n {\n \"start\": 161.94,\n \"end\": 163.74,\n \"text\": \"我也發現其實它能夠做得很好\"\n },\n {\n \"start\": 163.74,\n \"end\": 165.14,\n \"text\": \"然後這件事情對我來說\"\n },\n {\n \"start\": 165.14,\n \"end\": 166.14,\n \"text\": \"有一個很大的感觸\"\n },\n {\n \"start\": 166.14,\n \"end\": 169.54,\n \"text\": \"就是其實在模型從最開始\"\n },\n {\n \"start\": 169.54,\n \"end\": 171.34,\n \"text\": \"按照Token去聊天\"\n },\n {\n \"start\": 171.34,\n \"end\": 173.34,\n \"text\": \"到現在能夠變成一個Agent\"\n },\n {\n \"start\": 173.34,\n \"end\": 174.94,\n \"text\": \"變成一個龍蝦\"\n },\n {\n \"start\": 174.94,\n \"end\": 176.34,\n \"text\": \"能夠幫你去完成任務\"\n },\n {\n \"start\": 176.34,\n \"end\": 178.94,\n \"text\": \"它對於我們整個的AI的想像力空間\"\n },\n {\n \"start\": 178.94,\n \"end\": 180.54,\n \"text\": \"已經做得一個很大的提升\"\n },\n {\n \"start\": 180.54,\n \"end\": 184.14,\n \"text\": \"但同時它對於整個系統的能力的要求\"\n },\n {\n \"start\": 184.14,\n \"end\": 184.74,\n \"text\": \"也變得很大\"\n },\n {\n \"start\": 184.74,\n \"end\": 186.34,\n \"text\": \"這也是為什麼我一開始用文化的\"\n },\n {\n \"start\": 186.34,\n \"end\": 188.34,\n \"text\": \"會覺得它有點卡著這個原因\"\n },\n {\n \"start\": 188.34,\n \"end\": 189.74,\n \"text\": \"對 那所以呢\"\n },\n {\n \"start\": 189.74,\n \"end\": 193.14,\n \"text\": \"我作為這樣的一個技術設施存的一個廠商\"\n },\n {\n \"start\": 193.14,\n \"end\": 194.94,\n \"text\": \"我其實看到的是\"\n },\n {\n \"start\": 194.94,\n \"end\": 197.74,\n \"text\": \"可到對於整個AI後面的一個\"\n },\n {\n \"start\": 197.74,\n \"end\": 200.14,\n \"text\": \"整個大型的系統和生態都帶來的\"\n },\n {\n \"start\": 200.14,\n \"end\": 201.54,\n \"text\": \"共多的機率和挑戰\"\n },\n {\n \"start\": 201.54,\n \"end\": 203.54,\n \"text\": \"因為我們現在的這個\"\n },\n {\n \"start\": 203.54,\n \"end\": 205.54,\n \"text\": \"所有的能夠用到的資源\"\n },\n {\n \"start\": 205.54,\n \"end\": 208.34,\n \"text\": \"想要支撐起這樣一個快速增長的時代\"\n },\n {\n \"start\": 208.34,\n \"end\": 209.04,\n \"text\": \"是不夠的\"\n },\n {\n \"start\": 209.04,\n \"end\": 210.74,\n \"text\": \"比如說就拿我們公司來說\"\n },\n {\n \"start\": 210.74,\n \"end\": 211.74,\n \"text\": \"我們公司在\"\n },\n {\n \"start\": 211.74,\n \"end\": 214.34,\n \"text\": \"月底開始基本上沒兩週\"\n },\n {\n \"start\": 214.34,\n \"end\": 215.94,\n \"text\": \"我們的Token量就翻一翻\"\n },\n {\n \"start\": 215.94,\n \"end\": 217.74,\n \"text\": \"到現在基本上翻了10倍\"\n },\n {\n \"start\": 217.74,\n \"end\": 219.54,\n \"text\": \"對 上次見到這個速度\"\n },\n {\n \"start\": 219.54,\n \"end\": 221.54,\n \"text\": \"還是那個當時三制一的時候\"\n },\n {\n \"start\": 221.54,\n \"end\": 222.74,\n \"text\": \"手機流量的那種感覺\"\n },\n {\n \"start\": 222.74,\n \"end\": 224.54,\n \"text\": \"對 所以我現在有種感覺\"\n },\n {\n \"start\": 224.54,\n \"end\": 226.94,\n \"text\": \"就是就是現在的這個Token用量\"\n },\n {\n \"start\": 226.94,\n \"end\": 228.94,\n \"text\": \"就跟當年我們有100兆的\"\n },\n {\n \"start\": 228.94,\n \"end\": 231.74,\n \"text\": \"為100兆每個月的這個手機流量一樣\"\n },\n {\n \"start\": 231.74,\n \"end\": 233.74,\n \"text\": \"對那個時代那這種情況下\"\n },\n {\n \"start\": 233.74,\n \"end\": 236.94,\n \"text\": \"其實我們所有的資源是需要進行\"\n },\n {\n \"start\": 236.94,\n \"end\": 239.14,\n \"text\": \"更好的優化 進行更好的整合\"\n },\n {\n \"start\": 239.14,\n \"end\": 240.94,\n \"text\": \"然後讓我們的每一個人\"\n },\n {\n \"start\": 240.94,\n \"end\": 242.74,\n \"text\": \"每一個不是這個\"\n },\n {\n \"start\": 242.74,\n \"end\": 243.94,\n \"text\": \"不僅僅在AI領域\"\n },\n {\n \"start\": 243.94,\n \"end\": 245.34,\n \"text\": \"而是在整個社會上\"\n },\n {\n \"start\": 245.34,\n \"end\": 246.94,\n \"text\": \"就是每一個先活的人\"\n },\n {\n \"start\": 246.94,\n \"end\": 248.44,\n \"text\": \"都能夠去把這個\"\n },\n {\n \"start\": 248.44,\n \"end\": 250.34,\n \"text\": \"Own Club把這樣的AI能力的用起來\"\n },\n {\n \"start\": 250.34,\n \"end\": 251.94,\n \"text\": \"所以我作為技術設施的玩家\"\n },\n {\n \"start\": 251.94,\n \"end\": 255.94,\n \"text\": \"其實對這個時代是非常激動和感觸的\"\n },\n {\n \"start\": 255.94,\n \"end\": 257.94,\n \"text\": \"而且認為這裡面有很多的優化空間\"\n },\n {\n \"start\": 257.94,\n \"end\": 259.94,\n \"text\": \"是我們仍然應該去探索\"\n },\n {\n \"start\": 259.94,\n \"end\": 260.94,\n \"text\": \"應該去嘗試的\"\n },\n {\n \"start\": 260.94,\n \"end\": 262.94,\n \"text\": \"好 謝謝\"\n },\n {\n \"start\": 262.94,\n \"end\": 265.94,\n \"text\": \"我自己是把Own Club當作Agent的\"\n },\n {\n \"start\": 265.94,\n \"end\": 267.94,\n \"text\": \"框架的一個非常革命性\"\n },\n {\n \"start\": 267.94,\n \"end\": 269.94,\n \"text\": \"和顛覆性的一個事件來看待的\"\n },\n {\n \"start\": 269.94,\n \"end\": 272.94,\n \"text\": \"雖然我知道就是其實我身邊\"\n },\n {\n \"start\": 272.94,\n \"end\": 276.94,\n \"text\": \"所有在進行非常深度扣定的人\"\n },\n {\n \"start\": 276.94,\n \"end\": 278.94,\n \"text\": \"可能它的第一選擇還是靠扣的\"\n },\n {\n \"start\": 278.94,\n \"end\": 281.94,\n \"text\": \"但是我相信只有英國Own Club\"\n },\n {\n \"start\": 281.94,\n \"end\": 284.94,\n \"text\": \"人會獨特地感受到這個框架\"\n },\n {\n \"start\": 284.94,\n \"end\": 287.94,\n \"text\": \"它其實有很多在Agent框架上的設計性\"\n },\n {\n \"start\": 287.94,\n \"end\": 288.94,\n \"text\": \"是你先與靠扣的\"\n },\n {\n \"start\": 288.94,\n \"end\": 291.94,\n \"text\": \"包括最近靠扣的有很多最新的更新\"\n },\n {\n \"start\": 291.94,\n \"end\": 294.94,\n \"text\": \"其實都是像在Own Club去靠近\"\n },\n {\n \"start\": 294.94,\n \"end\": 297.94,\n \"text\": \"我自己去使用Own Club的話\"\n },\n {\n \"start\": 297.94,\n \"end\": 301.94,\n \"text\": \"我感覺這個框架給我自己帶來\"\n },\n {\n \"start\": 301.94,\n \"end\": 305.94,\n \"text\": \"來更多是一個想像力的隨時隨地的一個擴展\"\n },\n {\n \"start\": 305.94,\n \"end\": 309.94,\n \"text\": \"就靠扣的可能最開始我只能在我的桌面上\"\n },\n {\n \"start\": 309.94,\n \"end\": 312.94,\n \"text\": \"然後去延長我的創意\"\n },\n {\n \"start\": 312.94,\n \"end\": 315.94,\n \"text\": \"但是在Own Club可以隨時隨地去延長我的創意\"\n },\n {\n \"start\": 315.94,\n \"end\": 319.94,\n \"text\": \"我後面發現其實Own Club其實它帶的\"\n },\n {\n \"start\": 319.94,\n \"end\": 321.94,\n \"text\": \"主要核心的價值在於兩個\"\n },\n {\n \"start\": 321.94,\n \"end\": 323.94,\n \"text\": \"第一個是它是開源\"\n },\n {\n \"start\": 323.94,\n \"end\": 326.94,\n \"text\": \"開源其實是一個非常有利於\"\n },\n {\n \"start\": 326.94,\n \"end\": 329.94,\n \"text\": \"整個社區去深入去參與去\"\n },\n {\n \"start\": 329.94,\n \"end\": 334.94,\n \"text\": \"重視改進去投入到Agent框架這個事兒\"\n },\n {\n \"start\": 334.94,\n \"end\": 337.94,\n \"text\": \"一個非常重要的一個前世條件\"\n },\n {\n \"start\": 337.94,\n \"end\": 341.94,\n \"text\": \"像Own Club像靠扣的這樣的一些Agent框架\"\n },\n {\n \"start\": 341.94,\n \"end\": 344.94,\n \"text\": \"我認為它很大了一個價值\"\n },\n {\n \"start\": 344.94,\n \"end\": 349.94,\n \"text\": \"是在把國內的可能沒有非常接近幣元\"\n },\n {\n \"start\": 349.94,\n \"end\": 352.94,\n \"text\": \"模型但是水平還是在四幣元模型的\"\n },\n {\n \"start\": 352.94,\n \"end\": 356.94,\n \"text\": \"這樣一個賽道上的模型的上線給拿到非常高\"\n },\n {\n \"start\": 356.94,\n \"end\": 359.94,\n \"text\": \"在絕大部分的場景其實我們能發現\"\n },\n {\n \"start\": 359.94,\n \"end\": 363.94,\n \"text\": \"它的任務的完成度已經非常接近Klaude的最新的模型\"\n },\n {\n \"start\": 363.94,\n \"end\": 366.94,\n \"text\": \"然後同時它又把下線給保證得非常好\"\n },\n {\n \"start\": 366.94,\n \"end\": 369.94,\n \"text\": \"因為它可以靠一套哈利斯系統\"\n },\n {\n \"start\": 369.94,\n \"end\": 374.94,\n \"text\": \"或者說Klaude的體系等等很多的速度設計\"\n },\n {\n \"start\": 374.94,\n \"end\": 376.94,\n \"text\": \"保證它的任務的完成度和準確率\"\n },\n {\n \"start\": 376.94,\n \"end\": 379.94,\n \"text\": \"所以我覺得Own Club其實給我自己帶的\"\n },\n {\n \"start\": 379.94,\n \"end\": 383.94,\n \"text\": \"我認為從一個基礎大模型的角度來說\"\n },\n {\n \"start\": 383.94,\n \"end\": 386.94,\n \"text\": \"它是保證了基礎大模型的下線\"\n },\n {\n \"start\": 386.94,\n \"end\": 388.94,\n \"text\": \"然後拉伸了它的上線\"\n },\n {\n \"start\": 388.94,\n \"end\": 391.94,\n \"text\": \"然後此外我自己認為Own Club它其實\"\n },\n {\n \"start\": 391.94,\n \"end\": 394.94,\n \"text\": \"給整個社區帶的價值是它更多去點燃\"\n },\n {\n \"start\": 394.94,\n \"end\": 398.94,\n \"text\": \"大家發現原來在大模型外的這一層\"\n },\n {\n \"start\": 398.94,\n \"end\": 400.94,\n \"text\": \"更重要的AZN的這一層\"\n },\n {\n \"start\": 400.94,\n \"end\": 403.94,\n \"text\": \"它有非常多的想像力和空間可以做\"\n },\n {\n \"start\": 403.94,\n \"end\": 407.94,\n \"text\": \"然後這是我看到最近社區在\"\n },\n {\n \"start\": 407.94,\n \"end\": 412.94,\n \"text\": \"有更多除了研究原因外人在去參與到\"\n },\n {\n \"start\": 412.94,\n \"end\": 414.94,\n \"text\": \"AGA的變革當中\"\n },\n {\n \"start\": 414.94,\n \"end\": 418.94,\n \"text\": \"然後也有更多人去接觸到更強的AZN框架\"\n },\n {\n \"start\": 418.94,\n \"end\": 420.94,\n \"text\": \"哈利斯或SK4的等等\"\n },\n {\n \"start\": 420.94,\n \"end\": 427.94,\n \"text\": \"然後去移民程度上說是在去替代自己的工作\"\n },\n {\n \"start\": 427.94,\n \"end\": 432.94,\n \"text\": \"然後一定程度上是在釋放自己的時間\"\n },\n {\n \"start\": 432.94,\n \"end\": 434.94,\n \"text\": \"然後去做給我想像力的事情\"\n },\n {\n \"start\": 434.94,\n \"end\": 436.94,\n \"text\": \"對\"\n },\n {\n \"start\": 436.94,\n \"end\": 439.94,\n \"text\": \"我感覺首先可能從交互模式上來講\"\n },\n {\n \"start\": 439.94,\n \"end\": 441.94,\n \"text\": \"為什麼Own Club這次會爆火\"\n },\n {\n \"start\": 441.94,\n \"end\": 443.94,\n \"text\": \"我感覺首先可能第一個就是\"\n },\n {\n \"start\": 443.94,\n \"end\": 446.94,\n \"text\": \"給了大家一種更有活人感的一種感覺\"\n },\n {\n \"start\": 446.94,\n \"end\": 448.94,\n \"text\": \"因為比如說我們其實做AZN也有一兩年\"\n },\n {\n \"start\": 448.94,\n \"end\": 452.94,\n \"text\": \"但之前包括像Cursor,Crop Code這些AZN\"\n },\n {\n \"start\": 452.94,\n \"end\": 455.94,\n \"text\": \"其實大家給人感覺更強的一種有種工具感\"\n },\n {\n \"start\": 455.94,\n \"end\": 456.94,\n \"text\": \"對\"\n },\n {\n \"start\": 456.94,\n \"end\": 458.94,\n \"text\": \"然後我覺得Own Club首先第一次它的交互模式\"\n },\n {\n \"start\": 458.94,\n \"end\": 460.94,\n \"text\": \"以這種IM軟件簽入的方式\"\n },\n {\n \"start\": 460.94,\n \"end\": 462.94,\n \"text\": \"可以讓大家可能更有一種活人感\"\n },\n {\n \"start\": 462.94,\n \"end\": 465.94,\n \"text\": \"更有一點更接近於自己想像中的\"\n },\n {\n \"start\": 465.94,\n \"end\": 467.94,\n \"text\": \"個人價為私障的概念\"\n },\n {\n \"start\": 467.94,\n \"end\": 469.94,\n \"text\": \"所以我覺得這可能是交互模式上的\"\n },\n {\n \"start\": 469.94,\n \"end\": 471.94,\n \"text\": \"然後另外一個我覺得它帶給整個\"\n },\n {\n \"start\": 471.94,\n \"end\": 473.94,\n \"text\": \"大家的一個啟發的話是說\"\n },\n {\n \"start\": 473.94,\n \"end\": 477.94,\n \"text\": \"可能從當他們架構AZN loop這種非常簡單\"\n },\n {\n \"start\": 477.94,\n \"end\": 480.94,\n \"text\": \"但高效的一種框架其實也是再次被證明\"\n },\n {\n \"start\": 480.94,\n \"end\": 482.94,\n \"text\": \"然後還有就是它可能整體來說\"\n },\n {\n \"start\": 482.94,\n \"end\": 485.94,\n \"text\": \"就會讓我們從一思考就是說我們是否需要一個\"\n },\n {\n \"start\": 485.94,\n \"end\": 487.94,\n \"text\": \"Own 1非常強大的智能體\"\n },\n {\n \"start\": 487.94,\n \"end\": 489.94,\n \"text\": \"然後能夠幫我們做很多事情\"\n },\n {\n \"start\": 489.94,\n \"end\": 492.94,\n \"text\": \"還是需要一個比較好的一個像中歷史\"\n },\n {\n \"start\": 492.94,\n \"end\": 494.94,\n \"text\": \"一個輕量級操作系統或是角左架\"\n },\n {\n \"start\": 494.94,\n \"end\": 495.94,\n \"text\": \"一樣的一個小管架\"\n },\n {\n \"start\": 495.94,\n \"end\": 498.94,\n \"text\": \"我覺得它帶來的是說可以通過一個小的\"\n },\n {\n \"start\": 498.94,\n \"end\": 499.94,\n \"text\": \"這樣一個OpenCrow\"\n },\n {\n \"start\": 499.94,\n \"end\": 501.94,\n \"text\": \"或者是這樣一個龍蝦的一個操績\"\n },\n {\n \"start\": 501.94,\n \"end\": 504.94,\n \"text\": \"一個生態可以去把整個的這種包括\"\n },\n {\n \"start\": 504.94,\n \"end\": 507.94,\n \"text\": \"讓大家都我覺得讓整個社區\"\n },\n {\n \"start\": 507.94,\n \"end\": 509.94,\n \"text\": \"大家更有玩起來的心態\"\n },\n {\n \"start\": 509.94,\n \"end\": 511.94,\n \"text\": \"然後是跳動整個生態裡面所有的工具\"\n },\n {\n \"start\": 511.94,\n \"end\": 513.94,\n \"text\": \"包括隨著像Skills\"\n },\n {\n \"start\": 513.94,\n \"end\": 514.94,\n \"text\": \"或者Harlene是這些\"\n },\n {\n \"start\": 514.94,\n \"end\": 516.94,\n \"text\": \"大家也越來越多的人可以去設計\"\n },\n {\n \"start\": 516.94,\n \"end\": 518.94,\n \"text\": \"更面向於像OpenCrow\"\n },\n {\n \"start\": 518.94,\n \"end\": 520.94,\n \"text\": \"這樣的一些系統裡面的一些應用\"\n },\n {\n \"start\": 520.94,\n \"end\": 522.94,\n \"text\": \"然後去負冷於各種各業\"\n },\n {\n \"start\": 522.94,\n \"end\": 525.94,\n \"text\": \"我覺得它這個其實和天然就和整個的開源\"\n },\n {\n \"start\": 525.94,\n \"end\": 527.94,\n \"text\": \"生態結合的非常緊密\"\n },\n {\n \"start\": 527.94,\n \"end\": 529.94,\n \"text\": \"所以我覺得這兩點其實相對來說\"\n },\n {\n \"start\": 529.94,\n \"end\": 532.94,\n \"text\": \"我覺得是帶給我們最大的一個啟發\"\n },\n {\n \"start\": 532.94,\n \"end\": 535.94,\n \"text\": \"那順著這個剛才一直在討論OpenCrow\"\n },\n {\n \"start\": 535.94,\n \"end\": 538.94,\n \"text\": \"然後看想問一下\"\n },\n {\n \"start\": 538.94,\n \"end\": 540.94,\n \"text\": \"張鵬就是看到最近智庫\"\n },\n {\n \"start\": 540.94,\n \"end\": 544.94,\n \"text\": \"也發布了新的GROM5 Turbo模型\"\n },\n {\n \"start\": 544.94,\n \"end\": 547.94,\n \"text\": \"然後我理解就是對這個Agent能力\"\n },\n {\n \"start\": 547.94,\n \"end\": 549.94,\n \"text\": \"其實也做了很大的增強\"\n },\n {\n \"start\": 549.94,\n \"end\": 551.94,\n \"text\": \"所以能不能給大家介紹一下\"\n },\n {\n \"start\": 551.94,\n \"end\": 554.94,\n \"text\": \"這個新模型和其他模型的不同之處\"\n },\n {\n \"start\": 554.94,\n \"end\": 556.94,\n \"text\": \"以及就是說我們也觀察到\"\n },\n {\n \"start\": 556.94,\n \"end\": 559.94,\n \"text\": \"有一個我們提架的這個策略\"\n },\n {\n \"start\": 559.94,\n \"end\": 562.94,\n \"text\": \"然後這個反應了什麼樣的一個市場的信號\"\n },\n {\n \"start\": 562.94,\n \"end\": 563.94,\n \"text\": \"好\"\n },\n {\n \"start\": 563.94,\n \"end\": 565.94,\n \"text\": \"這是一個很好的問題\"\n },\n {\n \"start\": 565.94,\n \"end\": 569.94,\n \"text\": \"就是前兩天確實我們也這個緊急的更新的一波\"\n },\n {\n \"start\": 569.94,\n \"end\": 572.94,\n \"text\": \"當然這個是其實是在我們這個\"\n },\n {\n \"start\": 572.94,\n \"end\": 575.94,\n \"text\": \"整個的發展目標當中的其中的一個階段\"\n },\n {\n \"start\": 575.94,\n \"end\": 577.94,\n \"text\": \"我們提前可能把它放出來\"\n },\n {\n \"start\": 577.94,\n \"end\": 581.94,\n \"text\": \"那這個事情其實最主要的目的還是要\"\n },\n {\n \"start\": 581.94,\n \"end\": 585.94,\n \"text\": \"主打一個從原來的簡單的對話到幹活\"\n },\n {\n \"start\": 585.94,\n \"end\": 588.94,\n \"text\": \"也是剛才各位講的非常讚同的一點\"\n },\n {\n \"start\": 588.94,\n \"end\": 590.94,\n \"text\": \"就是OpenCrow真的讓大家覺得\"\n },\n {\n \"start\": 590.94,\n \"end\": 593.94,\n \"text\": \"大陸型不再是簡單的能夠聊天\"\n },\n {\n \"start\": 593.94,\n \"end\": 595.94,\n \"text\": \"真的能幫我幹活\"\n },\n {\n \"start\": 595.94,\n \"end\": 598.94,\n \"text\": \"但這個幹活的背後隱含了這個能力的需求\"\n },\n {\n \"start\": 598.94,\n \"end\": 599.94,\n \"text\": \"是非常高的\"\n },\n {\n \"start\": 599.94,\n \"end\": 602.94,\n \"text\": \"就是它需要自己去長程的做任務的規劃\"\n },\n {\n \"start\": 602.94,\n \"end\": 603.94,\n \"text\": \"不斷的重視\"\n },\n {\n \"start\": 603.94,\n \"end\": 605.94,\n \"text\": \"不斷的去壓縮自己的上下文\"\n },\n {\n \"start\": 605.94,\n \"end\": 607.94,\n \"text\": \"然後第八個等等\"\n },\n {\n \"start\": 607.94,\n \"end\": 609.94,\n \"text\": \"還要可能涉及到多麼太細細的處理\"\n },\n {\n \"start\": 609.94,\n \"end\": 612.94,\n \"text\": \"所以這個對於模型本身的能力的要求\"\n },\n {\n \"start\": 612.94,\n \"end\": 615.94,\n \"text\": \"其實是有跟傳統的面向對話的\"\n },\n {\n \"start\": 615.94,\n \"end\": 617.94,\n \"text\": \"其實人的這種通用翻用模型\"\n },\n {\n \"start\": 617.94,\n \"end\": 619.94,\n \"text\": \"其實是有一些不一樣的\"\n },\n {\n \"start\": 619.94,\n \"end\": 622.94,\n \"text\": \"所以監務特朗是在這方面\"\n },\n {\n \"start\": 622.94,\n \"end\": 624.94,\n \"text\": \"有一些專門的加強\"\n },\n {\n \"start\": 624.94,\n \"end\": 625.94,\n \"text\": \"尤其是你剛才你提到的\"\n },\n {\n \"start\": 625.94,\n \"end\": 627.94,\n \"text\": \"比如說我們要讓他幹活長程的\"\n },\n {\n \"start\": 627.94,\n \"end\": 629.94,\n \"text\": \"七十二次要是如何不停地自己去\"\n },\n {\n \"start\": 629.94,\n \"end\": 631.94,\n \"text\": \"露譜這個事兒\"\n },\n {\n \"start\": 631.94,\n \"end\": 634.94,\n \"text\": \"這裡面是做了很多的一些工作\"\n },\n {\n \"start\": 634.94,\n \"end\": 636.94,\n \"text\": \"另外就是大家也提到關於\"\n },\n {\n \"start\": 636.94,\n \"end\": 638.94,\n \"text\": \"偷肯消耗量的問題\"\n },\n {\n \"start\": 638.94,\n \"end\": 640.94,\n \"text\": \"所以你要讓他這個聰明的模型\"\n },\n {\n \"start\": 640.94,\n \"end\": 641.94,\n \"text\": \"讓他乾重複雜的任務\"\n },\n {\n \"start\": 641.94,\n \"end\": 644.94,\n \"text\": \"其實偷肯的消耗量是非常巨大的\"\n },\n {\n \"start\": 644.94,\n \"end\": 646.94,\n \"text\": \"可能一般人可能體會不出來\"\n },\n {\n \"start\": 646.94,\n \"end\": 648.94,\n \"text\": \"但是只會看到自己的帳單上的那個錢\"\n },\n {\n \"start\": 648.94,\n \"end\": 649.94,\n \"text\": \"再不停推完他票\"\n },\n {\n \"start\": 649.94,\n \"end\": 651.94,\n \"text\": \"所以在這方面我們也做了一些優化\"\n },\n {\n \"start\": 651.94,\n \"end\": 653.94,\n \"text\": \"在面臨複雜任務的時候\"\n },\n {\n \"start\": 653.94,\n \"end\": 656.94,\n \"text\": \"他可能會用更高效的偷肯效力\"\n },\n {\n \"start\": 656.94,\n \"end\": 657.94,\n \"text\": \"去完成這些事情\"\n },\n {\n \"start\": 657.94,\n \"end\": 660.94,\n \"text\": \"所以這個是主要在這幾個方面\"\n },\n {\n \"start\": 660.94,\n \"end\": 661.94,\n \"text\": \"你說了一些優化\"\n },\n {\n \"start\": 661.94,\n \"end\": 663.94,\n \"text\": \"但本質上的很模型的架構\"\n },\n {\n \"start\": 663.94,\n \"end\": 667.94,\n \"text\": \"還是一種多任務協同的這樣一種\"\n },\n {\n \"start\": 667.94,\n \"end\": 669.94,\n \"text\": \"通用的模型的這個架構\"\n },\n {\n \"start\": 669.94,\n \"end\": 672.94,\n \"text\": \"只是在能力上有一些偏向性的加強\"\n },\n {\n \"start\": 672.94,\n \"end\": 674.94,\n \"text\": \"那麼提價這個事情\"\n },\n {\n \"start\": 674.94,\n \"end\": 677.94,\n \"text\": \"其實也很順暢的能夠跟大家解釋\"\n },\n {\n \"start\": 677.94,\n \"end\": 678.94,\n \"text\": \"這方面也提到了\"\n },\n {\n \"start\": 678.94,\n \"end\": 680.94,\n \"text\": \"我們現在不再是簡單的問一個問題\"\n },\n {\n \"start\": 680.94,\n \"end\": 681.94,\n \"text\": \"他回答\"\n },\n {\n \"start\": 681.94,\n \"end\": 683.94,\n \"text\": \"他背後有思考的這個戀路是很長的\"\n },\n {\n \"start\": 683.94,\n \"end\": 684.94,\n \"text\": \"然後包括很多任務\"\n },\n {\n \"start\": 684.94,\n \"end\": 686.94,\n \"text\": \"他通過寫在馬的方式\"\n },\n {\n \"start\": 686.94,\n \"end\": 690.94,\n \"text\": \"跟底層的這些去技術設施進行打交道\"\n },\n {\n \"start\": 690.94,\n \"end\": 692.94,\n \"text\": \"還要去第八個\"\n },\n {\n \"start\": 692.94,\n \"end\": 694.94,\n \"text\": \"隨時的是改正自己的錯\"\n },\n {\n \"start\": 694.94,\n \"end\": 696.94,\n \"text\": \"其實這個消耗量非常非常的大\"\n },\n {\n \"start\": 696.94,\n \"end\": 698.94,\n \"text\": \"完成一個任務可能他需要的偷肯量\"\n },\n {\n \"start\": 698.94,\n \"end\": 701.94,\n \"text\": \"是原來的回答一個簡單的問題\"\n },\n {\n \"start\": 701.94,\n \"end\": 703.94,\n \"text\": \"偷肯量的可能十倍甚至百倍\"\n },\n {\n \"start\": 703.94,\n \"end\": 705.94,\n \"text\": \"所以這個價格上\"\n },\n {\n \"start\": 705.94,\n \"end\": 709.94,\n \"text\": \"是需要成本上是有一定提高的\"\n },\n {\n \"start\": 709.94,\n \"end\": 711.94,\n \"text\": \"所以模型也變得更大了\"\n },\n {\n \"start\": 711.94,\n \"end\": 714.94,\n \"text\": \"然後他的推理的成本也相應的提高了\"\n },\n {\n \"start\": 714.94,\n \"end\": 718.94,\n \"text\": \"所以我們一般他回歸到一個正常的商業價值上來\"\n },\n {\n \"start\": 718.94,\n \"end\": 720.94,\n \"text\": \"因為長期地靠低價的競爭\"\n },\n {\n \"start\": 720.94,\n \"end\": 722.94,\n \"text\": \"也不利於整個行業的發展\"\n },\n {\n \"start\": 722.94,\n \"end\": 725.94,\n \"text\": \"所以這是我們的一個考量\"\n },\n {\n \"start\": 725.94,\n \"end\": 728.94,\n \"text\": \"也讓我們能夠持續地在商業化的路徑上\"\n },\n {\n \"start\": 728.94,\n \"end\": 730.94,\n \"text\": \"有一個很好的良性的必須\"\n },\n {\n \"start\": 730.94,\n \"end\": 732.94,\n \"text\": \"不斷地去優化模型的能力\"\n },\n {\n \"start\": 732.94,\n \"end\": 734.94,\n \"text\": \"能夠跟持續地給大家提供\"\n },\n {\n \"start\": 734.94,\n \"end\": 738.94,\n \"text\": \"更好的這樣的模型和相應的這些偷肯的服務\"\n },\n {\n \"start\": 739.94,\n \"end\": 742.94,\n \"text\": \"非常好的這個分享\"\n },\n {\n \"start\": 742.94,\n \"end\": 744.94,\n \"text\": \"那其實因為現在有這個開源模型\"\n },\n {\n \"start\": 744.94,\n \"end\": 748.94,\n \"text\": \"然後跟這個推理算力\"\n },\n {\n \"start\": 748.94,\n \"end\": 750.94,\n \"text\": \"我感覺現在也開始形成一個深海\"\n },\n {\n \"start\": 750.94,\n \"end\": 752.94,\n \"text\": \"然後各種各樣的開源模型\"\n },\n {\n \"start\": 752.94,\n \"end\": 754.94,\n \"text\": \"可以在各種各樣的推理算力上\"\n },\n {\n \"start\": 754.94,\n \"end\": 757.94,\n \"text\": \"然後呢去給用戶提供更多的價值\"\n },\n {\n \"start\": 757.94,\n \"end\": 759.94,\n \"text\": \"然後隨著這個偷肯量的報價\"\n },\n {\n \"start\": 759.94,\n \"end\": 763.94,\n \"text\": \"現在也可能逐漸從訓練時代變成了這個推理時代\"\n },\n {\n \"start\": 763.94,\n \"end\": 765.94,\n \"text\": \"所以想請教一下歷史也就是說\"\n },\n {\n \"start\": 765.94,\n \"end\": 769.94,\n \"text\": \"從Infra的層面這個推理時代\"\n },\n {\n \"start\": 769.94,\n \"end\": 772.94,\n \"text\": \"對於這個文藍說他意味著什麼\"\n },\n {\n \"start\": 772.94,\n \"end\": 774.94,\n \"text\": \"感謝指令人對於確實\"\n },\n {\n \"start\": 774.94,\n \"end\": 779.94,\n \"text\": \"因為我們是一個誕生在AI時代的一個技術設施的廠商\"\n },\n {\n \"start\": 779.94,\n \"end\": 781.94,\n \"text\": \"然後我們現在也為Kimi為支撲\"\n },\n {\n \"start\": 781.94,\n \"end\": 784.94,\n \"text\": \"包括也在跟Mimo去做合作\"\n },\n {\n \"start\": 784.94,\n \"end\": 787.94,\n \"text\": \"然後讓大家能夠把我們這樣一個偷肯工廠\"\n },\n {\n \"start\": 787.94,\n \"end\": 788.94,\n \"text\": \"更高效的一個用起來\"\n },\n {\n \"start\": 788.94,\n \"end\": 792.94,\n \"text\": \"然後當然我們也在跟很多高橋開源所野的合作\"\n },\n {\n \"start\": 792.94,\n \"end\": 794.94,\n \"text\": \"所以我們其實一直都在思考一件事情\"\n },\n {\n \"start\": 794.94,\n \"end\": 798.94,\n \"text\": \"就是AGM時代需要的技術設施它會是什麼樣子的\"\n },\n {\n \"start\": 798.94,\n \"end\": 801.94,\n \"text\": \"然後我們怎麼能夠一步步的去\"\n },\n {\n \"start\": 801.94,\n \"end\": 803.94,\n \"text\": \"在這個過程中去實現它和推演它\"\n },\n {\n \"start\": 803.94,\n \"end\": 806.94,\n \"text\": \"那我們現在呢也是做好了充分的準備\"\n },\n {\n \"start\": 806.94,\n \"end\": 811.94,\n \"text\": \"也看到了就是短期中期和長期不同階段\"\n },\n {\n \"start\": 811.94,\n \"end\": 812.94,\n \"text\": \"我們需要解決的問題\"\n },\n {\n \"start\": 812.94,\n \"end\": 814.94,\n \"text\": \"那當前腳下的一個問題\"\n },\n {\n \"start\": 814.94,\n \"end\": 815.94,\n \"text\": \"其實就是剛剛大家也聊到的\"\n },\n {\n \"start\": 815.94,\n \"end\": 821.94,\n \"text\": \"現在整個這個像渴到帶起來的整個偷肯量的報征\"\n },\n {\n \"start\": 821.94,\n \"end\": 823.94,\n \"text\": \"對於我們的系統效率其實帶來的\"\n },\n {\n \"start\": 823.94,\n \"end\": 825.94,\n \"text\": \"更大的優化的需求\"\n },\n {\n \"start\": 825.94,\n \"end\": 827.94,\n \"text\": \"包括其實價格的增長\"\n },\n {\n \"start\": 827.94,\n \"end\": 830.94,\n \"text\": \"也是大家在這個需求下的一種解決方式\"\n },\n {\n \"start\": 830.94,\n \"end\": 832.94,\n \"text\": \"那我們呢其實一直以來都是\"\n },\n {\n \"start\": 832.94,\n \"end\": 834.94,\n \"text\": \"從這種軟硬件打通的方式\"\n },\n {\n \"start\": 834.94,\n \"end\": 836.94,\n \"text\": \"來去做布局和解決的\"\n },\n {\n \"start\": 836.94,\n \"end\": 838.94,\n \"text\": \"包括我們其實揭露了\"\n },\n {\n \"start\": 838.94,\n \"end\": 841.94,\n \"text\": \"我們能夠看到的幾乎所有種類的這種計算芯片\"\n },\n {\n \"start\": 841.94,\n \"end\": 844.94,\n \"text\": \"我們把國內的其實實際種類的這種芯片\"\n },\n {\n \"start\": 844.94,\n \"end\": 848.94,\n \"text\": \"和幾十個不同的算力集群都給同意的連接起來\"\n },\n {\n \"start\": 848.94,\n \"end\": 850.94,\n \"text\": \"這樣我們能夠去解決\"\n },\n {\n \"start\": 850.94,\n \"end\": 851.94,\n \"text\": \"就是AI的這樣一個系統中\"\n },\n {\n \"start\": 851.94,\n \"end\": 854.94,\n \"text\": \"對於算力四元的一個確的問題\"\n },\n {\n \"start\": 854.94,\n \"end\": 856.94,\n \"text\": \"因為我們當資源不足的時候\"\n },\n {\n \"start\": 856.94,\n \"end\": 857.94,\n \"text\": \"最好的辦法就是第一\"\n },\n {\n \"start\": 857.94,\n \"end\": 859.94,\n \"text\": \"把能夠用的資源都用起來\"\n },\n {\n \"start\": 859.94,\n \"end\": 861.94,\n \"text\": \"第二讓每一個算力都用在刀耳上\"\n },\n {\n \"start\": 861.94,\n \"end\": 864.94,\n \"text\": \"讓每一個資源都發揮出最大的\"\n },\n {\n \"start\": 864.94,\n \"end\": 865.94,\n \"text\": \"這樣一個轉化效率價值\"\n },\n {\n \"start\": 865.94,\n \"end\": 866.94,\n \"text\": \"所以在這個時代\"\n },\n {\n \"start\": 866.94,\n \"end\": 868.94,\n \"text\": \"我們在當前腳下要解決\"\n },\n {\n \"start\": 868.94,\n \"end\": 871.94,\n \"text\": \"就是怎麼能夠去進一步的打造一個\"\n },\n {\n \"start\": 871.94,\n \"end\": 872.94,\n \"text\": \"更高效的偷肯工廠\"\n },\n {\n \"start\": 872.94,\n \"end\": 873.94,\n \"text\": \"對\"\n },\n {\n \"start\": 873.94,\n \"end\": 875.94,\n \"text\": \"那這裡邊我們做了很多的優化\"\n },\n {\n \"start\": 875.94,\n \"end\": 877.94,\n \"text\": \"包括我們讓這個模型和硬件上的各種\"\n },\n {\n \"start\": 878.94,\n \"end\": 881.94,\n \"text\": \"這種顯存各種各樣的這些技術進行\"\n },\n {\n \"start\": 881.94,\n \"end\": 882.94,\n \"text\": \"一個最優質的失配\"\n },\n {\n \"start\": 882.94,\n \"end\": 883.94,\n \"text\": \"包括我們也在看\"\n },\n {\n \"start\": 883.94,\n \"end\": 886.94,\n \"text\": \"會不會在最新的模型結構和硬件結構下\"\n },\n {\n \"start\": 886.94,\n \"end\": 889.94,\n \"text\": \"能夠進行一些更深度的化學反應\"\n },\n {\n \"start\": 889.94,\n \"end\": 890.94,\n \"text\": \"對\"\n },\n {\n \"start\": 890.94,\n \"end\": 892.94,\n \"text\": \"那解決這個腳下的效率問題\"\n },\n {\n \"start\": 892.94,\n \"end\": 894.94,\n \"text\": \"其實我們還只是打造了一個\"\n },\n {\n \"start\": 894.94,\n \"end\": 897.94,\n \"text\": \"偷肯的一個標準化的一個偷肯工廠\"\n },\n {\n \"start\": 897.94,\n \"end\": 899.94,\n \"text\": \"但是面向 agent 時代我們認為\"\n },\n {\n \"start\": 899.94,\n \"end\": 900.94,\n \"text\": \"這還是不夠的\"\n },\n {\n \"start\": 900.94,\n \"end\": 901.94,\n \"text\": \"因為就像剛剛說的\"\n },\n {\n \"start\": 901.94,\n \"end\": 903.94,\n \"text\": \"agent 它更像是一個人\"\n },\n {\n \"start\": 903.94,\n \"end\": 905.94,\n \"text\": \"就是我們能夠交給它一個任務\"\n },\n {\n \"start\": 905.94,\n \"end\": 908.94,\n \"text\": \"那我認為我其實很堅定的認為\"\n },\n {\n \"start\": 908.94,\n \"end\": 911.94,\n \"text\": \"當前的很多的雲計算時代的技術設施\"\n },\n {\n \"start\": 911.94,\n \"end\": 915.94,\n \"text\": \"其實是為服務一個程序\"\n },\n {\n \"start\": 915.94,\n \"end\": 918.94,\n \"text\": \"服務一個人類工程師所設計的\"\n },\n {\n \"start\": 918.94,\n \"end\": 920.94,\n \"text\": \"而不是為 AI 設計\"\n },\n {\n \"start\": 920.94,\n \"end\": 921.94,\n \"text\": \"那有點像什麼\"\n },\n {\n \"start\": 921.94,\n \"end\": 922.94,\n \"text\": \"我們做了一個技術設施\"\n },\n {\n \"start\": 922.94,\n \"end\": 924.94,\n \"text\": \"然後它上面有一個接口\"\n },\n {\n \"start\": 924.94,\n \"end\": 926.94,\n \"text\": \"這個接口是為人類工程師做的\"\n },\n {\n \"start\": 926.94,\n \"end\": 928.94,\n \"text\": \"然後在上面要再保一層\"\n },\n {\n \"start\": 928.94,\n \"end\": 929.94,\n \"text\": \"再去接入 agent\"\n },\n {\n \"start\": 929.94,\n \"end\": 931.94,\n \"text\": \"那這種方式其實是用\"\n },\n {\n \"start\": 931.94,\n \"end\": 933.94,\n \"text\": \"人的操作的一些能力限制了\"\n },\n {\n \"start\": 933.94,\n \"end\": 934.94,\n \"text\": \"agent 發揮空間\"\n },\n {\n \"start\": 934.94,\n \"end\": 935.94,\n \"text\": \"我舉個例子比如說\"\n },\n {\n \"start\": 935.94,\n \"end\": 938.94,\n \"text\": \"agent 它能夠做到苗到毫秒級別\"\n },\n {\n \"start\": 938.94,\n \"end\": 940.94,\n \"text\": \"去思考和發起任務\"\n },\n {\n \"start\": 940.94,\n \"end\": 942.94,\n \"text\": \"那這件事情其實在我們之前的\"\n },\n {\n \"start\": 942.94,\n \"end\": 943.94,\n \"text\": \"這些比如說\"\n },\n {\n \"start\": 943.94,\n \"end\": 945.94,\n \"text\": \"幾層的KBIOS這些能力上\"\n },\n {\n \"start\": 945.94,\n \"end\": 946.94,\n \"text\": \"其實是沒有做好這個準備的\"\n },\n {\n \"start\": 946.94,\n \"end\": 948.94,\n \"text\": \"因為人類大概發起一個任務\"\n },\n {\n \"start\": 948.94,\n \"end\": 949.94,\n \"text\": \"是分中級別的\"\n },\n {\n \"start\": 949.94,\n \"end\": 951.94,\n \"text\": \"那我們這樣的一些功能\"\n },\n {\n \"start\": 951.94,\n \"end\": 952.94,\n \"text\": \"其實是需要進一步的能力\"\n },\n {\n \"start\": 952.94,\n \"end\": 953.94,\n \"text\": \"我們將它稱作\"\n },\n {\n \"start\": 953.94,\n \"end\": 954.94,\n \"text\": \"Agentic 的英文\"\n },\n {\n \"start\": 954.94,\n \"end\": 955.94,\n \"text\": \"就是我們叫做\"\n },\n {\n \"start\": 955.94,\n \"end\": 956.94,\n \"text\": \"我們需要打造一個\"\n },\n {\n \"start\": 956.94,\n \"end\": 959.94,\n \"text\": \"更智慧化的一個偷肯工廠\"\n },\n {\n \"start\": 959.94,\n \"end\": 960.94,\n \"text\": \"那這個是我們\"\n },\n {\n \"start\": 960.94,\n \"end\": 962.94,\n \"text\": \"我已經從現在正在做的事情\"\n },\n {\n \"start\": 962.94,\n \"end\": 964.94,\n \"text\": \"然後從更長遠的未來\"\n },\n {\n \"start\": 964.94,\n \"end\": 966.94,\n \"text\": \"就是真正AGI的實在帶來的時候\"\n },\n {\n \"start\": 966.94,\n \"end\": 968.94,\n \"text\": \"那我們認為連基礎設施\"\n },\n {\n \"start\": 968.94,\n \"end\": 969.94,\n \"text\": \"都應該是一個智能體\"\n },\n {\n \"start\": 969.94,\n \"end\": 971.94,\n \"text\": \"就是我們認為我們自己\"\n },\n {\n \"start\": 971.94,\n \"end\": 974.94,\n \"text\": \"做打造的這套工廠本身\"\n },\n {\n \"start\": 974.94,\n \"end\": 976.94,\n \"text\": \"也應該是一個能夠自我進化\"\n },\n {\n \"start\": 976.94,\n \"end\": 977.94,\n \"text\": \"自我跌代的\"\n },\n {\n \"start\": 977.94,\n \"end\": 979.94,\n \"text\": \"它能夠形成一個自主的組織\"\n },\n {\n \"start\": 979.94,\n \"end\": 981.94,\n \"text\": \"然後相當於它有一個 CEO\"\n },\n {\n \"start\": 981.94,\n \"end\": 982.94,\n \"text\": \"這個 CEO是一個 agent\"\n },\n {\n \"start\": 982.94,\n \"end\": 983.94,\n \"text\": \"它可能是一個 curl\"\n },\n {\n \"start\": 983.94,\n \"end\": 984.94,\n \"text\": \"在管理整個基礎設施\"\n },\n {\n \"start\": 984.94,\n \"end\": 986.94,\n \"text\": \"然後根據它的 AI 客戶的需求\"\n },\n {\n \"start\": 986.94,\n \"end\": 989.94,\n \"text\": \"自己去提需求跌代自己的基礎設施\"\n },\n {\n \"start\": 989.94,\n \"end\": 991.94,\n \"text\": \"然後這兩樣 AI 和 AI 之間\"\n },\n {\n \"start\": 991.94,\n \"end\": 993.94,\n \"text\": \"才能夠去更好的形成我和\"\n },\n {\n \"start\": 993.94,\n \"end\": 995.94,\n \"text\": \"所以我們也在做一些比如說\"\n },\n {\n \"start\": 995.94,\n \"end\": 997.94,\n \"text\": \"我們能夠讓 agent和 agent的之間\"\n },\n {\n \"start\": 997.94,\n \"end\": 998.94,\n \"text\": \"更好通信\"\n },\n {\n \"start\": 998.94,\n \"end\": 999.94,\n \"text\": \"然後做catch to catch\"\n },\n {\n \"start\": 999.94,\n \"end\": 1000.94,\n \"text\": \"這樣一些複製的能力\"\n },\n {\n \"start\": 1000.94,\n \"end\": 1002.94,\n \"text\": \"所以我們其實一直說在思考\"\n },\n {\n \"start\": 1002.94,\n \"end\": 1005.94,\n \"text\": \"就是基礎設施和 AI 的發展\"\n },\n {\n \"start\": 1005.94,\n \"end\": 1007.94,\n \"text\": \"不應該是一個隔離的狀態\"\n },\n {\n \"start\": 1007.94,\n \"end\": 1008.94,\n \"text\": \"說我接受一個需求\"\n },\n {\n \"start\": 1008.94,\n \"end\": 1009.94,\n \"text\": \"我就去製作\"\n },\n {\n \"start\": 1009.94,\n \"end\": 1012.94,\n \"text\": \"而是應該是產生非常豐富的化學反應\"\n },\n {\n \"start\": 1012.94,\n \"end\": 1014.94,\n \"text\": \"那這件事情才是我認為\"\n },\n {\n \"start\": 1014.94,\n \"end\": 1015.94,\n \"text\": \"真正的做到所謂的軟硬\"\n },\n {\n \"start\": 1015.94,\n \"end\": 1017.94,\n \"text\": \"協同做到這種算法\"\n },\n {\n \"start\": 1017.94,\n \"end\": 1018.94,\n \"text\": \"和基礎設施的協同\"\n },\n {\n \"start\": 1018.94,\n \"end\": 1020.94,\n \"text\": \"這時候供應行程一直想實現的\"\n },\n {\n \"start\": 1020.94,\n \"end\": 1021.94,\n \"text\": \"一個自己的使命\"\n },\n {\n \"start\": 1021.94,\n \"end\": 1022.94,\n \"text\": \"謝謝\"\n },\n {\n \"start\": 1022.94,\n \"end\": 1024.94,\n \"text\": \"接下來想問問福利\"\n },\n {\n \"start\": 1024.94,\n \"end\": 1028.94,\n \"text\": \"小米最近也是通過發布性的模型\"\n },\n {\n \"start\": 1028.94,\n \"end\": 1030.94,\n \"text\": \"包括開源那些背後的技術\"\n },\n {\n \"start\": 1030.94,\n \"end\": 1032.94,\n \"text\": \"我覺得對這個社區做出了\"\n },\n {\n \"start\": 1032.94,\n \"end\": 1033.94,\n \"text\": \"很大的貢獻\"\n },\n {\n \"start\": 1033.94,\n \"end\": 1034.94,\n \"text\": \"所以想問一下\"\n },\n {\n \"start\": 1034.94,\n \"end\": 1037.94,\n \"text\": \"小米在做大模型方面\"\n },\n {\n \"start\": 1037.94,\n \"end\": 1040.94,\n \"text\": \"你覺得會有什麼獨特的優勢\"\n },\n {\n \"start\": 1040.94,\n \"end\": 1046.24,\n \"text\": \"我覺得我們先拋開\"\n },\n {\n \"start\": 1046.24,\n \"end\": 1048.24,\n \"text\": \"小米在做大模型方面\"\n },\n {\n \"start\": 1048.24,\n \"end\": 1049.24,\n \"text\": \"沒有什麼獨特優勢\"\n },\n {\n \"start\": 1049.24,\n \"end\": 1051.24,\n \"text\": \"這個話題\"\n },\n {\n \"start\": 1051.24,\n \"end\": 1053.24,\n \"text\": \"就是我更想談一下\"\n },\n {\n \"start\": 1053.24,\n \"end\": 1055.24,\n \"text\": \"就是其實\"\n },\n {\n \"start\": 1055.24,\n \"end\": 1057.24,\n \"text\": \"中國的做大模型的團隊\"\n },\n {\n \"start\": 1057.24,\n \"end\": 1059.24,\n \"text\": \"在做大模型上的一個優勢\"\n },\n {\n \"start\": 1059.24,\n \"end\": 1060.24,\n \"text\": \"我覺得這個話題\"\n },\n {\n \"start\": 1060.24,\n \"end\": 1061.24,\n \"text\": \"更具備\"\n },\n {\n \"start\": 1061.24,\n \"end\": 1063.24,\n \"text\": \"更廣泛的一個價值\"\n },\n {\n \"start\": 1063.24,\n \"end\": 1065.24,\n \"text\": \"其實在\"\n },\n {\n \"start\": 1065.24,\n \"end\": 1067.24,\n \"text\": \"大概兩年前\"\n },\n {\n \"start\": 1067.24,\n \"end\": 1070.24,\n \"text\": \"我看到中國的基礎大模型團隊\"\n },\n {\n \"start\": 1070.24,\n \"end\": 1072.24,\n \"text\": \"已經在開始了一個\"\n },\n {\n \"start\": 1072.24,\n \"end\": 1074.24,\n \"text\": \"非常好的突破\"\n },\n {\n \"start\": 1074.24,\n \"end\": 1075.24,\n \"text\": \"這個突破叫做\"\n },\n {\n \"start\": 1075.24,\n \"end\": 1076.24,\n \"text\": \"我們在\"\n },\n {\n \"start\": 1076.24,\n \"end\": 1078.24,\n \"text\": \"怎麼在有限的\"\n },\n {\n \"start\": 1078.24,\n \"end\": 1080.24,\n \"text\": \"算理尤其是\"\n },\n {\n \"start\": 1080.24,\n \"end\": 1081.24,\n \"text\": \"在一些\"\n },\n {\n \"start\": 1081.24,\n \"end\": 1083.24,\n \"text\": \"V-Link互聯帶寬送線的\"\n },\n {\n \"start\": 1083.24,\n \"end\": 1084.24,\n \"text\": \"一些順利的情況下\"\n },\n {\n \"start\": 1084.24,\n \"end\": 1087.24,\n \"text\": \"怎麼去突破這些\"\n },\n {\n \"start\": 1087.24,\n \"end\": 1089.24,\n \"text\": \"一端算理的線紙\"\n },\n {\n \"start\": 1089.24,\n \"end\": 1091.24,\n \"text\": \"然後去做一些\"\n },\n {\n \"start\": 1091.24,\n \"end\": 1092.24,\n \"text\": \"看似是\"\n },\n {\n \"start\": 1092.24,\n \"end\": 1094.24,\n \"text\": \"為了效率妥協的一些\"\n },\n {\n \"start\": 1094.24,\n \"end\": 1095.24,\n \"text\": \"模型結構的創新\"\n },\n {\n \"start\": 1095.24,\n \"end\": 1097.24,\n \"text\": \"相對於V-203系列的\"\n },\n {\n \"start\": 1097.24,\n \"end\": 1100.24,\n \"text\": \"系度M-O-E和M-A這也等等\"\n },\n {\n \"start\": 1100.24,\n \"end\": 1101.24,\n \"text\": \"但是其實我們\"\n },\n {\n \"start\": 1101.24,\n \"end\": 1102.24,\n \"text\": \"後面能看到\"\n },\n {\n \"start\": 1102.24,\n \"end\": 1104.24,\n \"text\": \"有這樣一些\"\n },\n {\n \"start\": 1104.24,\n \"end\": 1105.24,\n \"text\": \"創新引發的\"\n },\n {\n \"start\": 1105.24,\n \"end\": 1106.24,\n \"text\": \"是一個變革\"\n },\n {\n \"start\": 1106.24,\n \"end\": 1108.24,\n \"text\": \"這個變革就是\"\n },\n {\n \"start\": 1108.24,\n \"end\": 1110.24,\n \"text\": \"我們怎麼在\"\n },\n {\n \"start\": 1110.24,\n \"end\": 1112.24,\n \"text\": \"算理一定的情況下\"\n },\n {\n \"start\": 1112.24,\n \"end\": 1114.24,\n \"text\": \"然後怎麼去\"\n },\n {\n \"start\": 1114.24,\n \"end\": 1116.24,\n \"text\": \"發揮一定算理的\"\n },\n {\n \"start\": 1116.24,\n \"end\": 1118.24,\n \"text\": \"最高的紙紋水平\"\n },\n {\n \"start\": 1118.24,\n \"end\": 1119.24,\n \"text\": \"我覺得這個是\"\n },\n {\n \"start\": 1119.24,\n \"end\": 1120.24,\n \"text\": \"有Dipsy可帶來\"\n },\n {\n \"start\": 1120.24,\n \"end\": 1123.24,\n \"text\": \"給所有古來的基礎大模型團隊的\"\n },\n {\n \"start\": 1123.24,\n \"end\": 1125.24,\n \"text\": \"一個勇氣\"\n },\n {\n \"start\": 1125.24,\n \"end\": 1127.24,\n \"text\": \"一個信心\"\n },\n {\n \"start\": 1127.24,\n \"end\": 1129.24,\n \"text\": \"在今天\"\n },\n {\n \"start\": 1129.24,\n \"end\": 1131.24,\n \"text\": \"我們其實\"\n },\n {\n \"start\": 1131.24,\n \"end\": 1133.24,\n \"text\": \"我們自己的國產芯片也好\"\n },\n {\n \"start\": 1133.24,\n \"end\": 1135.24,\n \"text\": \"尤其是推理芯片\"\n },\n {\n \"start\": 1135.24,\n \"end\": 1137.24,\n \"text\": \"有我們的訓練芯片\"\n },\n {\n \"start\": 1137.24,\n \"end\": 1139.24,\n \"text\": \"其實是\"\n },\n {\n \"start\": 1139.24,\n \"end\": 1141.24,\n \"text\": \"不再受這個線紙\"\n },\n {\n \"start\": 1141.24,\n \"end\": 1142.24,\n \"text\": \"但是我們能看到\"\n },\n {\n \"start\": 1142.24,\n \"end\": 1144.24,\n \"text\": \"在這樣的線紙的情況下\"\n },\n {\n \"start\": 1144.24,\n \"end\": 1146.24,\n \"text\": \"催生了我們對於\"\n },\n {\n \"start\": 1146.24,\n \"end\": 1148.24,\n \"text\": \"更高的訓練效率\"\n },\n {\n \"start\": 1148.24,\n \"end\": 1150.24,\n \"text\": \"更低的推理效率\"\n },\n {\n \"start\": 1150.24,\n \"end\": 1152.24,\n \"text\": \"的模型結構的全新的探索\"\n },\n {\n \"start\": 1152.24,\n \"end\": 1154.24,\n \"text\": \"就像最近的\"\n },\n {\n \"start\": 1154.24,\n \"end\": 1156.24,\n \"text\": \"hybrid or sparse\"\n },\n {\n \"start\": 1156.24,\n \"end\": 1158.24,\n \"text\": \"或是拎熱推薦的結構\"\n },\n {\n \"start\": 1158.24,\n \"end\": 1160.24,\n \"text\": \"Dipsy and S.A\"\n },\n {\n \"start\": 1160.24,\n \"end\": 1162.24,\n \"text\": \"然後Kimi\"\n },\n {\n \"start\": 1162.24,\n \"end\": 1163.24,\n \"text\": \"小秘也有新的\"\n },\n {\n \"start\": 1163.24,\n \"end\": 1165.24,\n \"text\": \"面向下一代結構的\"\n },\n {\n \"start\": 1165.24,\n \"end\": 1166.24,\n \"text\": \"hy sparse的結構\"\n },\n {\n \"start\": 1166.24,\n \"end\": 1168.24,\n \"text\": \"這是區域與Mimovir\"\n },\n {\n \"start\": 1168.24,\n \"end\": 1169.24,\n \"text\": \"這一代結構\"\n },\n {\n \"start\": 1169.24,\n \"end\": 1170.24,\n \"text\": \"我們面向於\"\n },\n {\n \"start\": 1170.24,\n \"end\": 1171.24,\n \"text\": \"Azine的這個時代\"\n },\n {\n \"start\": 1171.24,\n \"end\": 1172.24,\n \"text\": \"去思考的\"\n },\n {\n \"start\": 1172.24,\n \"end\": 1174.24,\n \"text\": \"如何在Azine的這個時代\"\n },\n {\n \"start\": 1174.24,\n \"end\": 1175.24,\n \"text\": \"我們去做\"\n },\n {\n \"start\": 1175.24,\n \"end\": 1177.24,\n \"text\": \"更好的模型結構的創新\"\n },\n {\n \"start\": 1177.24,\n \"end\": 1179.24,\n \"text\": \"我為什麼覺得\"\n },\n {\n \"start\": 1179.24,\n \"end\": 1180.24,\n \"text\": \"結構的創新\"\n },\n {\n \"start\": 1180.24,\n \"end\": 1181.24,\n \"text\": \"是如此的重要\"\n },\n {\n \"start\": 1181.24,\n \"end\": 1182.24,\n \"text\": \"是因為我們\"\n },\n {\n \"start\": 1182.24,\n \"end\": 1183.24,\n \"text\": \"其實剛剛探討到\"\n },\n {\n \"start\": 1183.24,\n \"end\": 1185.24,\n \"text\": \"OpenColo這個話題\"\n },\n {\n \"start\": 1185.24,\n \"end\": 1186.24,\n \"text\": \"OpenColo\"\n },\n {\n \"start\": 1186.24,\n \"end\": 1187.24,\n \"text\": \"其實\"\n },\n {\n \"start\": 1187.24,\n \"end\": 1188.24,\n \"text\": \"大家如果真實的\"\n },\n {\n \"start\": 1188.24,\n \"end\": 1189.24,\n \"text\": \"去用會發現\"\n },\n {\n \"start\": 1189.24,\n \"end\": 1190.24,\n \"text\": \"你越用越好\"\n },\n {\n \"start\": 1190.24,\n \"end\": 1191.24,\n \"text\": \"用越用越聰明\"\n },\n {\n \"start\": 1191.24,\n \"end\": 1192.24,\n \"text\": \"那麽它的一個\"\n },\n {\n \"start\": 1192.24,\n \"end\": 1193.24,\n \"text\": \"前提是你的\"\n },\n {\n \"start\": 1193.24,\n \"end\": 1194.24,\n \"text\": \"推理的contest\"\n },\n {\n \"start\": 1194.24,\n \"end\": 1196.24,\n \"text\": \"只是當contest\"\n },\n {\n \"start\": 1196.24,\n \"end\": 1197.24,\n \"text\": \"是一個\"\n },\n {\n \"start\": 1197.24,\n \"end\": 1198.24,\n \"text\": \"我們談論了\"\n },\n {\n \"start\": 1198.24,\n \"end\": 1199.24,\n \"text\": \"很久的一個話題\"\n },\n {\n \"start\": 1199.24,\n \"end\": 1201.24,\n \"text\": \"但是其實真正\"\n },\n {\n \"start\": 1201.24,\n \"end\": 1202.24,\n \"text\": \"現在你發現\"\n },\n {\n \"start\": 1202.24,\n \"end\": 1203.24,\n \"text\": \"能有一個模型\"\n },\n {\n \"start\": 1203.24,\n \"end\": 1205.24,\n \"text\": \"它在當contest的情況下\"\n },\n {\n \"start\": 1205.24,\n \"end\": 1206.24,\n \"text\": \"表現\"\n },\n {\n \"start\": 1206.24,\n \"end\": 1207.24,\n \"text\": \"非常好\"\n },\n {\n \"start\": 1207.24,\n \"end\": 1208.24,\n \"text\": \"性能\"\n },\n {\n \"start\": 1208.24,\n \"end\": 1209.24,\n \"text\": \"非常的強勁\"\n },\n {\n \"start\": 1209.24,\n \"end\": 1211.24,\n \"text\": \"推理成本的\"\n },\n {\n \"start\": 1211.24,\n \"end\": 1212.24,\n \"text\": \"非常地低\"\n },\n {\n \"start\": 1212.24,\n \"end\": 1213.24,\n \"text\": \"其實很多模型\"\n },\n {\n \"start\": 1213.24,\n \"end\": 1214.24,\n \"text\": \"它不是做不到\"\n },\n {\n \"start\": 1214.24,\n \"end\": 1215.24,\n \"text\": \"比如說\"\n },\n {\n \"start\": 1215.24,\n \"end\": 1216.24,\n \"text\": \"遺照或使照的contest\"\n },\n {\n \"start\": 1216.24,\n \"end\": 1217.24,\n \"text\": \"而是因為它去推\"\n },\n {\n \"start\": 1217.24,\n \"end\": 1218.24,\n \"text\": \"遺照使照\"\n },\n {\n \"start\": 1218.24,\n \"end\": 1219.24,\n \"text\": \"它的成本\"\n },\n {\n \"start\": 1219.24,\n \"end\": 1220.24,\n \"text\": \"太貴了\"\n },\n {\n \"start\": 1220.24,\n \"end\": 1222.24,\n \"text\": \"速度太慢了\"\n },\n {\n \"start\": 1222.24,\n \"end\": 1223.24,\n \"text\": \"那麽怎麽\"\n },\n {\n \"start\": 1223.24,\n \"end\": 1224.24,\n \"text\": \"你能做到\"\n },\n {\n \"start\": 1224.24,\n \"end\": 1225.24,\n \"text\": \"在遺照或使照\"\n },\n {\n \"start\": 1225.24,\n \"end\": 1226.24,\n \"text\": \"這個contest\"\n },\n {\n \"start\": 1226.24,\n \"end\": 1227.24,\n \"text\": \"的情況下\"\n },\n {\n \"start\": 1227.24,\n \"end\": 1228.24,\n \"text\": \"推了成本\"\n },\n {\n \"start\": 1228.24,\n \"end\": 1229.24,\n \"text\": \"夠低速度夠快\"\n },\n {\n \"start\": 1229.24,\n \"end\": 1230.24,\n \"text\": \"然後\"\n },\n {\n \"start\": 1230.24,\n \"end\": 1231.24,\n \"text\": \"在這樣的情況下\"\n },\n {\n \"start\": 1231.24,\n \"end\": 1233.24,\n \"text\": \"才會有真正的\"\n },\n {\n \"start\": 1233.24,\n \"end\": 1235.24,\n \"text\": \"高生產力價值的\"\n },\n {\n \"start\": 1235.24,\n \"end\": 1237.24,\n \"text\": \"任務交給這個模型\"\n },\n {\n \"start\": 1237.24,\n \"end\": 1238.24,\n \"text\": \"從而去激發\"\n },\n {\n \"start\": 1238.24,\n \"end\": 1239.24,\n \"text\": \"在這種\"\n },\n {\n \"start\": 1239.24,\n \"end\": 1240.24,\n \"text\": \"囊抗隨的情況下\"\n },\n {\n \"start\": 1240.24,\n \"end\": 1241.24,\n \"text\": \"它去完成\"\n },\n {\n \"start\": 1241.24,\n \"end\": 1242.24,\n \"text\": \"更高複雜度的任務\"\n },\n {\n \"start\": 1242.24,\n \"end\": 1243.24,\n \"text\": \"可能我們\"\n },\n {\n \"start\": 1243.24,\n \"end\": 1244.24,\n \"text\": \"才能在這樣\"\n },\n {\n \"start\": 1244.24,\n \"end\": 1245.24,\n \"text\": \"使照\"\n },\n {\n \"start\": 1245.24,\n \"end\": 1246.24,\n \"text\": \"甚至一百照康徹\"\n },\n {\n \"start\": 1246.24,\n \"end\": 1247.24,\n \"text\": \"的情況下\"\n },\n {\n \"start\": 1247.24,\n \"end\": 1248.24,\n \"text\": \"實現\"\n },\n {\n \"start\": 1248.24,\n \"end\": 1249.24,\n \"text\": \"模型的資疊帶\"\n },\n {\n \"start\": 1249.24,\n \"end\": 1250.24,\n \"text\": \"所以模型的資疊\"\n },\n {\n \"start\": 1250.24,\n \"end\": 1251.24,\n \"text\": \"大概就是它可以\"\n },\n {\n \"start\": 1251.24,\n \"end\": 1252.24,\n \"text\": \"再一個複雜的環境\"\n },\n {\n \"start\": 1252.24,\n \"end\": 1253.24,\n \"text\": \"一邊\"\n },\n {\n \"start\": 1253.24,\n \"end\": 1254.24,\n \"text\": \"依靠於超長的contest\"\n },\n {\n \"start\": 1254.24,\n \"end\": 1256.24,\n \"text\": \"然後完成對自我\"\n },\n {\n \"start\": 1256.24,\n \"end\": 1257.24,\n \"text\": \"的這個進化\"\n },\n {\n \"start\": 1257.24,\n \"end\": 1258.24,\n \"text\": \"這個進化\"\n },\n {\n \"start\": 1258.24,\n \"end\": 1259.24,\n \"text\": \"有可能是對\"\n },\n {\n \"start\": 1259.24,\n \"end\": 1260.24,\n \"text\": \"一種框架本身的\"\n },\n {\n \"start\": 1260.24,\n \"end\": 1261.24,\n \"text\": \"也有可能是對\"\n },\n {\n \"start\": 1261.24,\n \"end\": 1263.24,\n \"text\": \"模型參數本身的\"\n },\n {\n \"start\": 1263.24,\n \"end\": 1264.24,\n \"text\": \"因為我們認為\"\n },\n {\n \"start\": 1264.24,\n \"end\": 1265.24,\n \"text\": \"Nuncaus本身\"\n },\n {\n \"start\": 1265.24,\n \"end\": 1266.24,\n \"text\": \"其實就是對參數的\"\n },\n {\n \"start\": 1266.24,\n \"end\": 1267.24,\n \"text\": \"一個進化\"\n },\n {\n \"start\": 1267.24,\n \"end\": 1268.24,\n \"text\": \"所以\"\n },\n {\n \"start\": 1268.24,\n \"end\": 1269.24,\n \"text\": \"其實\"\n },\n {\n \"start\": 1269.24,\n \"end\": 1270.24,\n \"text\": \"怎麽實現\"\n },\n {\n \"start\": 1270.24,\n \"end\": 1272.24,\n \"text\": \"一個Nuncaus infusion的\"\n },\n {\n \"start\": 1272.24,\n \"end\": 1273.24,\n \"text\": \"架構\"\n },\n {\n \"start\": 1273.24,\n \"end\": 1274.24,\n \"text\": \"以及怎麽在推理\"\n },\n {\n \"start\": 1274.24,\n \"end\": 1276.24,\n \"text\": \"測做到Nuncaus infusion\"\n },\n {\n \"start\": 1276.24,\n \"end\": 1277.24,\n \"text\": \"其實\"\n },\n {\n \"start\": 1277.24,\n \"end\": 1278.24,\n \"text\": \"它是一個\"\n },\n {\n \"start\": 1278.24,\n \"end\": 1279.24,\n \"text\": \"進化的進程\"\n },\n {\n \"start\": 1279.24,\n \"end\": 1280.24,\n \"text\": \"除了我剛剛提到的\"\n },\n {\n \"start\": 1280.24,\n \"end\": 1281.24,\n \"text\": \"在預訓練\"\n },\n {\n \"start\": 1281.24,\n \"end\": 1282.24,\n \"text\": \"做好一個\"\n },\n {\n \"start\": 1282.24,\n \"end\": 1283.24,\n \"text\": \"Nuncaus infusion的架構\"\n },\n {\n \"start\": 1283.24,\n \"end\": 1284.24,\n \"text\": \"這個可能是我們一年前\"\n },\n {\n \"start\": 1284.24,\n \"end\": 1285.24,\n \"text\": \"就去探索\"\n },\n {\n \"start\": 1285.24,\n \"end\": 1286.24,\n \"text\": \"一個問題\"\n },\n {\n \"start\": 1286.24,\n \"end\": 1287.24,\n \"text\": \"然後而當今\"\n },\n {\n \"start\": 1287.24,\n \"end\": 1288.24,\n \"text\": \"你怎麽做到\"\n },\n {\n \"start\": 1288.24,\n \"end\": 1289.24,\n \"text\": \"Nuncaus\"\n },\n {\n \"start\": 1289.24,\n \"end\": 1291.24,\n \"text\": \"真實的這種\"\n },\n {\n \"start\": 1291.24,\n \"end\": 1292.24,\n \"text\": \"長\"\n },\n {\n \"start\": 1292.24,\n \"end\": 1293.24,\n \"text\": \"長成距離\"\n },\n {\n \"start\": 1293.24,\n \"end\": 1294.24,\n \"text\": \"的任務上的\"\n },\n {\n \"start\": 1294.24,\n \"end\": 1295.24,\n \"text\": \"一個穩定性\"\n },\n {\n \"start\": 1295.24,\n \"end\": 1296.24,\n \"text\": \"和非常\"\n },\n {\n \"start\": 1296.24,\n \"end\": 1297.24,\n \"text\": \"高上線的一個效果\"\n },\n {\n \"start\": 1297.24,\n \"end\": 1298.24,\n \"text\": \"就是我們\"\n },\n {\n \"start\": 1298.24,\n \"end\": 1299.24,\n \"text\": \"現在後續練\"\n },\n {\n \"start\": 1299.24,\n \"end\": 1300.24,\n \"text\": \"再去疊帶的\"\n },\n {\n \"start\": 1300.24,\n \"end\": 1301.24,\n \"text\": \"創新的一個\"\n },\n {\n \"start\": 1301.24,\n \"end\": 1302.24,\n \"text\": \"範世\"\n },\n {\n \"start\": 1302.24,\n \"end\": 1303.24,\n \"text\": \"我們再想\"\n },\n {\n \"start\": 1303.24,\n \"end\": 1305.24,\n \"text\": \"怎麽去構造\"\n },\n {\n \"start\": 1305.24,\n \"end\": 1306.24,\n \"text\": \"更有效的學習算法\"\n },\n {\n \"start\": 1306.24,\n \"end\": 1308.24,\n \"text\": \"然後怎麽去採集到\"\n },\n {\n \"start\": 1308.24,\n \"end\": 1310.24,\n \"text\": \"真實的這種\"\n },\n {\n \"start\": 1310.24,\n \"end\": 1311.24,\n \"text\": \"在於照\"\n },\n {\n \"start\": 1311.24,\n \"end\": 1312.24,\n \"text\": \"十兆一百糟上下\"\n },\n {\n \"start\": 1312.24,\n \"end\": 1313.24,\n \"text\": \"為你們\"\n },\n {\n \"start\": 1313.24,\n \"end\": 1314.24,\n \"text\": \"真的都具有長去\"\n },\n {\n \"start\": 1314.24,\n \"end\": 1315.24,\n \"text\": \"依賴的這種\"\n },\n {\n \"start\": 1315.24,\n \"end\": 1316.24,\n \"text\": \"這種紋本\"\n },\n {\n \"start\": 1316.24,\n \"end\": 1317.24,\n \"text\": \"以及結合複雜的\"\n },\n {\n \"start\": 1317.24,\n \"end\": 1318.24,\n \"text\": \"環境產生的\"\n },\n {\n \"start\": 1318.24,\n \"end\": 1320.24,\n \"text\": \"這個\"\n },\n {\n \"start\": 1320.24,\n \"end\": 1321.24,\n \"text\": \"純潔和Terry\"\n },\n {\n \"start\": 1321.24,\n \"end\": 1322.24,\n \"text\": \"這是我們\"\n },\n {\n \"start\": 1322.24,\n \"end\": 1323.24,\n \"text\": \"現在在後續練\"\n },\n {\n \"start\": 1323.24,\n \"end\": 1324.24,\n \"text\": \"正在經歷的事情\"\n },\n {\n \"start\": 1324.24,\n \"end\": 1325.24,\n \"text\": \"但是我能看到\"\n },\n {\n \"start\": 1325.24,\n \"end\": 1326.24,\n \"text\": \"更長期的事情\"\n },\n {\n \"start\": 1326.24,\n \"end\": 1327.24,\n \"text\": \"是由於\"\n },\n {\n \"start\": 1327.24,\n \"end\": 1328.24,\n \"text\": \"大模型\"\n },\n {\n \"start\": 1328.24,\n \"end\": 1329.24,\n \"text\": \"本身\"\n },\n {\n \"start\": 1329.24,\n \"end\": 1330.24,\n \"text\": \"在飛速的進步\"\n },\n {\n \"start\": 1330.24,\n \"end\": 1331.24,\n \"text\": \"由於我們\"\n },\n {\n \"start\": 1331.24,\n \"end\": 1332.24,\n \"text\": \"有Agent的框架\"\n },\n {\n \"start\": 1332.24,\n \"end\": 1333.24,\n \"text\": \"更好的\"\n },\n {\n \"start\": 1333.24,\n \"end\": 1334.24,\n \"text\": \"一個加持\"\n },\n {\n \"start\": 1334.24,\n \"end\": 1335.24,\n \"text\": \"我們能看到\"\n },\n {\n \"start\": 1335.24,\n \"end\": 1336.24,\n \"text\": \"張麗雪說的\"\n },\n {\n \"start\": 1336.24,\n \"end\": 1337.24,\n \"text\": \"我們推理需求\"\n },\n {\n \"start\": 1337.24,\n \"end\": 1338.24,\n \"text\": \"我相信在今年\"\n },\n {\n \"start\": 1338.24,\n \"end\": 1339.24,\n \"text\": \"已經發生的事情\"\n },\n {\n \"start\": 1339.24,\n \"end\": 1341.24,\n \"text\": \"是在過去一段時間\"\n },\n {\n \"start\": 1341.24,\n \"end\": 1342.24,\n \"text\": \"已經進實\"\n },\n {\n \"start\": 1342.24,\n \"end\": 1343.24,\n \"text\": \"被一個增長\"\n },\n {\n \"start\": 1343.24,\n \"end\": 1344.24,\n \"text\": \"那麽今年整個\"\n },\n {\n \"start\": 1344.24,\n \"end\": 1345.24,\n \"text\": \"偷奮的增長\"\n },\n {\n \"start\": 1345.24,\n \"end\": 1346.24,\n \"text\": \"會不會到一百倍\"\n },\n {\n \"start\": 1346.24,\n \"end\": 1347.24,\n \"text\": \"那麽這裡\"\n },\n {\n \"start\": 1347.24,\n \"end\": 1348.24,\n \"text\": \"其實我們又到了\"\n },\n {\n \"start\": 1348.24,\n \"end\": 1349.24,\n \"text\": \"另外一個\"\n },\n {\n \"start\": 1349.24,\n \"end\": 1350.24,\n \"text\": \"圍堵的一個競爭\"\n },\n {\n \"start\": 1350.24,\n \"end\": 1351.24,\n \"text\": \"競爭就是\"\n },\n {\n \"start\": 1351.24,\n \"end\": 1352.24,\n \"text\": \"算你\"\n },\n {\n \"start\": 1352.24,\n \"end\": 1353.24,\n \"text\": \"或說是\"\n },\n {\n \"start\": 1353.24,\n \"end\": 1354.24,\n \"text\": \"推理芯片\"\n },\n {\n \"start\": 1354.24,\n \"end\": 1355.24,\n \"text\": \"甚至嚇到\"\n },\n {\n \"start\": 1355.24,\n \"end\": 1356.24,\n \"text\": \"能源\"\n },\n {\n \"start\": 1356.24,\n \"end\": 1357.24,\n \"text\": \"所以我\"\n },\n {\n \"start\": 1357.24,\n \"end\": 1358.24,\n \"text\": \"我認為\"\n },\n {\n \"start\": 1358.24,\n \"end\": 1359.24,\n \"text\": \"我們\"\n },\n {\n \"start\": 1359.24,\n \"end\": 1360.24,\n \"text\": \"如果大家\"\n },\n {\n \"start\": 1360.24,\n \"end\": 1361.24,\n \"text\": \"去思考這個問題\"\n },\n {\n \"start\": 1361.24,\n \"end\": 1362.24,\n \"text\": \"然後\"\n },\n {\n \"start\": 1362.24,\n \"end\": 1364.24,\n \"text\": \"我\"\n },\n {\n \"start\": 1364.24,\n \"end\": 1365.24,\n \"text\": \"我認為可能\"\n },\n {\n \"start\": 1365.24,\n \"end\": 1366.24,\n \"text\": \"我\"\n },\n {\n \"start\": 1366.24,\n \"end\": 1367.24,\n \"text\": \"我會從\"\n },\n {\n \"start\": 1367.24,\n \"end\": 1368.24,\n \"text\": \"大家身上\"\n },\n {\n \"start\": 1368.24,\n \"end\": 1369.24,\n \"text\": \"學到更多\"\n },\n {\n \"start\": 1369.24,\n \"end\": 1370.24,\n \"text\": \"對\"\n },\n {\n \"start\": 1371.24,\n \"end\": 1372.24,\n \"text\": \"OK\"\n },\n {\n \"start\": 1372.24,\n \"end\": 1373.24,\n \"text\": \"非常有印象\"\n },\n {\n \"start\": 1373.24,\n \"end\": 1374.24,\n \"text\": \"的分享\"\n },\n {\n \"start\": 1374.24,\n \"end\": 1375.24,\n \"text\": \"下面想問一下\"\n },\n {\n \"start\": 1375.24,\n \"end\": 1376.24,\n \"text\": \"黃超\"\n },\n {\n \"start\": 1376.24,\n \"end\": 1377.24,\n \"text\": \"因為你也開發了\"\n },\n {\n \"start\": 1377.24,\n \"end\": 1378.24,\n \"text\": \"一些非常有影響力的\"\n },\n {\n \"start\": 1378.24,\n \"end\": 1379.24,\n \"text\": \"這個\"\n },\n {\n \"start\": 1379.24,\n \"end\": 1380.24,\n \"text\": \"Agent的\"\n },\n {\n \"start\": 1380.24,\n \"end\": 1381.24,\n \"text\": \"像\"\n },\n {\n \"start\": 1381.24,\n \"end\": 1382.24,\n \"text\": \"包括像\"\n },\n {\n \"start\": 1382.24,\n \"end\": 1383.24,\n \"text\": \"這個\"\n },\n {\n \"start\": 1383.24,\n \"end\": 1384.24,\n \"text\": \"Nanobot\"\n },\n {\n \"start\": 1384.24,\n \"end\": 1385.24,\n \"text\": \"也有很多\"\n },\n {\n \"start\": 1385.24,\n \"end\": 1386.24,\n \"text\": \"社區的粉絲\"\n },\n {\n \"start\": 1386.24,\n \"end\": 1387.24,\n \"text\": \"然後想問一下\"\n },\n {\n \"start\": 1387.24,\n \"end\": 1388.24,\n \"text\": \"就是你自己覺得說\"\n },\n {\n \"start\": 1388.24,\n \"end\": 1389.24,\n \"text\": \"從Agent的\"\n },\n {\n \"start\": 1389.24,\n \"end\": 1390.24,\n \"text\": \"Harney\"\n },\n {\n \"start\": 1390.24,\n \"end\": 1391.24,\n \"text\": \"或者說\"\n },\n {\n \"start\": 1391.24,\n \"end\": 1392.24,\n \"text\": \"應用的這個層面\"\n },\n {\n \"start\": 1392.24,\n \"end\": 1393.24,\n \"text\": \"接下來\"\n },\n {\n \"start\": 1393.24,\n \"end\": 1394.24,\n \"text\": \"會有什麼技術上\"\n },\n {\n \"start\": 1394.24,\n \"end\": 1395.24,\n \"text\": \"方向是\"\n },\n {\n \"start\": 1395.24,\n \"end\": 1396.24,\n \"text\": \"你覺得比較\"\n },\n {\n \"start\": 1396.24,\n \"end\": 1397.24,\n \"text\": \"比較重要\"\n },\n {\n \"start\": 1397.24,\n \"end\": 1398.24,\n \"text\": \"可能大家需要去關注\"\n },\n {\n \"start\": 1398.24,\n \"end\": 1399.24,\n \"text\": \"OK\"\n },\n {\n \"start\": 1399.24,\n \"end\": 1400.24,\n \"text\": \"感謝志靈\"\n },\n {\n \"start\": 1400.24,\n \"end\": 1401.24,\n \"text\": \"我覺得首先\"\n },\n {\n \"start\": 1401.24,\n \"end\": 1402.24,\n \"text\": \"就是從\"\n },\n {\n \"start\": 1402.24,\n \"end\": 1403.24,\n \"text\": \"如果我們是\"\n },\n {\n \"start\": 1403.24,\n \"end\": 1404.24,\n \"text\": \"把Agent的\"\n },\n {\n \"start\": 1404.24,\n \"end\": 1405.24,\n \"text\": \"一些技術粗向\"\n },\n {\n \"start\": 1405.24,\n \"end\": 1406.24,\n \"text\": \"出來\"\n },\n {\n \"start\": 1406.24,\n \"end\": 1407.24,\n \"text\": \"它關鍵幾個點\"\n },\n {\n \"start\": 1407.24,\n \"end\": 1408.24,\n \"text\": \"比如說\"\n },\n {\n \"start\": 1408.24,\n \"end\": 1409.24,\n \"text\": \"Planning\"\n },\n {\n \"start\": 1409.24,\n \"end\": 1410.24,\n \"text\": \"然後還有\"\n },\n {\n \"start\": 1410.24,\n \"end\": 1411.24,\n \"text\": \"就是\"\n },\n {\n \"start\": 1411.24,\n \"end\": 1412.24,\n \"text\": \"Memory\"\n },\n {\n \"start\": 1412.24,\n \"end\": 1413.24,\n \"text\": \"和To Use\"\n },\n {\n \"start\": 1413.24,\n \"end\": 1414.24,\n \"text\": \"這幾個模塊的話\"\n },\n {\n \"start\": 1414.24,\n \"end\": 1415.24,\n \"text\": \"比如說\"\n },\n {\n \"start\": 1415.24,\n \"end\": 1416.24,\n \"text\": \"Planning來講話\"\n },\n {\n \"start\": 1416.24,\n \"end\": 1417.24,\n \"text\": \"我覺得\"\n },\n {\n \"start\": 1417.24,\n \"end\": 1418.24,\n \"text\": \"現在的問題\"\n },\n {\n \"start\": 1418.24,\n \"end\": 1419.24,\n \"text\": \"就是\"\n },\n {\n \"start\": 1419.24,\n \"end\": 1420.24,\n \"text\": \"還是面向\"\n },\n {\n \"start\": 1420.24,\n \"end\": 1421.24,\n \"text\": \"一些長程的任務\"\n },\n {\n \"start\": 1421.24,\n \"end\": 1422.24,\n \"text\": \"或是非常复杂的\"\n },\n {\n \"start\": 1422.24,\n \"end\": 1423.24,\n \"text\": \"我覺得它\"\n },\n {\n \"start\": 1423.24,\n \"end\": 1424.24,\n \"text\": \"不一定能夠\"\n },\n {\n \"start\": 1424.24,\n \"end\": 1425.24,\n \"text\": \"去做\"\n },\n {\n \"start\": 1425.24,\n \"end\": 1426.24,\n \"text\": \"很好的一個Planning\"\n },\n {\n \"start\": 1426.24,\n \"end\": 1427.24,\n \"text\": \"因為我覺得它\"\n },\n {\n \"start\": 1427.24,\n \"end\": 1428.24,\n \"text\": \"本質上可能\"\n },\n {\n \"start\": 1428.24,\n \"end\": 1429.24,\n \"text\": \"不具備\"\n },\n {\n \"start\": 1429.24,\n \"end\": 1430.24,\n \"text\": \"這樣的一個\"\n },\n {\n \"start\": 1430.24,\n \"end\": 1431.24,\n \"text\": \"隱形的知識\"\n },\n {\n \"start\": 1431.24,\n \"end\": 1432.24,\n \"text\": \"有特別是\"\n },\n {\n \"start\": 1432.24,\n \"end\": 1433.24,\n \"text\": \"帶一些\"\n },\n {\n \"start\": 1433.24,\n \"end\": 1434.24,\n \"text\": \"很多一些\"\n },\n {\n \"start\": 1434.24,\n \"end\": 1435.24,\n \"text\": \"复杂的一些\"\n },\n {\n \"start\": 1435.24,\n \"end\": 1436.24,\n \"text\": \"隨著領域\"\n },\n {\n \"start\": 1436.24,\n \"end\": 1437.24,\n \"text\": \"我覺得這塊可能\"\n },\n {\n \"start\": 1437.24,\n \"end\": 1438.24,\n \"text\": \"Planning\"\n },\n {\n \"start\": 1438.24,\n \"end\": 1439.24,\n \"text\": \"我覺得未來可能\"\n },\n {\n \"start\": 1439.24,\n \"end\": 1440.24,\n \"text\": \"是需要\"\n },\n {\n \"start\": 1440.24,\n \"end\": 1441.24,\n \"text\": \"去對於\"\n },\n {\n \"start\": 1441.24,\n \"end\": 1442.24,\n \"text\": \"很多\"\n },\n {\n \"start\": 1442.24,\n \"end\": 1443.24,\n \"text\": \"把以有的\"\n },\n {\n \"start\": 1443.24,\n \"end\": 1444.24,\n \"text\": \"各種一些\"\n },\n {\n \"start\": 1444.24,\n \"end\": 1445.24,\n \"text\": \"比多复杂\"\n },\n {\n \"start\": 1445.24,\n \"end\": 1446.24,\n \"text\": \"任務的知識\"\n },\n {\n \"start\": 1446.24,\n \"end\": 1447.24,\n \"text\": \"把它固化到\"\n },\n {\n \"start\": 1447.24,\n \"end\": 1448.24,\n \"text\": \"模型裡面去做\"\n },\n {\n \"start\": 1448.24,\n \"end\": 1449.24,\n \"text\": \"這塊我覺得可能會\"\n },\n {\n \"start\": 1449.24,\n \"end\": 1450.24,\n \"text\": \"是一個點\"\n },\n {\n \"start\": 1450.24,\n \"end\": 1451.24,\n \"text\": \"當然\"\n },\n {\n \"start\": 1451.24,\n \"end\": 1452.24,\n \"text\": \"比如說\"\n },\n {\n \"start\": 1452.24,\n \"end\": 1453.24,\n \"text\": \"高質量的\"\n },\n {\n \"start\": 1453.24,\n \"end\": 1454.24,\n \"text\": \"Skill它\"\n },\n {\n \"start\": 1454.24,\n \"end\": 1455.24,\n \"text\": \"本質上\"\n },\n {\n \"start\": 1455.24,\n \"end\": 1456.24,\n \"text\": \"也是在蓋的模型\"\n },\n {\n \"start\": 1456.24,\n \"end\": 1457.24,\n \"text\": \"去完成一些\"\n },\n {\n \"start\": 1457.24,\n \"end\": 1458.24,\n \"text\": \"比較難的一些\"\n },\n {\n \"start\": 1458.24,\n \"end\": 1459.24,\n \"text\": \"Task\"\n },\n {\n \"start\": 1459.24,\n \"end\": 1460.24,\n \"text\": \"這個是針對於Planning\"\n },\n {\n \"start\": 1460.24,\n \"end\": 1461.24,\n \"text\": \"然後\"\n },\n {\n \"start\": 1461.24,\n \"end\": 1462.24,\n \"text\": \"的話\"\n },\n {\n \"start\": 1462.24,\n \"end\": 1463.24,\n \"text\": \"就是\"\n },\n {\n \"start\": 1463.24,\n \"end\": 1464.24,\n \"text\": \"memory的話\"\n },\n {\n \"start\": 1464.24,\n \"end\": 1465.24,\n \"text\": \"我覺得\"\n },\n {\n \"start\": 1465.24,\n \"end\": 1466.24,\n \"text\": \"就是\"\n },\n {\n \"start\": 1466.24,\n \"end\": 1467.24,\n \"text\": \"memory\"\n },\n {\n \"start\": 1467.24,\n \"end\": 1468.24,\n \"text\": \"肯定\"\n },\n {\n \"start\": 1468.24,\n \"end\": 1469.24,\n \"text\": \"我感覺\"\n },\n {\n \"start\": 1469.24,\n \"end\": 1471.24,\n \"text\": \"memory帶給我們那種感受\"\n },\n {\n \"start\": 1471.24,\n \"end\": 1472.24,\n \"text\": \"就是好像它\"\n },\n {\n \"start\": 1472.24,\n \"end\": 1473.24,\n \"text\": \"永遠都會存在著\"\n },\n {\n \"start\": 1473.24,\n \"end\": 1474.24,\n \"text\": \"一個\"\n },\n {\n \"start\": 1474.24,\n \"end\": 1475.24,\n \"text\": \"比如說\"\n },\n {\n \"start\": 1475.24,\n \"end\": 1477.24,\n \"text\": \"信息壓縮不準確\"\n },\n {\n \"start\": 1477.24,\n \"end\": 1478.24,\n \"text\": \"包括縮不準\"\n },\n {\n \"start\": 1478.24,\n \"end\": 1479.24,\n \"text\": \"這樣的一些問題\"\n },\n {\n \"start\": 1479.24,\n \"end\": 1480.24,\n \"text\": \"我覺得\"\n },\n {\n \"start\": 1480.24,\n \"end\": 1481.24,\n \"text\": \"包括像各類的\"\n },\n {\n \"start\": 1481.24,\n \"end\": 1482.24,\n \"text\": \"農家們\"\n },\n {\n \"start\": 1482.24,\n \"end\": 1483.24,\n \"text\": \"其實大家都採用的是\"\n },\n {\n \"start\": 1483.24,\n \"end\": 1484.24,\n \"text\": \"最簡單的一些\"\n },\n {\n \"start\": 1484.24,\n \"end\": 1485.24,\n \"text\": \"像文學系統\"\n },\n {\n \"start\": 1485.24,\n \"end\": 1486.24,\n \"text\": \"這樣\"\n },\n {\n \"start\": 1486.24,\n \"end\": 1487.24,\n \"text\": \"Makadang\"\n },\n {\n \"start\": 1487.24,\n \"end\": 1488.24,\n \"text\": \"格式的一些memory\"\n },\n {\n \"start\": 1488.24,\n \"end\": 1489.24,\n \"text\": \"通過我們是否\"\n },\n {\n \"start\": 1489.24,\n \"end\": 1490.24,\n \"text\": \"去共享一些文件\"\n },\n {\n \"start\": 1490.24,\n \"end\": 1491.24,\n \"text\": \"這樣的方式\"\n },\n {\n \"start\": 1491.24,\n \"end\": 1492.24,\n \"text\": \"來去做\"\n },\n {\n \"start\": 1492.24,\n \"end\": 1493.24,\n \"text\": \"我覺得這一點其實\"\n },\n {\n \"start\": 1493.24,\n \"end\": 1494.24,\n \"text\": \"可能未來memory\"\n },\n {\n \"start\": 1494.24,\n \"end\": 1495.24,\n \"text\": \"應該是\"\n },\n {\n \"start\": 1495.24,\n \"end\": 1496.24,\n \"text\": \"做可能類似於\"\n },\n {\n \"start\": 1496.24,\n \"end\": 1497.24,\n \"text\": \"會走向一些\"\n },\n {\n \"start\": 1497.24,\n \"end\": 1498.24,\n \"text\": \"分層的設計\"\n },\n {\n \"start\": 1498.24,\n \"end\": 1499.24,\n \"text\": \"包括\"\n },\n {\n \"start\": 1499.24,\n \"end\": 1500.24,\n \"text\": \"如何去更加的\"\n },\n {\n \"start\": 1500.24,\n \"end\": 1501.24,\n \"text\": \"去讓memory\"\n },\n {\n \"start\": 1501.24,\n \"end\": 1502.24,\n \"text\": \"做得更通用\"\n },\n {\n \"start\": 1502.24,\n \"end\": 1503.24,\n \"text\": \"因為其實\"\n },\n {\n \"start\": 1503.24,\n \"end\": 1504.24,\n \"text\": \"老師講\"\n },\n {\n \"start\": 1504.24,\n \"end\": 1505.24,\n \"text\": \"現在我覺得\"\n },\n {\n \"start\": 1505.24,\n \"end\": 1506.24,\n \"text\": \"整個memory\"\n },\n {\n \"start\": 1506.24,\n \"end\": 1507.24,\n \"text\": \"機制\"\n },\n {\n \"start\": 1507.24,\n \"end\": 1508.24,\n \"text\": \"其實很難做到\"\n },\n {\n \"start\": 1508.24,\n \"end\": 1509.24,\n \"text\": \"很通用\"\n },\n {\n \"start\": 1509.24,\n \"end\": 1510.24,\n \"text\": \"如何去對它\"\n },\n {\n \"start\": 1510.24,\n \"end\": 1511.24,\n \"text\": \"這些memory\"\n },\n {\n \"start\": 1511.24,\n \"end\": 1512.24,\n \"text\": \"已經很好的一個\"\n },\n {\n \"start\": 1512.24,\n \"end\": 1513.24,\n \"text\": \"解鎖\"\n },\n {\n \"start\": 1513.24,\n \"end\": 1514.24,\n \"text\": \"然後更高效的\"\n },\n {\n \"start\": 1514.24,\n \"end\": 1515.24,\n \"text\": \"我覺得這個\"\n },\n {\n \"start\": 1515.24,\n \"end\": 1516.24,\n \"text\": \"永遠是在做一個\"\n },\n {\n \"start\": 1516.24,\n \"end\": 1517.24,\n \"text\": \"吹到\"\n },\n {\n \"start\": 1517.24,\n \"end\": 1518.24,\n \"text\": \"就是可能是\"\n },\n {\n \"start\": 1518.24,\n \"end\": 1519.24,\n \"text\": \"我覺得這一點\"\n },\n {\n \"start\": 1519.24,\n \"end\": 1520.24,\n \"text\": \"是整個memory的\"\n },\n {\n \"start\": 1520.24,\n \"end\": 1521.24,\n \"text\": \"然後當另外的一點\"\n },\n {\n \"start\": 1521.24,\n \"end\": 1522.24,\n \"text\": \"就是說\"\n },\n {\n \"start\": 1522.24,\n \"end\": 1523.24,\n \"text\": \"現在其實\"\n },\n {\n \"start\": 1523.24,\n \"end\": 1524.24,\n \"text\": \"農家其實\"\n },\n {\n \"start\": 1524.24,\n \"end\": 1525.24,\n \"text\": \"讓大家對於\"\n },\n {\n \"start\": 1525.24,\n \"end\": 1526.24,\n \"text\": \"創建一件的這件事情\"\n },\n {\n \"start\": 1526.24,\n \"end\": 1527.24,\n \"text\": \"的門檻\"\n },\n {\n \"start\": 1527.24,\n \"end\": 1528.24,\n \"text\": \"大幅降低之後\"\n },\n {\n \"start\": 1528.24,\n \"end\": 1529.24,\n \"text\": \"其實未來\"\n },\n {\n \"start\": 1529.24,\n \"end\": 1530.24,\n \"text\": \"可能不止一個農家\"\n },\n {\n \"start\": 1530.24,\n \"end\": 1531.24,\n \"text\": \"我看到\"\n },\n {\n \"start\": 1531.24,\n \"end\": 1532.24,\n \"text\": \"Kimi其實\"\n },\n {\n \"start\": 1532.24,\n \"end\": 1533.24,\n \"text\": \"也有\"\n },\n {\n \"start\": 1533.24,\n \"end\": 1534.24,\n \"text\": \"一件事我們這樣\"\n },\n {\n \"start\": 1534.24,\n \"end\": 1535.24,\n \"text\": \"那些\"\n },\n {\n \"start\": 1535.24,\n \"end\": 1536.24,\n \"text\": \"一些整個的機制出來\"\n },\n {\n \"start\": 1536.24,\n \"end\": 1538.24,\n \"text\": \"其實如果覺得\"\n },\n {\n \"start\": 1538.24,\n \"end\": 1539.24,\n \"text\": \"抱著我覺得這個\"\n },\n {\n \"start\": 1539.24,\n \"end\": 1540.24,\n \"text\": \"大家可以想像\"\n },\n {\n \"start\": 1540.24,\n \"end\": 1541.24,\n \"text\": \"會非常的大\"\n },\n {\n \"start\": 1541.24,\n \"end\": 1542.24,\n \"text\": \"這個也是帶給\"\n },\n {\n \"start\": 1542.24,\n \"end\": 1543.24,\n \"text\": \"memory的一個壓力\"\n },\n {\n \"start\": 1543.24,\n \"end\": 1544.24,\n \"text\": \"就如何在\"\n },\n {\n \"start\": 1544.24,\n \"end\": 1545.24,\n \"text\": \"我覺得現在\"\n },\n {\n \"start\": 1545.24,\n \"end\": 1546.24,\n \"text\": \"其實還沒有一套\"\n },\n {\n \"start\": 1546.24,\n \"end\": 1547.24,\n \"text\": \"很好的一套機制\"\n },\n {\n \"start\": 1547.24,\n \"end\": 1548.24,\n \"text\": \"如何去管理\"\n },\n {\n \"start\": 1548.24,\n \"end\": 1549.24,\n \"text\": \"這種一群\"\n },\n {\n \"start\": 1549.24,\n \"end\": 1550.24,\n \"text\": \"弄一下帶來的\"\n },\n {\n \"start\": 1550.24,\n \"end\": 1551.24,\n \"text\": \"一直在上下文\"\n },\n {\n \"start\": 1551.24,\n \"end\": 1552.24,\n \"text\": \"因為這個我們感覺\"\n },\n {\n \"start\": 1552.24,\n \"end\": 1553.24,\n \"text\": \"特別是對於\"\n },\n {\n \"start\": 1553.24,\n \"end\": 1554.24,\n \"text\": \"複雜的\"\n },\n {\n \"start\": 1554.24,\n \"end\": 1555.24,\n \"text\": \"扣定\"\n },\n {\n \"start\": 1555.24,\n \"end\": 1556.24,\n \"text\": \"可言發現這種\"\n },\n {\n \"start\": 1556.24,\n \"end\": 1557.24,\n \"text\": \"我覺得這個\"\n },\n {\n \"start\": 1557.24,\n \"end\": 1558.24,\n \"text\": \"其實帶一個\"\n },\n {\n \"start\": 1558.24,\n \"end\": 1559.24,\n \"text\": \"不管是模型\"\n },\n {\n \"start\": 1559.24,\n \"end\": 1560.24,\n \"text\": \"還是整個的這種\"\n },\n {\n \"start\": 1560.24,\n \"end\": 1561.24,\n \"text\": \"就是\"\n },\n {\n \"start\": 1561.24,\n \"end\": 1562.24,\n \"text\": \"Agent的架構\"\n },\n {\n \"start\": 1562.24,\n \"end\": 1563.24,\n \"text\": \"其實都是\"\n },\n {\n \"start\": 1563.24,\n \"end\": 1564.24,\n \"text\": \"壓力挺大的\"\n },\n {\n \"start\": 1564.24,\n \"end\": 1565.24,\n \"text\": \"然後對於整個\"\n },\n {\n \"start\": 1565.24,\n \"end\": 1567.24,\n \"text\": \"我覺得\"\n },\n {\n \"start\": 1567.24,\n \"end\": 1568.24,\n \"text\": \"MCP本身存在的\"\n },\n {\n \"start\": 1568.24,\n \"end\": 1569.24,\n \"text\": \"問題其實現在\"\n },\n {\n \"start\": 1569.24,\n \"end\": 1570.24,\n \"text\": \"Skill其實還是會存在\"\n },\n {\n \"start\": 1570.24,\n \"end\": 1571.24,\n \"text\": \"比如說MCP當時\"\n },\n {\n \"start\": 1571.24,\n \"end\": 1572.24,\n \"text\": \"是有\"\n },\n {\n \"start\": 1572.24,\n \"end\": 1573.24,\n \"text\": \"質量不保障\"\n },\n {\n \"start\": 1573.24,\n \"end\": 1574.24,\n \"text\": \"然後包括\"\n },\n {\n \"start\": 1574.24,\n \"end\": 1575.24,\n \"text\": \"它可能會存在\"\n },\n {\n \"start\": 1575.24,\n \"end\": 1576.24,\n \"text\": \"一些安全問題\"\n },\n {\n \"start\": 1576.24,\n \"end\": 1577.24,\n \"text\": \"然後現在Skill的話\"\n },\n {\n \"start\": 1577.24,\n \"end\": 1578.24,\n \"text\": \"我覺得存在的首先\"\n },\n {\n \"start\": 1578.24,\n \"end\": 1579.24,\n \"text\": \"就是\"\n },\n {\n \"start\": 1579.24,\n \"end\": 1580.24,\n \"text\": \"現在看是確實\"\n },\n {\n \"start\": 1580.24,\n \"end\": 1581.24,\n \"text\": \"會有很多Skill\"\n },\n {\n \"start\": 1581.24,\n \"end\": 1582.24,\n \"text\": \"但是高質量的\"\n },\n {\n \"start\": 1582.24,\n \"end\": 1583.24,\n \"text\": \"Skill我們覺得\"\n },\n {\n \"start\": 1583.24,\n \"end\": 1584.24,\n \"text\": \"是比較少的\"\n },\n {\n \"start\": 1584.24,\n \"end\": 1585.24,\n \"text\": \"然後這樣的話\"\n },\n {\n \"start\": 1585.24,\n \"end\": 1586.24,\n \"text\": \"其實\"\n },\n {\n \"start\": 1586.24,\n \"end\": 1587.24,\n \"text\": \"低質量Skill會\"\n },\n {\n \"start\": 1587.24,\n \"end\": 1588.24,\n \"text\": \"很影響整個Agent的\"\n },\n {\n \"start\": 1588.24,\n \"end\": 1589.24,\n \"text\": \"完成任務的\"\n },\n {\n \"start\": 1589.24,\n \"end\": 1590.24,\n \"text\": \"一個完成度\"\n },\n {\n \"start\": 1590.24,\n \"end\": 1591.24,\n \"text\": \"然後另外的話\"\n },\n {\n \"start\": 1591.24,\n \"end\": 1592.24,\n \"text\": \"還有就是\"\n },\n {\n \"start\": 1592.24,\n \"end\": 1593.24,\n \"text\": \"Skill這個\"\n },\n {\n \"start\": 1593.24,\n \"end\": 1594.24,\n \"text\": \"我們覺得\"\n },\n {\n \"start\": 1594.24,\n \"end\": 1595.24,\n \"text\": \"很難\"\n },\n {\n \"start\": 1595.24,\n \"end\": 1596.24,\n \"text\": \"就很多時候\"\n },\n {\n \"start\": 1596.24,\n \"end\": 1596.74,\n \"text\": \"也會存在一些\"\n },\n {\n \"start\": 1596.74,\n \"end\": 1597.74,\n \"text\": \"我們覺得從\"\n },\n {\n \"start\": 1597.74,\n \"end\": 1598.74,\n \"text\": \"To Use這塊的話\"\n },\n {\n \"start\": 1598.74,\n \"end\": 1600.74,\n \"text\": \"我覺得可能得需要靠整個社區\"\n },\n {\n \"start\": 1600.74,\n \"end\": 1601.74,\n \"text\": \"然後來去把\"\n },\n {\n \"start\": 1601.74,\n \"end\": 1602.74,\n \"text\": \"整個Skill發展得\"\n },\n {\n \"start\": 1602.74,\n \"end\": 1603.74,\n \"text\": \"更好\"\n },\n {\n \"start\": 1603.74,\n \"end\": 1604.74,\n \"text\": \"甚至讓如何去通過這種\"\n },\n {\n \"start\": 1604.74,\n \"end\": 1605.74,\n \"text\": \"在執行過程中\"\n },\n {\n \"start\": 1605.74,\n \"end\": 1606.74,\n \"text\": \"去進化出來一些\"\n },\n {\n \"start\": 1606.74,\n \"end\": 1607.74,\n \"text\": \"Skill\"\n },\n {\n \"start\": 1607.74,\n \"end\": 1608.74,\n \"text\": \"所以我覺得這個可能是\"\n },\n {\n \"start\": 1608.74,\n \"end\": 1609.74,\n \"text\": \"整個不管是\"\n },\n {\n \"start\": 1609.74,\n \"end\": 1610.74,\n \"text\": \"From Planning\"\n },\n {\n \"start\": 1610.74,\n \"end\": 1611.74,\n \"text\": \"Memory Haskill\"\n },\n {\n \"start\": 1611.74,\n \"end\": 1612.74,\n \"text\": \"裡面我們覺得\"\n },\n {\n \"start\": 1612.74,\n \"end\": 1613.74,\n \"text\": \"當下Agent的可能存在的\"\n },\n {\n \"start\": 1613.74,\n \"end\": 1614.74,\n \"text\": \"一些痛點\"\n },\n {\n \"start\": 1614.74,\n \"end\": 1615.74,\n \"text\": \"和未來可能會\"\n },\n {\n \"start\": 1615.74,\n \"end\": 1617.74,\n \"text\": \"潛在的一些方向\"\n },\n {\n \"start\": 1617.74,\n \"end\": 1618.74,\n \"text\": \"可以看到\"\n },\n {\n \"start\": 1618.74,\n \"end\": 1619.74,\n \"text\": \"剛剛兩位嘉賓\"\n },\n {\n \"start\": 1619.74,\n \"end\": 1620.74,\n \"text\": \"也是從不同的\"\n },\n {\n \"start\": 1620.74,\n \"end\": 1621.74,\n \"text\": \"其實四角\"\n },\n {\n \"start\": 1621.74,\n \"end\": 1623.74,\n \"text\": \"然後去討論\"\n },\n {\n \"start\": 1623.74,\n \"end\": 1624.74,\n \"text\": \"一個問題\"\n },\n {\n \"start\": 1624.74,\n \"end\": 1625.74,\n \"text\": \"就是說\"\n },\n {\n \"start\": 1625.74,\n \"end\": 1626.74,\n \"text\": \"任務複雜度的增加\"\n },\n {\n \"start\": 1626.74,\n \"end\": 1627.74,\n \"text\": \"你的上下文\"\n },\n {\n \"start\": 1627.74,\n \"end\": 1628.74,\n \"text\": \"會爆炸\"\n },\n {\n \"start\": 1628.74,\n \"end\": 1629.74,\n \"text\": \"那可能從模型層面\"\n },\n {\n \"start\": 1629.74,\n \"end\": 1631.74,\n \"text\": \"是可以去提升你的\"\n },\n {\n \"start\": 1631.74,\n \"end\": 1632.74,\n \"text\": \"原生的上下文長度\"\n },\n {\n \"start\": 1632.74,\n \"end\": 1633.74,\n \"text\": \"然後可能從Agent\"\n },\n {\n \"start\": 1633.74,\n \"end\": 1634.74,\n \"text\": \"Harness層面\"\n },\n {\n \"start\": 1634.74,\n \"end\": 1635.74,\n \"text\": \"可能是像剛剛說的\"\n },\n {\n \"start\": 1635.74,\n \"end\": 1636.74,\n \"text\": \"這種\"\n },\n {\n \"start\": 1636.74,\n \"end\": 1637.74,\n \"text\": \"Planning memory\"\n },\n {\n \"start\": 1637.74,\n \"end\": 1638.74,\n \"text\": \"Bokem Multi-Agent的\"\n },\n {\n \"start\": 1638.74,\n \"end\": 1639.74,\n \"text\": \"Harness實際上\"\n },\n {\n \"start\": 1639.74,\n \"end\": 1640.74,\n \"text\": \"也是\"\n },\n {\n \"start\": 1640.74,\n \"end\": 1641.74,\n \"text\": \"能讓你可能\"\n },\n {\n \"start\": 1641.74,\n \"end\": 1642.74,\n \"text\": \"在一個\"\n },\n {\n \"start\": 1642.74,\n \"end\": 1644.74,\n \"text\": \"特定的模型的能力下\"\n },\n {\n \"start\": 1644.74,\n \"end\": 1645.74,\n \"text\": \"能夠去支持更複雜的任務\"\n },\n {\n \"start\": 1645.74,\n \"end\": 1646.74,\n \"text\": \"我覺得\"\n },\n {\n \"start\": 1646.74,\n \"end\": 1647.74,\n \"text\": \"可能這兩個方向\"\n },\n {\n \"start\": 1647.74,\n \"end\": 1648.74,\n \"text\": \"接下來也會\"\n },\n {\n \"start\": 1648.74,\n \"end\": 1649.74,\n \"text\": \"有更多的這種\"\n },\n {\n \"start\": 1649.74,\n \"end\": 1650.74,\n \"text\": \"產生化學反應\"\n },\n {\n \"start\": 1650.74,\n \"end\": 1652.74,\n \"text\": \"然後能夠\"\n },\n {\n \"start\": 1652.74,\n \"end\": 1653.74,\n \"text\": \"提升去\"\n },\n {\n \"start\": 1653.74,\n \"end\": 1654.74,\n \"text\": \"完成這個\"\n },\n {\n \"start\": 1654.74,\n \"end\": 1655.74,\n \"text\": \"任務的這個\"\n },\n {\n \"start\": 1655.74,\n \"end\": 1656.74,\n \"text\": \"這個難度\"\n },\n {\n \"start\": 1656.74,\n \"end\": 1657.74,\n \"text\": \"那最後\"\n },\n {\n \"start\": 1657.74,\n \"end\": 1658.74,\n \"text\": \"我們來一個\"\n },\n {\n \"start\": 1658.74,\n \"end\": 1659.74,\n \"text\": \"開放式的展望\"\n },\n {\n \"start\": 1659.74,\n \"end\": 1660.74,\n \"text\": \"所以\"\n },\n {\n \"start\": 1660.74,\n \"end\": 1661.74,\n \"text\": \"想請各位\"\n },\n {\n \"start\": 1661.74,\n \"end\": 1662.74,\n \"text\": \"用一個\"\n },\n {\n \"start\": 1662.74,\n \"end\": 1663.74,\n \"text\": \"詩\"\n },\n {\n \"start\": 1663.74,\n \"end\": 1664.74,\n \"text\": \"來描述一下\"\n },\n {\n \"start\": 1664.74,\n \"end\": 1665.74,\n \"text\": \"接下來\"\n },\n {\n \"start\": 1665.74,\n \"end\": 1666.74,\n \"text\": \"十二個月\"\n },\n {\n \"start\": 1666.74,\n \"end\": 1667.74,\n \"text\": \"大模型發展的\"\n },\n {\n \"start\": 1667.74,\n \"end\": 1668.74,\n \"text\": \"趨勢以及你的期望\"\n },\n {\n \"start\": 1668.74,\n \"end\": 1669.74,\n \"text\": \"那這次\"\n },\n {\n \"start\": 1669.74,\n \"end\": 1670.74,\n \"text\": \"我們先從這個\"\n },\n {\n \"start\": 1670.74,\n \"end\": 1672.74,\n \"text\": \"黃超這邊開始\"\n },\n {\n \"start\": 1672.74,\n \"end\": 1673.74,\n \"text\": \"我感覺\"\n },\n {\n \"start\": 1673.74,\n \"end\": 1674.74,\n \"text\": \"十二個月\"\n },\n {\n \"start\": 1674.74,\n \"end\": 1675.74,\n \"text\": \"在Agent看起來\"\n },\n {\n \"start\": 1675.74,\n \"end\": 1676.74,\n \"text\": \"一個好遙遠\"\n },\n {\n \"start\": 1676.74,\n \"end\": 1677.74,\n \"text\": \"都不知道十二個月之後\"\n },\n {\n \"start\": 1677.74,\n \"end\": 1678.74,\n \"text\": \"會發展的是什麼樣子\"\n },\n {\n \"start\": 1678.74,\n \"end\": 1679.74,\n \"text\": \"裡面有來寫的是五年\"\n },\n {\n \"start\": 1679.74,\n \"end\": 1680.74,\n \"text\": \"我給了\"\n },\n {\n \"start\": 1680.74,\n \"end\": 1681.74,\n \"text\": \"對\"\n },\n {\n \"start\": 1681.74,\n \"end\": 1682.74,\n \"text\": \"對\"\n },\n {\n \"start\": 1682.74,\n \"end\": 1683.74,\n \"text\": \"首先我感覺\"\n },\n {\n \"start\": 1683.74,\n \"end\": 1684.74,\n \"text\": \"從我這邊的話\"\n },\n {\n \"start\": 1684.74,\n \"end\": 1685.74,\n \"text\": \"我感覺一個詞應該\"\n },\n {\n \"start\": 1685.74,\n \"end\": 1686.74,\n \"text\": \"要生態吧\"\n },\n {\n \"start\": 1686.74,\n \"end\": 1687.74,\n \"text\": \"我覺得未來\"\n },\n {\n \"start\": 1687.74,\n \"end\": 1688.74,\n \"text\": \"其實現在\"\n },\n {\n \"start\": 1688.74,\n \"end\": 1689.74,\n \"text\": \"龍蝦讓大家\"\n },\n {\n \"start\": 1689.74,\n \"end\": 1690.74,\n \"text\": \"這麼火躍了\"\n },\n {\n \"start\": 1690.74,\n \"end\": 1691.74,\n \"text\": \"但是我覺得未來\"\n },\n {\n \"start\": 1691.74,\n \"end\": 1692.74,\n \"text\": \"Agent真的是\"\n },\n {\n \"start\": 1692.74,\n \"end\": 1693.74,\n \"text\": \"要去\"\n },\n {\n \"start\": 1693.74,\n \"end\": 1694.74,\n \"text\": \"從真正的個人助手\"\n },\n {\n \"start\": 1694.74,\n \"end\": 1695.74,\n \"text\": \"特別是\"\n },\n {\n \"start\": 1695.74,\n \"end\": 1696.74,\n \"text\": \"轉換完打工人\"\n },\n {\n \"start\": 1696.74,\n \"end\": 1697.74,\n \"text\": \"我覺得還是很重要的\"\n },\n {\n \"start\": 1697.74,\n \"end\": 1698.74,\n \"text\": \"因為現在可能\"\n },\n {\n \"start\": 1698.74,\n \"end\": 1699.74,\n \"text\": \"大家很多時候玩龍蝦\"\n },\n {\n \"start\": 1699.74,\n \"end\": 1700.74,\n \"text\": \"也是一個新鮮感\"\n },\n {\n \"start\": 1700.74,\n \"end\": 1701.74,\n \"text\": \"然後可能是覺得好玩\"\n },\n {\n \"start\": 1701.74,\n \"end\": 1702.74,\n \"text\": \"但是我覺得\"\n },\n {\n \"start\": 1702.74,\n \"end\": 1703.74,\n \"text\": \"未來可能真正\"\n },\n {\n \"start\": 1703.74,\n \"end\": 1704.74,\n \"text\": \"得讓龍蝦們\"\n },\n {\n \"start\": 1704.74,\n \"end\": 1705.74,\n \"text\": \"真的成電下來\"\n },\n {\n \"start\": 1705.74,\n \"end\": 1706.74,\n \"text\": \"真的\"\n },\n {\n \"start\": 1706.74,\n \"end\": 1707.74,\n \"text\": \"應該是成為\"\n },\n {\n \"start\": 1707.74,\n \"end\": 1708.74,\n \"text\": \"大家的一個\"\n },\n {\n \"start\": 1708.74,\n \"end\": 1709.74,\n \"text\": \"班章的工具\"\n },\n {\n \"start\": 1709.74,\n \"end\": 1710.74,\n \"text\": \"或者是\"\n },\n {\n \"start\": 1710.74,\n \"end\": 1711.74,\n \"text\": \"成為真正的\"\n },\n {\n \"start\": 1711.74,\n \"end\": 1712.74,\n \"text\": \"一個Kowalker\"\n },\n {\n \"start\": 1712.74,\n \"end\": 1713.74,\n \"text\": \"包括整個\"\n },\n {\n \"start\": 1713.74,\n \"end\": 1714.74,\n \"text\": \"我就看完這款\"\n },\n {\n \"start\": 1714.74,\n \"end\": 1715.74,\n \"text\": \"其實就是一個\"\n },\n {\n \"start\": 1715.74,\n \"end\": 1716.74,\n \"text\": \"很重要的\"\n },\n {\n \"start\": 1716.74,\n \"end\": 1718.74,\n \"text\": \"把所有的相關的一些\"\n },\n {\n \"start\": 1718.74,\n \"end\": 1719.74,\n \"text\": \"一些技術探索\"\n },\n {\n \"start\": 1719.74,\n \"end\": 1720.74,\n \"text\": \"包括整個模型的\"\n },\n {\n \"start\": 1720.74,\n \"end\": 1721.74,\n \"text\": \"很多器物都看出\"\n },\n {\n \"start\": 1721.74,\n \"end\": 1722.74,\n \"text\": \"就需要整個生態\"\n },\n {\n \"start\": 1722.74,\n \"end\": 1723.74,\n \"text\": \"大家一起來供建\"\n },\n {\n \"start\": 1723.74,\n \"end\": 1724.74,\n \"text\": \"不管是對於這種\"\n },\n {\n \"start\": 1724.74,\n \"end\": 1725.74,\n \"text\": \"模型的疊帶\"\n },\n {\n \"start\": 1725.74,\n \"end\": 1726.74,\n \"text\": \"還是對於這種\"\n },\n {\n \"start\": 1726.74,\n \"end\": 1727.74,\n \"text\": \"Skills平台的疊帶\"\n },\n {\n \"start\": 1727.74,\n \"end\": 1728.74,\n \"text\": \"還有各種工具的\"\n },\n {\n \"start\": 1728.74,\n \"end\": 1729.74,\n \"text\": \"我覺得這款\"\n },\n {\n \"start\": 1729.74,\n \"end\": 1730.74,\n \"text\": \"都需要去\"\n },\n {\n \"start\": 1730.74,\n \"end\": 1731.74,\n \"text\": \"更好的去面向\"\n },\n {\n \"start\": 1731.74,\n \"end\": 1732.74,\n \"text\": \"龍蝦去創造\"\n },\n {\n \"start\": 1732.74,\n \"end\": 1733.74,\n \"text\": \"更好的生態\"\n },\n {\n \"start\": 1733.74,\n \"end\": 1734.74,\n \"text\": \"就從我自己感覺\"\n },\n {\n \"start\": 1734.74,\n \"end\": 1736.74,\n \"text\": \"比較明顯的就是\"\n },\n {\n \"start\": 1736.74,\n \"end\": 1737.74,\n \"text\": \"我覺得未來的\"\n },\n {\n \"start\": 1737.74,\n \"end\": 1738.74,\n \"text\": \"很多軟件\"\n },\n {\n \"start\": 1738.74,\n \"end\": 1739.74,\n \"text\": \"包括現在有了龍蝦\"\n },\n {\n \"start\": 1739.74,\n \"end\": 1740.74,\n \"text\": \"然後大家又會再考慮一個問題\"\n },\n {\n \"start\": 1740.74,\n \"end\": 1741.74,\n \"text\": \"就是\"\n },\n {\n \"start\": 1741.74,\n \"end\": 1742.74,\n \"text\": \"還是會給人用的嗎\"\n },\n {\n \"start\": 1742.74,\n \"end\": 1743.74,\n \"text\": \"我們相信可能\"\n },\n {\n \"start\": 1743.74,\n \"end\": 1744.74,\n \"text\": \"未來大部分很多的\"\n },\n {\n \"start\": 1744.74,\n \"end\": 1745.74,\n \"text\": \"軟件都不一定會\"\n },\n {\n \"start\": 1745.74,\n \"end\": 1746.74,\n \"text\": \"順便向人\"\n },\n {\n \"start\": 1746.74,\n \"end\": 1747.74,\n \"text\": \"面向冷淚了\"\n },\n {\n \"start\": 1747.74,\n \"end\": 1748.74,\n \"text\": \"因為人類是需要\"\n },\n {\n \"start\": 1748.74,\n \"end\": 1749.74,\n \"text\": \"G-I當了可能\"\n },\n {\n \"start\": 1749.74,\n \"end\": 1750.74,\n \"text\": \"順便向Agent的\"\n },\n {\n \"start\": 1750.74,\n \"end\": 1751.74,\n \"text\": \"原生的去使用的\"\n },\n {\n \"start\": 1751.74,\n \"end\": 1752.74,\n \"text\": \"這樣我感覺\"\n },\n {\n \"start\": 1752.74,\n \"end\": 1753.74,\n \"text\": \"一個比較有趣的就是\"\n },\n {\n \"start\": 1753.74,\n \"end\": 1754.74,\n \"text\": \"人質會去使用\"\n },\n {\n \"start\": 1754.74,\n \"end\": 1755.74,\n \"text\": \"讓自己快樂的G-I\"\n },\n {\n \"start\": 1755.74,\n \"end\": 1756.74,\n \"text\": \"有可能會有這樣一個感受\"\n },\n {\n \"start\": 1756.74,\n \"end\": 1757.74,\n \"text\": \"所以我覺得\"\n },\n {\n \"start\": 1757.74,\n \"end\": 1758.74,\n \"text\": \"現在整個的生態\"\n },\n {\n \"start\": 1758.74,\n \"end\": 1760.74,\n \"text\": \"又從不管是G-I\"\n },\n {\n \"start\": 1760.74,\n \"end\": 1761.74,\n \"text\": \"還SMP又轉到了\"\n },\n {\n \"start\": 1761.74,\n \"end\": 1762.74,\n \"text\": \"C-I這樣的一個模式\"\n },\n {\n \"start\": 1762.74,\n \"end\": 1763.74,\n \"text\": \"所以我覺得\"\n },\n {\n \"start\": 1763.74,\n \"end\": 1764.74,\n \"text\": \"這個就是需要一個整個生態\"\n },\n {\n \"start\": 1764.74,\n \"end\": 1765.74,\n \"text\": \"就需要生態\"\n },\n {\n \"start\": 1765.74,\n \"end\": 1767.74,\n \"text\": \"去把不管是軟件系統\"\n },\n {\n \"start\": 1767.74,\n \"end\": 1768.74,\n \"text\": \"還是我們的整個\"\n },\n {\n \"start\": 1768.74,\n \"end\": 1769.74,\n \"text\": \"這種數據\"\n },\n {\n \"start\": 1769.74,\n \"end\": 1770.74,\n \"text\": \"還是整個的這種\"\n },\n {\n \"start\": 1770.74,\n \"end\": 1771.74,\n \"text\": \"就是各種一些\"\n },\n {\n \"start\": 1771.74,\n \"end\": 1772.74,\n \"text\": \"各種技術\"\n },\n {\n \"start\": 1772.74,\n \"end\": 1773.74,\n \"text\": \"其實都需要把它\"\n },\n {\n \"start\": 1773.74,\n \"end\": 1774.74,\n \"text\": \"變成Agent\"\n },\n {\n \"start\": 1774.74,\n \"end\": 1775.74,\n \"text\": \"原生化\"\n },\n {\n \"start\": 1775.74,\n \"end\": 1776.74,\n \"text\": \"Agent Lative的模式\"\n },\n {\n \"start\": 1776.74,\n \"end\": 1777.74,\n \"text\": \"這樣可能能夠\"\n },\n {\n \"start\": 1777.74,\n \"end\": 1778.74,\n \"text\": \"才能讓整個的這種\"\n },\n {\n \"start\": 1778.74,\n \"end\": 1779.74,\n \"text\": \"Agent的發展\"\n },\n {\n \"start\": 1779.74,\n \"end\": 1780.74,\n \"text\": \"可能會更加豐富\"\n },\n {\n \"start\": 1782.74,\n \"end\": 1783.74,\n \"text\": \"對我覺得\"\n },\n {\n \"start\": 1783.74,\n \"end\": 1784.74,\n \"text\": \"把這個問題\"\n },\n {\n \"start\": 1784.74,\n \"end\": 1786.74,\n \"text\": \"縮小到一年\"\n },\n {\n \"start\": 1786.74,\n \"end\": 1787.74,\n \"text\": \"我覺得非常有意義\"\n },\n {\n \"start\": 1787.74,\n \"end\": 1788.74,\n \"text\": \"因為我覺得五年\"\n },\n {\n \"start\": 1788.74,\n \"end\": 1789.74,\n \"text\": \"從我心目中對於\"\n },\n {\n \"start\": 1789.74,\n \"end\": 1790.74,\n \"text\": \"AGD而言\"\n },\n {\n \"start\": 1790.74,\n \"end\": 1791.74,\n \"text\": \"我覺得已經實現了\"\n },\n {\n \"start\": 1791.74,\n \"end\": 1792.74,\n \"text\": \"所以\"\n },\n {\n \"start\": 1792.74,\n \"end\": 1793.74,\n \"text\": \"所以就是\"\n },\n {\n \"start\": 1793.74,\n \"end\": 1795.74,\n \"text\": \"如果說要用一個\"\n },\n {\n \"start\": 1795.74,\n \"end\": 1796.74,\n \"text\": \"子來描述\"\n },\n {\n \"start\": 1796.74,\n \"end\": 1797.74,\n \"text\": \"接下來一年\"\n },\n {\n \"start\": 1797.74,\n \"end\": 1798.74,\n \"text\": \"我覺得AGD\"\n },\n {\n \"start\": 1798.74,\n \"end\": 1800.74,\n \"text\": \"成立的最關鍵的一個事情的話\"\n },\n {\n \"start\": 1800.74,\n \"end\": 1802.74,\n \"text\": \"我認為會是\"\n },\n {\n \"start\": 1802.74,\n \"end\": 1803.74,\n \"text\": \"自徑化\"\n },\n {\n \"start\": 1803.74,\n \"end\": 1804.74,\n \"text\": \"雖然這個詞\"\n },\n {\n \"start\": 1804.74,\n \"end\": 1805.74,\n \"text\": \"它是一個非常\"\n },\n {\n \"start\": 1805.74,\n \"end\": 1806.74,\n \"text\": \"有點懸患的一個概念\"\n },\n {\n \"start\": 1806.74,\n \"end\": 1807.74,\n \"text\": \"我覺得在過去一年\"\n },\n {\n \"start\": 1807.74,\n \"end\": 1809.74,\n \"text\": \"大家也多次提到\"\n },\n {\n \"start\": 1809.74,\n \"end\": 1810.74,\n \"text\": \"但是我\"\n },\n {\n \"start\": 1810.74,\n \"end\": 1811.74,\n \"text\": \"我也是最近\"\n },\n {\n \"start\": 1811.74,\n \"end\": 1812.74,\n \"text\": \"才對這個詞\"\n },\n {\n \"start\": 1812.74,\n \"end\": 1813.74,\n \"text\": \"有了\"\n },\n {\n \"start\": 1813.74,\n \"end\": 1815.74,\n \"text\": \"一個更深的一個體會\"\n },\n {\n \"start\": 1815.74,\n \"end\": 1816.74,\n \"text\": \"或說\"\n },\n {\n \"start\": 1816.74,\n \"end\": 1817.74,\n \"text\": \"去體自徑化\"\n },\n {\n \"start\": 1817.74,\n \"end\": 1818.74,\n \"text\": \"這個事兒怎麼做\"\n },\n {\n \"start\": 1818.74,\n \"end\": 1819.74,\n \"text\": \"會有一個\"\n },\n {\n \"start\": 1819.74,\n \"end\": 1820.74,\n \"text\": \"更務實的\"\n },\n {\n \"start\": 1820.74,\n \"end\": 1821.74,\n \"text\": \"一個\"\n },\n {\n \"start\": 1821.74,\n \"end\": 1822.74,\n \"text\": \"十操可行的方案\"\n },\n {\n \"start\": 1822.74,\n \"end\": 1823.74,\n \"text\": \"對\"\n },\n {\n \"start\": 1823.74,\n \"end\": 1824.74,\n \"text\": \"然後\"\n },\n {\n \"start\": 1824.74,\n \"end\": 1825.74,\n \"text\": \"這裡面的原因\"\n },\n {\n \"start\": 1825.74,\n \"end\": 1826.74,\n \"text\": \"是確實\"\n },\n {\n \"start\": 1826.74,\n \"end\": 1828.74,\n \"text\": \"是藉助於很強大的模型\"\n },\n {\n \"start\": 1828.74,\n \"end\": 1829.74,\n \"text\": \"其實我們之前在\"\n },\n {\n \"start\": 1829.74,\n \"end\": 1830.74,\n \"text\": \"插頭那個方式下\"\n },\n {\n \"start\": 1830.74,\n \"end\": 1831.74,\n \"text\": \"根本沒有發揮出來\"\n },\n {\n \"start\": 1831.74,\n \"end\": 1832.74,\n \"text\": \"這個預訓練模型\"\n },\n {\n \"start\": 1832.74,\n \"end\": 1833.74,\n \"text\": \"它的上線\"\n },\n {\n \"start\": 1833.74,\n \"end\": 1834.74,\n \"text\": \"然後這個上線線\"\n },\n {\n \"start\": 1834.74,\n \"end\": 1835.74,\n \"text\": \"是被AGD的框架\"\n },\n {\n \"start\": 1835.74,\n \"end\": 1836.74,\n \"text\": \"給機會到了\"\n },\n {\n \"start\": 1836.74,\n \"end\": 1837.74,\n \"text\": \"我們現在\"\n },\n {\n \"start\": 1837.74,\n \"end\": 1839.74,\n \"text\": \"觸到了它可能\"\n },\n {\n \"start\": 1839.74,\n \"end\": 1840.74,\n \"text\": \"當它執行\"\n },\n {\n \"start\": 1840.74,\n \"end\": 1842.74,\n \"text\": \"更長時間的任務的時候\"\n },\n {\n \"start\": 1842.74,\n \"end\": 1844.74,\n \"text\": \"然後我們發現這個模型\"\n },\n {\n \"start\": 1844.74,\n \"end\": 1845.74,\n \"text\": \"它可以\"\n },\n {\n \"start\": 1845.74,\n \"end\": 1847.74,\n \"text\": \"自己去學習和進化\"\n },\n {\n \"start\": 1847.74,\n \"end\": 1849.74,\n \"text\": \"很簡單一個嘗試\"\n },\n {\n \"start\": 1849.74,\n \"end\": 1850.74,\n \"text\": \"就是說\"\n },\n {\n \"start\": 1850.74,\n \"end\": 1851.74,\n \"text\": \"當你在線了的\"\n },\n {\n \"start\": 1851.74,\n \"end\": 1852.74,\n \"text\": \"AGD的框架一邊\"\n },\n {\n \"start\": 1852.74,\n \"end\": 1853.74,\n \"text\": \"給它鐵架一個\"\n },\n {\n \"start\": 1853.74,\n \"end\": 1854.74,\n \"text\": \"可以\"\n },\n {\n \"start\": 1854.74,\n \"end\": 1855.74,\n \"text\": \"verify的\"\n },\n {\n \"start\": 1855.74,\n \"end\": 1857.74,\n \"text\": \"這樣的一個\"\n },\n {\n \"start\": 1857.74,\n \"end\": 1858.74,\n \"text\": \"條件限制\"\n },\n {\n \"start\": 1858.74,\n \"end\": 1860.74,\n \"text\": \"然後再給它設置一個路譜\"\n },\n {\n \"start\": 1860.74,\n \"end\": 1862.74,\n \"text\": \"然後讓這個模型\"\n },\n {\n \"start\": 1862.74,\n \"end\": 1863.74,\n \"text\": \"就不停下來\"\n },\n {\n \"start\": 1863.74,\n \"end\": 1864.74,\n \"text\": \"持續去跌代\"\n },\n {\n \"start\": 1864.74,\n \"end\": 1865.74,\n \"text\": \"有化這個目標\"\n },\n {\n \"start\": 1865.74,\n \"end\": 1867.74,\n \"text\": \"我們就能發現這個模型\"\n },\n {\n \"start\": 1867.74,\n \"end\": 1868.74,\n \"text\": \"會持續\"\n },\n {\n \"start\": 1868.74,\n \"end\": 1869.74,\n \"text\": \"拿出\"\n },\n {\n \"start\": 1869.74,\n \"end\": 1870.74,\n \"text\": \"更好的方案\"\n },\n {\n \"start\": 1870.74,\n \"end\": 1871.74,\n \"text\": \"如果這樣的\"\n },\n {\n \"start\": 1871.74,\n \"end\": 1872.74,\n \"text\": \"一種自徑化\"\n },\n {\n \"start\": 1872.74,\n \"end\": 1873.74,\n \"text\": \"能持續\"\n },\n {\n \"start\": 1873.74,\n \"end\": 1874.74,\n \"text\": \"現在其實\"\n },\n {\n \"start\": 1874.74,\n \"end\": 1875.74,\n \"text\": \"已經能跑\"\n },\n {\n \"start\": 1875.74,\n \"end\": 1876.74,\n \"text\": \"一兩天了\"\n },\n {\n \"start\": 1876.74,\n \"end\": 1877.74,\n \"text\": \"其實國內的模型\"\n },\n {\n \"start\": 1877.74,\n \"end\": 1878.74,\n \"text\": \"即將能跑一兩天了\"\n },\n {\n \"start\": 1878.74,\n \"end\": 1879.74,\n \"text\": \"當然跟任務的\"\n },\n {\n \"start\": 1879.74,\n \"end\": 1880.74,\n \"text\": \"難度有關\"\n },\n {\n \"start\": 1880.74,\n \"end\": 1881.74,\n \"text\": \"我們發現做\"\n },\n {\n \"start\": 1881.74,\n \"end\": 1882.74,\n \"text\": \"它在一些科學研究上\"\n },\n {\n \"start\": 1882.74,\n \"end\": 1883.74,\n \"text\": \"比如說去探索\"\n },\n {\n \"start\": 1883.74,\n \"end\": 1884.74,\n \"text\": \"一個更好的模型結構\"\n },\n {\n \"start\": 1884.74,\n \"end\": 1885.74,\n \"text\": \"然後因為模型結構\"\n },\n {\n \"start\": 1885.74,\n \"end\": 1886.74,\n \"text\": \"它有評估標準\"\n },\n {\n \"start\": 1886.74,\n \"end\": 1887.74,\n \"text\": \"比如說我們看\"\n },\n {\n \"start\": 1887.74,\n \"end\": 1888.74,\n \"text\": \"更低的PPL\"\n },\n {\n \"start\": 1888.74,\n \"end\": 1889.74,\n \"text\": \"這是一個評估標準\"\n },\n {\n \"start\": 1889.74,\n \"end\": 1890.74,\n \"text\": \"然後在這種\"\n },\n {\n \"start\": 1890.74,\n \"end\": 1891.74,\n \"text\": \"很確定的任務上\"\n },\n {\n \"start\": 1891.74,\n \"end\": 1892.74,\n \"text\": \"我們發現它已經\"\n },\n {\n \"start\": 1892.74,\n \"end\": 1893.74,\n \"text\": \"能自主的\"\n },\n {\n \"start\": 1893.74,\n \"end\": 1894.74,\n \"text\": \"運化和執行兩三天了\"\n },\n {\n \"start\": 1894.74,\n \"end\": 1896.74,\n \"text\": \"所以從我的角度上來看\"\n },\n {\n \"start\": 1896.74,\n \"end\": 1897.74,\n \"text\": \"我覺得\"\n },\n {\n \"start\": 1897.74,\n \"end\": 1898.74,\n \"text\": \"資金化是一個\"\n },\n {\n \"start\": 1898.74,\n \"end\": 1899.74,\n \"text\": \"唯一可以\"\n },\n {\n \"start\": 1899.74,\n \"end\": 1900.74,\n \"text\": \"創造一個新的東西\"\n },\n {\n \"start\": 1900.74,\n \"end\": 1901.74,\n \"text\": \"的地方\"\n },\n {\n \"start\": 1901.74,\n \"end\": 1902.74,\n \"text\": \"它不是去踢帶\"\n },\n {\n \"start\": 1902.74,\n \"end\": 1903.74,\n \"text\": \"我們現有的人的生產力\"\n },\n {\n \"start\": 1903.74,\n \"end\": 1904.74,\n \"text\": \"而是說\"\n },\n {\n \"start\": 1904.74,\n \"end\": 1906.74,\n \"text\": \"它是像頂尖的科學家一樣\"\n },\n {\n \"start\": 1906.74,\n \"end\": 1907.74,\n \"text\": \"去探索出來\"\n },\n {\n \"start\": 1907.74,\n \"end\": 1909.74,\n \"text\": \"這個事情上\"\n },\n {\n \"start\": 1909.74,\n \"end\": 1910.74,\n \"text\": \"沒有的一個東西\"\n },\n {\n \"start\": 1910.74,\n \"end\": 1911.74,\n \"text\": \"然後\"\n },\n {\n \"start\": 1911.74,\n \"end\": 1913.74,\n \"text\": \"其實在\"\n },\n {\n \"start\": 1913.74,\n \"end\": 1914.74,\n \"text\": \"一年前我會覺得\"\n },\n {\n \"start\": 1914.74,\n \"end\": 1915.74,\n \"text\": \"這個時間歷程\"\n },\n {\n \"start\": 1915.74,\n \"end\": 1916.74,\n \"text\": \"會拿到三到五年\"\n },\n {\n \"start\": 1916.74,\n \"end\": 1917.74,\n \"text\": \"但是也就是\"\n },\n {\n \"start\": 1917.74,\n \"end\": 1918.74,\n \"text\": \"在近期我會覺得\"\n },\n {\n \"start\": 1918.74,\n \"end\": 1919.74,\n \"text\": \"這個時間歷程\"\n },\n {\n \"start\": 1919.74,\n \"end\": 1920.74,\n \"text\": \"確實我們應該\"\n },\n {\n \"start\": 1920.74,\n \"end\": 1921.74,\n \"text\": \"說想到\"\n },\n {\n \"start\": 1921.74,\n \"end\": 1922.74,\n \"text\": \"最近一到兩年\"\n },\n {\n \"start\": 1922.74,\n \"end\": 1923.74,\n \"text\": \"可能我們就可以\"\n },\n {\n \"start\": 1923.74,\n \"end\": 1924.74,\n \"text\": \"讓\"\n },\n {\n \"start\": 1924.74,\n \"end\": 1925.74,\n \"text\": \"大模型\"\n },\n {\n \"start\": 1925.74,\n \"end\": 1927.74,\n \"text\": \"跌加一個非常強的\"\n },\n {\n \"start\": 1927.74,\n \"end\": 1928.74,\n \"text\": \"資金化的\"\n },\n {\n \"start\": 1928.74,\n \"end\": 1929.74,\n \"text\": \"一個一種框架\"\n },\n {\n \"start\": 1929.74,\n \"end\": 1931.74,\n \"text\": \"實現對於\"\n },\n {\n \"start\": 1931.74,\n \"end\": 1932.74,\n \"text\": \"科學研究的一個\"\n },\n {\n \"start\": 1932.74,\n \"end\": 1933.74,\n \"text\": \"至少\"\n },\n {\n \"start\": 1933.74,\n \"end\": 1934.74,\n \"text\": \"成\"\n },\n {\n \"start\": 1934.74,\n \"end\": 1936.74,\n \"text\": \"指出幾個家屬\"\n },\n {\n \"start\": 1936.74,\n \"end\": 1937.74,\n \"text\": \"因為我最近已經發現\"\n },\n {\n \"start\": 1937.74,\n \"end\": 1938.74,\n \"text\": \"我們組內\"\n },\n {\n \"start\": 1938.74,\n \"end\": 1940.74,\n \"text\": \"做大模型研究的同學\"\n },\n {\n \"start\": 1940.74,\n \"end\": 1941.74,\n \"text\": \"基本上\"\n },\n {\n \"start\": 1941.74,\n \"end\": 1942.74,\n \"text\": \"其實\"\n },\n {\n \"start\": 1942.74,\n \"end\": 1943.74,\n \"text\": \"它的workflow\"\n },\n {\n \"start\": 1943.74,\n \"end\": 1944.74,\n \"text\": \"是非常不確定的\"\n },\n {\n \"start\": 1944.74,\n \"end\": 1945.74,\n \"text\": \"是高度創造力的\"\n },\n {\n \"start\": 1945.74,\n \"end\": 1946.74,\n \"text\": \"然後其實\"\n },\n {\n \"start\": 1946.74,\n \"end\": 1947.74,\n \"text\": \"我們發現\"\n },\n {\n \"start\": 1947.74,\n \"end\": 1948.74,\n \"text\": \"介入\"\n },\n {\n \"start\": 1948.74,\n \"end\": 1949.74,\n \"text\": \"cloud code\"\n },\n {\n \"start\": 1949.74,\n \"end\": 1950.74,\n \"text\": \"加\"\n },\n {\n \"start\": 1950.74,\n \"end\": 1952.74,\n \"text\": \"加\"\n },\n {\n \"start\": 1952.74,\n \"end\": 1953.74,\n \"text\": \"基本上\"\n },\n {\n \"start\": 1953.74,\n \"end\": 1954.74,\n \"text\": \"已經\"\n },\n {\n \"start\": 1954.74,\n \"end\": 1955.74,\n \"text\": \"能夠加速\"\n },\n {\n \"start\": 1955.74,\n \"end\": 1956.74,\n \"text\": \"我們自己的研究效率\"\n },\n {\n \"start\": 1956.74,\n \"end\": 1957.74,\n \"text\": \"已經使用了\"\n },\n {\n \"start\": 1957.74,\n \"end\": 1958.74,\n \"text\": \"所以\"\n },\n {\n \"start\": 1958.74,\n \"end\": 1959.74,\n \"text\": \"我很期待\"\n },\n {\n \"start\": 1959.74,\n \"end\": 1960.74,\n \"text\": \"這樣的一種\"\n },\n {\n \"start\": 1960.74,\n \"end\": 1961.74,\n \"text\": \"範式\"\n },\n {\n \"start\": 1961.74,\n \"end\": 1962.74,\n \"text\": \"輔射到\"\n },\n {\n \"start\": 1962.74,\n \"end\": 1963.74,\n \"text\": \"更廣的學科領域\"\n },\n {\n \"start\": 1963.74,\n \"end\": 1964.74,\n \"text\": \"我覺得\"\n },\n {\n \"start\": 1964.74,\n \"end\": 1965.74,\n \"text\": \"自營化\"\n },\n {\n \"start\": 1965.74,\n \"end\": 1968.79,\n \"text\": \"是非常重要的\"\n },\n {\n \"start\": 1968.79,\n \"end\": 1969.79,\n \"text\": \"我的關鍵詞\"\n },\n {\n \"start\": 1969.79,\n \"end\": 1970.79,\n \"text\": \"叫\"\n },\n {\n \"start\": 1970.79,\n \"end\": 1971.79,\n \"text\": \"可持續頭看\"\n },\n {\n \"start\": 1971.79,\n \"end\": 1972.79,\n \"text\": \"因為\"\n },\n {\n \"start\": 1972.79,\n \"end\": 1973.79,\n \"text\": \"我也看到的\"\n },\n {\n \"start\": 1973.79,\n \"end\": 1974.79,\n \"text\": \"現在\"\n },\n {\n \"start\": 1974.79,\n \"end\": 1975.79,\n \"text\": \"整個A的發展\"\n },\n {\n \"start\": 1975.79,\n \"end\": 1976.79,\n \"text\": \"還在一個\"\n },\n {\n \"start\": 1976.79,\n \"end\": 1977.79,\n \"text\": \"長期持續的過程中\"\n },\n {\n \"start\": 1977.79,\n \"end\": 1978.79,\n \"text\": \"我們也希望\"\n },\n {\n \"start\": 1978.79,\n \"end\": 1979.79,\n \"text\": \"它是能夠\"\n },\n {\n \"start\": 1979.79,\n \"end\": 1980.79,\n \"text\": \"有長久的生命力的\"\n },\n {\n \"start\": 1980.79,\n \"end\": 1981.79,\n \"text\": \"這裡面\"\n },\n {\n \"start\": 1981.79,\n \"end\": 1982.79,\n \"text\": \"我們作為\"\n },\n {\n \"start\": 1982.79,\n \"end\": 1983.79,\n \"text\": \"即時設施繳\"\n },\n {\n \"start\": 1983.79,\n \"end\": 1984.79,\n \"text\": \"就看到的一個\"\n },\n {\n \"start\": 1984.79,\n \"end\": 1985.79,\n \"text\": \"很大的問題\"\n },\n {\n \"start\": 1985.79,\n \"end\": 1986.79,\n \"text\": \"就是\"\n },\n {\n \"start\": 1986.79,\n \"end\": 1987.79,\n \"text\": \"我們的資源中\"\n },\n {\n \"start\": 1987.79,\n \"end\": 1988.79,\n \"text\": \"就是有限的\"\n },\n {\n \"start\": 1988.79,\n \"end\": 1989.79,\n \"text\": \"就像我們\"\n },\n {\n \"start\": 1989.79,\n \"end\": 1990.79,\n \"text\": \"當年在\"\n },\n {\n \"start\": 1990.79,\n \"end\": 1991.79,\n \"text\": \"講可持續發展一下\"\n },\n {\n \"start\": 1991.79,\n \"end\": 1992.79,\n \"text\": \"我們現在\"\n },\n {\n \"start\": 1992.79,\n \"end\": 1993.79,\n \"text\": \"作為一個頭看工廠\"\n },\n {\n \"start\": 1993.79,\n \"end\": 1994.79,\n \"text\": \"能否\"\n },\n {\n \"start\": 1994.79,\n \"end\": 1995.79,\n \"text\": \"給大家提供\"\n },\n {\n \"start\": 1995.79,\n \"end\": 1996.79,\n \"text\": \"持續穩定\"\n },\n {\n \"start\": 1996.79,\n \"end\": 1997.79,\n \"text\": \"然後\"\n },\n {\n \"start\": 1997.79,\n \"end\": 1998.79,\n \"text\": \"大規模\"\n },\n {\n \"start\": 1998.79,\n \"end\": 2000.79,\n \"text\": \"能夠用起來的這些\"\n },\n {\n \"start\": 2000.79,\n \"end\": 2001.79,\n \"text\": \"就是我們看到\"\n },\n {\n \"start\": 2001.79,\n \"end\": 2002.79,\n \"text\": \"一個很重要的問題\"\n },\n {\n \"start\": 2002.79,\n \"end\": 2003.79,\n \"text\": \"所以我們現在\"\n },\n {\n \"start\": 2003.79,\n \"end\": 2004.79,\n \"text\": \"也是需要的\"\n },\n {\n \"start\": 2004.79,\n \"end\": 2005.79,\n \"text\": \"就相當於\"\n },\n {\n \"start\": 2005.79,\n \"end\": 2006.79,\n \"text\": \"把整個的視角\"\n },\n {\n \"start\": 2006.79,\n \"end\": 2007.79,\n \"text\": \"放寬到\"\n },\n {\n \"start\": 2007.79,\n \"end\": 2008.79,\n \"text\": \"剛剛說到的整個生態\"\n },\n {\n \"start\": 2008.79,\n \"end\": 2009.79,\n \"text\": \"從最早的\"\n },\n {\n \"start\": 2009.79,\n \"end\": 2010.79,\n \"text\": \"能源到轉化\"\n },\n {\n \"start\": 2010.79,\n \"end\": 2011.79,\n \"text\": \"到算力\"\n },\n {\n \"start\": 2011.79,\n \"end\": 2012.79,\n \"text\": \"然後在\"\n },\n {\n \"start\": 2012.79,\n \"end\": 2013.79,\n \"text\": \"轉化到頭看\"\n },\n {\n \"start\": 2013.79,\n \"end\": 2014.79,\n \"text\": \"最終在轉換成\"\n },\n {\n \"start\": 2014.79,\n \"end\": 2015.79,\n \"text\": \"GDP\"\n },\n {\n \"start\": 2015.79,\n \"end\": 2016.79,\n \"text\": \"這樣一個練路\"\n },\n {\n \"start\": 2016.79,\n \"end\": 2017.79,\n \"text\": \"能夠去進行\"\n },\n {\n \"start\": 2017.79,\n \"end\": 2018.79,\n \"text\": \"持續的\"\n },\n {\n \"start\": 2018.79,\n \"end\": 2019.79,\n \"text\": \"經濟化的跌帶\"\n },\n {\n \"start\": 2019.79,\n \"end\": 2020.79,\n \"text\": \"包括我們\"\n },\n {\n \"start\": 2020.79,\n \"end\": 2021.79,\n \"text\": \"其實不只是\"\n },\n {\n \"start\": 2021.79,\n \"end\": 2022.79,\n \"text\": \"在跑國內的\"\n },\n {\n \"start\": 2022.79,\n \"end\": 2023.79,\n \"text\": \"各種算力用起來\"\n },\n {\n \"start\": 2023.79,\n \"end\": 2024.79,\n \"text\": \"也在把這些能力\"\n },\n {\n \"start\": 2024.79,\n \"end\": 2025.79,\n \"text\": \"輸出到海外\"\n },\n {\n \"start\": 2025.79,\n \"end\": 2026.79,\n \"text\": \"讓全球的\"\n },\n {\n \"start\": 2026.79,\n \"end\": 2027.79,\n \"text\": \"資源都能夠進行\"\n },\n {\n \"start\": 2027.79,\n \"end\": 2028.79,\n \"text\": \"打通和整合\"\n },\n {\n \"start\": 2028.79,\n \"end\": 2029.79,\n \"text\": \"所以我也認為\"\n },\n {\n \"start\": 2029.79,\n \"end\": 2030.79,\n \"text\": \"持續也是\"\n },\n {\n \"start\": 2030.79,\n \"end\": 2031.79,\n \"text\": \"其實也是想把\"\n },\n {\n \"start\": 2031.79,\n \"end\": 2032.79,\n \"text\": \"就是我們中國特色的\"\n },\n {\n \"start\": 2032.79,\n \"end\": 2033.79,\n \"text\": \"這個\"\n },\n {\n \"start\": 2033.79,\n \"end\": 2034.79,\n \"text\": \"頭看經濟學給做起來\"\n },\n {\n \"start\": 2034.79,\n \"end\": 2035.79,\n \"text\": \"對\"\n },\n {\n \"start\": 2035.79,\n \"end\": 2036.79,\n \"text\": \"因為其實我們在\"\n },\n {\n \"start\": 2036.79,\n \"end\": 2037.79,\n \"text\": \"之前的時代\"\n },\n {\n \"start\": 2037.79,\n \"end\": 2038.79,\n \"text\": \"我們是叫\"\n },\n {\n \"start\": 2038.79,\n \"end\": 2039.79,\n \"text\": \"Made in China\"\n },\n {\n \"start\": 2039.79,\n \"end\": 2040.79,\n \"text\": \"我們會發現\"\n },\n {\n \"start\": 2040.79,\n \"end\": 2041.79,\n \"text\": \"我們能夠把中國的\"\n },\n {\n \"start\": 2041.79,\n \"end\": 2043.79,\n \"text\": \"低價的這種\"\n },\n {\n \"start\": 2043.79,\n \"end\": 2044.79,\n \"text\": \"製造能力\"\n },\n {\n \"start\": 2044.79,\n \"end\": 2045.79,\n \"text\": \"然後變成好的商品\"\n },\n {\n \"start\": 2045.79,\n \"end\": 2046.79,\n \"text\": \"輸出到全球\"\n },\n {\n \"start\": 2046.79,\n \"end\": 2047.79,\n \"text\": \"那我們現在\"\n },\n {\n \"start\": 2047.79,\n \"end\": 2048.79,\n \"text\": \"想做的\"\n },\n {\n \"start\": 2048.79,\n \"end\": 2049.79,\n \"text\": \"就是\"\n },\n {\n \"start\": 2049.79,\n \"end\": 2050.79,\n \"text\": \"有點像AI\"\n },\n {\n \"start\": 2050.79,\n \"end\": 2051.79,\n \"text\": \"Made in China\"\n },\n {\n \"start\": 2051.79,\n \"end\": 2052.79,\n \"text\": \"就是我們能夠把\"\n },\n {\n \"start\": 2052.79,\n \"end\": 2053.79,\n \"text\": \"中國的這些\"\n },\n {\n \"start\": 2053.79,\n \"end\": 2054.79,\n \"text\": \"能源上的優勢\"\n },\n {\n \"start\": 2054.79,\n \"end\": 2055.79,\n \"text\": \"然後\"\n },\n {\n \"start\": 2055.79,\n \"end\": 2056.79,\n \"text\": \"通過這些\"\n },\n {\n \"start\": 2056.79,\n \"end\": 2057.79,\n \"text\": \"頭看工廠\"\n },\n {\n \"start\": 2057.79,\n \"end\": 2058.79,\n \"text\": \"可持續的\"\n },\n {\n \"start\": 2058.79,\n \"end\": 2060.79,\n \"text\": \"在輸出到全球\"\n },\n {\n \"start\": 2060.79,\n \"end\": 2061.79,\n \"text\": \"成為一個\"\n },\n {\n \"start\": 2061.79,\n \"end\": 2062.79,\n \"text\": \"世界的這個\"\n },\n {\n \"start\": 2062.79,\n \"end\": 2063.79,\n \"text\": \"頭看工廠\"\n },\n {\n \"start\": 2063.79,\n \"end\": 2064.79,\n \"text\": \"這是我想要在\"\n },\n {\n \"start\": 2064.79,\n \"end\": 2065.79,\n \"text\": \"今年看到的\"\n },\n {\n \"start\": 2065.79,\n \"end\": 2066.79,\n \"text\": \"整個中國\"\n },\n {\n \"start\": 2066.79,\n \"end\": 2067.79,\n \"text\": \"給世界的人工\"\n },\n {\n \"start\": 2067.79,\n \"end\": 2069.79,\n \"text\": \"只能帶來的價值\"\n },\n {\n \"start\": 2069.79,\n \"end\": 2071.79,\n \"text\": \"我就簡短一點\"\n },\n {\n \"start\": 2071.79,\n \"end\": 2072.79,\n \"text\": \"大家都可能\"\n },\n {\n \"start\": 2072.79,\n \"end\": 2073.79,\n \"text\": \"都在養望信空\"\n },\n {\n \"start\": 2073.79,\n \"end\": 2074.79,\n \"text\": \"我就落地一點\"\n },\n {\n \"start\": 2074.79,\n \"end\": 2075.79,\n \"text\": \"我覺得關鍵詞\"\n },\n {\n \"start\": 2075.79,\n \"end\": 2076.79,\n \"text\": \"讓我說\"\n },\n {\n \"start\": 2076.79,\n \"end\": 2077.79,\n \"text\": \"未來12個月\"\n },\n {\n \"start\": 2077.79,\n \"end\": 2078.79,\n \"text\": \"面臨的最大的問題\"\n },\n {\n \"start\": 2078.79,\n \"end\": 2079.79,\n \"text\": \"可能就是算力\"\n },\n {\n \"start\": 2079.79,\n \"end\": 2080.79,\n \"text\": \"因為剛才說到\"\n },\n {\n \"start\": 2080.79,\n \"end\": 2082.79,\n \"text\": \"所有的這個技術\"\n },\n {\n \"start\": 2082.79,\n \"end\": 2083.79,\n \"text\": \"包括\"\n },\n {\n \"start\": 2083.79,\n \"end\": 2084.79,\n \"text\": \"智能體框架\"\n },\n {\n \"start\": 2084.79,\n \"end\": 2085.79,\n \"text\": \"讓很多人\"\n },\n {\n \"start\": 2085.79,\n \"end\": 2086.79,\n \"text\": \"有很好的創調力\"\n },\n {\n \"start\": 2086.79,\n \"end\": 2087.79,\n \"text\": \"效率提升10倍\"\n },\n {\n \"start\": 2087.79,\n \"end\": 2088.79,\n \"text\": \"技條件就是\"\n },\n {\n \"start\": 2088.79,\n \"end\": 2089.79,\n \"text\": \"大家能夠用得起來\"\n },\n {\n \"start\": 2089.79,\n \"end\": 2090.79,\n \"text\": \"你不能因為說\"\n },\n {\n \"start\": 2090.79,\n \"end\": 2091.79,\n \"text\": \"算力不夠\"\n },\n {\n \"start\": 2091.79,\n \"end\": 2092.79,\n \"text\": \"一個問題提出去\"\n },\n {\n \"start\": 2092.79,\n \"end\": 2093.79,\n \"text\": \"讓他思考半天\"\n },\n {\n \"start\": 2093.79,\n \"end\": 2094.79,\n \"text\": \"也不給我答案\"\n },\n {\n \"start\": 2094.79,\n \"end\": 2095.79,\n \"text\": \"這個肯定是不行的\"\n },\n {\n \"start\": 2095.79,\n \"end\": 2096.79,\n \"text\": \"然後\"\n },\n {\n \"start\": 2096.79,\n \"end\": 2097.79,\n \"text\": \"對\"\n },\n {\n \"start\": 2097.79,\n \"end\": 2098.79,\n \"text\": \"也是\"\n },\n {\n \"start\": 2098.79,\n \"end\": 2099.79,\n \"text\": \"因為這樣的一些原因\"\n },\n {\n \"start\": 2099.79,\n \"end\": 2100.79,\n \"text\": \"可能\"\n },\n {\n \"start\": 2100.79,\n \"end\": 2101.79,\n \"text\": \"甚至連我們\"\n },\n {\n \"start\": 2101.79,\n \"end\": 2102.79,\n \"text\": \"在這個\"\n },\n {\n \"start\": 2102.79,\n \"end\": 2103.79,\n \"text\": \"研究的進展\"\n },\n {\n \"start\": 2103.79,\n \"end\": 2104.79,\n \"text\": \"包括\"\n },\n {\n \"start\": 2104.79,\n \"end\": 2105.79,\n \"text\": \"想要做的一些事情\"\n },\n {\n \"start\": 2105.79,\n \"end\": 2106.79,\n \"text\": \"其實都受阻了\"\n },\n {\n \"start\": 2106.79,\n \"end\": 2107.79,\n \"text\": \"前兩年\"\n },\n {\n \"start\": 2107.79,\n \"end\": 2108.79,\n \"text\": \"我記得是亞行院\"\n },\n {\n \"start\": 2108.79,\n \"end\": 2109.79,\n \"text\": \"是應該也是在\"\n },\n {\n \"start\": 2109.79,\n \"end\": 2110.79,\n \"text\": \"中央村論壇談了一間\"\n },\n {\n \"start\": 2110.79,\n \"end\": 2111.79,\n \"text\": \"說了一句話\"\n },\n {\n \"start\": 2111.79,\n \"end\": 2112.79,\n \"text\": \"叫\"\n },\n {\n \"start\": 2114.79,\n \"end\": 2116.79,\n \"text\": \"美卡美感情\"\n },\n {\n \"start\": 2116.79,\n \"end\": 2117.79,\n \"text\": \"談談傷感情\"\n },\n {\n \"start\": 2117.79,\n \"end\": 2119.79,\n \"text\": \"我就今天又到了這個地步了\"\n },\n {\n \"start\": 2119.79,\n \"end\": 2120.79,\n \"text\": \"但\"\n },\n {\n \"start\": 2120.79,\n \"end\": 2121.79,\n \"text\": \"情況又不一樣了\"\n },\n {\n \"start\": 2121.79,\n \"end\": 2122.79,\n \"text\": \"就是你剛才講的\"\n },\n {\n \"start\": 2122.79,\n \"end\": 2123.79,\n \"text\": \"可能我們又到了\"\n },\n {\n \"start\": 2123.79,\n \"end\": 2124.79,\n \"text\": \"推理的這個階段\"\n },\n {\n \"start\": 2124.79,\n \"end\": 2125.79,\n \"text\": \"轉向推理階段\"\n },\n {\n \"start\": 2125.79,\n \"end\": 2126.79,\n \"text\": \"是因為需求\"\n },\n {\n \"start\": 2126.79,\n \"end\": 2127.79,\n \"text\": \"正在爆發\"\n },\n {\n \"start\": 2127.79,\n \"end\": 2128.79,\n \"text\": \"10倍百倍的爆發\"\n },\n {\n \"start\": 2128.79,\n \"end\": 2129.79,\n \"text\": \"那你也說過去10倍\"\n },\n {\n \"start\": 2129.79,\n \"end\": 2131.79,\n \"text\": \"其實需求是100倍\"\n },\n {\n \"start\": 2131.79,\n \"end\": 2132.79,\n \"text\": \"還有很大量的需求\"\n },\n {\n \"start\": 2132.79,\n \"end\": 2133.79,\n \"text\": \"沒有滿足\"\n },\n {\n \"start\": 2133.79,\n \"end\": 2134.79,\n \"text\": \"怎麼辦\"\n },\n {\n \"start\": 2134.79,\n \"end\": 2135.79,\n \"text\": \"我們大家可能\"\n },\n {\n \"start\": 2135.79,\n \"end\": 2136.79,\n \"text\": \"一起來想想辦法\"\n },\n {\n \"start\": 2136.79,\n \"end\": 2137.79,\n \"text\": \"好謝謝\"\n },\n {\n \"start\": 2137.79,\n \"end\": 2138.79,\n \"text\": \"好\"\n },\n {\n \"start\": 2138.79,\n \"end\": 2139.79,\n \"text\": \"感謝各位的精采分享\"\n },\n {\n \"start\": 2139.79,\n \"end\": 2140.79,\n \"text\": \"謝謝大家\"\n }\n ],\n \"language\": \"zh\"\n}"}}, "web/package.json": {"1386625aa2926fbacf326b0d7a02905140096957": {"t": 1785599233.1767294, "n": 618, "h": "6d5a8a81f539eff1", "c": "{\n \"name\": \"noteking-web\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"scripts\": {\n \"dev\": \"next dev --port 3000\",\n \"build\": \"next build\",\n \"start\": \"next start --port 3000\"\n },\n \"dependencies\": {\n \"next\": \"^14.2.0\",\n \"react\": \"^18.3.0\",\n \"react-dom\": \"^18.3.0\",\n \"react-markdown\": \"^9.0.0\",\n \"markmap-lib\": \"^0.17.0\",\n \"markmap-view\": \"^0.17.0\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.5.0\",\n \"@types/node\": \"^22.0.0\",\n \"@types/react\": \"^18.3.0\",\n \"@types/react-dom\": \"^18.3.0\",\n \"tailwindcss\": \"^3.4.0\",\n \"postcss\": \"^8.4.0\",\n \"autoprefixer\": \"^10.4.0\"\n }\n}\n"}}, "run_demo_pdf.py": {"04250c62f5bed6c95a45a57cf22cd9fc1175b4c3": {"t": 1785599233.2380724, "n": 5699, "h": "7b04c6e46509690f", "c": "\"\"\"\nNoteKing Pro Demo — 为 OpenClaw 圆桌会议生成 LaTeX PDF 精美图文讲义\n复用已有的 ASR 转录结果 + 提取视频关键帧 + MiniMax M2.7 生成讲义内容\n\"\"\"\nimport sys\nimport re\nimport json\nimport time\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent))\n\nVIDEO_PATH = Path(\"/Users/daishanghao/Desktop/20260405_会议视频录音/OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持 - 001 - OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持.mp4\")\nOUTPUT_DIR = Path(__file__).parent / \"demos\" / \"openclaw_meeting\"\nFRAMES_DIR = OUTPUT_DIR / \"frames\"\nLATEX_BUILD = OUTPUT_DIR / \"latex_build\"\nMINIMAX_KEY = \"sk-cp-vX4T-YhmjkytkOexcwZ-uAdmALWR8ggXmtGOymuJQ1lfNLOR1phT0Ju09VggOTENL-y1pGe-KC4fTQppbzn_X_WPxVIApwG71PlvZHCGgfaIRH2zYoAI_RA\"\n\nFRAMES_DIR.mkdir(parents=True, exist_ok=True)\nLATEX_BUILD.mkdir(parents=True, exist_ok=True)\n\nprint(\"=\" * 60)\nprint(\"NoteKing Pro — 生成 LaTeX PDF 精美图文讲义\")\nprint(\"=\" * 60)\n\n# ── Step 1: 提取视频关键帧 ────────────────────────────────────────\nprint(\"\\n[1/4] 提取视频关键帧...\")\nfrom core.pdf_engine import SmartFrameExtractor, ScoredFrame, SubtitleFrameAligner\n\nextractor = SmartFrameExtractor(max_frames=12, min_interval=60.0)\nframes = extractor.extract(VIDEO_PATH, FRAMES_DIR)\nprint(f\" ✓ 提取 {len(frames)} 张关键帧\")\nfor i, f in enumerate(frames):\n print(f\" Fig.{i+1}: {f.time_str} (score={f.total_score:.2f})\")\n\n# ── Step 2: 加载转录结果并对齐帧 ──────────────────────────────────\nprint(\"\\n[2/4] 加载转录并对齐字幕...\")\nwith open(OUTPUT_DIR / \"transcript.json\", encoding=\"utf-8\") as f:\n transcript_data = json.load(f)\nfull_text = transcript_data[\"full_text\"]\nsegments = transcript_data[\"segments\"]\n\naligner = SubtitleFrameAligner(tolerance=30.0)\nframes = aligner.align(frames, segments)\nfor i, f in enumerate(frames):\n print(f\" Fig.{i+1} [{f.time_str}]: {f.subtitle_text[:50]}...\" if f.subtitle_text else f\" Fig.{i+1} [{f.time_str}]: (无字幕)\")\n\n# ── Step 3: 用 MiniMax 生成图文讲义内容 ──────────────────────────\nprint(\"\\n[3/4] 调用 MiniMax M2.7 生成图文讲义...\")\nfrom openai import OpenAI\nimport httpx\n\nclient = OpenAI(\n api_key=MINIMAX_KEY,\n base_url=\"https://api.minimax.chat/v1\",\n timeout=httpx.Timeout(300.0, connect=60.0),\n max_retries=3,\n)\n\nframes_desc = \"\\n\".join(\n f\" Fig.{i+1} at {f.time_str}: {f.subtitle_text[:80] or '(会议画面)'}\"\n for i, f in enumerate(frames)\n)\n\nMAX_CHARS = 35000\ntranscript_for_llm = full_text[:MAX_CHARS]\n\nprompt = f\"\"\"你是专业教育内容专家。请根据以下会议转录文字和关键帧信息,生成一份详细的中文图文讲义。\n\n会议信息:\n- 标题:OpenClaw与AI开源圆桌会议\n- 主持人:杨植麟(月之暗面/Kimi创始人)\n- 参与者:AI领域多位专家\n- 时长:约35分钟\n- 来源:Bilibili BV1GX9pB9E6N\n\n可用关键帧(共{len(frames)}张):\n{frames_desc}\n\n转录内容:\n{transcript_for_llm}\n\n格式要求:\n1. 用 ## 作为大节标题,### 作为子节标题\n2. 在适当位置插入 {{IMAGE:N}} 标记来引用关键帧(如 {{IMAGE:1}} 到 {{IMAGE:{len(frames)}}})\n3. 每张图片下方要有描述性文字说明\n4. 使用高亮框标记重要概念:\n {{IMPORTANT}}重点内容{{/IMPORTANT}}\n {{KNOWLEDGE}}背景知识补充{{/KNOWLEDGE}}\n {{WARNING}}注意事项{{/WARNING}}\n5. 适当使用表格对比不同观点\n6. 全部使用中文\n\n内容要求:\n- 按会议议题分节,结构清晰\n- 每节开头有要点概述\n- 引用嘉宾原话时使用引用格式\n- 对关键概念(OpenClaw、Agent、MCP等)进行解释\n- 包含技术要点和行业洞察\n- 末尾添加总结部分和学习要点回顾\n- 适合存档学习和分享的高质量讲义\n\n请直接输出讲义内容,不要输出其他说明。\"\"\"\n\nsystem = \"你是专业的技术教育内容创作者。直接输出结构化的Markdown讲义内容,使用指定的标记格式。不要使用think标签。\"\n\nprint(\" 请求 MiniMax M2.7...\")\nresp = client.chat.completions.create(\n model=\"MiniMax-M2.7\",\n messages=[\n {\"role\": \"system\", \"content\": system},\n {\"role\": \"user\", \"content\": prompt},\n ],\n max_tokens=6000,\n temperature=0.2,\n)\nnotes_md = resp.choices[0].message.content or \"\"\nnotes_md = re.sub(r\"<think>.*?</think>\\s*\", \"\", notes_md, flags=re.DOTALL).strip()\nprint(f\" ✓ 讲义内容生成完成 ({len(notes_md)} 字)\")\n\n# ── Step 4: 编译 LaTeX PDF ────────────────────────────────────────\nprint(\"\\n[4/4] 编译 LaTeX PDF...\")\nfrom core.pdf_engine import LaTeXNoteBuilder\n\nlatex_builder = LaTeXNoteBuilder()\ntitle = \"OpenClaw与AI开源圆桌会议 图文讲义\"\nmeta = {\n \"uploader\": \"杨植麟(月之暗面/Kimi创始人)\",\n \"duration\": \"35m42s\",\n \"url\": \"https://www.bilibili.com/video/BV1GX9pB9E6N/\",\n}\n\ntex_content = latex_builder.build_tex(\n notes_md=notes_md,\n frames=frames,\n title=title,\n meta=meta,\n)\n\ntex_path = LATEX_BUILD / \"meeting_notes.tex\"\ntex_path.write_text(tex_content, encoding=\"utf-8\")\nprint(f\" ✓ LaTeX 源文件: {tex_path.name}\")\n\npdf_result = latex_builder.compile_pdf(tex_path, LATEX_BUILD)\n\nif pdf_result and pdf_result.exists():\n import shutil\n final_pdf = OUTPUT_DIR / \"OpenClaw圆桌会议_图文讲义.pdf\"\n shutil.copy2(pdf_result, final_pdf)\n print(f\" ✓ PDF 编译成功: {final_pdf.name} ({final_pdf.stat().st_size // 1024} KB)\")\n\n notes_md_path = OUTPUT_DIR / \"图文讲义.md\"\n notes_md_path.write_text(f\"# {title}\\n\\n{notes_md}\", encoding=\"utf-8\")\n print(f\" ✓ 讲义Markdown: {notes_md_path.name}\")\nelse:\n print(\" ⚠ LaTeX 编译失败,尝试 HTML → PDF...\")\n from core.pdf_engine import HTMLNoteBuilder\n html_builder = HTMLNoteBuilder()\n html_content = html_builder.build_html(\n notes_md=notes_md,\n frames=frames,\n title=title,\n meta=meta,\n cover_path=frames[0].path if frames else None,\n )\n html_path = OUTPUT_DIR / \"OpenClaw圆桌会议_图文讲义.html\"\n html_path.write_text(html_content, encoding=\"utf-8\")\n print(f\" ✓ HTML 讲义: {html_path.name}\")\n\n final_pdf = OUTPUT_DIR / \"OpenClaw圆桌会议_图文讲义.pdf\"\n ok = HTMLNoteBuilder.html_to_pdf(html_path, final_pdf)\n if ok:\n print(f\" ✓ PDF (via Chrome): {final_pdf.name} ({final_pdf.stat().st_size // 1024} KB)\")\n else:\n print(\" ⚠ Chrome PDF 也失败了\")\n\n notes_md_path = OUTPUT_DIR / \"图文讲义.md\"\n notes_md_path.write_text(f\"# {title}\\n\\n{notes_md}\", encoding=\"utf-8\")\n print(f\" ✓ 讲义Markdown: {notes_md_path.name}\")\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"✅ LaTeX PDF 讲义生成完成!\")\nprint(f\"输出目录: {OUTPUT_DIR}\")\nfor f in sorted(OUTPUT_DIR.iterdir()):\n if f.is_file():\n size = f.stat().st_size\n label = f\"{size//1024}KB\" if size > 1024 else f\"{size}B\"\n print(f\" {f.name} ({label})\")\n"}, "fd366b09224e247ae64e886a34c2b45bf178a580": {"t": 1785674012.2835495, "n": 5699, "h": "7b04c6e46509690f", "c": "\"\"\"\nNoteKing Pro Demo — 为 OpenClaw 圆桌会议生成 LaTeX PDF 精美图文讲义\n复用已有的 ASR 转录结果 + 提取视频关键帧 + MiniMax M2.7 生成讲义内容\n\"\"\"\nimport sys\nimport re\nimport json\nimport time\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent))\n\nVIDEO_PATH = Path(\"/Users/daishanghao/Desktop/20260405_会议视频录音/OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持 - 001 - OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持.mp4\")\nOUTPUT_DIR = Path(__file__).parent / \"demos\" / \"openclaw_meeting\"\nFRAMES_DIR = OUTPUT_DIR / \"frames\"\nLATEX_BUILD = OUTPUT_DIR / \"latex_build\"\nMINIMAX_KEY = \"sk-cp-vX4T-YhmjkytkOexcwZ-uAdmALWR8ggXmtGOymuJQ1lfNLOR1phT0Ju09VggOTENL-y1pGe-KC4fTQppbzn_X_WPxVIApwG71PlvZHCGgfaIRH2zYoAI_RA\"\n\nFRAMES_DIR.mkdir(parents=True, exist_ok=True)\nLATEX_BUILD.mkdir(parents=True, exist_ok=True)\n\nprint(\"=\" * 60)\nprint(\"NoteKing Pro — 生成 LaTeX PDF 精美图文讲义\")\nprint(\"=\" * 60)\n\n# ── Step 1: 提取视频关键帧 ────────────────────────────────────────\nprint(\"\\n[1/4] 提取视频关键帧...\")\nfrom core.pdf_engine import SmartFrameExtractor, ScoredFrame, SubtitleFrameAligner\n\nextractor = SmartFrameExtractor(max_frames=12, min_interval=60.0)\nframes = extractor.extract(VIDEO_PATH, FRAMES_DIR)\nprint(f\" ✓ 提取 {len(frames)} 张关键帧\")\nfor i, f in enumerate(frames):\n print(f\" Fig.{i+1}: {f.time_str} (score={f.total_score:.2f})\")\n\n# ── Step 2: 加载转录结果并对齐帧 ──────────────────────────────────\nprint(\"\\n[2/4] 加载转录并对齐字幕...\")\nwith open(OUTPUT_DIR / \"transcript.json\", encoding=\"utf-8\") as f:\n transcript_data = json.load(f)\nfull_text = transcript_data[\"full_text\"]\nsegments = transcript_data[\"segments\"]\n\naligner = SubtitleFrameAligner(tolerance=30.0)\nframes = aligner.align(frames, segments)\nfor i, f in enumerate(frames):\n print(f\" Fig.{i+1} [{f.time_str}]: {f.subtitle_text[:50]}...\" if f.subtitle_text else f\" Fig.{i+1} [{f.time_str}]: (无字幕)\")\n\n# ── Step 3: 用 MiniMax 生成图文讲义内容 ──────────────────────────\nprint(\"\\n[3/4] 调用 MiniMax M2.7 生成图文讲义...\")\nfrom openai import OpenAI\nimport httpx\n\nclient = OpenAI(\n api_key=MINIMAX_KEY,\n base_url=\"https://api.minimax.chat/v1\",\n timeout=httpx.Timeout(300.0, connect=60.0),\n max_retries=3,\n)\n\nframes_desc = \"\\n\".join(\n f\" Fig.{i+1} at {f.time_str}: {f.subtitle_text[:80] or '(会议画面)'}\"\n for i, f in enumerate(frames)\n)\n\nMAX_CHARS = 35000\ntranscript_for_llm = full_text[:MAX_CHARS]\n\nprompt = f\"\"\"你是专业教育内容专家。请根据以下会议转录文字和关键帧信息,生成一份详细的中文图文讲义。\n\n会议信息:\n- 标题:OpenClaw与AI开源圆桌会议\n- 主持人:杨植麟(月之暗面/Kimi创始人)\n- 参与者:AI领域多位专家\n- 时长:约35分钟\n- 来源:Bilibili BV1GX9pB9E6N\n\n可用关键帧(共{len(frames)}张):\n{frames_desc}\n\n转录内容:\n{transcript_for_llm}\n\n格式要求:\n1. 用 ## 作为大节标题,### 作为子节标题\n2. 在适当位置插入 {{IMAGE:N}} 标记来引用关键帧(如 {{IMAGE:1}} 到 {{IMAGE:{len(frames)}}})\n3. 每张图片下方要有描述性文字说明\n4. 使用高亮框标记重要概念:\n {{IMPORTANT}}重点内容{{/IMPORTANT}}\n {{KNOWLEDGE}}背景知识补充{{/KNOWLEDGE}}\n {{WARNING}}注意事项{{/WARNING}}\n5. 适当使用表格对比不同观点\n6. 全部使用中文\n\n内容要求:\n- 按会议议题分节,结构清晰\n- 每节开头有要点概述\n- 引用嘉宾原话时使用引用格式\n- 对关键概念(OpenClaw、Agent、MCP等)进行解释\n- 包含技术要点和行业洞察\n- 末尾添加总结部分和学习要点回顾\n- 适合存档学习和分享的高质量讲义\n\n请直接输出讲义内容,不要输出其他说明。\"\"\"\n\nsystem = \"你是专业的技术教育内容创作者。直接输出结构化的Markdown讲义内容,使用指定的标记格式。不要使用think标签。\"\n\nprint(\" 请求 MiniMax M2.7...\")\nresp = client.chat.completions.create(\n model=\"MiniMax-M2.7\",\n messages=[\n {\"role\": \"system\", \"content\": system},\n {\"role\": \"user\", \"content\": prompt},\n ],\n max_tokens=6000,\n temperature=0.2,\n)\nnotes_md = resp.choices[0].message.content or \"\"\nnotes_md = re.sub(r\"<think>.*?</think>\\s*\", \"\", notes_md, flags=re.DOTALL).strip()\nprint(f\" ✓ 讲义内容生成完成 ({len(notes_md)} 字)\")\n\n# ── Step 4: 编译 LaTeX PDF ────────────────────────────────────────\nprint(\"\\n[4/4] 编译 LaTeX PDF...\")\nfrom core.pdf_engine import LaTeXNoteBuilder\n\nlatex_builder = LaTeXNoteBuilder()\ntitle = \"OpenClaw与AI开源圆桌会议 图文讲义\"\nmeta = {\n \"uploader\": \"杨植麟(月之暗面/Kimi创始人)\",\n \"duration\": \"35m42s\",\n \"url\": \"https://www.bilibili.com/video/BV1GX9pB9E6N/\",\n}\n\ntex_content = latex_builder.build_tex(\n notes_md=notes_md,\n frames=frames,\n title=title,\n meta=meta,\n)\n\ntex_path = LATEX_BUILD / \"meeting_notes.tex\"\ntex_path.write_text(tex_content, encoding=\"utf-8\")\nprint(f\" ✓ LaTeX 源文件: {tex_path.name}\")\n\npdf_result = latex_builder.compile_pdf(tex_path, LATEX_BUILD)\n\nif pdf_result and pdf_result.exists():\n import shutil\n final_pdf = OUTPUT_DIR / \"OpenClaw圆桌会议_图文讲义.pdf\"\n shutil.copy2(pdf_result, final_pdf)\n print(f\" ✓ PDF 编译成功: {final_pdf.name} ({final_pdf.stat().st_size // 1024} KB)\")\n\n notes_md_path = OUTPUT_DIR / \"图文讲义.md\"\n notes_md_path.write_text(f\"# {title}\\n\\n{notes_md}\", encoding=\"utf-8\")\n print(f\" ✓ 讲义Markdown: {notes_md_path.name}\")\nelse:\n print(\" ⚠ LaTeX 编译失败,尝试 HTML → PDF...\")\n from core.pdf_engine import HTMLNoteBuilder\n html_builder = HTMLNoteBuilder()\n html_content = html_builder.build_html(\n notes_md=notes_md,\n frames=frames,\n title=title,\n meta=meta,\n cover_path=frames[0].path if frames else None,\n )\n html_path = OUTPUT_DIR / \"OpenClaw圆桌会议_图文讲义.html\"\n html_path.write_text(html_content, encoding=\"utf-8\")\n print(f\" ✓ HTML 讲义: {html_path.name}\")\n\n final_pdf = OUTPUT_DIR / \"OpenClaw圆桌会议_图文讲义.pdf\"\n ok = HTMLNoteBuilder.html_to_pdf(html_path, final_pdf)\n if ok:\n print(f\" ✓ PDF (via Chrome): {final_pdf.name} ({final_pdf.stat().st_size // 1024} KB)\")\n else:\n print(\" ⚠ Chrome PDF 也失败了\")\n\n notes_md_path = OUTPUT_DIR / \"图文讲义.md\"\n notes_md_path.write_text(f\"# {title}\\n\\n{notes_md}\", encoding=\"utf-8\")\n print(f\" ✓ 讲义Markdown: {notes_md_path.name}\")\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"✅ LaTeX PDF 讲义生成完成!\")\nprint(f\"输出目录: {OUTPUT_DIR}\")\nfor f in sorted(OUTPUT_DIR.iterdir()):\n if f.is_file():\n size = f.stat().st_size\n label = f\"{size//1024}KB\" if size > 1024 else f\"{size}B\"\n print(f\" {f.name} ({label})\")\n"}}, "web/src/app/page.tsx": {"66dc16d9519474dd604e7824a3e58b5a0458871b": {"t": 1785599233.890576, "n": 27126, "h": "c185fee6cbd534b4", "c": "\"use client\";\n\nimport { useState, useEffect, useRef, useCallback } from \"react\";\nimport ReactMarkdown from \"react-markdown\";\n\nconst VIDEO_TEMPLATES = [\n { name: \"brief\", label: \"简要总结\", icon: \"⚡\" },\n { name: \"detailed\", label: \"详细学习笔记\", icon: \"📝\" },\n { name: \"mindmap\", label: \"思维导图\", icon: \"🧠\" },\n { name: \"flashcard\", label: \"闪卡 (Anki)\", icon: \"🃏\" },\n { name: \"quiz\", label: \"测验题\", icon: \"❓\" },\n { name: \"timeline\", label: \"时间线笔记\", icon: \"⏱️\" },\n { name: \"exam\", label: \"考试复习笔记\", icon: \"📚\" },\n { name: \"tutorial\", label: \"教程步骤\", icon: \"🛠️\" },\n { name: \"news\", label: \"新闻速览\", icon: \"📰\" },\n { name: \"podcast\", label: \"播客摘要\", icon: \"🎙️\" },\n { name: \"xhs_note\", label: \"小红书笔记\", icon: \"📕\" },\n { name: \"latex_pdf\", label: \"LaTeX PDF\", icon: \"📄\" },\n { name: \"custom\", label: \"自定义\", icon: \"✏️\" },\n];\n\nconst RECORDING_TEMPLATES = [\n { name: \"meeting_minutes\", label: \"会议纪要\", icon: \"📋\" },\n { name: \"lecture_notes\", label: \"课堂笔记\", icon: \"🎓\" },\n { name: \"interview\", label: \"访谈记录\", icon: \"🎤\" },\n { name: \"brainstorm\", label: \"灵感记录\", icon: \"💡\" },\n { name: \"news_digest\", label: \"新闻摘要\", icon: \"📰\" },\n { name: \"exam_prep\", label: \"考试复习\", icon: \"📝\" },\n { name: \"cornell_notes\", label: \"康奈尔笔记\", icon: \"📓\" },\n { name: \"podcast_shownotes\", label: \"播客笔记\", icon: \"🎙️\" },\n { name: \"entertainment\", label: \"娱乐内容\", icon: \"🎬\" },\n { name: \"smart_summary\", label: \"智能摘要\", icon: \"🤖\" },\n];\n\nconst ALL_TEMPLATES = [...VIDEO_TEMPLATES, ...RECORDING_TEMPLATES];\n\nconst SCENES = [\n { name: \"meeting\", label: \"会议\", icon: \"🤝\", desc: \"参会人、议题、决议、行动项\" },\n { name: \"lecture\", label: \"课堂\", icon: \"🎓\", desc: \"知识点、公式、习题\" },\n { name: \"interview\", label: \"访谈\", icon: \"🎤\", desc: \"Q&A、观点、立场\" },\n { name: \"brainstorm\", label: \"灵感\", icon: \"💡\", desc: \"想法、思维导图\" },\n { name: \"news\", label: \"新闻\", icon: \"📰\", desc: \"5W1H、引用、背景\" },\n { name: \"exam\", label: \"考试\", icon: \"📝\", desc: \"闪卡、模拟题\" },\n { name: \"entertainment\", label: \"娱乐\", icon: \"🎬\", desc: \"高光、金句\" },\n];\n\nconst API_BASE =\n typeof window !== \"undefined\"\n ? `${window.location.protocol}//${window.location.hostname}:8000`\n : \"http://api:8000\";\n\nconst HISTORY_KEY = \"noteking_history\";\nconst MAX_HISTORY = 50;\n\ntype Result = {\n title: string;\n content: string;\n template: string;\n source: string;\n platform: string;\n duration: number;\n num_speakers?: number;\n output_files?: Record<string, string>;\n};\n\ntype HistoryItem = Result & { id: string; url: string; timestamp: number; contentPreview: string };\n\nfunction loadHistory(): HistoryItem[] {\n if (typeof window === \"undefined\") return [];\n try { return JSON.parse(localStorage.getItem(HISTORY_KEY) || \"[]\"); } catch { return []; }\n}\nfunction saveHistory(items: HistoryItem[]) {\n localStorage.setItem(HISTORY_KEY, JSON.stringify(items.slice(0, MAX_HISTORY)));\n}\n\ntype TabMode = \"video\" | \"recording\";\n\nexport default function Home() {\n const [mode, setMode] = useState<TabMode>(\"video\");\n const [url, setUrl] = useState(\"\");\n const [template, setTemplate] = useState(\"detailed\");\n const [customPrompt, setCustomPrompt] = useState(\"\");\n const [loading, setLoading] = useState(false);\n const [result, setResult] = useState<Result | null>(null);\n const [error, setError] = useState(\"\");\n const [darkMode, setDarkMode] = useState(false);\n const [streamContent, setStreamContent] = useState(\"\");\n const [streamStage, setStreamStage] = useState(\"\");\n const [streamTitle, setStreamTitle] = useState(\"\");\n const [remaining, setRemaining] = useState<number | null>(null);\n const [showHistory, setShowHistory] = useState(false);\n const [history, setHistory] = useState<HistoryItem[]>([]);\n const [copied, setCopied] = useState(false);\n const abortRef = useRef<AbortController | null>(null);\n\n // Recording mode state\n const [uploadFile, setUploadFile] = useState<File | null>(null);\n const [scene, setScene] = useState(\"meeting\");\n const [context, setContext] = useState(\"\");\n const [numSpeakers, setNumSpeakers] = useState<number | undefined>();\n const [denoiseLevel, setDenoiseLevel] = useState(1);\n const [outputFormats, setOutputFormats] = useState([\"markdown\"]);\n const [dragOver, setDragOver] = useState(false);\n const fileInputRef = useRef<HTMLInputElement>(null);\n\n useEffect(() => { setHistory(loadHistory()); }, []);\n\n useEffect(() => {\n if (mode === \"video\") setTemplate(\"detailed\");\n else setTemplate(\"meeting_minutes\");\n }, [mode]);\n\n const handleVideoSubmit = useCallback(async () => {\n if (!url.trim() || loading) return;\n setLoading(true); setError(\"\"); setResult(null);\n setStreamContent(\"\"); setStreamStage(\"正在连接...\"); setStreamTitle(\"\");\n abortRef.current = new AbortController();\n try {\n const resp = await fetch(`${API_BASE}/api/v1/summarize/stream`, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ url: url.trim(), template, custom_prompt: customPrompt }),\n signal: abortRef.current.signal,\n });\n if (resp.status === 429) {\n const e = await resp.json().catch(() => ({}));\n setRemaining(0); throw new Error(e.detail || \"今日免费次数已用完\");\n }\n if (!resp.ok) { const e = await resp.json().catch(() => ({})); throw new Error(e.detail || `HTTP ${resp.status}`); }\n const lh = resp.headers.get(\"X-RateLimit-Remaining\");\n if (lh !== null) setRemaining(parseInt(lh, 10));\n const reader = resp.body?.getReader();\n if (!reader) throw new Error(\"No response body\");\n const decoder = new TextDecoder(); let buffer = \"\"; let accumulated = \"\";\n while (true) {\n const { done, value } = await reader.read(); if (done) break;\n buffer += decoder.decode(value, { stream: true });\n const lines = buffer.split(\"\\n\"); buffer = lines.pop() || \"\";\n for (const line of lines) {\n if (!line.startsWith(\"data: \")) continue;\n try {\n const ev = JSON.parse(line.slice(6).trim());\n if (ev.stage === \"info\") { setStreamStage(ev.message || \"\"); if (ev.title) setStreamTitle(ev.title); }\n else if (ev.stage === \"subtitle\") setStreamStage(ev.message || \"提取字幕...\");\n else if (ev.stage === \"generating\") { setStreamStage(\"AI 正在生成笔记...\"); if (ev.content) { accumulated += ev.content; setStreamContent(accumulated); } }\n else if (ev.stage === \"done\") {\n const r: Result = { title: ev.title || streamTitle, content: ev.content || accumulated, template: ev.template || template, source: ev.source || \"\", platform: ev.platform || \"\", duration: ev.duration || 0 };\n setResult(r);\n const items = loadHistory();\n items.unshift({ ...r, id: Date.now().toString(36), url: url.trim(), timestamp: Date.now(), contentPreview: r.content.slice(0, 120).replace(/\\n/g, \" \") });\n saveHistory(items); setHistory(loadHistory());\n }\n else if (ev.stage === \"error\") throw new Error(ev.message);\n } catch (pe: any) { if (pe.message && !pe.message.includes(\"JSON\")) throw pe; }\n }\n }\n } catch (e: any) { if (e.name !== \"AbortError\") setError(e.message || \"生成失败\"); }\n finally { setLoading(false); setStreamStage(\"\"); abortRef.current = null; }\n }, [url, template, customPrompt, loading, streamTitle]);\n\n const handleRecordingSubmit = useCallback(async () => {\n if (!uploadFile || loading) return;\n setLoading(true); setError(\"\"); setResult(null);\n setStreamContent(\"\"); setStreamStage(\"上传文件...\");\n try {\n // Upload file\n const formData = new FormData();\n formData.append(\"file\", uploadFile);\n const uploadResp = await fetch(`${API_BASE}/api/v1/recording/upload`, { method: \"POST\", body: formData });\n if (!uploadResp.ok) throw new Error(\"上传失败\");\n const { file_id } = await uploadResp.json();\n\n // Process\n setStreamStage(\"处理中...\");\n const processForm = new FormData();\n processForm.append(\"file_id\", file_id);\n processForm.append(\"template\", template);\n if (context) processForm.append(\"context\", context);\n processForm.append(\"scene\", scene);\n if (numSpeakers) processForm.append(\"num_speakers\", String(numSpeakers));\n processForm.append(\"denoise_level\", String(denoiseLevel));\n processForm.append(\"output_formats\", outputFormats.join(\",\"));\n\n const resp = await fetch(`${API_BASE}/api/v1/recording/process`, { method: \"POST\", body: processForm });\n if (!resp.ok) { const e = await resp.json().catch(() => ({})); throw new Error(e.detail || \"处理失败\"); }\n const data = await resp.json();\n setResult({\n title: data.title || uploadFile.name,\n content: data.content || \"\",\n template: data.template || template,\n source: \"recording\",\n platform: \"local\",\n duration: data.duration || 0,\n num_speakers: data.num_speakers,\n output_files: data.output_files,\n });\n setStreamStage(\"完成!\");\n } catch (e: any) { setError(e.message || \"处理失败\"); }\n finally { setLoading(false); setStreamStage(\"\"); }\n }, [uploadFile, template, context, scene, numSpeakers, denoiseLevel, outputFormats, loading]);\n\n const handleDrop = (e: React.DragEvent) => {\n e.preventDefault(); setDragOver(false);\n const file = e.dataTransfer.files[0];\n if (file) setUploadFile(file);\n };\n\n const handleCancel = () => { abortRef.current?.abort(); setLoading(false); setStreamStage(\"\"); };\n\n const handleCopy = () => {\n const text = result?.content || streamContent;\n if (text) { navigator.clipboard.writeText(text); setCopied(true); setTimeout(() => setCopied(false), 2000); }\n };\n\n const handleDownload = () => {\n const content = result?.content; if (!content) return;\n const isLatex = template === \"latex_pdf\";\n const ext = isLatex ? \".tex\" : \".md\";\n const mime = isLatex ? \"application/x-tex\" : \"text/markdown\";\n const blob = new Blob([content], { type: mime });\n const a = document.createElement(\"a\"); a.href = URL.createObjectURL(blob);\n a.download = `${result?.title || \"notes\"}_${template}${ext}`; a.click();\n };\n\n const displayContent = result?.content || streamContent;\n const currentTemplates = mode === \"video\" ? VIDEO_TEMPLATES : RECORDING_TEMPLATES;\n\n return (\n <div className={darkMode ? \"dark\" : \"\"}>\n <div className=\"min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)]\">\n {/* Header */}\n <header className=\"border-b border-[var(--border)] bg-[var(--bg-secondary)]\">\n <div className=\"max-w-6xl mx-auto px-4 py-4 flex items-center justify-between\">\n <div className=\"flex items-center gap-3\">\n <span className=\"text-3xl\">👑</span>\n <div>\n <h1 className=\"text-xl font-bold\">NoteKing Pro 笔记之王</h1>\n <p className=\"text-xs text-[var(--text-secondary)]\">\n 全网最强视频/录音处理工具 | 23种模板 | 说话人分离 | 降噪增强\n </p>\n </div>\n </div>\n <div className=\"flex items-center gap-2\">\n {remaining !== null && (\n <span className=\"text-xs text-[var(--text-secondary)] px-2 py-1 rounded border border-[var(--border)]\">\n 今日剩余 {remaining} 次\n </span>\n )}\n <button onClick={() => { setShowHistory(!showHistory); setResult(null); }}\n className=\"px-3 py-1.5 rounded-lg border border-[var(--border)] hover:bg-[var(--bg-primary)] transition text-sm\">\n 📋 历史\n </button>\n <button onClick={() => setDarkMode(!darkMode)}\n className=\"px-3 py-1.5 rounded-lg border border-[var(--border)] hover:bg-[var(--bg-primary)] transition text-sm\">\n {darkMode ? \"☀️\" : \"🌙\"}\n </button>\n </div>\n </div>\n </header>\n\n <main className=\"max-w-6xl mx-auto px-4 py-8\">\n {/* Mode Tabs */}\n <div className=\"flex gap-1 mb-6 bg-[var(--bg-secondary)] rounded-xl p-1 border border-[var(--border)] w-fit\">\n <button onClick={() => setMode(\"video\")}\n className={`px-5 py-2.5 rounded-lg text-sm font-medium transition ${mode === \"video\" ? \"bg-[var(--accent)] text-white\" : \"hover:bg-[var(--bg-primary)]\"}`}>\n 🌐 在线视频\n </button>\n <button onClick={() => setMode(\"recording\")}\n className={`px-5 py-2.5 rounded-lg text-sm font-medium transition ${mode === \"recording\" ? \"bg-[var(--accent)] text-white\" : \"hover:bg-[var(--bg-primary)]\"}`}>\n 🎙️ 本地录音/视频\n </button>\n </div>\n\n {/* Input Area */}\n <div className=\"bg-[var(--bg-secondary)] rounded-2xl p-6 border border-[var(--border)] mb-8\">\n {mode === \"video\" ? (\n /* Video URL Input */\n <div className=\"flex gap-3 mb-4\">\n <input type=\"text\" value={url} onChange={(e) => setUrl(e.target.value)}\n placeholder=\"粘贴视频链接... (B站、YouTube、抖音、小红书等)\"\n className=\"flex-1 px-4 py-3 rounded-xl bg-[var(--bg-primary)] border border-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)]\"\n onKeyDown={(e) => e.key === \"Enter\" && handleVideoSubmit()} />\n {loading ? (\n <button onClick={handleCancel} className=\"px-6 py-3 bg-red-500 text-white rounded-xl font-medium hover:opacity-90 transition whitespace-nowrap\">取消</button>\n ) : (\n <button onClick={handleVideoSubmit} disabled={!url.trim()}\n className=\"px-6 py-3 bg-[var(--accent)] text-white rounded-xl font-medium hover:opacity-90 transition disabled:opacity-50 whitespace-nowrap\">\n 生成笔记\n </button>\n )}\n </div>\n ) : (\n /* Recording Upload */\n <>\n <div className={`border-2 border-dashed rounded-xl p-8 text-center mb-4 transition cursor-pointer ${dragOver ? \"border-[var(--accent)] bg-[var(--accent)]/5\" : \"border-[var(--border)]\"}`}\n onDragOver={(e) => { e.preventDefault(); setDragOver(true); }}\n onDragLeave={() => setDragOver(false)}\n onDrop={handleDrop}\n onClick={() => fileInputRef.current?.click()}>\n <input ref={fileInputRef} type=\"file\" className=\"hidden\"\n accept=\"video/*,audio/*,.mp4,.mp3,.wav,.m4a,.flac,.mkv,.avi,.mov,.ogg,.aac\"\n onChange={(e) => e.target.files?.[0] && setUploadFile(e.target.files[0])} />\n {uploadFile ? (\n <div>\n <p className=\"text-lg font-medium\">📎 {uploadFile.name}</p>\n <p className=\"text-sm text-[var(--text-secondary)] mt-1\">\n {(uploadFile.size / 1024 / 1024).toFixed(1)} MB | 点击更换文件\n </p>\n </div>\n ) : (\n <div>\n <p className=\"text-lg mb-2\">🎙️ 拖放录音/视频文件到这里</p>\n <p className=\"text-sm text-[var(--text-secondary)]\">\n 支持 MP4, MP3, WAV, M4A, FLAC, MKV, AVI, MOV, OGG, AAC\n </p>\n </div>\n )}\n </div>\n\n {/* Scene Selection */}\n <div className=\"mb-4\">\n <label className=\"text-sm font-medium mb-2 block\">场景类型:</label>\n <div className=\"flex flex-wrap gap-2\">\n {SCENES.map((s) => (\n <button key={s.name} onClick={() => setScene(s.name)}\n className={`px-3 py-2 rounded-lg text-sm transition ${scene === s.name ? \"bg-[var(--accent)] text-white\" : \"bg-[var(--bg-primary)] border border-[var(--border)] hover:border-[var(--accent)]\"}`}>\n {s.icon} {s.label}\n <span className=\"text-xs opacity-70 ml-1\">({s.desc})</span>\n </button>\n ))}\n </div>\n </div>\n\n {/* Context */}\n <div className=\"mb-4\">\n <label className=\"text-sm font-medium mb-2 block\">内容描述 (可选):</label>\n <input type=\"text\" value={context} onChange={(e) => setContext(e.target.value)}\n placeholder=\"例如: AI开源项目圆桌讨论, 数学课第3讲, 产品周会...\"\n className=\"w-full px-4 py-2.5 rounded-xl bg-[var(--bg-primary)] border border-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)] text-sm\" />\n </div>\n\n {/* Advanced Options */}\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4 mb-4\">\n <div>\n <label className=\"text-sm font-medium mb-1 block\">说话人数量:</label>\n <input type=\"number\" min={1} max={20}\n value={numSpeakers || \"\"}\n onChange={(e) => setNumSpeakers(e.target.value ? parseInt(e.target.value) : undefined)}\n placeholder=\"自动检测\"\n className=\"w-full px-3 py-2 rounded-lg bg-[var(--bg-primary)] border border-[var(--border)] text-sm\" />\n </div>\n <div>\n <label className=\"text-sm font-medium mb-1 block\">降噪强度:</label>\n <select value={denoiseLevel} onChange={(e) => setDenoiseLevel(parseInt(e.target.value))}\n className=\"w-full px-3 py-2 rounded-lg bg-[var(--bg-primary)] border border-[var(--border)] text-sm\">\n <option value={0}>无降噪</option>\n <option value={1}>轻度 (推荐)</option>\n <option value={2}>中度</option>\n <option value={3}>重度 (嘈杂环境)</option>\n </select>\n </div>\n <div>\n <label className=\"text-sm font-medium mb-1 block\">输出格式:</label>\n <div className=\"flex flex-wrap gap-1\">\n {[\"markdown\", \"srt\", \"vtt\", \"json\"].map(fmt => (\n <button key={fmt} onClick={() => {\n setOutputFormats(prev => prev.includes(fmt) ? prev.filter(f => f !== fmt) : [...prev, fmt]);\n }}\n className={`px-2 py-1 rounded text-xs ${outputFormats.includes(fmt) ? \"bg-[var(--accent)] text-white\" : \"bg-[var(--bg-primary)] border border-[var(--border)]\"}`}>\n {fmt}\n </button>\n ))}\n </div>\n </div>\n </div>\n\n <button onClick={handleRecordingSubmit} disabled={!uploadFile || loading}\n className=\"w-full py-3 bg-[var(--accent)] text-white rounded-xl font-medium hover:opacity-90 transition disabled:opacity-50\">\n {loading ? \"处理中...\" : \"开始处理\"}\n </button>\n </>\n )}\n\n {/* Template Selection */}\n <div className=\"mt-4\">\n <div className=\"flex flex-wrap gap-2\">\n {currentTemplates.map((t) => (\n <button key={t.name} onClick={() => setTemplate(t.name)}\n className={`px-3 py-1.5 rounded-lg text-sm transition ${template === t.name ? \"bg-[var(--accent)] text-white\" : \"bg-[var(--bg-primary)] border border-[var(--border)] hover:border-[var(--accent)]\"}`}>\n {t.icon} {t.label}\n </button>\n ))}\n </div>\n </div>\n\n {template === \"custom\" && (\n <textarea value={customPrompt} onChange={(e) => setCustomPrompt(e.target.value)}\n placeholder=\"输入自定义 Prompt...\"\n className=\"w-full mt-3 px-4 py-3 rounded-xl bg-[var(--bg-primary)] border border-[var(--border)] focus:outline-none focus:ring-2 focus:ring-[var(--accent)] min-h-[80px] text-sm\" />\n )}\n </div>\n\n {/* Error */}\n {error && (\n <div className=\"bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-300 rounded-xl p-4 mb-6\">\n {error}\n </div>\n )}\n\n {/* Streaming progress */}\n {loading && (\n <div className=\"bg-[var(--bg-secondary)] rounded-2xl border border-[var(--border)] mb-6\">\n <div className=\"px-6 py-4 border-b border-[var(--border)] flex items-center gap-3\">\n <div className=\"w-5 h-5 border-2 border-[var(--accent)] border-t-transparent rounded-full animate-spin\" />\n <span className=\"text-sm font-medium\">{streamStage}</span>\n {streamTitle && <span className=\"text-xs text-[var(--text-secondary)] ml-auto truncate max-w-[300px]\">{streamTitle}</span>}\n </div>\n {streamContent && (\n <div className=\"p-6 note-content prose prose-slate dark:prose-invert max-w-none max-h-[60vh] overflow-y-auto\">\n <ReactMarkdown>{streamContent}</ReactMarkdown>\n </div>\n )}\n </div>\n )}\n\n {/* Result */}\n {result && !loading && (\n <div className=\"bg-[var(--bg-secondary)] rounded-2xl border border-[var(--border)]\">\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-[var(--border)]\">\n <div>\n <h2 className=\"font-semibold text-lg\">{result.title}</h2>\n <div className=\"flex gap-3 text-xs text-[var(--text-secondary)] mt-1\">\n <span>来源: {result.source}</span>\n {result.platform && <span>平台: {result.platform}</span>}\n {result.duration > 0 && <span>时长: {Math.round(result.duration / 60)} 分钟</span>}\n {result.num_speakers && <span>说话人: {result.num_speakers}</span>}\n </div>\n </div>\n <div className=\"flex gap-2\">\n <button onClick={handleCopy}\n className=\"px-3 py-1.5 rounded-lg border border-[var(--border)] hover:bg-[var(--bg-primary)] transition text-sm\">\n {copied ? \"已复制!\" : \"复制\"}\n </button>\n <button onClick={handleDownload}\n className=\"px-3 py-1.5 rounded-lg border border-[var(--border)] hover:bg-[var(--bg-primary)] transition text-sm\">\n 下载 {template === \"latex_pdf\" ? \".tex\" : \".md\"}\n </button>\n </div>\n </div>\n <div className=\"p-6 note-content prose prose-slate dark:prose-invert max-w-none\">\n <ReactMarkdown>{result.content}</ReactMarkdown>\n </div>\n </div>\n )}\n\n {/* History */}\n {showHistory && !loading && !result && (\n <div className=\"bg-[var(--bg-secondary)] rounded-2xl border border-[var(--border)]\">\n <div className=\"flex items-center justify-between px-6 py-4 border-b border-[var(--border)]\">\n <h2 className=\"font-semibold\">历史记录 ({history.length})</h2>\n {history.length > 0 && (\n <button onClick={() => { localStorage.removeItem(HISTORY_KEY); setHistory([]); }}\n className=\"text-xs text-red-500 hover:underline\">清空</button>\n )}\n </div>\n {history.length === 0 ? (\n <div className=\"p-8 text-center text-[var(--text-secondary)]\">暂无记录</div>\n ) : (\n <div className=\"divide-y divide-[var(--border)]\">\n {history.map((item) => (\n <button key={item.id} onClick={() => { setResult(item); setTemplate(item.template); setShowHistory(false); }}\n className=\"w-full text-left px-6 py-4 hover:bg-[var(--bg-primary)] transition\">\n <div className=\"flex items-center justify-between mb-1\">\n <span className=\"font-medium text-sm truncate max-w-[70%]\">{item.title}</span>\n <span className=\"text-xs text-[var(--text-secondary)]\">\n {new Date(item.timestamp).toLocaleDateString(\"zh-CN\", { month: \"short\", day: \"numeric\", hour: \"2-digit\", minute: \"2-digit\" })}\n </span>\n </div>\n <p className=\"text-xs text-[var(--text-secondary)] line-clamp-1\">{item.contentPreview}</p>\n </button>\n ))}\n </div>\n )}\n </div>\n )}\n\n {/* Features */}\n {!result && !loading && !showHistory && (\n <div className=\"grid grid-cols-1 md:grid-cols-3 gap-4 mt-8\">\n <FC icon=\"🌐\" title=\"30+ 平台\" desc=\"B站、YouTube、抖音、小红书、TikTok 等\" />\n <FC icon=\"📋\" title=\"23 种模板\" desc=\"笔记、会议纪要、思维导图、闪卡、考试复习等\" />\n <FC icon=\"🎙️\" title=\"录音处理\" desc=\"本地视频/录音文件上传,支持多文件合并\" />\n <FC icon=\"🗣️\" title=\"说话人分离\" desc=\"自动识别多人对话,标注谁说了什么\" />\n <FC icon=\"🔇\" title=\"降噪增强\" desc=\"三级降噪,嘈杂环境也能清晰转录\" />\n <FC icon=\"📄\" title=\"LaTeX PDF\" desc=\"自动生成带目录、公式的精美 PDF 讲义\" />\n <FC icon=\"🌍\" title=\"多语言\" desc=\"中英混合、50+ 语言自动检测\" />\n <FC icon=\"📦\" title=\"批量处理\" desc=\"支持整个播放列表和多段录音合并\" />\n <FC icon=\"⚡\" title=\"多格式输出\" desc=\"Markdown、SRT字幕、思维导图、JSON等\" />\n </div>\n )}\n </main>\n\n <footer className=\"border-t border-[var(--border)] mt-16\">\n <div className=\"max-w-6xl mx-auto px-4 py-6 flex flex-col md:flex-row items-center justify-between gap-4\">\n <div className=\"text-sm text-[var(--text-secondary)]\">\n NoteKing Pro 笔记之王 - 全网最强视频/录音处理工具 | 开源免费\n </div>\n <a href=\"https://github.com/bcefghj/noteking\" target=\"_blank\"\n className=\"text-[var(--accent)] hover:underline flex items-center gap-1 text-sm\">\n <svg className=\"w-4 h-4\" viewBox=\"0 0 16 16\" fill=\"currentColor\"><path d=\"M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z\"/></svg>\n GitHub\n </a>\n </div>\n </footer>\n </div>\n </div>\n );\n}\n\nfunction FC({ icon, title, desc }: { icon: string; title: string; desc: string }) {\n return (\n <div className=\"bg-[var(--bg-secondary)] rounded-xl p-5 border border-[var(--border)]\">\n <span className=\"text-2xl\">{icon}</span>\n <h3 className=\"font-semibold mt-2\">{title}</h3>\n <p className=\"text-sm text-[var(--text-secondary)] mt-1\">{desc}</p>\n </div>\n );\n}\n"}}, "core/templates/xhs_note.py": {"a94ff73bc314b1524379097c9323ef112cdb89a5": {"t": 1785599235.6374118, "n": 910, "h": "c4bbb5882dd5d330", "c": "\"\"\"Xiaohongshu (Little Red Book) note format template.\"\"\"\n\nfrom .base import BaseTemplate, TemplateContext, _truncate_transcript\n\n\nclass XHSNoteTemplate(BaseTemplate):\n name = \"xhs_note\"\n display_name = \"小红书笔记\"\n description = \"Xiaohongshu-style note with emojis, tags, and engagement hooks\"\n\n def build_prompt(self, ctx: TemplateContext) -> str:\n transcript = _truncate_transcript(ctx.transcript)\n return f\"\"\"请根据以下视频内容,生成一篇小红书风格的笔记。\n\n视频标题: {ctx.meta.title}\n作者: {ctx.meta.uploader}\n\n视频内容转录:\n{transcript}\n\n要求:\n1. 标题: 20 字以内,使用「痛点标题法」,加 2-4 个相关 emoji\n2. 正文: 600-800 字\n - 开头用一句话引起共鸣\n - 用 emoji 引导每个段落\n - 内容分 3-5 个要点展开\n - 加入 2-3 个互动提示 (如 \"你觉得呢?\" \"评论区聊聊\")\n - 语气亲切自然,像朋友分享\n3. 结尾:\n - 总结一句话收获\n - 引导收藏/点赞\n4. 标签: 生成 5-10 个相关标签,格式为 #标签名\n5. 整体风格: 实用干货 + 亲和力 + 高信息密度\n\n示例格式:\n---\n[emoji] [标题]\n\n[emoji] 第一个要点\n内容...\n\n[emoji] 第二个要点\n内容...\n\n💬 你觉得呢?评论区聊聊~\n\n#标签1 #标签2 #标签3\n---\n\n请直接输出小红书笔记:\"\"\"\n"}}, "mcp/package.json": {"80758a6f0ff42d3158e6bfd66dec962aaae77bb4": {"t": 1785599236.3696592, "n": 597, "h": "c5279378b45a326a", "c": "{\n \"name\": \"@noteking/mcp-server\",\n \"version\": \"0.1.0\",\n \"description\": \"NoteKing MCP Server - Video/blog to notes with 13 templates, 30+ platforms\",\n \"main\": \"dist/index.js\",\n \"type\": \"module\",\n \"scripts\": {\n \"build\": \"tsc\",\n \"start\": \"node dist/index.js\",\n \"dev\": \"tsx src/index.ts\"\n },\n \"keywords\": [\"mcp\", \"video\", \"notes\", \"bilibili\", \"youtube\", \"summary\"],\n \"license\": \"MIT\",\n \"dependencies\": {\n \"@modelcontextprotocol/sdk\": \"^1.0.0\",\n \"zod\": \"^3.23.0\"\n },\n \"devDependencies\": {\n \"typescript\": \"^5.5.0\",\n \"tsx\": \"^4.16.0\",\n \"@types/node\": \"^22.0.0\"\n }\n}\n"}}, "docker-compose.yml": {"a161206e93d445c55e6b7c24e84e841901d2752a": {"t": 1785599236.6380732, "n": 700, "h": "a61263298ff5f252", "c": "version: \"3.9\"\n\nservices:\n web:\n build: .\n ports:\n - \"8000:8000\"\n environment:\n - NOTEKING_LLM_API_KEY=${NOTEKING_LLM_API_KEY}\n - NOTEKING_LLM_BASE_URL=${NOTEKING_LLM_BASE_URL:-}\n - NOTEKING_LLM_MODEL=${NOTEKING_LLM_MODEL:-gpt-4o-mini}\n - NOTEKING_PROXY=${NOTEKING_PROXY:-}\n - BILIBILI_SESSDATA=${BILIBILI_SESSDATA:-}\n - HF_TOKEN=${HF_TOKEN:-}\n volumes:\n - noteking_output:/app/noteking_output\n - noteking_cache:/root/.noteking\n restart: unless-stopped\n healthcheck:\n test: [\"CMD\", \"curl\", \"-f\", \"http://localhost:8000/health\"]\n interval: 30s\n timeout: 10s\n retries: 3\n\nvolumes:\n noteking_output:\n noteking_cache:\n"}}, "run_demo.py": {"5188bfcbac5a5197d0e13215923314e01d28955d": {"t": 1785599236.7509167, "n": 8640, "h": "b6894df50f22c6e6", "c": "\"\"\"\nNoteKing Pro Demo — 处理 OpenClaw 圆桌会议视频\n使用 MiniMax M2.7 生成会议纪要、思维导图、闪卡等多种格式\n\"\"\"\nimport sys\nimport os\nimport json\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent))\n\n# ── 配置 ──────────────────────────────────────────────────────────────\nVIDEO_PATH = Path(\"/Users/daishanghao/Desktop/20260405_会议视频录音/OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持 - 001 - OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持.mp4\")\nOUTPUT_DIR = Path(__file__).parent / \"demos\" / \"openclaw_meeting\"\nMINIMAX_KEY = \"sk-cp-vX4T-YhmjkytkOexcwZ-uAdmALWR8ggXmtGOymuJQ1lfNLOR1phT0Ju09VggOTENL-y1pGe-KC4fTQppbzn_X_WPxVIApwG71PlvZHCGgfaIRH2zYoAI_RA\"\nMINIMAX_BASE_URL = \"https://api.minimax.chat/v1\"\nMINIMAX_MODEL = \"MiniMax-M2.7\"\n\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# ── 配置对象 ───────────────────────────────────────────────────────────\nfrom core.config import AppConfig, LLMConfig, ASRConfig, RecordingConfig\n\nconfig = AppConfig(\n llm=LLMConfig(\n api_key=MINIMAX_KEY,\n base_url=MINIMAX_BASE_URL,\n model=MINIMAX_MODEL,\n temperature=0.3,\n max_tokens=8000,\n language=\"zh-CN\",\n ),\n asr=ASRConfig(\n default_engine=\"faster_whisper\",\n faster_whisper_model=\"base\",\n ),\n recording=RecordingConfig(\n denoise_level=1,\n enable_diarization=False,\n default_scene=\"meeting\",\n ),\n output_dir=str(OUTPUT_DIR),\n)\n\nprint(\"=\" * 60)\nprint(\"NoteKing Pro — OpenClaw 圆桌会议处理\")\nprint(\"=\" * 60)\n\n# ── Step 1: 提取音频 ──────────────────────────────────────────────────\nprint(\"\\n[1/5] 提取音频...\")\nimport subprocess\naudio_path = OUTPUT_DIR / \"meeting_audio.wav\"\nif not audio_path.exists():\n result = subprocess.run([\n \"ffmpeg\", \"-i\", str(VIDEO_PATH),\n \"-vn\", \"-acodec\", \"pcm_s16le\",\n \"-ar\", \"16000\", \"-ac\", \"1\",\n \"-y\", str(audio_path)\n ], capture_output=True, text=True)\n if result.returncode != 0:\n print(f\" 音频提取失败: {result.stderr[:200]}\")\n sys.exit(1)\n print(f\" ✓ 音频已提取: {audio_path.name}\")\nelse:\n print(f\" ✓ 使用缓存音频: {audio_path.name}\")\n\n# ── Step 2: ASR 转录 ──────────────────────────────────────────────────\nprint(\"\\n[2/5] ASR 语音转文字 (faster-whisper)...\")\ntranscript_cache = OUTPUT_DIR / \"transcript.json\"\nif transcript_cache.exists():\n print(\" ✓ 使用缓存转录结果\")\n import json\n with open(transcript_cache) as f:\n transcript_data = json.load(f)\n full_text = transcript_data[\"full_text\"]\n segments = transcript_data[\"segments\"]\nelse:\n from core.transcriber import transcribe\n result = transcribe(audio_path, config)\n full_text = result.full_text\n segments = [{\"start\": s.start, \"end\": s.end, \"text\": s.text} for s in result.segments]\n with open(transcript_cache, \"w\", encoding=\"utf-8\") as f:\n json.dump({\"full_text\": full_text, \"segments\": segments, \"language\": result.language}, f, ensure_ascii=False, indent=2)\n print(f\" ✓ 转录完成,共 {len(segments)} 段,{len(full_text)} 字符\")\n\nprint(f\" 预览: {full_text[:200]}...\")\n\n# ── Step 3: 生成 SRT 字幕文件 ─────────────────────────────────────────\nprint(\"\\n[3/5] 生成 SRT 字幕...\")\nsrt_path = OUTPUT_DIR / \"meeting.srt\"\ndef sec_to_srt(t):\n h = int(t // 3600)\n m = int((t % 3600) // 60)\n s = int(t % 60)\n ms = int((t - int(t)) * 1000)\n return f\"{h:02d}:{m:02d}:{s:02d},{ms:03d}\"\n\nsrt_lines = []\nfor i, seg in enumerate(segments, 1):\n srt_lines.append(str(i))\n srt_lines.append(f\"{sec_to_srt(seg['start'])} --> {sec_to_srt(seg['end'])}\")\n srt_lines.append(seg['text'].strip())\n srt_lines.append(\"\")\nsrt_path.write_text(\"\\n\".join(srt_lines), encoding=\"utf-8\")\nprint(f\" ✓ SRT 字幕: {srt_path.name} ({len(segments)} 条)\")\n\n# ── Step 4: 用 MiniMax 生成多种输出 ──────────────────────────────────\nprint(\"\\n[4/5] 调用 MiniMax M2.7 生成内容...\")\nfrom core.llm import chat\n\n# 准备转录文本(控制长度在 token 限制内)\nMAX_CHARS = 40000\ntranscript_for_llm = full_text[:MAX_CHARS]\nif len(full_text) > MAX_CHARS:\n transcript_for_llm += f\"\\n\\n[注:原文共{len(full_text)}字,此处截取前{MAX_CHARS}字]\"\n\ncontext_info = \"\"\"\n会议名称:OpenClaw与AI开源圆桌会议\n主持人:杨植麟(月之暗面创始人)\n参与者:AI领域开源生态相关人士\n主题:OpenClaw平台与AI开源生态讨论\n来源:Bilibili视频 BV1GX9pB9E6N\n\"\"\"\n\noutputs = {}\n\n# 4a. 会议纪要(Markdown)\nprint(\" 生成会议纪要...\")\nmeeting_prompt = f\"\"\"你是专业会议记录员。请根据以下会议转录文字,生成一份完整的中文会议纪要。\n\n会议背景:\n{context_info}\n\n转录内容:\n{transcript_for_llm}\n\n请生成包含以下内容的详细会议纪要(Markdown格式):\n1. 会议基本信息(时间、地点、参会人员、主题)\n2. 会议议程概述\n3. 主要讨论内容(按议题分节,包含发言要点)\n4. 重要观点与金句(直接引用原话)\n5. 达成的共识与结论\n6. 待办事项与行动计划\n7. 会议总结\n\n要求:结构清晰,重点突出,保留关键细节,适合存档和分享。\"\"\"\n\nmeeting_md = chat(meeting_prompt, config, max_tokens=4000)\noutputs[\"meeting_minutes\"] = meeting_md\n(OUTPUT_DIR / \"会议纪要.md\").write_text(meeting_md, encoding=\"utf-8\")\nprint(f\" ✓ 会议纪要 ({len(meeting_md)} 字)\")\n\n# 4b. 思维导图(Mermaid)\nprint(\" 生成思维导图...\")\nmindmap_prompt = f\"\"\"根据以下OpenClaw圆桌会议内容,生成一个Mermaid格式的思维导图。\n\n会议背景:{context_info}\n转录摘要:{transcript_for_llm[:15000]}\n\n请生成mindmap格式的Mermaid代码,要求:\n- 根节点:OpenClaw圆桌会议\n- 主要分支:核心议题(3-5个)\n- 每个议题下有子要点(2-4个)\n- 包含关键人物和观点\n\n只输出Mermaid代码块(```mermaid ... ```),不要其他内容。\"\"\"\n\nmindmap_raw = chat(mindmap_prompt, config, max_tokens=1500)\noutputs[\"mindmap\"] = mindmap_raw\n(OUTPUT_DIR / \"思维导图.md\").write_text(f\"# OpenClaw圆桌会议思维导图\\n\\n{mindmap_raw}\", encoding=\"utf-8\")\nprint(f\" ✓ 思维导图 ({len(mindmap_raw)} 字)\")\n\n# 4c. 核心金句与观点\nprint(\" 提取核心金句...\")\nquotes_prompt = f\"\"\"从以下OpenClaw圆桌会议转录中,提取最有价值的金句和核心观点。\n\n转录内容:{transcript_for_llm[:20000]}\n\n请整理:\n1. 10-15条精彩金句(原话引用,注明发言人如可辨识)\n2. 5-8个核心观点/洞察(概括性陈述)\n3. 3-5个值得思考的问题\n\n用Markdown格式输出,适合社交媒体分享。\"\"\"\n\nquotes_md = chat(quotes_prompt, config, max_tokens=2000)\noutputs[\"quotes\"] = quotes_md\n(OUTPUT_DIR / \"核心金句与观点.md\").write_text(quotes_md, encoding=\"utf-8\")\nprint(f\" ✓ 核心金句 ({len(quotes_md)} 字)\")\n\n# 4d. 简报摘要(适合快速阅读)\nprint(\" 生成简报摘要...\")\nbrief_prompt = f\"\"\"为以下OpenClaw圆桌会议生成一份简洁的中文简报,适合3分钟快速阅读。\n\n会议背景:{context_info}\n转录内容:{transcript_for_llm[:20000]}\n\n请生成:\n## TL;DR(一句话总结)\n[50字以内]\n\n## 核心要点\n[5个最重要的讨论点,每点2-3句话]\n\n## 关键数据与事实\n[列出会议中提到的具体数据、产品、公司名称]\n\n## 为什么重要\n[这次会议对AI开源生态的意义,3-4句话]\"\"\"\n\nbrief_md = chat(brief_prompt, config, max_tokens=1500)\noutputs[\"brief\"] = brief_md\n(OUTPUT_DIR / \"简报摘要.md\").write_text(brief_md, encoding=\"utf-8\")\nprint(f\" ✓ 简报摘要 ({len(brief_md)} 字)\")\n\n# 4e. 学习闪卡(Q&A格式)\nprint(\" 生成学习闪卡...\")\nflashcard_prompt = f\"\"\"根据OpenClaw圆桌会议内容,生成15-20张学习闪卡(问答卡片格式)。\n\n转录内容:{transcript_for_llm[:15000]}\n\n生成适合学习的Q&A卡片,涵盖:\n- OpenClaw是什么,它的核心功能\n- AI开源生态的现状与挑战\n- 各参会嘉宾的核心观点\n- 重要概念解释\n\n格式(每张卡片):\n**Q: [问题]**\nA: [答案,2-4句话]\n\n---\"\"\"\n\nflashcard_md = chat(flashcard_prompt, config, max_tokens=2500)\noutputs[\"flashcards\"] = flashcard_md\n(OUTPUT_DIR / \"学习闪卡.md\").write_text(f\"# OpenClaw圆桌会议学习闪卡\\n\\n{flashcard_md}\", encoding=\"utf-8\")\nprint(f\" ✓ 学习闪卡 ({len(flashcard_md)} 字)\")\n\n# ── Step 5: 生成 README 展示文件 ──────────────────────────────────────\nprint(\"\\n[5/5] 生成案例展示 README...\")\n\nreadme_content = f\"\"\"# 🎯 案例演示:OpenClaw与AI开源圆桌会议\n\n> **NoteKing Pro 实际处理案例** — 展示如何将一段35分钟的圆桌会议视频,\n> 自动转化为多种高价值文档输出。\n\n## 📹 原始视频信息\n\n| 项目 | 详情 |\n|------|------|\n| 视频标题 | OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持 |\n| 来源 | [Bilibili BV1GX9pB9E6N](https://www.bilibili.com/video/BV1GX9pB9E6N/) |\n| 时长 | 约 35 分钟 |\n| 主持人 | 杨植麟(月之暗面 / Kimi 创始人)|\n| ASR引擎 | faster-whisper (base 模型) |\n| LLM | MiniMax M2.7 (200K context) |\n\n## 📂 生成文件列表\n\n| 文件 | 格式 | 说明 |\n|------|------|------|\n| [会议纪要.md](会议纪要.md) | Markdown | 完整结构化会议记录,含议程、要点、行动项 |\n| [简报摘要.md](简报摘要.md) | Markdown | 3分钟速读版,TL;DR + 核心要点 |\n| [核心金句与观点.md](核心金句与观点.md) | Markdown | 精彩金句提炼,适合社交媒体分享 |\n| [思维导图.md](思维导图.md) | Mermaid | 会议内容可视化思维导图 |\n| [学习闪卡.md](学习闪卡.md) | Markdown | 15-20张Q&A学习卡片 |\n| [meeting.srt](meeting.srt) | SRT | 完整中文字幕文件 |\n| [transcript.json](transcript.json) | JSON | 原始ASR转录数据(含时间戳) |\n\n## 🚀 如何复现\n\n```bash\n# 1. 安装 NoteKing Pro\ngit clone https://github.com/bcefghj/noteking-pro\ncd noteking-pro\npip install -e .\n\n# 2. 配置 MiniMax API(中国区)\nnoteking setup \\\\\n --api-key \"你的MiniMax_Key\" \\\\\n --base-url \"https://api.minimax.chat/v1\" \\\\\n --model \"MiniMax-M2.7\"\n\n# 3. 处理本地视频文件\nnoteking process 你的会议.mp4 \\\\\n --scene meeting \\\\\n --formats markdown,srt,mindmap,flashcard \\\\\n --output ./output\n```\n\n## 💡 处理流程\n\n```\n视频文件 (.mp4)\n │\n ▼\n[FFmpeg] 音频提取 → WAV 16kHz 单声道\n │\n ▼\n[faster-whisper] 语音识别 → 带时间戳文本段落\n │\n ▼\n[MiniMax M2.7] LLM 处理\n ├── 会议纪要生成\n ├── 思维导图提炼\n ├── 金句萃取\n ├── 简报摘要\n └── 学习闪卡\n │\n ▼\n多格式输出文件\n```\n\n## 📊 处理统计\n\n- 视频时长:35.7 分钟\n- 转录字数:{len(full_text):,} 字符\n- 字幕条数:{len(segments)} 条\n- 输出文件:7 个\n- LLM模型:MiniMax M2.7\n\n---\n\n*由 [NoteKing Pro](https://github.com/bcefghj/noteking-pro) 自动生成*\n\"\"\"\n\n(OUTPUT_DIR / \"README.md\").write_text(readme_content, encoding=\"utf-8\")\nprint(f\" ✓ README.md 已生成\")\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"✅ 处理完成!\")\nprint(f\"输出目录: {OUTPUT_DIR}\")\nprint(\"\\n生成文件:\")\nfor f in sorted(OUTPUT_DIR.iterdir()):\n size = f.stat().st_size\n print(f\" {f.name} ({size//1024}KB)\" if size > 1024 else f\" {f.name} ({size}B)\")\n"}, "fd366b09224e247ae64e886a34c2b45bf178a580": {"t": 1785674010.1565256, "n": 8640, "h": "b6894df50f22c6e6", "c": "\"\"\"\nNoteKing Pro Demo — 处理 OpenClaw 圆桌会议视频\n使用 MiniMax M2.7 生成会议纪要、思维导图、闪卡等多种格式\n\"\"\"\nimport sys\nimport os\nimport json\nfrom pathlib import Path\n\nsys.path.insert(0, str(Path(__file__).parent))\n\n# ── 配置 ──────────────────────────────────────────────────────────────\nVIDEO_PATH = Path(\"/Users/daishanghao/Desktop/20260405_会议视频录音/OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持 - 001 - OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持.mp4\")\nOUTPUT_DIR = Path(__file__).parent / \"demos\" / \"openclaw_meeting\"\nMINIMAX_KEY = \"sk-cp-vX4T-YhmjkytkOexcwZ-uAdmALWR8ggXmtGOymuJQ1lfNLOR1phT0Ju09VggOTENL-y1pGe-KC4fTQppbzn_X_WPxVIApwG71PlvZHCGgfaIRH2zYoAI_RA\"\nMINIMAX_BASE_URL = \"https://api.minimax.chat/v1\"\nMINIMAX_MODEL = \"MiniMax-M2.7\"\n\nOUTPUT_DIR.mkdir(parents=True, exist_ok=True)\n\n# ── 配置对象 ───────────────────────────────────────────────────────────\nfrom core.config import AppConfig, LLMConfig, ASRConfig, RecordingConfig\n\nconfig = AppConfig(\n llm=LLMConfig(\n api_key=MINIMAX_KEY,\n base_url=MINIMAX_BASE_URL,\n model=MINIMAX_MODEL,\n temperature=0.3,\n max_tokens=8000,\n language=\"zh-CN\",\n ),\n asr=ASRConfig(\n default_engine=\"faster_whisper\",\n faster_whisper_model=\"base\",\n ),\n recording=RecordingConfig(\n denoise_level=1,\n enable_diarization=False,\n default_scene=\"meeting\",\n ),\n output_dir=str(OUTPUT_DIR),\n)\n\nprint(\"=\" * 60)\nprint(\"NoteKing Pro — OpenClaw 圆桌会议处理\")\nprint(\"=\" * 60)\n\n# ── Step 1: 提取音频 ──────────────────────────────────────────────────\nprint(\"\\n[1/5] 提取音频...\")\nimport subprocess\naudio_path = OUTPUT_DIR / \"meeting_audio.wav\"\nif not audio_path.exists():\n result = subprocess.run([\n \"ffmpeg\", \"-i\", str(VIDEO_PATH),\n \"-vn\", \"-acodec\", \"pcm_s16le\",\n \"-ar\", \"16000\", \"-ac\", \"1\",\n \"-y\", str(audio_path)\n ], capture_output=True, text=True)\n if result.returncode != 0:\n print(f\" 音频提取失败: {result.stderr[:200]}\")\n sys.exit(1)\n print(f\" ✓ 音频已提取: {audio_path.name}\")\nelse:\n print(f\" ✓ 使用缓存音频: {audio_path.name}\")\n\n# ── Step 2: ASR 转录 ──────────────────────────────────────────────────\nprint(\"\\n[2/5] ASR 语音转文字 (faster-whisper)...\")\ntranscript_cache = OUTPUT_DIR / \"transcript.json\"\nif transcript_cache.exists():\n print(\" ✓ 使用缓存转录结果\")\n import json\n with open(transcript_cache) as f:\n transcript_data = json.load(f)\n full_text = transcript_data[\"full_text\"]\n segments = transcript_data[\"segments\"]\nelse:\n from core.transcriber import transcribe\n result = transcribe(audio_path, config)\n full_text = result.full_text\n segments = [{\"start\": s.start, \"end\": s.end, \"text\": s.text} for s in result.segments]\n with open(transcript_cache, \"w\", encoding=\"utf-8\") as f:\n json.dump({\"full_text\": full_text, \"segments\": segments, \"language\": result.language}, f, ensure_ascii=False, indent=2)\n print(f\" ✓ 转录完成,共 {len(segments)} 段,{len(full_text)} 字符\")\n\nprint(f\" 预览: {full_text[:200]}...\")\n\n# ── Step 3: 生成 SRT 字幕文件 ─────────────────────────────────────────\nprint(\"\\n[3/5] 生成 SRT 字幕...\")\nsrt_path = OUTPUT_DIR / \"meeting.srt\"\ndef sec_to_srt(t):\n h = int(t // 3600)\n m = int((t % 3600) // 60)\n s = int(t % 60)\n ms = int((t - int(t)) * 1000)\n return f\"{h:02d}:{m:02d}:{s:02d},{ms:03d}\"\n\nsrt_lines = []\nfor i, seg in enumerate(segments, 1):\n srt_lines.append(str(i))\n srt_lines.append(f\"{sec_to_srt(seg['start'])} --> {sec_to_srt(seg['end'])}\")\n srt_lines.append(seg['text'].strip())\n srt_lines.append(\"\")\nsrt_path.write_text(\"\\n\".join(srt_lines), encoding=\"utf-8\")\nprint(f\" ✓ SRT 字幕: {srt_path.name} ({len(segments)} 条)\")\n\n# ── Step 4: 用 MiniMax 生成多种输出 ──────────────────────────────────\nprint(\"\\n[4/5] 调用 MiniMax M2.7 生成内容...\")\nfrom core.llm import chat\n\n# 准备转录文本(控制长度在 token 限制内)\nMAX_CHARS = 40000\ntranscript_for_llm = full_text[:MAX_CHARS]\nif len(full_text) > MAX_CHARS:\n transcript_for_llm += f\"\\n\\n[注:原文共{len(full_text)}字,此处截取前{MAX_CHARS}字]\"\n\ncontext_info = \"\"\"\n会议名称:OpenClaw与AI开源圆桌会议\n主持人:杨植麟(月之暗面创始人)\n参与者:AI领域开源生态相关人士\n主题:OpenClaw平台与AI开源生态讨论\n来源:Bilibili视频 BV1GX9pB9E6N\n\"\"\"\n\noutputs = {}\n\n# 4a. 会议纪要(Markdown)\nprint(\" 生成会议纪要...\")\nmeeting_prompt = f\"\"\"你是专业会议记录员。请根据以下会议转录文字,生成一份完整的中文会议纪要。\n\n会议背景:\n{context_info}\n\n转录内容:\n{transcript_for_llm}\n\n请生成包含以下内容的详细会议纪要(Markdown格式):\n1. 会议基本信息(时间、地点、参会人员、主题)\n2. 会议议程概述\n3. 主要讨论内容(按议题分节,包含发言要点)\n4. 重要观点与金句(直接引用原话)\n5. 达成的共识与结论\n6. 待办事项与行动计划\n7. 会议总结\n\n要求:结构清晰,重点突出,保留关键细节,适合存档和分享。\"\"\"\n\nmeeting_md = chat(meeting_prompt, config, max_tokens=4000)\noutputs[\"meeting_minutes\"] = meeting_md\n(OUTPUT_DIR / \"会议纪要.md\").write_text(meeting_md, encoding=\"utf-8\")\nprint(f\" ✓ 会议纪要 ({len(meeting_md)} 字)\")\n\n# 4b. 思维导图(Mermaid)\nprint(\" 生成思维导图...\")\nmindmap_prompt = f\"\"\"根据以下OpenClaw圆桌会议内容,生成一个Mermaid格式的思维导图。\n\n会议背景:{context_info}\n转录摘要:{transcript_for_llm[:15000]}\n\n请生成mindmap格式的Mermaid代码,要求:\n- 根节点:OpenClaw圆桌会议\n- 主要分支:核心议题(3-5个)\n- 每个议题下有子要点(2-4个)\n- 包含关键人物和观点\n\n只输出Mermaid代码块(```mermaid ... ```),不要其他内容。\"\"\"\n\nmindmap_raw = chat(mindmap_prompt, config, max_tokens=1500)\noutputs[\"mindmap\"] = mindmap_raw\n(OUTPUT_DIR / \"思维导图.md\").write_text(f\"# OpenClaw圆桌会议思维导图\\n\\n{mindmap_raw}\", encoding=\"utf-8\")\nprint(f\" ✓ 思维导图 ({len(mindmap_raw)} 字)\")\n\n# 4c. 核心金句与观点\nprint(\" 提取核心金句...\")\nquotes_prompt = f\"\"\"从以下OpenClaw圆桌会议转录中,提取最有价值的金句和核心观点。\n\n转录内容:{transcript_for_llm[:20000]}\n\n请整理:\n1. 10-15条精彩金句(原话引用,注明发言人如可辨识)\n2. 5-8个核心观点/洞察(概括性陈述)\n3. 3-5个值得思考的问题\n\n用Markdown格式输出,适合社交媒体分享。\"\"\"\n\nquotes_md = chat(quotes_prompt, config, max_tokens=2000)\noutputs[\"quotes\"] = quotes_md\n(OUTPUT_DIR / \"核心金句与观点.md\").write_text(quotes_md, encoding=\"utf-8\")\nprint(f\" ✓ 核心金句 ({len(quotes_md)} 字)\")\n\n# 4d. 简报摘要(适合快速阅读)\nprint(\" 生成简报摘要...\")\nbrief_prompt = f\"\"\"为以下OpenClaw圆桌会议生成一份简洁的中文简报,适合3分钟快速阅读。\n\n会议背景:{context_info}\n转录内容:{transcript_for_llm[:20000]}\n\n请生成:\n## TL;DR(一句话总结)\n[50字以内]\n\n## 核心要点\n[5个最重要的讨论点,每点2-3句话]\n\n## 关键数据与事实\n[列出会议中提到的具体数据、产品、公司名称]\n\n## 为什么重要\n[这次会议对AI开源生态的意义,3-4句话]\"\"\"\n\nbrief_md = chat(brief_prompt, config, max_tokens=1500)\noutputs[\"brief\"] = brief_md\n(OUTPUT_DIR / \"简报摘要.md\").write_text(brief_md, encoding=\"utf-8\")\nprint(f\" ✓ 简报摘要 ({len(brief_md)} 字)\")\n\n# 4e. 学习闪卡(Q&A格式)\nprint(\" 生成学习闪卡...\")\nflashcard_prompt = f\"\"\"根据OpenClaw圆桌会议内容,生成15-20张学习闪卡(问答卡片格式)。\n\n转录内容:{transcript_for_llm[:15000]}\n\n生成适合学习的Q&A卡片,涵盖:\n- OpenClaw是什么,它的核心功能\n- AI开源生态的现状与挑战\n- 各参会嘉宾的核心观点\n- 重要概念解释\n\n格式(每张卡片):\n**Q: [问题]**\nA: [答案,2-4句话]\n\n---\"\"\"\n\nflashcard_md = chat(flashcard_prompt, config, max_tokens=2500)\noutputs[\"flashcards\"] = flashcard_md\n(OUTPUT_DIR / \"学习闪卡.md\").write_text(f\"# OpenClaw圆桌会议学习闪卡\\n\\n{flashcard_md}\", encoding=\"utf-8\")\nprint(f\" ✓ 学习闪卡 ({len(flashcard_md)} 字)\")\n\n# ── Step 5: 生成 README 展示文件 ──────────────────────────────────────\nprint(\"\\n[5/5] 生成案例展示 README...\")\n\nreadme_content = f\"\"\"# 🎯 案例演示:OpenClaw与AI开源圆桌会议\n\n> **NoteKing Pro 实际处理案例** — 展示如何将一段35分钟的圆桌会议视频,\n> 自动转化为多种高价值文档输出。\n\n## 📹 原始视频信息\n\n| 项目 | 详情 |\n|------|------|\n| 视频标题 | OpenClaw与AI开源圆桌会议 月之暗面创始人杨植麟主持 |\n| 来源 | [Bilibili BV1GX9pB9E6N](https://www.bilibili.com/video/BV1GX9pB9E6N/) |\n| 时长 | 约 35 分钟 |\n| 主持人 | 杨植麟(月之暗面 / Kimi 创始人)|\n| ASR引擎 | faster-whisper (base 模型) |\n| LLM | MiniMax M2.7 (200K context) |\n\n## 📂 生成文件列表\n\n| 文件 | 格式 | 说明 |\n|------|------|------|\n| [会议纪要.md](会议纪要.md) | Markdown | 完整结构化会议记录,含议程、要点、行动项 |\n| [简报摘要.md](简报摘要.md) | Markdown | 3分钟速读版,TL;DR + 核心要点 |\n| [核心金句与观点.md](核心金句与观点.md) | Markdown | 精彩金句提炼,适合社交媒体分享 |\n| [思维导图.md](思维导图.md) | Mermaid | 会议内容可视化思维导图 |\n| [学习闪卡.md](学习闪卡.md) | Markdown | 15-20张Q&A学习卡片 |\n| [meeting.srt](meeting.srt) | SRT | 完整中文字幕文件 |\n| [transcript.json](transcript.json) | JSON | 原始ASR转录数据(含时间戳) |\n\n## 🚀 如何复现\n\n```bash\n# 1. 安装 NoteKing Pro\ngit clone https://github.com/bcefghj/noteking-pro\ncd noteking-pro\npip install -e .\n\n# 2. 配置 MiniMax API(中国区)\nnoteking setup \\\\\n --api-key \"你的MiniMax_Key\" \\\\\n --base-url \"https://api.minimax.chat/v1\" \\\\\n --model \"MiniMax-M2.7\"\n\n# 3. 处理本地视频文件\nnoteking process 你的会议.mp4 \\\\\n --scene meeting \\\\\n --formats markdown,srt,mindmap,flashcard \\\\\n --output ./output\n```\n\n## 💡 处理流程\n\n```\n视频文件 (.mp4)\n │\n ▼\n[FFmpeg] 音频提取 → WAV 16kHz 单声道\n │\n ▼\n[faster-whisper] 语音识别 → 带时间戳文本段落\n │\n ▼\n[MiniMax M2.7] LLM 处理\n ├── 会议纪要生成\n ├── 思维导图提炼\n ├── 金句萃取\n ├── 简报摘要\n └── 学习闪卡\n │\n ▼\n多格式输出文件\n```\n\n## 📊 处理统计\n\n- 视频时长:35.7 分钟\n- 转录字数:{len(full_text):,} 字符\n- 字幕条数:{len(segments)} 条\n- 输出文件:7 个\n- LLM模型:MiniMax M2.7\n\n---\n\n*由 [NoteKing Pro](https://github.com/bcefghj/noteking-pro) 自动生成*\n\"\"\"\n\n(OUTPUT_DIR / \"README.md\").write_text(readme_content, encoding=\"utf-8\")\nprint(f\" ✓ README.md 已生成\")\n\nprint(\"\\n\" + \"=\" * 60)\nprint(\"✅ 处理完成!\")\nprint(f\"输出目录: {OUTPUT_DIR}\")\nprint(\"\\n生成文件:\")\nfor f in sorted(OUTPUT_DIR.iterdir()):\n size = f.stat().st_size\n print(f\" {f.name} ({size//1024}KB)\" if size > 1024 else f\" {f.name} ({size}B)\")\n"}}, "web/src/app/layout.tsx": {"441b50e11191eba17e7687a7ba2aa2adac64746c": {"t": 1785599237.3580153, "n": 483, "h": "c6894d834c73d483", "c": "import type { Metadata } from \"next\";\nimport \"./globals.css\";\n\nexport const metadata: Metadata = {\n title: \"NoteKing Pro - 全网最强视频/录音处理工具\",\n description:\n \"全网最强视频/录音处理工具。支持30+平台视频、本地录音/视频上传、说话人分离、降噪增强、23种输出模板。\",\n};\n\nexport default function RootLayout({\n children,\n}: {\n children: React.ReactNode;\n}) {\n return (\n <html lang=\"zh-CN\">\n <body className=\"min-h-screen bg-[var(--bg-primary)] text-[var(--text-primary)]\">\n {children}\n </body>\n </html>\n );\n}\n"}}}} |