From 9259a3731aca5aacaaa19a03cf91ae74e8497d7e Mon Sep 17 00:00:00 2001 From: starmen1265 Date: Mon, 16 Feb 2026 22:19:25 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=9F=A5=E4=B9=8E?= =?UTF-8?q?=E8=A7=86=E9=A2=91=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增: - kb-zhihu-content/: 知乎视频下载模块 - zhihu_content.py: 视频下载工具(使用 yt-dlp) - SKILL.md: 技能说明文档 修改: - kb_workflow.py: - CONTENT_AGENTS 添加 zhihu - VIDEO_CONTENT_TYPES 添加 zvideo - SKILL.md: - 添加 4.1.2 知乎视频处理流程 - 更新子技能清单,添加知乎视频条目 - 更新内容类型判断表格 --- kb-workflow/SKILL.md | 31 ++++- kb-workflow/kb_workflow.py | 3 +- kb-zhihu-content/SKILL.md | 58 +++++++++ kb-zhihu-content/zhihu_content.py | 197 ++++++++++++++++++++++++++++++ 4 files changed, 283 insertions(+), 6 deletions(-) create mode 100644 kb-zhihu-content/SKILL.md create mode 100644 kb-zhihu-content/zhihu_content.py diff --git a/kb-workflow/SKILL.md b/kb-workflow/SKILL.md index 13619cc..b899b1b 100644 --- a/kb-workflow/SKILL.md +++ b/kb-workflow/SKILL.md @@ -157,11 +157,12 @@ python3 /path/to/zhihu_metadata.py --id "[ID]" --type question --output "${metad ### 步骤 4: 内容下载 > ⚠️ **根据内容类型选择处理方式**: -> - **视频 (video)**:下载字幕/视频 +> - **视频 (video)**:下载字幕/视频(B站视频) +> - **知乎视频 (zvideo)**:下载视频 > - **专栏文章 (article)**:提取文字内容(跳过步骤5) > - **知乎问答/专栏**:提取文字内容(跳过步骤5) -#### 4.1 视频内容处理(type=video) +#### 4.1 视频内容处理(B站视频 type=video) 1. 获取目录路径: ```bash @@ -183,6 +184,22 @@ python3 /path/to/bilibili_content.py --id "[ID]" --type subtitle-download --outp > ⚠️ **重要提示**:没有找到官方字幕是**正常的**!很多视频没有自动生成的字幕。不要因为没有字幕就停止工作流,继续执行 Whisper 步骤即可。 +#### 4.1.2 知乎视频处理(type=zvideo) + +1. 获取目录路径: +```bash +python3 kb_workflow.py --content_downloads +python3 kb_workflow.py --content +``` + +2. 启动子代理调用知乎 content 技能: +```bash +# 知乎视频下载 +python3 /path/to/zhihu_content.py --id "[ID]" --type video --output [content_dir] +``` + +3. 知乎视频没有官方字幕,直接进入步骤 5(Whisper 语音识别) + #### 4.2 专栏文章处理(type=article/zhuanlan/question) 1. 获取目录路径: @@ -321,10 +338,13 @@ python3 kb_workflow.py --archive-file "[分析报告文件路径]" | 2 | kb-link-parser | `--url "BVxxx"` | `{p}_{id}.link.json` | | | 3 | kb-bilibili-metadata | `--id "BVxxx" --type video` | `{p}_{id}.meta.json` | | | 3 | kb-bilibili-metadata | `--id "cvxxx" --type article` | `{p}_{id}.meta.json` | 专栏元数据 | -| 3 | kb-zhihu-metadata | `--id "xxx" --type question` | `{p}_{id}.meta.json` | | +| 3 | kb-zhihu-metadata | `--id "xxx" --type question` | `{p}_{id}.meta.json` | 知乎问答 | +| 3 | kb-zhihu-metadata | `--id "xxx" --type answer` | `{p}_{id}.meta.json` | 知乎回答 | +| 3 | kb-zhihu-metadata | `--id "xxx" --type zvideo` | `{p}_{id}.meta.json` | 知乎视频 | | 4 | kb-bilibili-content | `--id "BVxxx" --type subtitle-download` | `{p}_{id}.srt` | 如果没有字幕是正常的 | | 4 | kb-bilibili-content | `--id "BVxxx" --type video` | `{p}_{id}.mp4` | 如果需要下载视频 | | 4 | kb-bilibili-content | `--id "cvxxx" --type article` | `{p}_{id}.txt` | **专栏文章提取文字** | +| 4 | kb-zhihu-content | `--id "xxx" --type video` | `{p}_{id}.mp4` | **知乎视频下载** | | 5 | kb-whisper | `--input xxx.mp4 --model base` | `{p}_{id}.json`, `{p}_{id}.txt` | 仅当没有官方字幕时执行 | | 5 | kb-whisper-diarization | `--input xxx.mp4 --model base --diarize` | `{p}_{id}_diarized.json` | 带说话者分离(需 HF_TOKEN) | | 6 | kb-deep-analyzer | `--metadata x.meta.json --content x.json --output x.md --provider zhipu --model glm-5` | `{p}_{id}.md` | **使用 g5 模型,必须成功** | @@ -337,13 +357,14 @@ python3 kb_workflow.py --archive-file "[分析报告文件路径]" | type 值 | 内容类型 | 处理方式 | |---------|---------|---------| -| `video` | 视频 | 下载视频/字幕,可能需要 Whisper | +| `video` | B站视频 | 下载视频/字幕,可能需要 Whisper | +| `zvideo` | 知乎视频 | 下载视频,需要 Whisper 语音识别 | | `article` | B站专栏 | 直接提取文字内容 | | `zhuanlan` | 知乎专栏 | 直接提取文字内容 | | `question` / `answer` | 知乎问答 | 直接提取文字内容 | | `note` | 小红书 | 下载图片 | -### 视频处理流程(type=video) +### 视频处理流程(type=video/zvideo) ``` 步骤3: 获取元数据 diff --git a/kb-workflow/kb_workflow.py b/kb-workflow/kb_workflow.py index 3f41c23..979ff08 100644 --- a/kb-workflow/kb_workflow.py +++ b/kb-workflow/kb_workflow.py @@ -30,13 +30,14 @@ METADATA_AGENTS = { CONTENT_AGENTS = { "bilibili": "kb-bilibili-content", + "zhihu": "kb-zhihu-content", } # ==================== 内容类型判断 ==================== # 需要下载视频/字幕的内容类型 -VIDEO_CONTENT_TYPES = {"video"} +VIDEO_CONTENT_TYPES = {"video", "zvideo"} # 需要提取文字的内容类型 TEXT_CONTENT_TYPES = {"article", "zhuanlan", "question", "answer"} diff --git a/kb-zhihu-content/SKILL.md b/kb-zhihu-content/SKILL.md new file mode 100644 index 0000000..8d5151f --- /dev/null +++ b/kb-zhihu-content/SKILL.md @@ -0,0 +1,58 @@ +# KB Zhihu Content Skill + +**kb-zhihu-content** - Download Zhihu videos and extract content. + +## Features + +- Video download using **yt-dlp** +- Subtitle extraction (returns no subtitle, triggers Whisper) +- Supports `ZHIHU_COOKIE` environment variable + +## Environment Variables + +| Variable | Required | Description | +|----------|----------|-------------| +| `ZHIHU_COOKIE` | No | 知乎登录 Cookie (for member-only videos) | + +## Commands + +```bash +# Download video +python zhihu_content.py --id "12345678" --type video --output "/path/to/downloads/" + +# Extract subtitle (returns no subtitle, triggers Whisper) +python zhihu_content.py --id "12345678" --type subtitle --output "/path/to/subtitles/" +``` + +## Output + +```json +{ + "success": true, + "type": "video", + "filename": "video_title.mp4", + "filepath": "/path/to/downloads/video_title.mp4", + "message": "下载完成: video_title.mp4" +} +``` + +## Installation + +```bash +# Already included in kb-skills repo +# Ensure yt-dlp is installed +pip install yt-dlp aiohttp +``` + +## How It Works + +Video download uses **yt-dlp**: +1. Parses `ZHIHU_COOKIE` environment variable (optional) +2. Creates temporary Netscape-format cookie file +3. Runs `yt-dlp -f best -o ` +4. Cleans up temporary file + +## Notes + +- 知乎视频通常没有官方字幕,将自动触发 Whisper 语音识别 +- 视频下载需要能访问知乎(可能需要代理) diff --git a/kb-zhihu-content/zhihu_content.py b/kb-zhihu-content/zhihu_content.py new file mode 100644 index 0000000..e909198 --- /dev/null +++ b/kb-zhihu-content/zhihu_content.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +知乎内容获取工具 + +支持: +- zvideo: 知乎视频下载 +- 从页面提取视频播放地址 +""" + +import argparse +import asyncio +import json +import os +import re +import subprocess +import sys +import tempfile +from datetime import datetime +from pathlib import Path +from typing import Optional, Dict, Any, List + + +class ZhihuContent: + """知乎内容获取器""" + + def __init__(self, output_dir: str = "./output", cookie: str = None): + self.output_dir = Path(output_dir) + self.output_dir.mkdir(parents=True, exist_ok=True) + self.cookie = cookie + + def _create_cookie_file(self) -> str: + """创建 Netscape 格式的 Cookie 文件""" + if not self.cookie: + return None + + cookies = {} + for item in self.cookie.split(';'): + item = item.strip() + if '=' in item: + key, value = item.split('=', 1) + cookies[key.strip()] = value.strip() + + cookie_content = """# Netscape HTTP Cookie File +# Generated by zhihu_content.py + +""" + + for name, value in cookies.items(): + cookie_content += f".zhihu.com\tTRUE\t/\tFALSE\t0\t{name}\t{value}\n" + + fd, path = tempfile.mkstemp(suffix='.txt') + with os.fdopen(fd, 'w') as f: + f.write(cookie_content) + + return path + + async def download_video(self, video_id: str, quality: int = 720) -> Dict[str, Any]: + """下载知乎视频(使用 yt-dlp)""" + result = {"success": False, "video_id": video_id, "message": "", "filepath": None, "metadata": {}} + + try: + print(f"获取知乎视频信息: {video_id}") + + cookie_file = self._create_cookie_file() + url = f"https://www.zhihu.com/zvideo/{video_id}" + + # 先获取视频标题 + try: + if cookie_file: + cmd = ["yt-dlp", "--cookies", cookie_file, "--get-title", url] + else: + cmd = ["yt-dlp", "--get-title", url] + + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + + title = stdout.decode('utf-8').strip() + if not title or 'error' in title.lower(): + title = f"zhihu_video_{video_id}" + except Exception as e: + title = f"zhihu_video_{video_id}" + + # 构建 yt-dlp 命令 + safe_title = "".join(c for c in title if c.isalnum() or c in (' ', '-', '_')).strip()[:50] + + print(f"下载视频: {title}") + output_template = str(self.output_dir / f"{safe_title}.%(ext)s") + + # 构建下载命令 + cmd = [ + "yt-dlp", + "-f", "bestvideo+bestaudio/best", + "-o", output_template, + "--no-download-archive", + url + ] + + if cookie_file: + cmd.extend(["--cookies", cookie_file]) + + print(f"执行命令: {' '.join(cmd[:4])}...") + + proc = await asyncio.create_subprocess_exec( + *cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE + ) + stdout, stderr = await proc.communicate() + + # 查找下载的文件 + downloaded_files = list(self.output_dir.glob(f"{safe_title}.*")) + if downloaded_files: + video_file = downloaded_files[0] + result["success"] = True + result["filepath"] = str(video_file) + result["message"] = f"下载完成: {video_file.name}" + result["metadata"] = {"title": title, "video_id": video_id} + print(f"✅ {result['message']}") + else: + result["message"] = "未找到下载的文件" + print(f"❌ {result['message']}") + + # 清理临时 Cookie 文件 + if cookie_file and os.path.exists(cookie_file): + os.remove(cookie_file) + + except Exception as e: + result["message"] = f"错误: {str(e)}" + print(f"❌ {result['message']}") + + return result + + async def extract_subtitle(self, video_id: str) -> Dict[str, Any]: + """ + 提取字幕 + + 知乎视频通常没有官方字幕,需要使用 Whisper 进行语音识别。 + 此函数返回 has_subtitle=False,让工作流继续执行 Whisper 步骤。 + """ + result = {"success": True, "video_id": video_id, "has_subtitle": False, "message": "知乎视频无官方字幕,将使用 Whisper 进行语音识别"} + + return result + + +async def main(): + parser = argparse.ArgumentParser(description='知乎内容获取工具') + parser.add_argument('--id', '-i', required=True, help='知乎视频ID') + parser.add_argument('--type', '-t', required=True, choices=['video', 'subtitle'], help='内容类型') + parser.add_argument('--output', '-o', default='./output', help='输出目录') + parser.add_argument('--quality', '-q', type=int, default=720, help='视频质量') + + args = parser.parse_args() + + cookie = os.environ.get('ZHIHU_COOKIE') + downloader = ZhihuContent(output_dir=args.output, cookie=cookie) + + if args.type == 'video': + result = await downloader.download_video(args.id, quality=args.quality) + else: + result = await downloader.extract_subtitle(args.id) + + # 统一输出格式 + if result.get("success"): + output = { + "success": True, + "type": args.type, + "has_subtitle": result.get("has_subtitle", False), + "filename": Path(result.get('filepath', '')).name if result.get('filepath') else None, + "filepath": str(Path(result.get('filepath', '')).resolve()) if result.get('filepath') else None, + "message": result.get('message', '处理成功') + } + else: + output = { + "success": False, + "error": result.get('message', '处理失败'), + "detail": f"ID: {args.id}, 类型: {args.type}" + } + + print(json.dumps(output, ensure_ascii=False, indent=2)) + return result + + +if __name__ == '__main__': + try: + result = asyncio.run(main()) + sys.exit(0 if result.get("success") else 1) + except KeyboardInterrupt: + print(json.dumps({"success": False, "error": "用户取消"}, ensure_ascii=False, indent=2)) + sys.exit(1) + except Exception as e: + print(json.dumps({"success": False, "error": str(e)}, ensure_ascii=False, indent=2)) + sys.exit(1)