Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, uploader, re.IGNORECASE):\n uploader = '小红书用户' # 或其他平台默认名\n \n # 时长处理(小红书等平台可能没有 duration_string)\n duration = video_info.get('duration_string', '')\n if not duration:\n duration_sec = video_info.get('duration', 0)\n if duration_sec and duration_sec > 0:\n mins = int(duration_sec // 60)\n secs = int(duration_sec % 60)\n duration = f\"{mins}:{secs:02d}\"\n else:\n duration = 'Unknown'\n \n view_count = video_info.get('view_count', 0)\n like_count = video_info.get('like_count', 0)\n comment_count = video_info.get('comment_count', 0)\n # 封面 URL:优先使用传入的 cover_url 参数(OSS 上传后的链接)\n thumbnail = cover_url if cover_url else video_info.get('thumbnail', '')\n webpage_url = video_info.get('webpage_url', '')\n upload_date = video_info.get('upload_date', '')\n \n # 从 URL 提取视频来源平台\n source = \"Unknown\"\n if webpage_url:\n if \"bilibili.com\" in webpage_url or \"b23.tv\" in webpage_url:\n source = \"Bilibili\"\n elif \"xiaohongshu.com\" in webpage_url:\n source = \"小红书\"\n elif \"douyin.com\" in webpage_url:\n source = \"抖音\"\n elif \"youtube.com\" in webpage_url or \"youtu.be\" in webpage_url:\n source = \"YouTube\"\n \n # 格式化日期(处理时间戳和字符串两种格式)\n if upload_date:\n if isinstance(upload_date, int):\n # 时间戳格式(秒)- 如 B 站 API 的 pubdate\n try:\n ts = int(upload_date)\n if ts > 10000000000: # 毫秒时间戳\n ts = ts // 1000\n publish_date = datetime.fromtimestamp(ts).strftime(\"%Y-%m-%d\")\n except:\n publish_date = datetime.now().strftime(\"%Y-%m-%d\")\n elif isinstance(upload_date, str) and upload_date.isdigit() and len(upload_date) >= 8:\n # 字符串格式 YYYYMMDD - 如 yt-dlp 的 upload_date\n publish_date = f\"{upload_date[:4]}-{upload_date[4:6]}-{upload_date[6:8]}\"\n else:\n publish_date = datetime.now().strftime(\"%Y-%m-%d\")\n else:\n publish_date = datetime.now().strftime(\"%Y-%m-%d\")\n \n # 构建标签(四层策略:标题 hashtag → 元数据 tags → AI 关键词 → 默认值)\n # 1. 从标题提取 hashtag(兼容抖音/小红书等平台)\n title = video_info.get('title', '')\n hashtag_pattern = re.compile(r'#([\\w\\u4e00-\\u9fa5]+)')\n hashtag_tags = hashtag_pattern.findall(title)\n print(f\" 🏷️ 从标题提取 hashtag: {hashtag_tags}\", file=sys.stderr)\n \n # 2. 从元数据提取原始标签\n video_tags = video_info.get('tags', [])\n \n # 合并 hashtag 和元数据 tags(hashtag 优先)\n all_tags = hashtag_tags + video_tags\n \n # 3. 筛选高质量标签(去除过长/过短,保留 2-15 字符)\n # 放宽限制以兼容英文标签(如 \"openclaw\" 8 字符)\n filtered_tags = [t for t in all_tags if 2 \u003c= len(t) \u003c= 15]\n \n # 3. 去重并限制数量(最多 5 个)\n seen = set()\n unique_tags = []\n for t in filtered_tags:\n if t.lower() not in seen:\n seen.add(t.lower())\n unique_tags.append(t)\n if len(unique_tags) >= 5:\n break\n \n # 调试日志:输出原始 tags\n print(f\" 🏷️ 原始 tags: {video_tags} → 筛选后:{unique_tags}\", file=sys.stderr)\n \n # 4. 如果不足 5 个,从 AI 分析结果提取关键词补全\n if len(unique_tags) \u003c 5 and ai_result:\n print(f\" 🏷️ 从 AI 结果提取关键词 (当前{len(unique_tags)}个)...\", file=sys.stderr)\n # 4.1 从关键概念 (concepts) 提取术语\n concepts = ai_result.get('concepts', [])\n print(f\" - concepts 数量:{len(concepts)}\", file=sys.stderr)\n for concept in concepts:\n term = concept.get('term', '')\n if term and 2 \u003c= len(term) \u003c= 15 and term.lower() not in seen:\n seen.add(term.lower())\n unique_tags.append(term)\n print(f\" - 添加 concept 标签:{term}\", file=sys.stderr)\n if len(unique_tags) >= 5:\n break\n \n # 4.2 从核心要点标题提取关键词(去除 emoji 和通用词)\n if len(unique_tags) \u003c 5:\n generic_words = {'问题', '方法', '技巧', '总结', '分析', '介绍', '说明', '如何', '什么'}\n key_points = ai_result.get('key_points', [])\n print(f\" - key_points 数量:{len(key_points)}\", file=sys.stderr)\n for point in key_points:\n point_title = point.get('title', '') # 修复:避免覆盖外部的 title 变量\n # 简单分词:按空格/标点分割\n words = re.split(r'[\\s,,.。::!!??]+', point_title)\n for word in words:\n word = word.strip()\n if (2 \u003c= len(word) \u003c= 15 and \n word.lower() not in seen and \n word not in generic_words and\n not re.match(r'^[\\d]+

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, word)): # 排除纯数字\n seen.add(word.lower())\n unique_tags.append(word)\n print(f\" - 添加 key_point 标签:{word}\", file=sys.stderr)\n if len(unique_tags) >= 5:\n break\n elif len(unique_tags) \u003c 5:\n print(f\" 🏷️ AI 结果为空,跳过关键词提取\", file=sys.stderr)\n \n # 5. 仍不足 5 个时用默认值补齐\n default_tags = [\"视频总结\", \"AI 分析\", \"教程\", \"技巧\", \"知识分享\"]\n while len(unique_tags) \u003c 5:\n for t in default_tags:\n if t not in unique_tags:\n unique_tags.append(t)\n if len(unique_tags) >= 5:\n break\n \n tags = unique_tags[:5] # 确保正好 5 个\n \n # 格式化各个模块\n tags_md = format_tags(tags)\n \n # 计算截图时间戳(优先使用保存的时间戳)\n if screenshot_times is None:\n screenshot_times = calculate_screenshot_times(duration, len(screenshot_urls))\n \n # 构建视频章节(从核心要点提取章节信息,用于视频章节展示)\n video_chapters = []\n if ai_result:\n for point in ai_result.get('key_points', []):\n video_chapters.append({\n 'emoji': point.get('emoji', '🎬'),\n 'title': point.get('title', '章节'),\n 'time': point.get('time', '00:00')\n })\n \n # 分配截图 URL:全部给视频章节\n video_chapter_urls = screenshot_urls if screenshot_urls else []\n \n # 格式化各个模块(核心要点和注意事项不再需要截图)\n key_points_md = format_key_points(ai_result.get('key_points', []) if ai_result else [])\n video_chapters_md = format_video_chapters(video_chapters, video_chapter_urls, thumbnail)\n warnings_md = format_warnings(ai_result.get('warnings', []) if ai_result else [])\n concepts_md = format_concepts(ai_result.get('concepts', []) if ai_result else [])\n \n # 准备模板变量字典(键名必须与模板中的占位符完全一致)\n format_dict = {\n '视频标题': title,\n '标签 1': tags[0] if len(tags) > 0 else '视频总结',\n '标签 2': tags[1] if len(tags) > 1 else 'AI 分析',\n '标签 3': tags[2] if len(tags) > 2 else '教程',\n '标签 4': tags[3] if len(tags) > 3 else '技巧',\n '标签 5': tags[4] if len(tags) > 4 else '知识分享',\n 'UP 主名称': uploader,\n '封面 URL': thumbnail,\n '视频来源': source,\n '100-200 字概述': ai_result.get('note', '*AI 生成失败*') if ai_result else '*AI 生成失败*',\n '视频 URL': webpage_url,\n '视频时长': duration, # 修复:模板中是{视频时长}不是{时长}\n '时长': duration, # 保留兼容\n '发布日期': publish_date,\n '播放量': f\"{view_count:,}\" if view_count else \"0\",\n '点赞数': f\"{like_count:,}\" if like_count else \"0\",\n '评论数': f\"{comment_count:,}\" if comment_count else \"0\",\n '最终归纳段落': ai_result.get('summary', '*AI 生成失败*') if ai_result else '*AI 生成失败*',\n '生成日期': datetime.now().strftime(\"%Y-%m-%d\"),\n }\n \n # 加载模板\n template = load_template()\n \n # 使用自定义方式替换模板变量\n # 因为模板中有重复的占位符(如多个{emoji} {要点标题}),需要特殊处理\n md = template\n \n # 先替换唯一的变量\n for key, value in format_dict.items():\n md = md.replace('{' + key + '}', str(value))\n \n \n # 替换核心要点部分(整个区块替换)\n key_points_pattern = r'## 🎯 核心要点\\n\\n(.*?)\\n\\n---'\n key_points_replacement = f\"## 🎯 核心要点\\n\\n{key_points_md}\\n\\n---\"\n md = re.sub(key_points_pattern, key_points_replacement, md, flags=re.DOTALL)\n \n # 替换视频章节部分\n video_chapters_pattern = r'## 🎬 视频章节\\n\\n(.*?)\\n\\n---'\n video_chapters_replacement = f\"## 🎬 视频章节\\n\\n{video_chapters_md}\\n\\n---\"\n md = re.sub(video_chapters_pattern, video_chapters_replacement, md, flags=re.DOTALL)\n \n # 替换注意事项部分\n warnings_pattern = r'## ⚠️ 注意事项\\n\\n(.*?)\\n\\n---'\n warnings_replacement = f\"## ⚠️ 注意事项\\n\\n{warnings_md}\\n\\n---\"\n md = re.sub(warnings_pattern, warnings_replacement, md, flags=re.DOTALL)\n \n # 替换关键概念部分\n concepts_pattern = r'## 📚 关键概念\\n\\n(.*?)\\n\\n---'\n concepts_replacement = f\"## 📚 关键概念\\n\\n{concepts_md}\\n\\n---\"\n md = re.sub(concepts_pattern, concepts_replacement, md, flags=re.DOTALL)\n \n # 移除旧的视频帧截图章节(如果存在)\n screenshots_pattern = r'## 🎬 视频帧截图\\n\\n(.*?)\\n\\n---'\n md = re.sub(screenshots_pattern, '', md, flags=re.DOTALL)\n \n return md\n\n\ndef main():\n if len(sys.argv) \u003c 4:\n print(\"用法:python3 analyze-subtitles-ai.py \u003c字幕文件> \u003c元数据文件> \u003c输出文件>\")\n sys.exit(1)\n \n vtt_file = sys.argv[1]\n meta_file = sys.argv[2]\n output_file = sys.argv[3]\n \n print(\"=\" * 50)\n print(\"🧠 AI 字幕分析器 v1.0.13\")\n print(\"=\" * 50)\n print()\n \n output_dir = os.path.dirname(output_file)\n ai_json_file = os.path.join(output_dir, 'ai_result.json')\n \n # 检查是否已有 AI 结果(第二次调用,只渲染 Markdown)\n if os.path.exists(ai_json_file):\n print(f\"📊 加载已有 AI 结果:{ai_json_file}\")\n with open(ai_json_file, 'r', encoding='utf-8') as f:\n ai_result = json.load(f)\n \n print(f\"📊 加载元数据:{meta_file}\")\n with open(meta_file, 'r', encoding='utf-8') as f:\n video_info = json.load(f)\n print(f\" 视频:{video_info.get('title', 'Unknown')}\")\n print()\n \n # 加载截图 URL\n screenshot_urls = load_screenshot_urls(os.path.join(output_dir, 'screenshot_urls.txt'))\n print(f\" 📸 加载截图链接:{len(screenshot_urls)} 张\")\n \n # 加载截图时间戳(从 screenshot_times.txt 读取)\n screenshot_times = load_screenshot_times(output_dir)\n if screenshot_times:\n print(f\" 🕐 加载截图时间戳:{len(screenshot_times)} 个\")\n \n # 加载 OSS 封面 URL\n cover_url_file = os.path.join(output_dir, 'cover_url.txt')\n oss_cover_url = None\n if os.path.exists(cover_url_file):\n try:\n with open(cover_url_file, 'r', encoding='utf-8') as f:\n cover_data = json.load(f)\n oss_cover_url = cover_data.get('oss_url', '')\n except:\n pass\n \n # 生成 Markdown\n print(\"📝 生成结构化总结...\")\n md_content = generate_markdown(video_info, ai_result, screenshot_urls, screenshot_times, oss_cover_url)\n \n # 保存文件\n with open(output_file, 'w', encoding='utf-8') as f:\n f.write(md_content)\n \n print(f\"✅ 总结生成完成:{output_file}\")\n print()\n print(\"=\" * 50)\n print(\"✨ Markdown 渲染完成!\")\n print(\"=\" * 50)\n return\n \n # 第一次调用:执行完整 AI 分析\n # 解析字幕\n print(f\"📝 解析字幕:{vtt_file}\")\n subtitles = parse_vtt(vtt_file)\n print(f\" 找到 {len(subtitles)} 条字幕\")\n \n # 提取纯文本\n transcript = extract_transcript_text(subtitles)\n word_count = len(transcript.split())\n print(f\" 文本长度:{word_count} 字\")\n print()\n \n # 加载元数据\n print(f\"📊 加载元数据:{meta_file}\")\n with open(meta_file, 'r', encoding='utf-8') as f:\n video_info = json.load(f)\n print(f\" 视频:{video_info.get('title', 'Unknown')}\")\n print()\n \n # AI 分析\n print(\"🤖 AI 智能分析...\")\n ai_result = ai_analyze(transcript, video_info)\n \n if not ai_result:\n print(\" ⚠️ AI 分析失败,使用基础版本\")\n md_content = f\"\"\"# {video_info.get('title', 'Unknown')}\n\n**Tags:** `视频总结` `AI 分析` `教程` `技巧` `知识分享`\n\n**Status:** ⚠️ AI 分析失败\n\n**Author:** {video_info.get('uploader', 'Unknown')}\n\n**Cover:**\n![视频封面]({video_info.get('thumbnail', '')})\n\n---\n\n## 📝 Note\n\nAI 分析暂时不可用,请稍后重试。\n\n---\n\n## 📺 视频信息\n\n**链接:** {video_info.get('webpage_url', '')}\n**时长:** {video_info.get('duration_string', 'Unknown')}\n**发布:** {datetime.now().strftime(\"%Y-%m-%d\")}\n**播放:** 0+ | **点赞:** 0 | **评论:** 0\n\n---\n\n## 🎯 核心要点\n\n*AI 分析失败,无法提取要点*\n\n---\n\n## ⚠️ 注意事项\n\n- *AI 分析失败,无法提取注意事项*\n\n---\n\n## 📚 关键概念\n\n| 概念 | 解释 |\n|------|------|\n| *AI 分析失败* | *无法提取概念* |\n\n---\n\n## 🎬 视频帧截图\n\n*AI 分析失败,无法生成截图*\n\n---\n\n## 💡 总结\n\n*AI 分析失败,无法生成总结*\n\n---\n\n*生成时间:{datetime.now().strftime(\"%Y-%m-%d\")}*\n*技能版本:video-summarizer v1.0.13*\n\"\"\"\n with open(output_file, 'w', encoding='utf-8') as f:\n f.write(md_content)\n print(f\"✅ 基础版本已生成:{output_file}\")\n return\n \n print()\n \n # 保存 AI 分析结果为 JSON(供截图步骤使用)\n output_dir = os.path.dirname(output_file)\n ai_json_file = os.path.join(output_dir, 'ai_result.json')\n try:\n with open(ai_json_file, 'w', encoding='utf-8') as f:\n json.dump(ai_result, f, ensure_ascii=False, indent=2)\n print(f\" 💾 AI 结果已保存:{ai_json_file}\")\n except Exception as e:\n print(f\" ⚠️ 保存 AI JSON 失败:{e}\")\n \n # 加载截图 URL(此时截图已完成,供 Markdown 渲染使用)\n screenshots_dir = os.path.dirname(vtt_file)\n screenshot_urls = load_screenshot_urls(os.path.join(screenshots_dir, 'screenshot_urls.txt'))\n print(f\" 📸 加载截图链接:{len(screenshot_urls)} 张\")\n \n # 加载截图时间戳(从 screenshot_times.txt 读取)\n screenshot_times = load_screenshot_times(output_dir)\n if screenshot_times:\n print(f\" 🕐 加载截图时间戳:{len(screenshot_times)} 个\")\n \n # 生成 Markdown\n print(\"📝 生成结构化总结...\")\n md_content = generate_markdown(video_info, ai_result, screenshot_urls, screenshot_times)\n \n # 保存文件\n with open(output_file, 'w', encoding='utf-8') as f:\n f.write(md_content)\n \n print(f\"✅ 总结生成完成:{output_file}\")\n print()\n print(\"=\" * 50)\n print(\"✨ AI 分析完成!\")\n print(\"=\" * 50)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":31115,"content_sha256":"dce780d48023277a8e7e7e319c5d65095ca7061e3779da5469d9695dfe0c6af5"},{"filename":"scripts/bili-login.sh","content":"#!/bin/bash\n# bili-login.sh - B 站扫码登录获取 Cookies\n# 用法:./bili-login.sh [输出文件]\n# 默认输出路径:~/.cookies/bilibili_cookies.txt\n\nset -e\n\nCOOKIE_FILE=\"${1:-$HOME/.cookies/bilibili_cookies.txt}\"\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# biliup login 默认在当前工作目录保存 cookies.json\n# 使用临时目录,避免在 skill 目录内留下 cookie 文件\nBILIUP_COOKIE_DIR=$(mktemp -d)\nBILIUP_COOKIE=\"$BILIUP_COOKIE_DIR/cookies.json\"\n\n# 登录完成后清理临时 cookie 文件\ncleanup() {\n rm -rf \"$BILIUP_COOKIE_DIR\" 2>/dev/null || true\n}\ntrap cleanup EXIT\n\necho \"================================\"\necho \"📱 B 站扫码登录\"\necho \"================================\"\necho \"\"\n\n# 检查 biliup 是否安装\nif ! command -v biliup &>/dev/null; then\n echo \"❌ biliup 未安装\"\n echo \"\"\n echo \"安装命令:\"\n echo \" pip3 install biliup --break-system-packages\"\n exit 1\nfi\n\n# 创建输出目录\nmkdir -p \"$(dirname \"$COOKIE_FILE\")\"\n\n# 在临时目录执行登录,cookies.json 写入临时目录\ncd \"$BILIUP_COOKIE_DIR\"\n\n# 执行扫码登录\necho \"请使用 B 站 APP 扫码:\"\necho \"\"\nbiliup login\n\n# 检查登录是否成功\nif [[ ! -f \"$BILIUP_COOKIE\" ]]; then\n echo \"\"\n echo \"❌ 登录失败,未找到 cookies.json\"\n exit 1\nfi\n\necho \"\"\necho \"✅ 登录成功\"\necho \"\"\n\n# 转换格式\necho \"🔄 转换 Cookies 格式...\"\npython3 \"$SCRIPT_DIR/convert-bili-cookie.py\" \"$BILIUP_COOKIE\" \"$COOKIE_FILE\"\n\nif [[ $? -eq 0 && -f \"$COOKIE_FILE\" ]]; then\n # 限制文件权限:仅所有者可读写\n chmod 600 \"$COOKIE_FILE\"\n echo \"✅ Cookies 已保存:$COOKIE_FILE(权限 600)\"\n echo \"\"\n echo \"📊 统计:\"\n wc -l \"$COOKIE_FILE\" | awk '{print \" 共 \" $1 \" 行\"}'\n echo \"\"\n echo \"⚠️ 临时 cookies.json 已自动清理\"\nelse\n echo \"❌ 格式转换失败\"\n exit 1\nfi\n\necho \"\"\necho \"================================\"\necho \"✅ 登录完成\"\necho \"================================\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":2014,"content_sha256":"7c71cca64f372fd6ec35e063840e2fb4c0ac63b9c8801f501cae3bb3e2329533"},{"filename":"scripts/check-config.sh","content":"#!/bin/bash\n# check-config.sh - 检查 video-summarizer 配置是否就绪\n# 用法:./check-config.sh\n# 版本:v1.0.13\n\nENV_FILE=\"$HOME/.openclaw/.env\"\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\nPASS=0\nFAIL=0\n\ncheck_env() {\n local var=$1 desc=$2 required=${3:-true}\n local value=$(grep \"^${var}=\" \"$ENV_FILE\" 2>/dev/null | cut -d'=' -f2- | tr -d '\"')\n if [[ -n \"$value\" ]]; then\n echo \"✅ $desc\"\n PASS=$((PASS + 1))\n return 0\n else\n if [[ \"$required\" == \"true\" ]]; then\n echo \"❌ $desc\"\n FAIL=$((FAIL + 1))\n return 1\n else\n echo \"⚠️ $desc (可选)\"\n return 0\n fi\n fi\n}\n\ncheck_py() {\n local pkg=$1 install=$2\n if python3 -c \"import $pkg\" &>/dev/null; then\n echo \"✅ $pkg\"\n PASS=$((PASS + 1))\n else\n echo \"❌ $pkg (运行:pip3 install $install)\"\n FAIL=$((FAIL + 1))\n fi\n}\n\necho \"=== 依赖工具 ===\"\ncommand -v yt-dlp &>/dev/null && { echo \"✅ yt-dlp ($(yt-dlp --version))\"; PASS=$((PASS + 1)); } || { echo \"❌ yt-dlp (运行:pip3 install yt-dlp)\"; FAIL=$((FAIL + 1)); }\ncommand -v ffmpeg &>/dev/null && { echo \"✅ ffmpeg\"; PASS=$((PASS + 1)); } || { echo \"❌ ffmpeg (运行:apt install ffmpeg)\"; FAIL=$((FAIL + 1)); }\ncommand -v python3 &>/dev/null && { echo \"✅ $(python3 --version)\"; PASS=$((PASS + 1)); } || { echo \"❌ python3\"; FAIL=$((FAIL + 1)); exit 1; }\n\necho \"\"\necho \"=== Python 依赖 ===\"\ncheck_py \"requests\" \"requests\"\ncheck_py \"oss2\" \"oss2\"\ncheck_py \"dotenv\" \"python-dotenv\"\n\necho \"\"\necho \"=== LLM AI 分析配置 ===\"\nCONFIG_OK=true\nLLM_CONFIGURED=false\n\nLLM_API_KEY=$(grep \"^LLM_API_KEY=\" \"$ENV_FILE\" 2>/dev/null | cut -d'=' -f2- | tr -d '\"' | tr -d \"'\")\nLLM_BASE_URL=$(grep \"^LLM_BASE_URL=\" \"$ENV_FILE\" 2>/dev/null | cut -d'=' -f2- | tr -d '\"' | tr -d \"'\")\nLLM_MODEL=$(grep \"^LLM_MODEL=\" \"$ENV_FILE\" 2>/dev/null | cut -d'=' -f2- | tr -d '\"' | tr -d \"'\")\n\nMISSING_VARS=\"\"\n[[ -z \"$LLM_API_KEY\" ]] && MISSING_VARS=\"$MISSING_VARS LLM_API_KEY\"\n[[ -z \"$LLM_BASE_URL\" ]] && MISSING_VARS=\"$MISSING_VARS LLM_BASE_URL\"\n[[ -z \"$LLM_MODEL\" ]] && MISSING_VARS=\"$MISSING_VARS LLM_MODEL\"\n\nif [[ -z \"$MISSING_VARS\" ]]; then\n echo \"✅ LLM_API_KEY(已配置)\"\n echo \"✅ LLM_BASE_URL: $LLM_BASE_URL\"\n echo \"✅ LLM_MODEL: $LLM_MODEL\"\n LLM_CONFIGURED=true\n PASS=$((PASS + 3))\nelse\n echo \"❌ LLM 配置不完整,缺少:$MISSING_VARS\"\n echo \" └─ 请在 ~/.openclaw/.env 中配置:\"\n echo \" LLM_API_KEY=your_api_key\"\n echo \" LLM_BASE_URL=https://api.deepseek.com\"\n echo \" LLM_MODEL=deepseek-v4-pro\"\n FAIL=$((FAIL + 1))\nfi\n\nif [[ \"$LLM_CONFIGURED\" != \"true\" ]]; then\n CONFIG_OK=false\nfi\n\necho \"\"\necho \"=== OSS 图床配置 ===\"\ncheck_env \"ALIYUN_OSS_AK\" \"阿里云 OSS AccessKey\" || CONFIG_OK=false\ncheck_env \"ALIYUN_OSS_SK\" \"阿里云 OSS Secret\" || CONFIG_OK=false\ncheck_env \"ALIYUN_OSS_BUCKET_ID\" \"阿里云 OSS Bucket\" || CONFIG_OK=false\ncheck_env \"ALIYUN_OSS_ENDPOINT\" \"阿里云 OSS Endpoint\" || CONFIG_OK=false\n\necho \"\"\necho \"=== 可选配置 ===\"\nif grep -q \"^GROQ_API_KEY=\" \"$ENV_FILE\" 2>/dev/null; then\n echo \"✅ Groq API Key\"\n echo \" └─ Plan B 可用 (Groq)\"\n PASS=$((PASS + 1))\nelse\n echo \"⚠️ Groq API Key (可选)\"\n echo \" └─ Plan B 需本地 Whisper (pip install openai-whisper)\"\nfi\n\nif grep -q \"^NOTION_API_KEY=\" \"$ENV_FILE\" 2>/dev/null; then\n echo \"✅ Notion API Key\"\n echo \" └─ Notion 推送可用\"\n PASS=$((PASS + 1))\nelse\n echo \"⚠️ Notion API Key (可选)\"\n echo \" └─ Notion 推送不可用\"\nfi\n\necho \"\"\necho \"=== Cookies ===\"\nCOOKIE_FILE=\"$HOME/.cookies/bilibili_cookies.txt\"\nif [[ -f \"$COOKIE_FILE\" ]]; then\n # 检查文件年龄\n COOKIE_AGE=$(( ($(date +%s) - $(stat -c %Y \"$COOKIE_FILE\" 2>/dev/null || echo $(date +%s))) / 86400 ))\n if [[ $COOKIE_AGE -lt 30 ]]; then\n echo \"✅ B 站 Cookies (已更新$COOKIE_AGE 天前)\"\n echo \" └─ Plan A 可用 (官方字幕)\"\n PASS=$((PASS + 1))\n elif [[ $COOKIE_AGE -lt 60 ]]; then\n echo \"⚠️ B 站 Cookies (已更新$COOKIE_AGE 天前,建议更新)\"\n echo \" └─ Plan A 可用,但可能即将过期\"\n else\n echo \"❌ B 站 Cookies (已更新$COOKIE_AGE 天前,很可能过期)\"\n echo \" └─ 建议扫码登录更新\"\n FAIL=$((FAIL + 1))\n fi\nelse\n echo \"⚠️ B 站 Cookies (可选)\"\n echo \" └─ Plan A 仅可用自动字幕,无字幕时降级 Plan B\"\nfi\n\n# 检查扫码登录工具\nif command -v biliup &>/dev/null; then\n echo \"✅ biliup (扫码登录工具)\"\n PASS=$((PASS + 1))\nelse\n echo \"⚠️ biliup 未安装 (扫码登录工具)\"\n echo \" └─ 安装:pip3 install biliup --break-system-packages\"\nfi\n\necho \"\"\necho \"================================\"\necho \"总计:$PASS 通过 | $FAIL 失败\"\necho \"================================\"\necho \"\"\n\nif [[ \"$CONFIG_OK\" == \"true\" && $FAIL -eq 0 ]]; then\n echo \"✅ 配置就绪,可以开始使用!\"\n echo \"\"\n echo \"快速开始:\"\n echo \" $SCRIPT_DIR/video-summarize.sh \\\"视频 URL\\\" /tmp/output\"\n echo \"\"\n echo \"选项:\"\n echo \" --verbose 显示详细日志\"\n echo \" --push 自动推送到 Notion\"\n echo \" --keep-video 保留视频文件\"\n echo \"\"\n echo \"📱 扫码登录 (更新 B 站 Cookies):\"\n echo \" $SCRIPT_DIR/bili-login.sh\"\n exit 0\nelse\n echo \"❌ 配置不完整,请修复上方标 ❌ 的项目\"\n echo \"\"\n echo \"修复建议:\"\n echo \" 1. 编辑配置文件:~/.openclaw/.env\"\n echo \" 2. 安装缺失依赖:pip3 install requests oss2 python-dotenv\"\n echo \" 3. 扫码登录:$SCRIPT_DIR/bili-login.sh\"\n echo \" 4. 重新运行检查:$SCRIPT_DIR/check-config.sh\"\n exit 1\nfi\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":5843,"content_sha256":"a6d44db2288664151202e2e04de7ca251bdb0e1c7f6805fe56955e406f08678c"},{"filename":"scripts/convert-bili-cookie.py","content":"#!/usr/bin/env python3\n\"\"\"\nconvert-bili-cookie.py - 转换 B 站 Cookies 格式\n从 biliup 的 JSON 格式转换为 yt-dlp 的 Netscape 格式\n\n用法:python3 convert-bili-cookie.py \u003c输入 JSON> \u003c输出 txt>\n\"\"\"\n\nimport sys\nimport json\nimport time\n\ndef convert_cookie(json_file: str, output_file: str):\n \"\"\"转换 Cookies 格式\"\"\"\n \n # 读取 JSON\n with open(json_file, 'r', encoding='utf-8') as f:\n data = json.load(f)\n \n # 提取 Cookies(支持多种格式)\n cookies_dict = {}\n \n # 格式 1: cookie_info.cookies 数组(biliup 新格式)\n cookie_info = data.get('cookie_info', {})\n if cookie_info and 'cookies' in cookie_info:\n for cookie in cookie_info['cookies']:\n name = cookie.get('name', '')\n value = cookie.get('value', '')\n if name and value:\n cookies_dict[name] = value\n \n # 格式 2: cookie 对象(旧格式)\n if not cookies_dict and 'cookie' in data:\n cookies_dict = data.get('cookie', {})\n \n # 格式 3: 直接就是 cookie 字典(兼容模式)\n if not cookies_dict:\n cookies_dict = data\n \n cookies = cookies_dict\n \n # Netscape Cookie 格式\n # domain flag path secure expiration name value\n lines = [\"# Netscape HTTP Cookie File\", \"# https://curl.haxx.se/docs/http-cookies.html\", \"# This file was generated by bili-login.sh\", \"\"]\n \n # B 站主要 Cookie 字段\n fields = ['SESSDATA', 'bili_jct', 'buvid3', 'DedeUserID', 'DedeUserID__ckMd5', 'sid', 'aclu', 'blackside', 'current_theme', 'fingerprint', 'buvid_fp', 'buvid4', 'home_feed_column', 'PVID', 'enable_web_push', 'CURRENT_FNVAL', 'CURRENT_QUALITY', 'bp_video_offset', 'video_guid', 'bp_article_offset', 'b_nut', 'b_lsid', 'rpdid']\n \n # 尝试从原始数据获取过期时间(biliup 新格式)\n cookie_info = data.get('cookie_info', {})\n cookie_list = cookie_info.get('cookies', [])\n expires_map = {}\n for cookie in cookie_list:\n name = cookie.get('name', '')\n expires = cookie.get('expires', 0)\n if name and expires:\n expires_map[name] = expires\n \n for field in fields:\n value = cookies.get(field, '')\n if value:\n # 使用原始过期时间,如果没有则使用默认值(90 天后)\n expire_time = expires_map.get(field, int(time.time()) + 7776000)\n # Netscape 格式\n line = f\".bilibili.com\\tTRUE\\t/\\tTRUE\\t{expire_time}\\t{field}\\t{value}\"\n lines.append(line)\n \n # 写入文件\n with open(output_file, 'w', encoding='utf-8') as f:\n f.write('\\n'.join(lines))\n \n return len(lines) - 4 # 减去注释行数\n\n\ndef main():\n if len(sys.argv) \u003c 3:\n print(\"用法:python3 convert-bili-cookie.py \u003c输入 JSON> \u003c输出 txt>\")\n sys.exit(1)\n \n json_file = sys.argv[1]\n output_file = sys.argv[2]\n \n try:\n count = convert_cookie(json_file, output_file)\n print(f\"✅ 转换成功 | {count} 个 Cookie\")\n except Exception as e:\n print(f\"❌ 转换失败:{e}\")\n sys.exit(1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":3174,"content_sha256":"298a5f6c7a8ecdb916aae9f05378ceef62c933cae611802925998cae5aeaaa7a"},{"filename":"scripts/douyin_downloader.py","content":"#!/usr/bin/env python3\n\"\"\"\n抖音无水印视频下载和文案提取工具\n\n功能:\n1. 从抖音分享链接获取无水印视频下载链接\n2. 下载视频并提取音频\n3. 提取文案并保存到文件 (一个视频一个文件夹)\n\n注意:\n- info/download 操作无需 API 密钥\n- extract 操作需要配置 GROQ_API_KEY 或使用本地 Whisper 转录\n\n使用示例:\n # 获取下载链接 (无需 API 密钥)\n python douyin_downloader.py --link \"抖音分享链接\" --action info\n\n # 下载视频\n python douyin_downloader.py --link \"抖音分享链接\" --action download --output ./videos\n\n # 提取文案并保存到文件 (需要 GROQ_API_KEY 环境变量)\n python douyin_downloader.py --link \"抖音分享链接\" --action extract --output ./output\n\"\"\"\n\nimport os\nimport re\nimport sys\nimport json\nimport argparse\nimport tempfile\nimport shutil\nfrom pathlib import Path\nfrom typing import Optional\nfrom datetime import datetime\n\n\ndef check_dependencies():\n \"\"\"检查必要的依赖是否已安装\"\"\"\n missing = []\n try:\n import requests\n except ImportError:\n missing.append(\"requests\")\n try:\n import ffmpeg\n except ImportError:\n missing.append(\"ffmpeg-python\")\n\n if missing:\n print(f\"缺少依赖:{', '.join(missing)}\")\n print(f\"请运行:pip install {' '.join(missing)}\")\n sys.exit(1)\n\n\ncheck_dependencies()\n\nimport requests\nimport ffmpeg\n\n# 请求头,模拟移动端访问\nHEADERS = {\n 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) EdgiOS/121.0.2277.107 Version/17.0 Mobile/15E148 Safari/604.1'\n}\n\n# Groq API 配置 (与 transcribe-audio.py 一致)\nDEFAULT_GROQ_API_URL = \"https://api.groq.com/openai/v1/audio/transcriptions\"\nDEFAULT_MODEL = \"whisper-large-v3\"\n\n\nclass DouyinProcessor:\n \"\"\"抖音视频处理器\"\"\"\n\n def __init__(self, api_key: str = \"\", api_base_url: Optional[str] = None, model: Optional[str] = None):\n self.api_key = api_key\n self.api_base_url = api_base_url or DEFAULT_GROQ_API_URL\n self.model = model or DEFAULT_MODEL\n self.temp_dir = Path(tempfile.mkdtemp())\n \n def _format_duration(self, seconds: float) -> str:\n \"\"\"格式化时长为 MM:SS 或 HH:MM:SS 格式\"\"\"\n if not seconds or seconds \u003c= 0:\n return \"Unknown\"\n hours = int(seconds // 3600)\n minutes = int((seconds % 3600) // 60)\n secs = int(seconds % 60)\n if hours > 0:\n return f\"{hours}:{minutes:02d}:{secs:02d}\"\n else:\n return f\"{minutes}:{secs:02d}\"\n\n def __del__(self):\n \"\"\"清理临时目录\"\"\"\n if hasattr(self, 'temp_dir') and self.temp_dir.exists():\n shutil.rmtree(self.temp_dir, ignore_errors=True)\n\n def parse_share_url(self, share_text: str) -> dict:\n \"\"\"从分享文本中提取无水印视频链接\"\"\"\n # 提取分享链接\n urls = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\\(\\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', share_text)\n if not urls:\n raise ValueError(\"未找到有效的分享链接\")\n\n share_url = urls[0]\n share_response = requests.get(share_url, headers=HEADERS)\n video_id = share_response.url.split(\"?\")[0].strip(\"/\").split(\"/\")[-1]\n share_url = f'https://www.iesdouyin.com/share/video/{video_id}'\n\n # 获取视频页面内容\n response = requests.get(share_url, headers=HEADERS)\n response.raise_for_status()\n\n pattern = re.compile(\n pattern=r\"window\\._ROUTER_DATA\\s*=\\s*(.*?)\u003c/script>\",\n flags=re.DOTALL,\n )\n find_res = pattern.search(response.text)\n\n if not find_res or not find_res.group(1):\n raise ValueError(\"从 HTML 中解析视频信息失败\")\n\n # 解析 JSON 数据\n json_data = json.loads(find_res.group(1).strip())\n VIDEO_ID_PAGE_KEY = \"video_(id)/page\"\n NOTE_ID_PAGE_KEY = \"note_(id)/page\"\n\n if VIDEO_ID_PAGE_KEY in json_data[\"loaderData\"]:\n original_video_info = json_data[\"loaderData\"][VIDEO_ID_PAGE_KEY][\"videoInfoRes\"]\n elif NOTE_ID_PAGE_KEY in json_data[\"loaderData\"]:\n original_video_info = json_data[\"loaderData\"][NOTE_ID_PAGE_KEY][\"videoInfoRes\"]\n else:\n raise Exception(\"无法从 JSON 中解析视频或图集信息\")\n\n data = original_video_info[\"item_list\"][0]\n\n # 获取视频信息\n video_url = data[\"video\"][\"play_addr\"][\"url_list\"][0].replace(\"playwm\", \"play\")\n desc = data.get(\"desc\", \"\").strip() or f\"douyin_{video_id}\"\n \n # 获取作者信息\n author_info = data.get(\"author\", {})\n author_nickname = author_info.get(\"nickname\", \"抖音用户\")\n \n # 获取视频时长(毫秒转秒)\n duration_ms = data.get(\"video\", {}).get(\"duration\", 0)\n duration_sec = duration_ms / 1000 if duration_ms else 0\n \n # 获取封面图\n cover_url = data.get(\"video\", {}).get(\"cover\", {}).get(\"url_list\", [\"\"])[0] if data.get(\"video\", {}).get(\"cover\") else \"\"\n \n # 获取发布时间戳\n create_time = data.get(\"create_time\", 0)\n \n # 替换文件名中的非法字符\n desc = re.sub(r'[\\\\/:*?\"\u003c>|]', '_', desc)\n\n return {\n \"url\": video_url,\n \"title\": desc,\n \"video_id\": video_id,\n \"author\": author_nickname,\n \"duration\": duration_sec,\n \"duration_string\": self._format_duration(duration_sec),\n \"cover\": cover_url,\n \"create_time\": create_time\n }\n\n def download_video(self, video_info: dict, output_dir: Optional[Path] = None, show_progress: bool = True) -> Path:\n \"\"\"下载视频\"\"\"\n if output_dir is None:\n output_dir = self.temp_dir\n else:\n output_dir = Path(output_dir)\n output_dir.mkdir(parents=True, exist_ok=True)\n\n filename = f\"{video_info['video_id']}.mp4\"\n filepath = output_dir / filename\n\n if show_progress:\n print(f\"正在下载视频:{video_info['title']}\")\n\n response = requests.get(video_info['url'], headers=HEADERS, stream=True)\n response.raise_for_status()\n\n # 获取文件大小\n total_size = int(response.headers.get('content-length', 0))\n\n # 下载文件\n downloaded = 0\n with open(filepath, 'wb') as f:\n for chunk in response.iter_content(chunk_size=8192):\n if chunk:\n f.write(chunk)\n downloaded += len(chunk)\n if show_progress and total_size > 0:\n progress = downloaded / total_size * 100\n print(f\"\\r下载进度:{progress:.1f}%\", end=\"\", flush=True)\n\n if show_progress:\n print(f\"\\n视频下载完成:{filepath}\")\n return filepath\n\n def extract_audio(self, video_path: Path, show_progress: bool = True) -> Path:\n \"\"\"从视频文件中提取音频\"\"\"\n audio_path = video_path.with_suffix('.mp3')\n\n if show_progress:\n print(\"正在提取音频...\")\n try:\n (\n ffmpeg\n .input(str(video_path))\n .output(str(audio_path), acodec='libmp3lame', q=0)\n .run(capture_stdout=True, capture_stderr=True, overwrite_output=True)\n )\n if show_progress:\n print(f\"音频提取完成:{audio_path}\")\n return audio_path\n except Exception as e:\n raise Exception(f\"提取音频时出错:{str(e)}\")\n\n def get_audio_info(self, audio_path: Path) -> dict:\n \"\"\"获取音频文件信息(时长和大小)\"\"\"\n try:\n probe = ffmpeg.probe(str(audio_path))\n duration = float(probe['format'].get('duration', 0))\n size = audio_path.stat().st_size\n return {'duration': duration, 'size': size}\n except Exception:\n return {'duration': 0, 'size': audio_path.stat().st_size}\n\n def split_audio(self, audio_path: Path, segment_duration: int = 600, show_progress: bool = True) -> list:\n \"\"\"\n 将音频分割成多个片段\n\n 参数:\n audio_path: 音频文件路径\n segment_duration: 每段时长(秒),默认 10 分钟\n show_progress: 是否显示进度\n\n 返回:\n 分割后的音频文件路径列表\n \"\"\"\n audio_info = self.get_audio_info(audio_path)\n duration = audio_info['duration']\n\n if duration \u003c= segment_duration:\n return [audio_path]\n\n segments = []\n segment_index = 0\n current_time = 0\n\n if show_progress:\n total_segments = int(duration / segment_duration) + 1\n print(f\"音频时长 {duration:.0f} 秒,将分割为 {total_segments} 段...\")\n\n while current_time \u003c duration:\n segment_path = self.temp_dir / f\"segment_{segment_index}.mp3\"\n\n try:\n (\n ffmpeg\n .input(str(audio_path), ss=current_time, t=segment_duration)\n .output(str(segment_path), acodec='libmp3lame', q=0)\n .run(capture_stdout=True, capture_stderr=True, overwrite_output=True)\n )\n segments.append(segment_path)\n\n if show_progress:\n print(f\" 分割片段 {segment_index + 1}: {current_time:.0f}s - {min(current_time + segment_duration, duration):.0f}s\")\n\n except Exception as e:\n raise Exception(f\"分割音频片段 {segment_index} 时出错:{str(e)}\")\n\n current_time += segment_duration\n segment_index += 1\n\n return segments\n\n def transcribe_with_groq(self, audio_path: Path) -> str:\n \"\"\"使用 Groq API 转录单个音频文件\"\"\"\n headers = {\"Authorization\": f\"Bearer {self.api_key}\"}\n \n with open(audio_path, 'rb') as f:\n files = {\"file\": f}\n data = {\"model\": self.model, \"response_format\": \"verbose_json\"}\n response = requests.post(self.api_base_url, headers=headers, files=files, data=data, timeout=600)\n \n if response.status_code == 200:\n result = response.json()\n return result.get('text', '')\n else:\n raise Exception(f\"Groq API 错误:{response.status_code} - {response.text[:200]}\")\n\n def transcribe_with_faster_whisper(self, audio_path: Path) -> str:\n \"\"\"使用 Faster-Whisper 本地转录(降级方案)\"\"\"\n try:\n from faster_whisper import WhisperModel\n \n # GPU 检测\n try:\n import subprocess\n result = subprocess.run(\n ['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],\n capture_output=True, text=True, timeout=10\n )\n if result.returncode == 0:\n lines = result.stdout.strip().split('\\n')\n if lines:\n parts = lines[0].split(', ')\n gpu_name = parts[0]\n vram_gb = int(parts[1]) / 1024\n if vram_gb >= 8:\n model_size, device = 'large-v2', 'cuda'\n elif vram_gb >= 4:\n model_size, device = 'medium', 'cuda'\n elif vram_gb >= 2:\n model_size, device = 'small', 'cuda'\n else:\n model_size, device = 'base', 'cuda'\n print(f\" 🖥️ GPU: {gpu_name} ({vram_gb:.1f}GB) | 模型:{model_size}\")\n else:\n model_size, device = 'base', 'cpu'\n print(\" 🖥️ CPU (无 GPU)\")\n else:\n model_size, device = 'base', 'cpu'\n print(\" 🖥️ CPU (无 GPU)\")\n except:\n model_size, device = 'base', 'cpu'\n print(\" 🖥️ CPU (无 GPU)\")\n \n compute_type = \"float16\" if device == \"cuda\" else \"int8\"\n model = WhisperModel(model_size, device=device, compute_type=compute_type)\n \n segments, info = model.transcribe(\n str(audio_path), \n language='zh', \n vad_filter=True,\n initial_prompt=\"请添加适当的标点符号,使文本更易读。\"\n )\n \n return ''.join(seg.text for seg in segments)\n \n except ImportError:\n raise Exception(\"Faster-Whisper 未安装,运行:pip install faster-whisper\")\n except Exception as e:\n raise Exception(f\"Faster-Whisper 失败:{str(e)}\")\n\n def extract_text_from_audio(self, audio_path: Path, show_progress: bool = True) -> str:\n \"\"\"从音频文件中提取文字(Groq API 优先,本地降级)\"\"\"\n if not self.api_key:\n # 无 API key 时使用本地 Faster-Whisper\n if show_progress:\n print(\"未配置 GROQ_API_KEY,使用本地 Faster-Whisper 转录...\")\n return self.transcribe_with_faster_whisper(audio_path)\n \n # 检查文件大小和时长\n audio_info = self.get_audio_info(audio_path)\n max_duration = 3600 # 1 小时\n max_size = 50 * 1024 * 1024 # 50MB\n\n # 判断是否需要分段\n need_split = audio_info['duration'] > max_duration or audio_info['size'] > max_size\n\n if not need_split:\n # 文件在限制范围内,直接处理\n if show_progress:\n print(\"正在使用 Groq API 识别语音...\")\n return self.transcribe_with_groq(audio_path)\n\n # 需要分段处理\n if show_progress:\n print(f\"音频文件较大(时长:{audio_info['duration']:.0f}秒,大小:{audio_info['size'] / 1024 / 1024:.1f}MB)\")\n print(\"将自动分段处理...\")\n\n # 分割音频\n segments = self.split_audio(audio_path, segment_duration=540, show_progress=show_progress) # 9 分钟一段,留余量\n\n # 逐段转录\n all_texts = []\n for i, segment_path in enumerate(segments):\n if show_progress:\n print(f\"正在识别第 {i + 1}/{len(segments)} 段...\")\n\n text = self.transcribe_with_groq(segment_path)\n all_texts.append(text)\n\n # 清理分段文件\n if segment_path != audio_path:\n self.cleanup_files(segment_path)\n\n # 合并文本\n merged_text = ''.join(all_texts)\n\n if show_progress:\n print(f\"语音识别完成,共处理 {len(segments)} 个片段\")\n\n return merged_text\n\n def cleanup_files(self, *file_paths: Path):\n \"\"\"清理指定的文件\"\"\"\n for file_path in file_paths:\n if file_path.exists():\n file_path.unlink()\n\n\ndef get_video_info(share_link: str) -> dict:\n \"\"\"获取视频信息和下载链接\"\"\"\n processor = DouyinProcessor()\n return processor.parse_share_url(share_link)\n\n\ndef download_video(share_link: str, output_dir: str = \".\") -> Path:\n \"\"\"下载视频到指定目录\"\"\"\n processor = DouyinProcessor()\n video_info = processor.parse_share_url(share_link)\n return processor.download_video(video_info, Path(output_dir))\n\n\ndef extract_text(share_link: str, api_key: Optional[str] = None, output_dir: Optional[str] = None,\n save_video: bool = False, show_progress: bool = True) -> dict:\n \"\"\"\n 从视频中提取文案并保存到文件\n\n 返回:\n dict: 包含 video_info, text, output_path 的字典\n \"\"\"\n # 使用 GROQ_API_KEY 环境变量或传入的 api_key\n api_key = api_key or os.getenv('GROQ_API_KEY')\n # 不强制要求 API key,transcribe 方法会自动降级到本地\n\n processor = DouyinProcessor(api_key)\n\n if show_progress:\n print(\"正在解析抖音分享链接...\")\n video_info = processor.parse_share_url(share_link)\n\n if show_progress:\n print(\"正在下载视频...\")\n video_path = processor.download_video(video_info, show_progress=show_progress)\n\n if show_progress:\n print(\"正在提取音频...\")\n audio_path = processor.extract_audio(video_path, show_progress=show_progress)\n\n if show_progress:\n print(\"正在从音频中提取文本...\")\n text_content = processor.extract_text_from_audio(audio_path, show_progress=show_progress)\n\n result = {\n \"video_info\": video_info,\n \"text\": text_content,\n \"output_path\": None\n }\n\n # 保存到文件\n if output_dir:\n output_base = Path(output_dir)\n video_folder = output_base / video_info['video_id']\n video_folder.mkdir(parents=True, exist_ok=True)\n\n # 保存文案为 Markdown 格式\n transcript_path = video_folder / \"transcript.md\"\n with open(transcript_path, 'w', encoding='utf-8') as f:\n f.write(f\"# {video_info['title']}\\n\\n\")\n f.write(f\"| 属性 | 值 |\\n\")\n f.write(f\"|------|----|\\n\")\n f.write(f\"| 视频 ID | `{video_info['video_id']}` |\\n\")\n f.write(f\"| 提取时间 | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |\\n\")\n f.write(f\"| 下载链接 | [点击下载]({video_info['url']}) |\\n\\n\")\n f.write(f\"---\\n\\n\")\n f.write(f\"## 文案内容\\n\\n\")\n f.write(text_content)\n\n result[\"output_path\"] = str(video_folder)\n\n if show_progress:\n print(f\"文案已保存到:{transcript_path}\")\n\n # 保存视频 (可选)\n if save_video:\n saved_video_path = video_folder / f\"{video_info['video_id']}.mp4\"\n shutil.copy2(video_path, saved_video_path)\n if show_progress:\n print(f\"视频已保存到:{saved_video_path}\")\n\n # 清理临时文件\n if show_progress:\n print(\"正在清理临时文件...\")\n processor.cleanup_files(video_path, audio_path)\n\n return result\n\n\ndef main():\n parser = argparse.ArgumentParser(\n description=\"抖音无水印视频下载和文案提取工具\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\n示例:\n # 获取视频信息和下载链接\n python douyin_downloader.py --link \"抖音分享链接\" --action info\n\n # 下载视频\n python douyin_downloader.py --link \"抖音分享链接\" --action download --output ./videos\n\n # 提取文案并保存到文件 (需要设置 GROQ_API_KEY 环境变量)\n python douyin_downloader.py --link \"抖音分享链接\" --action extract --output ./output\n\n # 提取文案并同时保存视频\n python douyin_downloader.py --link \"抖音分享链接\" --action extract --output ./output --save-video\n \"\"\"\n )\n\n parser.add_argument(\"--link\", \"-l\", required=True, help=\"抖音分享链接或包含链接的文本\")\n parser.add_argument(\"--action\", \"-a\", choices=[\"info\", \"download\", \"extract\"],\n default=\"info\", help=\"操作类型:info(获取信息), download(下载视频), extract(提取文案)\")\n parser.add_argument(\"--output\", \"-o\", default=\"./output\", help=\"输出目录 (默认 ./output)\")\n parser.add_argument(\"--api-key\", \"-k\", help=\"Groq API 密钥 (也可通过 GROQ_API_KEY 环境变量设置)\")\n parser.add_argument(\"--save-video\", \"-v\", action=\"store_true\", help=\"提取文案时同时保存视频\")\n parser.add_argument(\"--quiet\", \"-q\", action=\"store_true\", help=\"安静模式,减少输出\")\n parser.add_argument(\"--json\", action=\"store_true\", help=\"输出 JSON 格式(便于脚本解析)\")\n\n args = parser.parse_args()\n\n try:\n if args.action == \"info\":\n info = get_video_info(args.link)\n \n if args.json:\n # 输出 JSON 格式(便于脚本解析)\n output = {\n \"video_id\": info.get(\"video_id\", \"\"),\n \"title\": info.get(\"title\", \"\"),\n \"url\": info.get(\"url\", \"\"),\n \"author\": info.get(\"author\", \"抖音用户\"),\n \"duration_string\": info.get(\"duration_string\", \"Unknown\"),\n \"cover\": info.get(\"cover\", \"\"),\n \"create_time\": info.get(\"create_time\", 0),\n \"upload_date\": datetime.fromtimestamp(info[\"create_time\"]).strftime(\"%Y%m%d\") if info.get(\"create_time\") else \"\"\n }\n import json\n print(json.dumps(output, ensure_ascii=False, indent=2))\n else:\n # 输出人类可读格式\n print(\"\\n\" + \"=\" * 50)\n print(\"视频信息:\")\n print(\"=\" * 50)\n print(f\"视频 ID: {info['video_id']}\")\n print(f\"标题:{info['title']}\")\n print(f\"下载链接:{info['url']}\")\n print(f\"作者:{info.get('author', '抖音用户')}\")\n print(f\"时长:{info.get('duration_string', 'Unknown')}\")\n print(f\"封面:{info.get('cover', '' )}\")\n create_time = info.get('create_time', 0)\n if create_time:\n pub_date = datetime.fromtimestamp(create_time).strftime('%Y%m%d')\n pub_datetime = datetime.fromtimestamp(create_time).isoformat()\n print(f\"发布时间:{pub_date} ({pub_datetime})\")\n else:\n print(f\"发布时间:Unknown\")\n print(\"=\" * 50)\n\n elif args.action == \"download\":\n video_path = download_video(args.link, args.output)\n print(f\"\\n视频已保存到:{video_path}\")\n\n elif args.action == \"extract\":\n result = extract_text(\n args.link,\n args.api_key,\n output_dir=args.output,\n save_video=args.save_video,\n show_progress=not args.quiet\n )\n\n if not args.quiet:\n print(\"\\n\" + \"=\" * 50)\n print(\"提取完成!\")\n print(\"=\" * 50)\n print(f\"视频 ID: {result['video_info']['video_id']}\")\n print(f\"标题:{result['video_info']['title']}\")\n if result['output_path']:\n print(f\"保存位置:{result['output_path']}\")\n print(\"=\" * 50)\n print(\"\\n文案内容:\\n\")\n print(result['text'][:500] + \"...\" if len(result['text']) > 500 else result['text'])\n print(\"\\n\" + \"=\" * 50)\n\n except Exception as e:\n print(f\"\\n错误:{e}\", file=sys.stderr)\n sys.exit(1)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":23028,"content_sha256":"3a1cdb034f4e6c80a75db8064b26424e63edfa65008dbf307505897da26f357f"},{"filename":"scripts/download-audio.sh","content":"#!/bin/bash\n# download-audio.sh - Plan B: 下载音频用于语音转录\n# 用法:./download-audio.sh \u003c视频 URL> [输出文件]\n\nset -e\n\nVIDEO_URL=\"$1\"\nOUTPUT_FILE=\"${2:-/tmp/audio.mp3}\"\n\n# 输入校验\nif [[ -z \"$VIDEO_URL\" ]]; then\n echo \"❌ 视频 URL 为空\" >&2\n exit 1\nfi\n\nif [[ ${#VIDEO_URL} -gt 2048 ]]; then\n echo \"❌ URL 过长\" >&2\n exit 1\nfi\n\n# 排除 shell 元字符\nif [[ \"$VIDEO_URL\" =~ [\\;\\|\\&\\(\\)\\{\\}\\`\\$\\\u003c\\>] ]]; then\n echo \"❌ URL 包含非法字符\" >&2\n exit 1\nfi\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# 日志级别函数\nlog_info() { echo \"ℹ️ $*\"; }\nlog_warn() { echo \"⚠️ $*\"; }\nlog_error() { echo \"❌ $*\"; }\nlog_success() { echo \"✅ $*\"; }\n\nlog_info \"下载音频 (Plan B)...\"\n\n# 检测平台\ndetect_platform() {\n if [[ \"$VIDEO_URL\" =~ (xiaohongshu\\.com|xhslink\\.com) ]]; then\n echo \"xhs\"\n elif [[ \"$VIDEO_URL\" =~ (douyin\\.com|iesdouyin\\.com|v\\.douyin\\.com) ]]; then\n echo \"douyin\"\n else\n echo \"other\"\n fi\n}\n\nPLATFORM=$(detect_platform)\n\n# 抖音平台特殊处理:使用专用下载工具(优先)\nif [[ \"$PLATFORM\" == \"douyin\" ]]; then\n DOUYIN_SCRIPT=\"$SCRIPT_DIR/douyin_downloader.py\"\n \n if [[ -f \"$DOUYIN_SCRIPT\" ]]; then\n log_info \"抖音平台:使用专用工具下载...\"\n \n # 获取下载链接\n VIDEO_INFO=$(python3 \"$DOUYIN_SCRIPT\" --link \"$VIDEO_URL\" --action info 2>&1)\n DOWNLOAD_URL=$(echo \"$VIDEO_INFO\" | grep \"下载链接\" | sed 's/下载链接://')\n \n if [[ -n \"$DOWNLOAD_URL\" ]]; then\n TEMP_VIDEO=\"/tmp/douyin_temp_$.mp4\"\n \n # 下载视频\n if curl -sL -o \"$TEMP_VIDEO\" \"$DOWNLOAD_URL\"; then\n log_success \"视频下载成功,提取音频...\"\n \n # 提取音频\n if ffmpeg -i \"$TEMP_VIDEO\" -vn -acodec libmp3lame -ab 128k \"$OUTPUT_FILE\" -y 2>/dev/null; then\n rm -f \"$TEMP_VIDEO\"\n log_success \"音频下载完成\"\n exit 0 # 成功则直接退出,不执行 yt-dlp\n else\n log_warn \"音频提取失败,回退到 yt-dlp\"\n fi\n else\n log_warn \"视频下载失败,回退到 yt-dlp\"\n fi\n rm -f \"$TEMP_VIDEO\" 2>/dev/null\n fi\n fi\nfi\n\n# 尝试 1: 根据平台选择最佳格式\nlog_info \"尝试 1/3: 下载分离音轨...\"\nif [[ \"$PLATFORM\" == \"xhs\" || \"$PLATFORM\" == \"douyin\" ]]; then\n # 小红书/抖音:使用 best 格式(这些平台可能没有单独的音频流)\n if yt-dlp -f \"best\" -x --audio-format mp3 -o \"$OUTPUT_FILE\" \"$VIDEO_URL\" 2>&1 && \\\n [[ -f \"$OUTPUT_FILE\" ]]; then\n log_success \"音频下载完成\"\n exit 0\n fi\nelse\n # 其他平台:优先音频流\n if yt-dlp -f \"bestaudio\" -x --audio-format mp3 -o \"$OUTPUT_FILE\" \"$VIDEO_URL\" 2>&1 && \\\n [[ -f \"$OUTPUT_FILE\" ]]; then\n log_success \"音频下载完成\"\n exit 0\n fi\nfi\nrm -f \"$OUTPUT_FILE\" 2>/dev/null\n\n# 尝试 2: 下载视频并提取音频(通用降级)\nlog_info \"尝试 2/3: 下载视频提取音频...\"\nTEMP_VIDEO=\"/tmp/video_temp_$\"\nif [[ \"$PLATFORM\" == \"xhs\" || \"$PLATFORM\" == \"douyin\" ]]; then\n # 小红书/抖音:不限制高度,使用最佳视频\n if yt-dlp -f \"best\" -o \"$TEMP_VIDEO.mp4\" \"$VIDEO_URL\" 2>&1 && \\\n ffmpeg -i \"$TEMP_VIDEO.mp4\" -vn -acodec libmp3lame -ab 128k \"$OUTPUT_FILE\" -y 2>/dev/null; then\n rm -f \"$TEMP_VIDEO.mp4\"\n if [[ -f \"$OUTPUT_FILE\" ]]; then\n log_success \"音频下载完成\"\n exit 0\n fi\n fi\nelse\n # 其他平台:限制高度以加快下载\n if yt-dlp -f \"best[height\u003c=480]\" -o \"$TEMP_VIDEO.mp4\" \"$VIDEO_URL\" 2>&1 && \\\n ffmpeg -i \"$TEMP_VIDEO.mp4\" -vn -acodec libmp3lame -ab 128k \"$OUTPUT_FILE\" -y 2>/dev/null; then\n rm -f \"$TEMP_VIDEO.mp4\"\n if [[ -f \"$OUTPUT_FILE\" ]]; then\n log_success \"音频下载完成\"\n exit 0\n fi\n fi\nfi\nrm -f \"$TEMP_VIDEO.mp4\" 2>/dev/null\n\n# 尝试 3: 强制下载(最后手段)\nlog_info \"尝试 3/3: 强制下载...\"\nif yt-dlp --format-sort \"res:desc\" -o \"$TEMP_VIDEO.mp4\" \"$VIDEO_URL\" 2>&1; then\n if ffmpeg -i \"$TEMP_VIDEO.mp4\" -vn -acodec libmp3lame -ab 128k \"$OUTPUT_FILE\" -y 2>/dev/null; then\n rm -f \"$TEMP_VIDEO.mp4\"\n if [[ -f \"$OUTPUT_FILE\" ]]; then\n log_success \"音频下载完成\"\n exit 0\n fi\n fi\nfi\nrm -f \"$TEMP_VIDEO.mp4\" 2>/dev/null\n\nlog_error \"音频下载失败(所有尝试均失败)\"\nexit 1\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":4656,"content_sha256":"db8cf14b636fba10c24215fce7ea87fcee9d6cd475c8a1646b26153f03a6626d"},{"filename":"scripts/llm_client.py","content":"#!/usr/bin/env python3\n\"\"\"\n多平台 LLM 客户端(OpenAI 兼容接口)。\n通过 LLM_API_KEY + LLM_BASE_URL + LLM_MODEL 三个环境变量统一配置,\n适配 DeepSeek / DashScope / OpenAI / Groq 等任意 OpenAI 兼容平台。\n\n用法:\n from llm_client import LLMClient\n client = LLMClient.from_env()\n result = client.chat(messages=[{'role': 'user', 'content': '你好'}])\n\n版本:v1.0.13\n\"\"\"\n\nimport os\nimport time\nimport sys\nfrom pathlib import Path\n\nimport requests\n\n# 可重试的 HTTP 状态码\nRETRYABLE_STATUS = {429, 500, 502, 503, 504}\n\n\nclass LLMClient:\n \"\"\"OpenAI-compatible 多平台 LLM 客户端。\n\n 环境变量要求(三者缺一不可):\n LLM_API_KEY API 密钥\n LLM_BASE_URL API 基础地址(例:https://api.deepseek.com)\n LLM_MODEL 模型名称(例:deepseek-v4-pro)\n \"\"\"\n\n def __init__(self, api_key: str, base_url: str, model: str,\n max_retries: int = 3, timeout_base: int = 300):\n self.api_key = api_key\n self.base_url = base_url.rstrip('/')\n self.model = model\n self.max_retries = max_retries\n self.timeout_base = timeout_base\n\n # ---- 工厂方法 ----\n\n @classmethod\n def from_env(cls):\n \"\"\"从环境变量构建客户端。读取 ~/.openclaw/.env。\"\"\"\n from dotenv import load_dotenv\n load_dotenv(Path.home() / '.openclaw' / '.env')\n\n api_key = os.getenv('LLM_API_KEY', '').strip()\n base_url = os.getenv('LLM_BASE_URL', '').strip()\n model = os.getenv('LLM_MODEL', '').strip()\n\n missing = []\n if not api_key:\n missing.append('LLM_API_KEY')\n if not base_url:\n missing.append('LLM_BASE_URL')\n if not model:\n missing.append('LLM_MODEL')\n\n if missing:\n raise ValueError(\n f\"\\n❌ 缺少 LLM 配置环境变量:{', '.join(missing)}\\n\"\n f\"\\n请在 ~/.openclaw/.env 中配置:\\n\"\n f\" LLM_API_KEY=your_api_key\\n\"\n f\" LLM_BASE_URL=https://api.deepseek.com\\n\"\n f\" LLM_MODEL=deepseek-v4-pro\\n\"\n )\n\n return cls(api_key, base_url, model)\n\n # ---- 核心 API ----\n\n def chat(self, messages: list[dict], system_prompt: str | None = None,\n temperature: float = 0.7, max_tokens: int | None = None) -> str | None:\n \"\"\"\n OpenAI-compatible /chat/completions。\n\n 请求:POST {base_url}/chat/completions\n 返回:AI 响应文本,失败返回 None。\n \"\"\"\n if system_prompt:\n full_msgs = [{'role': 'system', 'content': system_prompt}] + list(messages)\n else:\n full_msgs = list(messages)\n\n headers = {\n 'Authorization': f'Bearer {self.api_key}',\n 'Content-Type': 'application/json',\n }\n\n body = {\n 'model': self.model,\n 'messages': full_msgs,\n 'stream': False,\n }\n if temperature is not None:\n body['temperature'] = temperature\n if max_tokens is not None:\n body['max_tokens'] = max_tokens\n\n endpoint = f'{self.base_url}/chat/completions'\n last_error: Exception | None = None\n\n for attempt in range(self.max_retries):\n try:\n timeout = self.timeout_base * (attempt + 1)\n print(f\" 尝试 {attempt + 1}/{self.max_retries} (超时:{timeout}s)...\")\n\n response = requests.post(\n endpoint, headers=headers, json=body, timeout=timeout,\n )\n\n if response.status_code == 200:\n return response.json()['choices'][0]['message']['content']\n\n if response.status_code in RETRYABLE_STATUS:\n wait_s = 2 ** attempt\n print(f\" ⚠️ HTTP {response.status_code},{wait_s}s 后重试...\")\n time.sleep(wait_s)\n continue\n\n print(f\" ❌ HTTP {response.status_code}: {response.text[:300]}\")\n return None\n\n except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:\n print(f\" ⚠️ {type(e).__name__},准备重试...\")\n last_error = e\n if attempt \u003c self.max_retries - 1:\n time.sleep(2 ** attempt)\n\n if last_error:\n print(f\" ❌ 重试耗尽:{last_error}\")\n return None\n\n # ---- 便捷方法 ----\n\n def analyze_simple(self, system_prompt: str, user_prompt: str) -> str | None:\n \"\"\"简化调用:system + user 单轮对话。\"\"\"\n return self.chat(\n messages=[{'role': 'user', 'content': user_prompt}],\n system_prompt=system_prompt,\n )\n\n def __repr__(self) -> str:\n return f\"LLMClient(model={self.model}, base_url={self.base_url})\"\n\n\n# ============ 独立测试入口 ============\nif __name__ == '__main__':\n try:\n client = LLMClient.from_env()\n except ValueError as e:\n print(e, file=sys.stderr)\n sys.exit(1)\n\n print(f\"🚀 {client}\")\n print()\n\n result = client.chat(\n messages=[{'role': 'user', 'content': '请用一句话介绍你自己'}],\n )\n\n if result:\n print(f\"✅ 响应: {result}\")\n else:\n print(\"❌ 调用失败\")\n sys.exit(1)\n","content_type":"text/x-python; charset=utf-8","language":"python","size":5417,"content_sha256":"8c58c00a33df8941355c3805ca9a9171f7039781a3b4ae60cb95c91d71ffdb31"},{"filename":"scripts/push-to-notion.py","content":"#!/usr/bin/env python3\n\"\"\"\npush-to-notion.py - 将视频总结推送到 Notion\n用法:python3 push-to-notion.py \u003csummary.md> [Notion Database ID]\n\n版本:v1.0.13\n\"\"\"\n\nimport sys\nimport os\nimport re\nimport json\nimport requests\nfrom pathlib import Path\nfrom datetime import datetime\nfrom dotenv import load_dotenv\n\n# 读取环境变量\nload_dotenv(Path.home() / '.openclaw' / '.env')\n\nNOTION_API_KEY = os.getenv('NOTION_API_KEY')\nNOTION_DATABASE_ID = os.getenv('NOTION_VIDEO_SUMMARY_DATABASE_ID')\n\nif not NOTION_API_KEY:\n print(\"❌ 错误:缺少 NOTION_API_KEY\")\n print(\"说明:Notion 推送为可选功能,仅在 --push 模式时需要\")\n print(\"解决方案:\")\n print(\" 1. 配置环境变量:在 ~/.openclaw/.env 中添加 NOTION_API_KEY\")\n print(\" 2. 或者不使用 --push 参数,手动处理 Markdown 文件\")\n print(\" 3. 获取 Notion API Key: https://www.notion.so/my-integrations\")\n sys.exit(1)\n\n# Notion API 配置\nNOTION_VERSION = \"2025-09-03\"\nHEADERS = {\n \"Authorization\": f\"Bearer {NOTION_API_KEY}\",\n \"Notion-Version\": NOTION_VERSION,\n \"Content-Type\": \"application/json\"\n}\n\n\ndef parse_markdown(md_file):\n \"\"\"解析 Markdown 文件,提取关键信息\"\"\"\n md_dir = Path(md_file).parent\n metadata_file = md_dir / \"metadata.json\"\n \n # 优先从 metadata.json 获取元数据\n metadata = {}\n if metadata_file.exists():\n try:\n with open(metadata_file, 'r', encoding='utf-8') as f:\n metadata = json.load(f)\n except:\n pass\n \n # 第一步:确定平台来源(最高优先级)\n # 1. 从 metadata.json 的 platform 字段\n # 2. 从视频 URL 推断\n platform = metadata.get('platform', '')\n video_url = metadata.get('webpage_url', '')\n \n # 平台映射\n platform_map = {\n 'douyin': '抖音',\n 'bilibili': 'Bilibili',\n 'xiaohongshu': '小红书',\n 'youtube': 'YouTube'\n }\n source = platform_map.get(platform, 'Unknown')\n \n # 如果 metadata 中没有 platform,从 URL 推断\n if source == 'Unknown' and video_url:\n if \"bilibili.com\" in video_url or \"b23.tv\" in video_url:\n source = \"Bilibili\"\n elif \"xiaohongshu.com\" in video_url:\n source = \"小红书\"\n elif \"douyin.com\" in video_url or \"iesdouyin.com\" in video_url:\n source = \"抖音\"\n elif \"youtube.com\" in video_url or \"youtu.be\" in video_url:\n source = \"YouTube\"\n \n with open(md_file, 'r', encoding='utf-8') as f:\n content = f.read()\n \n # ========== 按平台分支处理所有提取要素 ==========\n \n # 初始化变量\n title = \"\"\n note = \"\"\n tags = []\n author = \"\"\n duration = \"\"\n publish_date = \"\"\n cover_url = \"\"\n \n # ========== Bilibili 分支 ==========\n if source == 'Bilibili':\n # 标题:从 Markdown 提取并清理 B 站特有的标签格式\n title_match = re.search(r'^#\\s+(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n raw_title = title_match.group(1).strip() if title_match else \"视频总结\"\n # B 站标题:移除 #标签 格式\n title = re.sub(r'#[A-Za-z0-9\\u4e00-\\u9fa5]+(?:\\s+[A-Za-z0-9\\u4e00-\\u9fa5]+)*', '', raw_title)\n title = ' '.join(title.split()).strip()\n # 截断过长的标题\n if len(title) > 40:\n if '【' in title:\n brackets = re.findall(r'[【](.*?)[】]', title)\n main_title = re.split(r'[【]', title)[0].strip()\n if main_title and brackets:\n main_clauses = re.split(r'[!!!]', main_title)\n short_main = main_clauses[0].strip()\n if len(short_main) > 25:\n short_main = re.split(r'[,,]', short_main)[0]\n title = f\"{short_main}[{brackets[0]}]...\"\n else:\n title = title[:30] + '...'\n else:\n clauses = re.split(r'[!!!,,]', title)\n if len(clauses) >= 2:\n title = clauses[0] + ',' + clauses[1] + '...'\n else:\n title = title[:30] + '...'\n \n # Note:从 Markdown 提取\n note_match = re.search(r'## 📝 Note\\n\\n(.*?)(?=\\n---|\\n##)', content, re.DOTALL)\n note = note_match.group(1).strip() if note_match else \"\"\n \n # Tags:三层策略(原视频 tags → 关键概念提取 → 默认值)\n # 1. 优先使用 metadata.tags 数组(yt-dlp 提取的原始标签)\n meta_tags = metadata.get('tags', [])\n seen = set()\n if meta_tags:\n # 筛选 2-15 字符的标签(兼容英文如 \"openclaw\")\n for t in meta_tags:\n if 2 \u003c= len(t) \u003c= 15 and t.lower() not in seen:\n seen.add(t.lower())\n tags.append(t)\n \n # 2. 如果不足 5 个,从 Markdown 内容提取关键概念补全\n if len(tags) \u003c 5:\n # 2.1 从关键概念表格提取术语\n concepts_match = re.search(r'## 📚 关键概念\\n\\n(.*?)(?=\\n---|\\n##)', content, re.DOTALL)\n if concepts_match:\n concepts_text = concepts_match.group(1)\n # 提取表格中的概念名(第一列加粗部分)\n concept_terms = re.findall(r'\\|\\s*\\*\\*([^*]+)\\*\\*\\s*\\|', concepts_text)\n for term in concept_terms:\n term = term.strip()\n if (2 \u003c= len(term) \u003c= 15 and \n term.lower() not in seen and\n term not in tags):\n seen.add(term.lower())\n tags.append(term)\n if len(tags) >= 5:\n break\n \n # 2.2 从核心要点标题提取关键词\n if len(tags) \u003c 5:\n generic_words = {'问题', '方法', '技巧', '总结', '分析', '介绍', '说明', '如何', '什么'}\n points_match = re.search(r'## 🎯 核心要点\\n\\n(.*?)(?=\\n---|\\n##)', content, re.DOTALL)\n if points_match:\n points_text = points_match.group(1)\n # 提取加粗的要点标题\n point_titles = re.findall(r'\\*\\*[🎯💡🚀⭐✅🔑📌📝🎬📊🛠️💻📚]\\s*([^*]+)\\*\\*', points_text)\n for title in point_titles:\n words = re.split(r'[\\s,,.。::!!??]+', title)\n for word in words:\n word = word.strip()\n if (2 \u003c= len(word) \u003c= 15 and \n word.lower() not in seen and \n word not in generic_words and\n not re.match(r'^[\\d]+

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, word)):\n seen.add(word.lower())\n tags.append(word)\n if len(tags) >= 5:\n break\n \n # 3. 仍不足 5 个时用默认值补齐\n default_tags = [\"视频总结\", \"AI 分析\", \"教程\", \"技巧\", \"知识分享\"]\n while len(tags) \u003c 5:\n for t in default_tags:\n if t not in tags:\n tags.append(t)\n if len(tags) >= 5:\n break\n \n # UP 主:从 Markdown 提取,或使用 metadata\n author_match = re.search(r'\\*\\*UP 主:\\*\\*\\s*(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n author = author_match.group(1).strip() if author_match else metadata.get('uploader', '')\n \n # 时长:从 metadata 获取\n duration = metadata.get('duration_string', '')\n if not duration:\n duration_match = re.search(r'\\*\\*时长:\\*\\*\\s*(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n duration = duration_match.group(1).strip() if duration_match else \"\"\n \n # 发布日期:从 metadata 获取\n publish_date = metadata.get('upload_date', '')\n if not publish_date:\n publish_match = re.search(r'\\*\\*发布:\\*\\*\\s*(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n publish_date = publish_match.group(1).strip() if publish_match else \"\"\n \n # 封面:优先从 Markdown 提取,其次 metadata\n cover_match = re.search(r'!\\[视频封面\\]\\(([^)]+)\\)', content)\n if cover_match:\n cover_url = cover_match.group(1).strip()\n else:\n screenshot_matches = re.findall(r'!\\[[^\\]]+\\]\\((https?://[^)]+)\\)', content)\n if screenshot_matches:\n cover_url = screenshot_matches[0].strip()\n else:\n cover_url = metadata.get('thumbnail', '')\n \n # ========== 抖音分支 ==========\n elif source == '抖音':\n # 标题:从 Markdown 提取,抖音标题通常较简洁\n title_match = re.search(r'^#\\s+(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n raw_title = title_match.group(1).strip() if title_match else \"视频总结\"\n # 抖音标题:移除 #话题 格式\n title = re.sub(r'#[^\\s#]+', '', raw_title).strip()\n # 截断过长的标题\n if len(title) > 40:\n title = title[:37] + '...'\n \n # Note:从 Markdown 提取\n note_match = re.search(r'## 📝 Note\\n\\n(.*?)(?=\\n---|\\n##)', content, re.DOTALL)\n note = note_match.group(1).strip() if note_match else \"\"\n \n # Tags:优先从 Markdown **Tags:** 行提取,其次从 video_desc 提取\n tags_match = re.search(r'\\*\\*Tags:\\*\\*\\s*(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n if tags_match:\n tags_line = tags_match.group(1).strip()\n # 解析 `标签 1` `标签 2` 格式\n markdown_tags = re.findall(r'`([^`]+)`', tags_line)\n if markdown_tags:\n tags = markdown_tags[:5]\n \n # 如果 Markdown 没有 Tags,从 video_desc 提取\n if not tags:\n video_desc = metadata.get('video_desc', '')\n if video_desc:\n douyin_tags = re.findall(r'#([^#]+)#', video_desc)\n tags.extend(douyin_tags)\n douyin_tags2 = re.findall(r'#([^\\s#]+)', video_desc)\n for t in douyin_tags2:\n if t not in tags:\n tags.append(t)\n \n # UP 主:抖音用户可能显示为\"抖音用户\",优先用 metadata\n author = metadata.get('uploader', '')\n if not author or author in ['抖音用户', 'Unknown', 'N/A']:\n author_match = re.search(r'\\*\\*UP 主:\\*\\*\\s*(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n author = author_match.group(1).strip() if author_match else \"抖音用户\"\n \n # 时长:从 metadata 获取\n duration = metadata.get('duration_string', '')\n \n # 发布日期:从 metadata 获取\n publish_date = metadata.get('upload_date', '')\n \n # 封面:抖音链接会过期,优先使用 OSS 截图\n screenshot_matches = re.findall(r'!\\[[^\\]]+\\]\\((https?://[^)]+)\\)', content)\n if screenshot_matches:\n cover_url = screenshot_matches[0].strip()\n else:\n cover_url = metadata.get('thumbnail', '')\n \n # ========== 小红书分支 ==========\n elif source == '小红书':\n # 标题:从 Markdown 提取\n title_match = re.search(r'^#\\s+(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n raw_title = title_match.group(1).strip() if title_match else \"视频总结\"\n # 小红书标题:移除 #话题 格式\n title = re.sub(r'#[^\\s#]+', '', raw_title).strip()\n if len(title) > 40:\n title = title[:37] + '...'\n \n # Note:从 Markdown 提取\n note_match = re.search(r'## 📝 Note\\n\\n(.*?)(?=\\n---|\\n##)', content, re.DOTALL)\n note = note_match.group(1).strip() if note_match else \"\"\n \n # Tags:三层策略(原视频 tags → AI 概念提取 → 默认值),最多 5 个\n seen = set()\n # 1. 优先使用 metadata.tags\n xhs_meta_tags = metadata.get('tags', [])\n if xhs_meta_tags:\n for t in xhs_meta_tags:\n if 2 \u003c= len(t) \u003c= 15 and t.lower() not in seen:\n seen.add(t.lower())\n tags.append(t)\n if len(tags) >= 5:\n break\n \n # 2. 从 desc(笔记描述)提取\n if len(tags) \u003c 5:\n desc = metadata.get('desc', '')\n if desc:\n xhs_tags = re.findall(r'#([^\\s#]+)', desc)\n for t in xhs_tags:\n if 2 \u003c= len(t) \u003c= 15 and t.lower() not in seen:\n seen.add(t.lower())\n tags.append(t)\n if len(tags) >= 5:\n break\n \n # UP 主:优先从 Markdown 提取(**Author:** 字段)\n author_match = re.search(r'\\*\\*Author:\\*\\*\\s*(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n author = author_match.group(1).strip() if author_match else ''\n if not author or author == 'N/A':\n # 从 metadata 获取(处理 None 的情况)\n author = metadata.get('uploader') or metadata.get('uploader_id', '')\n # 如果是 ID 格式(16 进制),显示为平台用户\n if author and re.match(r'^[0-9a-f]{16,}

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, author, re.IGNORECASE):\n author = '小红书用户'\n if not author:\n author = '小红书用户'\n \n # 时长:优先从 Markdown 提取,其次 metadata\n duration_match = re.search(r'\\*\\*时长:\\*\\*\\s*(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n duration = duration_match.group(1).strip() if duration_match else ''\n if not duration:\n duration = metadata.get('duration_string', '')\n \n # 发布日期:从 metadata 获取\n publish_date = metadata.get('upload_date', '')\n \n # 封面:优先使用 OSS 截图\n screenshot_matches = re.findall(r'!\\[[^\\]]+\\]\\((https?://[^)]+)\\)', content)\n if screenshot_matches:\n cover_url = screenshot_matches[0].strip()\n else:\n cover_url = metadata.get('thumbnail', '')\n \n # ========== YouTube 分支 ==========\n elif source == 'YouTube':\n # 标题:优先从 metadata 获取,其次从 Markdown 提取\n title = metadata.get('title', '')\n if not title or title == 'Unknown':\n title_match = re.search(r'^#\\s+(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n raw_title = title_match.group(1).strip() if title_match else \"Video Summary\"\n # YouTube 标题:移除标签\n title = re.sub(r'#[A-Za-z0-9]+', '', raw_title).strip()\n # 截断过长的标题\n if len(title) > 40:\n title = title[:37] + '...'\n \n # Note:从 Markdown 提取\n note_match = re.search(r'## 📝 Note\\n\\n(.*?)(?=\\n---|\\n##)', content, re.DOTALL)\n note = note_match.group(1).strip() if note_match else \"\"\n \n # Tags:三层策略(原视频 tags → AI 概念提取 → 默认值)\n seen = set()\n # 1. 优先使用 metadata.tags\n yt_tags = metadata.get('tags', [])\n if yt_tags:\n for t in yt_tags:\n if 2 \u003c= len(t) \u003c= 15 and t.lower() not in seen:\n seen.add(t.lower())\n tags.append(t)\n \n # 2. 从 categories 提取\n if len(tags) \u003c 5:\n categories = metadata.get('categories', [])\n for c in categories:\n if 2 \u003c= len(c) \u003c= 15 and c.lower() not in seen:\n seen.add(c.lower())\n tags.append(c)\n \n # 3. 从 Markdown 关键概念提取\n if len(tags) \u003c 5:\n concepts_match = re.search(r'## 📚 关键概念\\n\\n(.*?)(?=\\n---|\\n##)', content, re.DOTALL)\n if concepts_match:\n concepts_text = concepts_match.group(1)\n concept_terms = re.findall(r'\\|\\s*\\*\\*([^*]+)\\*\\*\\s*\\|', concepts_text)\n for term in concept_terms:\n term = term.strip()\n if 2 \u003c= len(term) \u003c= 15 and term.lower() not in seen:\n seen.add(term.lower())\n tags.append(term)\n if len(tags) >= 5:\n break\n \n # UP 主:从 metadata 获取 uploader\n author = metadata.get('uploader', '')\n if not author:\n author_match = re.search(r'\\*\\*Author:\\*\\*\\s*(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n author = author_match.group(1).strip() if author_match else \"\"\n \n # 时长:从 metadata 获取\n duration = metadata.get('duration_string', '')\n \n # 发布日期:从 metadata 获取\n publish_date = metadata.get('upload_date', '')\n \n # 封面:从 metadata 获取 thumbnail\n cover_url = metadata.get('thumbnail', '')\n \n # ========== 默认分支(Unknown)==========\n else:\n # 标题:从 Markdown 提取\n title_match = re.search(r'^#\\s+(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n title = title_match.group(1).strip() if title_match else \"视频总结\"\n if len(title) > 40:\n title = title[:37] + '...'\n \n # Note:从 Markdown 提取\n note_match = re.search(r'## 📝 Note\\n\\n(.*?)(?=\\n---|\\n##)', content, re.DOTALL)\n note = note_match.group(1).strip() if note_match else \"\"\n \n # Tags:使用默认\n tags = []\n \n # UP 主:从 metadata 获取\n author = metadata.get('uploader', '')\n \n # 时长:从 metadata 获取\n duration = metadata.get('duration_string', '')\n \n # 发布日期:从 metadata 获取\n publish_date = metadata.get('upload_date', '')\n \n # 封面:从 metadata 获取\n cover_url = metadata.get('thumbnail', '')\n \n # 如果没有视频 URL,从 Markdown 提取\n if not video_url:\n link_match = re.search(r'\\*\\*链接:\\*\\*\\s*(.+)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, content, re.MULTILINE)\n video_url = link_match.group(1).strip() if link_match else \"\"\n \n # 兜底:如果没有标签,使用默认\n if not tags:\n tags = ['视频总结', 'AI 分析', '教程', '技巧', '知识分享']\n \n return {\n 'title': title,\n 'note': note,\n 'tags': tags,\n 'author': author,\n 'video_url': video_url,\n 'duration': duration,\n 'publish_date': publish_date,\n 'cover_url': cover_url,\n 'source': source,\n 'full_content': content\n }\n\n\ndef search_database(database_id):\n \"\"\"查询 Notion Database(Data Source)\"\"\"\n url = f\"https://api.notion.com/v1/data_sources/{database_id}/query\"\n response = requests.post(url, headers=HEADERS, json={})\n if response.status_code == 200:\n return response.json()\n else:\n print(f\"❌ 查询 Database 失败:{response.status_code}\")\n print(response.text[:200])\n return None\n\n\ndef create_page_in_database(database_id, properties, blocks=None):\n \"\"\"在 Notion Database 中创建页面\"\"\"\n url = \"https://api.notion.com/v1/pages\"\n \n data = {\n \"parent\": {\"database_id\": database_id},\n \"properties\": properties\n }\n \n if blocks:\n # 如果需要添加内容块\n pass\n \n # 超时重试机制\n for attempt in range(3):\n try:\n timeout = 30 * (attempt + 1) # 30s, 60s, 90s\n response = requests.post(url, headers=HEADERS, json=data, timeout=timeout)\n if response.status_code == 200:\n break\n elif response.status_code == 400:\n # 验证错误,直接返回让上层处理\n break\n except requests.exceptions.Timeout:\n print(f\" ⚠️ 网络超时,尝试 {attempt + 2}/3...\")\n if attempt == 2:\n raise\n \n if response.status_code == 200:\n page_data = response.json()\n page_id = page_data['id']\n page_url = page_data.get('url', '')\n return page_id, page_url\n else:\n print(f\"❌ 创建页面失败:{response.status_code}\")\n print(f\"错误信息:{response.text[:300]}\")\n return None, None\n\n\ndef append_blocks_to_page(page_id, blocks):\n \"\"\"向页面添加内容块\"\"\"\n url = f\"https://api.notion.com/v1/blocks/{page_id}/children\"\n \n # 超时重试机制\n for attempt in range(3):\n try:\n timeout = 30 * (attempt + 1) # 30s, 60s, 90s\n response = requests.patch(url, headers=HEADERS, json={\"children\": blocks}, timeout=timeout)\n if response.status_code == 200:\n return True\n elif response.status_code == 400:\n print(f\"❌ 添加内容块失败:{response.status_code}\")\n print(response.text[:300])\n return False\n except requests.exceptions.Timeout:\n print(f\" ⚠️ 网络超时,尝试 {attempt + 2}/3...\")\n if attempt == 2:\n raise\n \n return False\n\n\ndef parse_inline_markdown(text):\n \"\"\"\n 解析行内 Markdown 格式(粗体、斜体、代码、链接)\n 返回 Notion rich_text 数组\n \"\"\"\n rich_text = []\n remaining = text\n \n while remaining:\n # 粗体 **text**\n bold_match = re.match(r'^(.*?)\\*\\*([^*]+)\\*\\*(.*)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, remaining)\n if bold_match:\n before, bold_content, after = bold_match.groups()\n if before:\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": before}})\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": bold_content, }})\n remaining = after\n continue\n \n # 斜体 *text*(单独一个星号)\n italic_match = re.match(r'^(.*?)(?\u003c!\\*)\\*([^*]+)\\*(?!\\*)(.*)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, remaining)\n if italic_match:\n before, italic_content, after = italic_match.groups()\n if before:\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": before}})\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": italic_content, }})\n remaining = after\n continue\n \n # 行内代码 `code`\n code_match = re.match(r'^(.*?)`([^`]+)`(.*)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, remaining)\n if code_match:\n before, code_content, after = code_match.groups()\n if before:\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": before}})\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": code_content, }})\n remaining = after\n continue\n \n # 链接 [text](url)\n link_match = re.match(r'^(.*?)\\[([^\\]]+)\\]\\(([^)]+)\\)(.*)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, remaining)\n if link_match:\n before, link_text, link_url, after = link_match.groups()\n if before:\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": before}})\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": link_text, \"link\": {\"url\": link_url}}})\n remaining = after\n continue\n \n # 时间戳 `[MM:SS]` 保持原样\n time_match = re.match(r'^(.*?)`\\[([^`\\]]+)\\]`(.*)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, remaining)\n if time_match:\n before, time_content, after = time_match.groups()\n if before:\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": before}})\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": f\"[{time_content}]\", }})\n remaining = after\n continue\n \n # 没有更多格式,添加剩余文本\n rich_text.append({\"type\": \"text\", \"text\": {\"content\": remaining}})\n break\n \n return rich_text\n\n\ndef markdown_to_notion_blocks(markdown_content):\n \"\"\"将 Markdown 转换为 Notion Blocks\"\"\"\n blocks = []\n lines = markdown_content.split('\\n')\n \n current_list = []\n in_code_block = False\n code_content = []\n code_language = \"\"\n in_table = False\n table_rows = []\n \n def flush_table():\n \"\"\"提交表格\"\"\"\n nonlocal in_table, table_rows\n if not in_table or len(table_rows) \u003c 2:\n table_rows = []\n in_table = False\n return\n \n # 第一行是表头\n header_row = table_rows[0]\n # 第二行是分隔符(|---|---|),跳过\n # 从第三行开始是数据行\n data_rows = table_rows[2:] if len(table_rows) > 2 else []\n \n # 创建表格块\n blocks.append({\n \"object\": \"block\",\n \"type\": \"table\",\n \"table\": {\n \"table_width\": len(header_row),\n \"has_column_header\": True,\n \"children\": [\n {\n \"type\": \"table_row\",\n \"table_row\": {\n \"cells\": [parse_inline_markdown(cell) for cell in header_row]\n }\n }\n ] + [\n {\n \"type\": \"table_row\",\n \"table_row\": {\n \"cells\": [parse_inline_markdown(cell) for cell in row]\n }\n }\n for row in data_rows\n ]\n }\n })\n table_rows = []\n in_table = False\n \n def flush_list():\n \"\"\"提交当前列表\"\"\"\n nonlocal current_list\n if not current_list:\n return\n for item in current_list:\n blocks.append({\n \"object\": \"block\",\n \"type\": \"bulleted_list_item\",\n \"bulleted_list_item\": {\n \"rich_text\": parse_inline_markdown(item)\n }\n })\n current_list = []\n \n for line in lines:\n # 图片处理(优先)- 格式:![alt](url)\n img_match = re.match(r'^!\\[([^\\]]*)\\]\\(([^)]+)\\)

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…

, line.strip())\n if img_match:\n alt_text = img_match.group(1)\n img_url = img_match.group(2)\n blocks.append({\n \"object\": \"block\",\n \"type\": \"image\",\n \"image\": {\n \"type\": \"external\",\n \"external\": {\"url\": img_url}\n }\n })\n continue\n \n # 代码块处理\n if line.startswith('```'):\n if not in_code_block:\n in_code_block = True\n code_language = line[3:].strip()\n code_content = []\n else:\n if code_content:\n blocks.append({\n \"object\": \"block\",\n \"type\": \"code\",\n \"code\": {\n \"rich_text\": [{\"type\": \"text\", \"text\": {\"content\": '\\n'.join(code_content)}}],\n \"language\": code_language if code_language else \"plain text\"\n }\n })\n in_code_block = False\n continue\n \n if in_code_block:\n code_content.append(line)\n continue\n \n # 表格处理\n stripped = line.strip()\n if stripped.startswith('|') and stripped.endswith('|'):\n if not in_table:\n in_table = True\n table_rows = []\n \n # 解析表格行\n cells = [cell.strip() for cell in stripped.split('|')[1:-1]]\n # 跳过分隔符行(|---|---|)\n if not (len(cells) > 0 and all(c.replace('-', '').replace(':', '') == '' for c in cells)):\n table_rows.append(cells)\n continue\n elif in_table:\n flush_table()\n \n # 标题处理\n if line.startswith('# '):\n flush_table()\n flush_list() # 刷新列表\n blocks.append({\n \"object\": \"block\",\n \"type\": \"heading_1\",\n \"heading_1\": {\n \"rich_text\": parse_inline_markdown(line[2:].strip())\n }\n })\n elif line.startswith('## '):\n flush_table()\n flush_list() # 刷新列表\n blocks.append({\n \"object\": \"block\",\n \"type\": \"heading_2\",\n \"heading_2\": {\n \"rich_text\": parse_inline_markdown(line[3:].strip())\n }\n })\n elif line.startswith('### '):\n flush_table()\n flush_list() # 刷新列表\n blocks.append({\n \"object\": \"block\",\n \"type\": \"heading_3\",\n \"heading_3\": {\n \"rich_text\": parse_inline_markdown(line[4:].strip())\n }\n })\n # 列表处理\n elif line.startswith('- ') or line.startswith('* '):\n current_list.append(line[2:].strip())\n # 空行\n elif not line.strip():\n flush_table()\n flush_list()\n blocks.append({\n \"object\": \"block\",\n \"type\": \"paragraph\",\n \"paragraph\": {\n \"rich_text\": [{\"type\": \"text\", \"text\": {\"content\": \"\"}}]\n }\n })\n # 普通段落\n else:\n flush_table()\n flush_list()\n \n # 普通段落,解析行内格式\n text = line.strip()\n if text:\n blocks.append({\n \"object\": \"block\",\n \"type\": \"paragraph\",\n \"paragraph\": {\n \"rich_text\": parse_inline_markdown(text)\n }\n })\n \n # 处理剩余的列表\n flush_list()\n \n # 处理剩余的表格\n if in_table:\n flush_table()\n \n return blocks\n\n\ndef push_to_notion(md_file, database_id=None):\n \"\"\"推送视频总结到 Notion\"\"\"\n \n # 使用配置的 Database ID 或参数\n db_id = database_id or NOTION_DATABASE_ID\n \n if not db_id:\n print(\"❌ 错误:未指定 Notion Database ID\")\n print(\"用法:python3 push-to-notion.py \u003csummary.md> [database_id]\")\n print(\"或在 ~/.openclaw/.env 中配置 NOTION_VIDEO_SUMMARY_DATABASE_ID\")\n return None, None\n \n # 解析 Markdown\n print(f\"📝 解析 Markdown 文件:{md_file}\")\n data = parse_markdown(md_file)\n \n print(f\" 标题:{data['title']}\")\n print(f\" UP 主:{data['author']}\")\n print(f\" 标签:{', '.join(data['tags'])}\")\n print(f\" 来源:{data.get('source', 'Unknown')}\")\n print(f\" 封面:{data.get('cover_url', 'N/A')}\")\n \n # 构建页面属性(适配 Notion 数据库字段类型)\n # 字段类型:Title=title, Source=rich_text, Author=rich_text, Url=url, \n # Tags=multi_select, PubDate=date, Length=rich_text, Cover=files\n properties = {\n \"Title\": {\n \"title\": [\n {\n \"type\": \"text\",\n \"text\": {\n \"content\": data['title'][:200] # 限制标题长度\n }\n }\n ]\n },\n \"Source\": {\n \"rich_text\": [\n {\n \"type\": \"text\",\n \"text\": {\n \"content\": data.get('source', 'Unknown')\n }\n }\n ]\n },\n \"Author\": {\n \"rich_text\": [\n {\n \"type\": \"text\",\n \"text\": {\n \"content\": data['author']\n }\n }\n ]\n },\n \"Url\": {\n \"url\": data['video_url']\n },\n \"Tags\": {\n \"multi_select\": [\n {\"name\": tag} for tag in data['tags'][:5] # 最多 5 个标签\n ]\n },\n \"PubDate\": {\n \"date\": {\n \"start\": data['publish_date'] if data.get('publish_date') else datetime.now().strftime(\"%Y-%m-%d\")\n }\n },\n \"Length\": {\n \"rich_text\": [\n {\n \"type\": \"text\",\n \"text\": {\n \"content\": data['duration']\n }\n }\n ]\n }\n }\n \n # 仅在封面 URL 有效时添加 Cover 字段\n if data.get('cover_url'):\n properties[\"Cover\"] = {\n \"files\": [\n {\n \"name\": \"封面图片\",\n \"type\": \"external\",\n \"external\": {\n \"url\": data['cover_url']\n }\n }\n ]\n }\n \n # 添加 ts 字段(当前时间戳,精确到秒,ISO 8601 格式,东八区)\n from datetime import timezone, timedelta\n # 东八区时区偏移\n tz_cn = timezone(timedelta(hours=8))\n timestamp_iso = datetime.now(tz_cn).strftime(\"%Y-%m-%dT%H:%M:%S+08:00\")\n properties[\"ts\"] = {\n \"date\": {\n \"start\": timestamp_iso\n }\n }\n \n # 创建页面(容错:如果字段不存在则移除后重试)\n print(f\"📤 创建 Notion 页面...\")\n page_id, page_url = create_page_in_database(db_id, properties)\n \n # 如果失败,分析错误原因并尝试修复\n if not page_id:\n print(\"⚠️ 创建失败,尝试移除可选字段后重试...\")\n # 只移除 Cover 字段(封面是可选的,其他字段必须保留)\n if \"Cover\" in properties:\n del properties[\"Cover\"]\n print(f\" 移除字段:Cover\")\n page_id, page_url = create_page_in_database(db_id, properties)\n \n if not page_id:\n return None, None\n \n print(f\"✅ 页面创建成功:{page_url}\")\n \n # 添加内容块(Note + 完整内容)\n print(f\"📝 添加页面内容...\")\n \n # 构建内容块\n blocks = []\n \n # 添加 Note\n if data['note']:\n blocks.append({\n \"object\": \"block\",\n \"type\": \"heading_2\",\n \"heading_2\": {\n \"rich_text\": [{\"type\": \"text\", \"text\": {\"content\": \"📝 概述\"}}]\n }\n })\n blocks.append({\n \"object\": \"block\",\n \"type\": \"paragraph\",\n \"paragraph\": {\n \"rich_text\": [{\"type\": \"text\", \"text\": {\"content\": data['note']}}]\n }\n })\n \n # 读取完整 Markdown 文件,转换为 blocks(分批发送)\n print(f\"📝 转换 Markdown 为 Notion 块...\")\n with open(md_file, 'r', encoding='utf-8') as f:\n md_content = f.read()\n \n # 跳过元数据头部(前 3 个分隔线之间的内容)\n md_lines = md_content.split('\\n')\n content_lines = []\n separator_count = 0\n in_metadata = True\n \n for line in md_lines:\n if line.strip() == '---':\n separator_count += 1\n if separator_count >= 3:\n in_metadata = False\n continue\n if in_metadata:\n continue\n # 跳过元数据行\n if line.startswith('**Tags:**') or line.startswith('**Status:**') or line.startswith('**Author:**') or line.startswith('**Cover:**'):\n continue\n if line.strip(): # 跳过空行\n content_lines.append(line)\n \n md_content_clean = '\\n'.join(content_lines)\n content_blocks = markdown_to_notion_blocks(md_content_clean)\n \n # 分批发送(Notion 限制每次最多 100 个块)\n batch_size = 100\n total_batches = (len(content_blocks) + batch_size - 1) // batch_size\n print(f\" 共 {len(content_blocks)} 个块,分 {total_batches} 批发送...\")\n \n for i in range(0, len(content_blocks), batch_size):\n batch = content_blocks[i:i + batch_size]\n batch_num = i // batch_size + 1\n print(f\" 发送批次 {batch_num}/{total_batches}...\")\n success = append_blocks_to_page(page_id, batch)\n if success:\n print(f\" ✅ 批次 {batch_num} 成功\")\n else:\n print(f\" ⚠️ 批次 {batch_num} 失败,继续下一批\")\n \n print(f\"✅ 内容添加完成\")\n return page_id, page_url\n\n\ndef main():\n if len(sys.argv) \u003c 2:\n print(\"用法:python3 push-to-notion.py \u003csummary.md> [Notion Database ID]\")\n sys.exit(1)\n \n md_file = sys.argv[1]\n database_id = sys.argv[2] if len(sys.argv) > 2 else None\n \n if not os.path.exists(md_file):\n print(f\"❌ 文件不存在:{md_file}\")\n sys.exit(1)\n \n print(\"=\" * 50)\n print(\"📤 Notion 推送工具\")\n print(\"=\" * 50)\n print()\n \n page_id, page_url = push_to_notion(md_file, database_id)\n \n print()\n print(\"=\" * 50)\n if page_url:\n print(f\"✅ 推送完成!\")\n print(f\"📎 Notion 链接:{page_url}\")\n else:\n print(f\"❌ 推送失败\")\n sys.exit(1)\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":37250,"content_sha256":"1efa0559affddc2a4ac3c94baef07d78ca5ff9ac14f3aa33b456b02e02abd478"},{"filename":"scripts/transcribe-audio.py","content":"#!/usr/bin/env python3\n\"\"\"\ntranscribe-audio.py - Plan B: 语音转录字幕\n三层降级方案(云端优先,本地降级):\n1. Groq API (whisper-large-v3) - 如果配置且可用\n2. Faster-Whisper (CPU/GPU 自适应) - Groq 不可用时本地方案\n3. Whisper.cpp / OpenAI Whisper - 保底方案\n\n用法:python3 transcribe-audio.py \u003c音频文件> [输出字幕文件]\n\n版本:v1.0.13\n更新:移除硅基流动依赖,Groq API 为可选配置\n\"\"\"\n\nimport sys\nimport os\nimport json\nimport subprocess\nfrom pathlib import Path\nfrom dotenv import load_dotenv\n\nload_dotenv(Path.home() / '.openclaw' / '.env')\n\n# 配置\nGROQ_API_KEY = os.getenv('GROQ_API_KEY')\nWHISPER_MODEL = os.getenv('WHISPER_MODEL', 'base') # faster-whisper 模型:tiny/base/small/medium/large\nWHISPER_CPP_MODEL = os.getenv('WHISPER_CPP_MODEL', 'base')\nFORCE_LOCAL = os.getenv('USE_LOCAL_WHISPER', 'false').lower() == 'true'\n\n\ndef check_gpu():\n \"\"\"检测 GPU 可用性,返回 (可用,显存 GB, 设备名)\"\"\"\n try:\n result = subprocess.run(\n ['nvidia-smi', '--query-gpu=name,memory.total', '--format=csv,noheader,nounits'],\n capture_output=True, text=True, timeout=10\n )\n if result.returncode == 0:\n lines = result.stdout.strip().split('\\n')\n if lines:\n parts = lines[0].split(', ')\n gpu_name = parts[0]\n vram_gb = int(parts[1]) / 1024 # MB → GB\n return True, vram_gb, gpu_name\n except:\n pass\n return False, 0, None\n\n\ndef select_faster_whisper_model(vram_gb: float):\n \"\"\"根据显存选择 Faster-Whisper 模型\"\"\"\n if vram_gb >= 8:\n return 'large-v2', 'GPU'\n elif vram_gb >= 4:\n return 'medium', 'GPU'\n elif vram_gb >= 2:\n return 'small', 'GPU'\n elif vram_gb >= 1:\n return 'base', 'GPU'\n else:\n # CPU 模式\n return 'base', 'CPU'\n\n\ndef transcribe_with_faster_whisper(audio_file: str) -> dict:\n \"\"\"使用 Faster-Whisper 转录(第二方案)\"\"\"\n try:\n from faster_whisper import WhisperModel\n \n # GPU 检测\n has_gpu, vram_gb, gpu_name = check_gpu()\n \n if has_gpu:\n model_size, device = select_faster_whisper_model(vram_gb)\n print(f\" 🖥️ Faster-Whisper | 设备:{device} ({gpu_name}, {vram_gb:.1f}GB) | 模型:{model_size}\")\n else:\n model_size, device = 'base', 'CPU'\n print(f\" 🖥️ Faster-Whisper | 设备:CPU (无 GPU) | 模型:{model_size}\")\n \n # 加载模型\n device_arg = \"cuda\" if has_gpu else \"cpu\"\n compute_type = \"float16\" if has_gpu else \"int8\"\n \n model = WhisperModel(model_size, device=device_arg, compute_type=compute_type)\n \n # 转录(添加标点参数)\n segments, info = model.transcribe(\n audio_file, \n language='zh', \n vad_filter=True,\n initial_prompt=\"请添加适当的标点符号,使文本更易读。\" # 提示添加标点\n )\n \n # 收集结果\n text_parts = []\n segs_list = []\n for seg in segments:\n text_parts.append(seg.text)\n segs_list.append({\n 'start': seg.start,\n 'end': seg.end,\n 'text': seg.text.strip()\n })\n \n return {\n 'success': True,\n 'text': ''.join(text_parts),\n 'segments': segs_list,\n 'language': info.language,\n 'model': f\"faster-whisper/{model_size}\"\n }\n \n except ImportError:\n return {\n 'success': False,\n 'error': \"Faster-Whisper 未安装,运行:pip install faster-whisper\"\n }\n except Exception as e:\n return {\n 'success': False,\n 'error': f\"Faster-Whisper 失败:{str(e)}\"\n }\n\n\ndef transcribe_with_groq(audio_file: str) -> dict:\n \"\"\"使用 Groq API 转录(第一方案)\"\"\"\n import requests\n \n print(\" 🌐 使用 Groq API 转录...\")\n \n url = \"https://api.groq.com/openai/v1/audio/transcriptions\"\n headers = {\"Authorization\": f\"Bearer {GROQ_API_KEY}\"}\n \n try:\n with open(audio_file, 'rb') as f:\n files = {\"file\": f}\n data = {\"model\": \"whisper-large-v3\", \"response_format\": \"verbose_json\"}\n response = requests.post(url, headers=headers, files=files, data=data, timeout=600)\n except requests.exceptions.Timeout:\n return {'success': False, 'error': 'Groq API 超时'}\n except requests.exceptions.ConnectionError as e:\n return {'success': False, 'error': f'Groq API 连接失败:{str(e)[:200]}'}\n except requests.exceptions.SSLError as e:\n return {'success': False, 'error': f'Groq API SSL 错误:{str(e)[:200]}'}\n except requests.exceptions.RequestException as e:\n return {'success': False, 'error': f'Groq API 请求异常:{str(e)[:200]}'}\n \n if response.status_code == 200:\n result = response.json()\n return {\n 'success': True,\n 'text': result.get('text', ''),\n 'segments': result.get('segments', [])\n }\n else:\n return {\n 'success': False,\n 'error': f\"Groq API 错误:{response.status_code} - {response.text[:200]}\"\n }\n\n\n\n\n\ndef transcribe_with_whisper_cpp(audio_file: str) -> dict:\n \"\"\"使用 Whisper.cpp 转录(保底方案)\"\"\"\n print(\" 🐌 使用 Whisper.cpp 转录 (保底,较慢)...\")\n \n # 查找 whisper.cpp 的 main 程序\n whisper_cpp_paths = [\n '/usr/local/bin/whisper-cpp',\n '/usr/bin/whisper-cpp',\n os.path.expanduser('~/whisper.cpp/main'),\n os.path.expanduser('~/.local/bin/whisper-cpp')\n ]\n \n whisper_cpp = None\n for p in whisper_cpp_paths:\n if os.path.isfile(p) and os.access(p, os.X_OK):\n whisper_cpp = p\n break\n \n if not whisper_cpp:\n # 尝试查找 ggml 模型\n for model_name in ['base', 'small', 'medium']:\n model_path = os.path.expanduser(f'~/whisper.cpp/models/ggml-{model_name}.bin')\n if os.path.exists(model_path):\n whisper_cpp = os.path.expanduser('~/whisper.cpp/main')\n WHISPER_CPP_MODEL = model_name\n break\n \n if not whisper_cpp or not os.path.exists(whisper_cpp):\n return {\n 'success': False,\n 'error': \"Whisper.cpp 未找到,请安装:git clone https://github.com/ggerganov/whisper.cpp\"\n }\n \n # 使用 openai-whisper 作为 whisper.cpp 的替代保底\n try:\n import whisper\n print(\" 🐌 使用 OpenAI Whisper (本地保底)...\")\n model = whisper.load_model('base')\n result = model.transcribe(audio_file, language='zh')\n return {\n 'success': True,\n 'text': result.get('text', ''),\n 'segments': result.get('segments', [])\n }\n except:\n return {\n 'success': False,\n 'error': \"Whisper.cpp 和 OpenAI Whisper 均不可用\"\n }\n\n\ndef segments_to_vtt(segments: list, output_file: str):\n \"\"\"将转录片段转换为 VTT 字幕格式\"\"\"\n def format_time(seconds: float) -> str:\n hours = int(seconds // 3600)\n minutes = int((seconds % 3600) // 60)\n secs = int(seconds % 60)\n millis = int((seconds % 1) * 1000)\n return f\"{hours:02d}:{minutes:02d}:{secs:02d}.{millis:03d}\"\n \n with open(output_file, 'w', encoding='utf-8') as f:\n f.write(\"WEBVTT\\n\\n\")\n for i, seg in enumerate(segments, 1):\n start = format_time(seg.get('start', 0))\n end = format_time(seg.get('end', 0))\n text = seg.get('text', '').strip()\n if text:\n f.write(f\"{i}\\n{start} --> {end}\\n{text}\\n\\n\")\n\n\ndef main():\n if len(sys.argv) \u003c 2:\n print(\"用法:python3 transcribe-audio.py \u003c音频文件> [输出字幕文件]\")\n sys.exit(1)\n \n audio_file = sys.argv[1]\n output_file = sys.argv[2] if len(sys.argv) > 2 else audio_file.rsplit('.', 1)[0] + '.vtt'\n \n if not os.path.exists(audio_file):\n print(f\"❌ 音频文件不存在:{audio_file}\")\n sys.exit(1)\n \n print(\"=\" * 60)\n print(\"🎤 语音转录 (Plan B) - 三层降级方案\")\n print(\"=\" * 60)\n print()\n \n # GPU 检测\n has_gpu, vram_gb, gpu_name = check_gpu()\n if has_gpu:\n print(f\"📊 GPU: {gpu_name} ({vram_gb:.1f}GB)\")\n else:\n print(\"📊 GPU: 未检测到 (使用 CPU)\")\n print()\n \n # 三层降级逻辑:Groq API → Faster-Whisper → Whisper.cpp\n result = {'success': False, 'error': '未尝试'}\n \n # 方案 1: Groq API (如果配置了 Key)\n if GROQ_API_KEY and GROQ_API_KEY.strip():\n print(\"【方案 1/3】Groq API (whisper-large-v3)\")\n result = transcribe_with_groq(audio_file)\n \n if result['success']:\n print(\" ✅ 成功\")\n else:\n print(f\" ❌ 失败:{result['error']}\")\n print(\" → 降级到本地 Faster-Whisper\")\n else:\n print(\"⚠️ 未配置 GROQ_API_KEY,跳过 Groq API\")\n \n # 方案 2: Faster-Whisper (本地,Groq 不可用时使用)\n if not result['success']:\n print(\"\\n【方案 2/3】Faster-Whisper (本地)\")\n result = transcribe_with_faster_whisper(audio_file)\n \n if result['success']:\n print(f\" ✅ 成功 | 模型:{result.get('model', 'unknown')}\")\n else:\n print(f\" ❌ 失败:{result['error']}\")\n print(\" → 降级到 Whisper.cpp 保底\")\n \n # 方案 3: Whisper.cpp / OpenAI Whisper (保底)\n if not result['success']:\n print(\"\\n【方案 3/3】Whisper 本地保底\")\n result = transcribe_with_whisper_cpp(audio_file)\n \n if result['success']:\n print(\" ✅ 成功\")\n else:\n print(f\" ❌ 失败:{result['error']}\")\n \n print()\n print(\"=\" * 60)\n \n if result['success']:\n print(f\"✅ 转录成功 | 文本长度:{len(result['text'])} 字符 | 片段数:{len(result['segments'])}\")\n \n # 保存 VTT\n segments_to_vtt(result['segments'], output_file)\n print(f\"📄 字幕已保存:{output_file}\")\n \n # 同时保存纯文本\n txt_file = output_file.rsplit('.', 1)[0] + '.txt'\n with open(txt_file, 'w', encoding='utf-8') as f:\n f.write(result['text'])\n print(f\"📄 文本已保存:{txt_file}\")\n else:\n print(f\"❌ 所有方案均失败:{result['error']}\")\n sys.exit(1)\n \n print()\n\n\nif __name__ == '__main__':\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":10818,"content_sha256":"91d54364b7d4521bfc8e07f8e5775646129f0935debabc4a4353530b5aa46f78"},{"filename":"scripts/upload-to-oss.py","content":"#!/usr/bin/env python3\n\"\"\"\n阿里云 OSS 图床上传脚本\n用于 video-summarizer 技能,自动上传截图到阿里云 OSS\n\n版本:v1.0.13\n\n路径规范:\n/screenshots/\u003c平台名>/\u003c视频 ID>_\u003c时间戳>/\u003c截图文件>\n/thumbnails/\u003c平台>/\u003c视频 ID>/cover.jpg\n\n支持平台:\n- bilibili (B 站)\n- douyin (抖音)\n- xhs (小红书)\n- youtube (YouTube)\n\n支持:\n1. 公开读 Bucket:直接返回永久访问链接\n2. 私有 Bucket:返回签名 URL(默认 2 小时有效期)\n\"\"\"\n\nimport os\nimport sys\nimport re\nimport json\nimport argparse\nfrom pathlib import Path\n\n# 读取环境变量\nfrom dotenv import load_dotenv\nload_dotenv(Path.home() / '.openclaw' / '.env')\n\n# 阿里云 OSS 配置\nALIYUN_OSS_AK = os.getenv('ALIYUN_OSS_AK')\nALIYUN_OSS_SK = os.getenv('ALIYUN_OSS_SK')\nALIYUN_OSS_BUCKET = os.getenv('ALIYUN_OSS_BUCKET_ID')\nALIYUN_OSS_ENDPOINT = os.getenv('ALIYUN_OSS_ENDPOINT')\n\nif not all([ALIYUN_OSS_AK, ALIYUN_OSS_SK, ALIYUN_OSS_BUCKET]):\n print(\"❌ 错误:缺少阿里云 OSS 配置,请检查 ~/.openclaw/.env\", file=sys.stderr)\n sys.exit(1)\n\nimport oss2\n\n\n# 平台识别规则\nPLATFORM_PATTERNS = {\n 'bilibili': [\n r'bilibili\\.com/video/(BV\\w+)',\n r'bilibili\\.com/video/(av\\d+)',\n ],\n 'douyin': [\n r'douyin\\.com/video/(\\d+)',\n r'iesdouyin\\.com/share/video/(\\d+)',\n r'v\\.douyin\\.com/([\\w-]+)',\n r'douyin\\.com/([\\w-]+)',\n ],\n 'xhs': [\n r'xiaohongshu\\.com/discovery/item/(\\w+)',\n r'xhslink\\.com/(\\w+)',\n ],\n 'youtube': [\n r'youtube\\.com/watch\\?v=([\\w-]+)',\n r'youtu\\.be/([\\w-]+)',\n ],\n}\n\n\ndef detect_platform(video_url: str) -> tuple:\n \"\"\"\n 从视频 URL 识别平台和视频 ID\n \n Returns:\n tuple: (platform, video_id),识别失败返回 ('unknown', 'unknown')\n \"\"\"\n for platform, patterns in PLATFORM_PATTERNS.items():\n for pattern in patterns:\n match = re.search(pattern, video_url)\n if match:\n video_id = match.group(1)\n # 清理视频 ID,移除特殊字符\n video_id = re.sub(r'[^a-zA-Z0-9_-]', '', video_id)\n return platform, video_id\n \n # 识别失败,返回默认值\n return 'unknown', 'unknown'\n\n\ndef upload_to_oss(local_file_path: str, remote_key: str = None, public: bool = False, expires: int = 7200) -> dict:\n \"\"\"\n 上传文件到阿里云 OSS\n \n Args:\n local_file_path: 本地文件路径\n remote_key: 远程文件键名(可选,默认使用文件名)\n public: 是否公开访问(True=公开 URL,False=签名 URL)\n expires: 签名 URL 过期时间(秒),默认 2 小时\n \n Returns:\n dict: {\n 'success': bool,\n 'url': str (成功时),\n 'error': str (失败时)\n }\n \"\"\"\n try:\n # 构建 Auth 和 Bucket 对象\n auth = oss2.Auth(ALIYUN_OSS_AK, ALIYUN_OSS_SK)\n bucket = oss2.Bucket(auth, f'https://{ALIYUN_OSS_ENDPOINT}', ALIYUN_OSS_BUCKET)\n \n # 如果没有指定 remote_key,使用文件名\n if remote_key is None:\n remote_key = Path(local_file_path).name\n \n # 上传文件\n with open(local_file_path, 'rb') as f:\n bucket.put_object(remote_key, f)\n \n # 构建访问 URL\n if public:\n # 公开读 URL\n url = f\"https://{ALIYUN_OSS_BUCKET}.{ALIYUN_OSS_ENDPOINT}/{remote_key}\"\n else:\n # 签名 URL(私有 Bucket)\n url = bucket.sign_url('GET', remote_key, expires)\n \n return {\n 'success': True,\n 'url': url,\n 'key': remote_key,\n 'endpoint': ALIYUN_OSS_ENDPOINT,\n 'public': public\n }\n \n except Exception as e:\n return {\n 'success': False,\n 'error': f\"异常:{str(e)}\"\n }\n\n\ndef upload_screenshots(screenshots_dir: str, prefix: str = \"screenshots/\", public: bool = False) -> list:\n \"\"\"\n 批量上传截图目录中的所有图片\n \n Args:\n screenshots_dir: 截图目录路径\n prefix: OSS 存储路径前缀(格式:screenshots/\u003c平台>/\u003c视频 ID_\u003c时间戳>/)\n public: 是否返回公开 URL\n \n Returns:\n list: 上传结果列表,每个元素包含 {local_path, oss_url, success}\n \"\"\"\n screenshots_path = Path(screenshots_dir)\n if not screenshots_path.exists():\n print(f\"❌ 目录不存在:{screenshots_dir}\", file=sys.stderr)\n return []\n \n # 获取所有图片文件\n image_extensions = {'.jpg', '.jpeg', '.png', '.gif', '.webp'}\n image_files = sorted([\n f for f in screenshots_path.iterdir() \n if f.is_file() and f.suffix.lower() in image_extensions\n ])\n \n if not image_files:\n print(f\"⚠️ 目录中没有找到图片文件\", file=sys.stderr)\n return []\n \n results = []\n for img_file in image_files:\n # 构建远程键名(使用正斜杠)\n # 确保 prefix 以 / 结尾\n prefix_normalized = prefix.rstrip('/') + '/'\n remote_key = f\"{prefix_normalized}{img_file.name}\".replace('\\\\', '/')\n \n print(f\"📤 上传:{img_file.name} ...\", file=sys.stderr)\n result = upload_to_oss(str(img_file), remote_key, public)\n \n if result['success']:\n print(f\"✅ 成功:{result['url'][:60]}...\", file=sys.stderr)\n results.append({\n 'local_path': str(img_file),\n 'oss_url': result['url'],\n 'remote_key': result['key'],\n 'success': True\n })\n else:\n print(f\"❌ 失败:{result['error']}\", file=sys.stderr)\n results.append({\n 'local_path': str(img_file),\n 'error': result['error'],\n 'success': False\n })\n \n return results\n\n\ndef build_prefix(video_url: str = None, metadata_file: str = None) -> str:\n \"\"\"\n 构建 OSS 上传路径前缀\n \n 格式:/screenshots/\u003c平台名>/\u003c视频 ID_\u003c时间戳>/\n \n Args:\n video_url: 视频 URL(可选)\n metadata_file: 元数据 JSON 文件路径(可选)\n \n Returns:\n str: OSS 路径前缀\n \"\"\"\n platform = 'unknown'\n video_id = 'unknown'\n \n # 尝试从 URL 识别平台\n if video_url:\n result = detect_platform(video_url)\n platform = result[0] if result[0] else 'unknown'\n video_id = result[1] if result[1] else 'unknown'\n \n # 如果 URL 识别失败,尝试从元数据文件获取\n if platform in ['unknown', None] and metadata_file and os.path.exists(metadata_file):\n try:\n import json\n with open(metadata_file, 'r', encoding='utf-8') as f:\n metadata = json.load(f)\n video_url = metadata.get('webpage_url', '')\n if video_url:\n result = detect_platform(video_url)\n platform = result[0] if result[0] else 'unknown'\n video_id = result[1] if result[1] else 'unknown'\n \n # 如果还是未知,使用 uploader 作为备用\n if platform in ['unknown', None]:\n uploader = metadata.get('uploader', 'unknown')\n platform = re.sub(r'[^a-zA-Z0-9]', '', uploader)[:20].lower()\n if not platform:\n platform = 'unknown'\n \n # 使用视频 ID 或标题\n if video_id == 'unknown':\n video_id = metadata.get('id', 'unknown')\n if not video_id or video_id == 'unknown':\n title = metadata.get('title', 'video')\n # 从标题生成安全 ID\n video_id = re.sub(r'[^a-zA-Z0-9]', '', title)[:30].lower()\n if not video_id:\n video_id = 'video'\n except Exception as e:\n print(f\"⚠️ 读取元数据失败:{e}\", file=sys.stderr)\n \n # 构建前缀\n timestamp = __import__('datetime').datetime.now().strftime('%Y%m%d_%H%M%S')\n prefix = f\"screenshots/{platform}/{video_id}_{timestamp}/\"\n \n print(f\"📁 OSS 路径:{prefix}\", file=sys.stderr)\n \n return prefix\n\n\ndef upload_thumbnail(metadata_file: str, output_file: str = None, public: bool = True) -> dict:\n \"\"\"\n 上传视频封面图到 OSS\n \n Args:\n metadata_file: 元数据 JSON 文件路径\n output_file: 输出文件路径(可选,保存上传结果)\n public: 是否公开访问\n \n Returns:\n dict: {success: bool, oss_url: str, error: str}\n \"\"\"\n try:\n import requests\n \n # 读取元数据\n with open(metadata_file, 'r', encoding='utf-8') as f:\n metadata = json.load(f)\n \n thumbnail_url = metadata.get('thumbnail', '')\n if not thumbnail_url:\n return {'success': False, 'error': '无封面图 URL'}\n \n # 构建 OSS 路径(不带时间戳,支持覆盖)\n # 格式:thumbnails/\u003c平台>/\u003c视频 ID>/cover.jpg\n platform = metadata.get('platform', 'unknown')\n video_id = metadata.get('id', 'unknown')\n \n # 如果 platform/video_id 缺失,尝试从 URL 识别\n if platform in ['unknown', None] or video_id in ['unknown', None]:\n video_url = metadata.get('webpage_url', '')\n if video_url:\n result = detect_platform(video_url)\n if not platform or platform == 'unknown':\n platform = result[0]\n if not video_id or video_id == 'unknown':\n video_id = result[1]\n \n # 清理平台名和视频 ID(移除特殊字符)\n platform = re.sub(r'[^a-zA-Z0-9]', '', platform)[:20].lower() or 'unknown'\n video_id = re.sub(r'[^a-zA-Z0-9_-]', '', video_id)[:50] or 'unknown'\n \n remote_key = f\"thumbnails/{platform}/{video_id}/cover.jpg\"\n \n # 下载封面图\n print(f\"🖼️ 下载封面:{thumbnail_url[:50]}...\", file=sys.stderr)\n response = requests.get(thumbnail_url, timeout=15, stream=True)\n if response.status_code != 200:\n return {'success': False, 'error': f'HTTP {response.status_code}'}\n \n # 保存到临时文件\n temp_file = '/tmp/thumbnail_temp.jpg'\n with open(temp_file, 'wb') as f:\n for chunk in response.iter_content(chunk_size=8192):\n f.write(chunk)\n \n # 上传到 OSS\n print(f\"☁️ 上传封面到 OSS: {remote_key}\", file=sys.stderr)\n result = upload_to_oss(temp_file, remote_key, public=public)\n \n # 清理临时文件\n if os.path.exists(temp_file):\n os.remove(temp_file)\n \n if result['success']:\n oss_url = result['url']\n print(f\"✅ 封面上传成功:{oss_url}\", file=sys.stderr)\n \n # 保存结果\n if output_file:\n cover_result = {\n 'success': True,\n 'oss_url': oss_url,\n 'remote_key': remote_key\n }\n with open(output_file, 'w', encoding='utf-8') as f:\n json.dump(cover_result, f, ensure_ascii=False, indent=2)\n \n return {'success': True, 'oss_url': oss_url}\n else:\n return result\n \n except Exception as e:\n return {'success': False, 'error': f'异常:{str(e)}'}\n\n\ndef main():\n parser = argparse.ArgumentParser(description='阿里云 OSS 图床上传工具')\n parser.add_argument('action', choices=['upload', 'batch', 'auto', 'thumbnail'], \n help='upload: 上传单文件 | batch: 批量上传目录 | auto: 自动识别平台 | thumbnail: 上传封面')\n parser.add_argument('path', help='文件路径或目录路径')\n parser.add_argument('--prefix', default=None, \n help='OSS 存储路径前缀(auto 模式自动识别)')\n parser.add_argument('--video-url', default=None,\n help='视频 URL(auto 模式用于识别平台)')\n parser.add_argument('--metadata', default=None,\n help='元数据 JSON 文件路径(auto 模式备用)')\n parser.add_argument('--public', action='store_true',\n help='返回公开 URL(需要 Bucket 配置为公开读)')\n parser.add_argument('--format', choices=['text', 'json'], default='text',\n help='输出格式')\n \n args = parser.parse_args()\n \n if args.action == 'upload':\n # 单文件上传\n result = upload_to_oss(args.path, public=args.public)\n if args.format == 'json':\n import json\n print(json.dumps(result, ensure_ascii=False, indent=2))\n else:\n if result['success']:\n print(f\"✅ 上传成功\")\n print(f\"📎 URL: {result['url']}\")\n print(f\"🔑 Key: {result['key']}\")\n print(f\"🌍 Endpoint: {result['endpoint']}\")\n else:\n print(f\"❌ 上传失败:{result['error']}\")\n sys.exit(1)\n \n elif args.action == 'batch':\n # 批量上传(使用指定前缀)\n prefix = args.prefix or f\"screenshots/unknown/unknown_{__import__('datetime').datetime.now().strftime('%Y%m%d_%H%M%S')}/\"\n results = upload_screenshots(args.path, prefix, args.public)\n \n if args.format == 'json':\n import json\n print(json.dumps(results, ensure_ascii=False, indent=2))\n else:\n success_count = sum(1 for r in results if r['success'])\n total_count = len(results)\n print(f\"\\n📊 上传完成:{success_count}/{total_count} 成功\")\n \n if success_count > 0:\n print(\"\\n📎 访问链接:\")\n for r in results:\n if r['success']:\n print(f\" - {r['oss_url'][:70]}...\")\n \n elif args.action == 'auto':\n # 自动识别平台并上传\n print(\"🔍 自动识别平台...\", file=sys.stderr)\n prefix = build_prefix(args.video_url, args.metadata)\n \n results = upload_screenshots(args.path, prefix, args.public)\n \n if args.format == 'json':\n import json\n print(json.dumps(results, ensure_ascii=False, indent=2))\n else:\n success_count = sum(1 for r in results if r['success'])\n total_count = len(results)\n print(f\"\\n📊 上传完成:{success_count}/{total_count} 成功\")\n \n if success_count > 0:\n print(\"\\n📎 访问链接:\")\n for r in results:\n if r['success']:\n print(f\" - {r['oss_url'][:70]}...\")\n \n elif args.action == 'thumbnail':\n # 上传封面图\n metadata_file = args.metadata or args.path\n output_file = args.path if args.path != metadata_file else None\n \n result = upload_thumbnail(metadata_file, output_file, args.public)\n \n if args.format == 'json':\n import json\n print(json.dumps(result, ensure_ascii=False, indent=2))\n else:\n if result['success']:\n print(f\"✅ 封面上传成功\")\n print(f\"📎 URL: {result['oss_url']}\")\n else:\n print(f\"❌ 封面上传失败:{result['error']}\")\n sys.exit(1)\n \n return 0\n\n\nif __name__ == '__main__':\n sys.exit(main())\n","content_type":"text/x-python; charset=utf-8","language":"python","size":15851,"content_sha256":"984f154ba836db2a1022d7befd183e3a38faf588d3534a728511fcc0bda2086a"},{"filename":"scripts/video-summarize.sh","content":"#!/bin/bash\n# video-summarize.sh - 视频总结生成完整流程 v1.0.13\n# 更新日期:2026-05-06\n# 用法:./video-summarize.sh \u003c视频 URL> [输出目录] [cookies 文件] [选项]\n\nset -e\n\n# ============== 错误处理与日志 ==============\n\n# 日志级别函数(ERROR_LOG 为空时不写入文件)\nlog_info() { echo \"ℹ️ $*\"; if [[ -n \"$ERROR_LOG\" ]]; then echo \"[INFO] $(date '+%Y-%m-%d %H:%M:%S') $*\" >> \"$ERROR_LOG\" 2>/dev/null; fi; }\nlog_warn() { echo \"⚠️ $*\"; if [[ -n \"$ERROR_LOG\" ]]; then echo \"[WARN] $(date '+%Y-%m-%d %H:%M:%S') $*\" >> \"$ERROR_LOG\" 2>/dev/null; fi; }\nlog_error() { echo \"❌ $*\"; if [[ -n \"$ERROR_LOG\" ]]; then echo \"[ERROR] $(date '+%Y-%m-%d %H:%M:%S') $*\" >> \"$ERROR_LOG\" 2>/dev/null; fi; }\nlog_debug() { if [[ \"$VERBOSE\" == \"true\" ]]; then echo \"🔍 $*\"; if [[ -n \"$ERROR_LOG\" ]]; then echo \"[DEBUG] $(date '+%Y-%m-%d %H:%M:%S') $*\" >> \"$ERROR_LOG\" 2>/dev/null; fi; fi; }\n\n# 错误捕获 trap\nERROR_LOG=\"\" # 在 OUTPUT_DIR 确定后设置\ncleanup_on_error() {\n local exit_code=$?\n if [[ $exit_code -ne 0 && -n \"$ERROR_LOG\" && -f \"$ERROR_LOG\" ]]; then\n log_error \"脚本执行失败 (退出码:$exit_code)\"\n log_error \"详细错误日志:$ERROR_LOG\"\n [[ \"$VERBOSE\" == \"true\" ]] && tail -30 \"$ERROR_LOG\"\n fi\n exit $exit_code\n}\ntrap cleanup_on_error ERR\n\n# 平台标识映射(统一小写)\n# bilibili, xhs, douyin, youtube\n\n# 解析参数\nVIDEO_URL=\"\"\nOUTPUT_DIR=\"\"\nUSER_SPECIFIED_OUTPUT=\"false\" # 标记用户是否手动指定了输出目录\nBILI_COOKIES_FILE=\"$HOME/.cookies/bilibili_cookies.txt\"\nDOUYIN_COOKIES_FILE=\"$HOME/.cookies/douyin_cookies.txt\"\nCOOKIES_FILE=\"\" # 将根据平台自动选择\nVERBOSE=\"false\"\nKEEP_VIDEO=\"false\"\nAUTO_PUSH=\"false\"\nRESUME=\"false\"\n\nfor arg in \"$@\"; do\n case $arg in\n --verbose|-v)\n VERBOSE=\"true\"\n ;;\n --keep-video)\n KEEP_VIDEO=\"true\"\n ;;\n --push|--auto-push)\n AUTO_PUSH=\"true\"\n ;;\n --resume)\n RESUME=\"true\"\n ;;\n *)\n if [[ -z \"$VIDEO_URL\" ]]; then\n VIDEO_URL=\"$arg\"\n elif [[ \"$USER_SPECIFIED_OUTPUT\" == \"false\" ]]; then\n OUTPUT_DIR=\"$arg\"\n USER_SPECIFIED_OUTPUT=\"true\"\n elif [[ \"$COOKIES_FILE\" == \"$HOME/.cookies/bilibili_cookies.txt\" ]]; then\n COOKIES_FILE=\"$arg\"\n fi\n ;;\n esac\ndone\n\nif [[ -z \"$VIDEO_URL\" ]]; then\n echo \"用法:./video-summarize.sh \u003c视频 URL> [输出目录] [cookies 文件] [选项]\"\n echo \"\"\n echo \"选项:\"\n echo \" --verbose, -v 显示详细日志(包括错误信息)\"\n echo \" --keep-video 保留视频/音频文件(默认清理)\"\n echo \" --push, --auto-push 完成后自动推送到 Notion\"\n echo \" --resume 从中断点恢复(检测进度文件)\"\n exit 1\nfi\n\nSCRIPT_DIR=\"$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\"\n\n# ============== 输入安全校验 ==============\n\n# URL 安全校验(阻断注入攻击)\nvalidate_url() {\n local url=\"$1\"\n \n # 最大长度 2048 字符\n if [[ ${#url} -gt 2048 ]]; then\n log_error \"URL 过长 (>2048 字符)\"\n exit 1\n fi\n \n # 字符黑名单:排除 shell 元字符\n if [[ \"$url\" =~ [\\;\\|\\&\\(\\)\\{\\}\\`\\$\\\u003c\\>] ]]; then\n log_error \"URL 包含非法字符\"\n exit 1\n fi\n \n # 协议白名单\n if [[ ! \"$url\" =~ ^https?:// ]]; then\n log_error \"仅支持 http/https 协议\"\n exit 1\n fi\n \n # 平台白名单\n if [[ \"$url\" =~ ^(https?://)([a-zA-Z0-9.-]*\\.)?(bilibili\\.com|b23\\.tv|xiaohongshu\\.com|xhslink\\.com|douyin\\.com|iesdouyin\\.com|v\\.douyin\\.com|youtube\\.com|youtu\\.be)/ ]]; then\n return 0\n fi\n \n log_error \"不支持的平台\"\n exit 1\n}\n\n# 输出目录安全校验(阻止路径遍历)\nvalidate_output_dir() {\n local dir=\"$1\"\n \n # 禁止路径遍历\n if [[ \"$dir\" =~ \\.\\. ]]; then\n log_error \"输出目录包含路径遍历字符 '..'\"\n exit 1\n fi\n \n # 确保绝对路径\n if [[ \"$dir\" != /* ]]; then\n dir=\"$(pwd)/$dir\"\n fi\n \n # 禁止写入系统敏感路径\n case \"$dir\" in\n /etc/*|/usr/*|/bin/*|/sbin/*|/root/*|/boot/*|/proc/*|/sys/*)\n log_error \"禁止写入系统目录\"\n exit 1\n ;;\n esac\n \n OUTPUT_DIR=\"$dir\"\n}\n\n# ============== 平台识别与输出目录生成 ==============\n\n# 提取平台标识\nextract_platform() {\n local url=\"$1\"\n \n # B 站:bilibili.com 或 b23.tv\n if [[ \"$url\" =~ (bilibili\\.com|b23\\.tv) ]]; then\n echo \"bilibili\"\n return\n fi\n \n # 小红书:xiaohongshu.com 或 xhslink.com\n if [[ \"$url\" =~ (xiaohongshu\\.com|xhslink\\.com) ]]; then\n echo \"xhs\"\n return\n fi\n \n # 抖音:douyin.com、iesdouyin.com 或 v.douyin.com(短链)\n if [[ \"$url\" =~ (douyin\\.com|iesdouyin\\.com|v\\.douyin\\.com) ]]; then\n echo \"douyin\"\n return\n fi\n \n # YouTube:youtube.com 或 youtu.be\n if [[ \"$url\" =~ (youtube\\.com|youtu\\.be) ]]; then\n echo \"youtube\"\n return\n fi\n \n # 未知平台\n echo \"unknown\"\n}\n\n# 提取视频 ID\nextract_video_id() {\n local url=\"$1\"\n local platform=\"$2\"\n \n case \"$platform\" in\n bilibili)\n # 提取 BV 号或 av 号\n if [[ \"$url\" =~ (BV[a-zA-Z0-9]+) ]]; then\n echo \"${BASH_REMATCH[1]}\"\n elif [[ \"$url\" =~ av([0-9]+) ]]; then\n echo \"av${BASH_REMATCH[1]}\"\n else\n # 短链:使用路径作为 ID\n local path=$(echo \"$url\" | sed -E 's|https?://[^/]+/||' | cut -d'?' -f1)\n echo \"${path:-b23_shortlink}\"\n fi\n ;;\n xhs)\n # 小红书笔记 ID(数字或带字母)\n if [[ \"$url\" =~ /([a-zA-Z0-9]{10,})(\\?|$|\\&|/) ]]; then\n echo \"${BASH_REMATCH[1]}\"\n else\n # 短链:使用路径作为 ID\n local path=$(echo \"$url\" | sed -E 's|https?://[^/]+/||' | cut -d'?' -f1)\n echo \"${path:-xhs_shortlink}\"\n fi\n ;;\n douyin)\n # 抖音:提取视频 ID(支持多种格式)\n # 格式 1: /video/1234567890\n if [[ \"$url\" =~ /video/([0-9]+) ]]; then\n echo \"${BASH_REMATCH[1]}\"\n # 格式 2: ?modal_id=1234567890 (课程/精选视频)\n elif [[ \"$url\" =~ modal_id=([0-9]+) ]]; then\n echo \"${BASH_REMATCH[1]}\"\n # 格式 3: 短链或其他\n elif [[ \"$url\" =~ /([a-zA-Z0-9_-]{10,})(\\?|$) ]]; then\n echo \"${BASH_REMATCH[1]}\"\n else\n # 短链:使用路径作为 ID\n local path=$(echo \"$url\" | sed -E 's|https?://[^/]+/||' | cut -d'?' -f1)\n echo \"${path:-douyin_shortlink}\"\n fi\n ;;\n youtube)\n # YouTube 视频 ID\n if [[ \"$url\" =~ v=([a-zA-Z0-9_-]+) ]]; then\n echo \"${BASH_REMATCH[1]}\"\n elif [[ \"$url\" =~ youtu\\.be/([a-zA-Z0-9_-]+) ]]; then\n echo \"${BASH_REMATCH[1]}\"\n else\n echo \"unknown\"\n fi\n ;;\n *)\n echo \"unknown\"\n ;;\n esac\n}\n\n# URL 安全校验\nvalidate_url \"$VIDEO_URL\"\n\n# 生成输出目录\nPLATFORM=$(extract_platform \"$VIDEO_URL\")\nVIDEO_ID=$(extract_video_id \"$VIDEO_URL\" \"$PLATFORM\")\n\nif [[ \"$USER_SPECIFIED_OUTPUT\" == \"true\" ]]; then\n validate_output_dir \"$OUTPUT_DIR\"\nelse\n OUTPUT_DIR=\"/tmp/video-summarizer/$PLATFORM/$VIDEO_ID\"\nfi\n\nmkdir -p \"$OUTPUT_DIR\"\n\n# 安全校验:.env 文件权限\nENV_FILE_CHECK=\"$HOME/.openclaw/.env\"\nif [[ -f \"$ENV_FILE_CHECK\" ]]; then\n ENV_PERMS=$(stat -c '%a' \"$ENV_FILE_CHECK\" 2>/dev/null)\n if [[ \"$ENV_PERMS\" != \"600\" && \"$ENV_PERMS\" != \"400\" ]]; then\n log_warn \".env 文件权限不安全 (当前: $ENV_PERMS),已修复为 600\"\n chmod 600 \"$ENV_FILE_CHECK\"\n fi\nfi\n\n# 安全校验:Cookie 文件权限\nif [[ -n \"$COOKIES_FILE\" && -f \"$COOKIES_FILE\" ]]; then\n COOKIE_PERMS=$(stat -c '%a' \"$COOKIES_FILE\" 2>/dev/null)\n if [[ \"$COOKIE_PERMS\" != \"600\" && \"$COOKIE_PERMS\" != \"400\" ]]; then\n log_warn \"Cookie 文件权限不安全 (当前: $COOKIE_PERMS),建议 600\"\n fi\nfi\n\n# 初始化错误日志文件(OUTPUT_DIR 已确定)\nERROR_LOG=\"$OUTPUT_DIR/error.log\"\necho \"\" > \"$ERROR_LOG\" # 清空旧日志\n\n# 根据平台选择 Cookies(抖音不使用 cookies,使用专用下载器)\nif [[ \"$PLATFORM\" == \"douyin\" ]]; then\n COOKIES_FILE=\"\" # 抖音不使用 cookies\nelse\n COOKIES_FILE=\"$BILI_COOKIES_FILE\"\nfi\n\n# 进度文件\nPROGRESS_FILE=\"$OUTPUT_DIR/.progress.json\"\n\necho \"📁 输出目录:$OUTPUT_DIR\"\necho \"🏷️ 平台:$PLATFORM | 视频 ID: $VIDEO_ID\"\nif [[ -n \"$COOKIES_FILE\" && -f \"$COOKIES_FILE\" ]]; then\n echo \"🍪 Cookies: $COOKIES_FILE\"\nelse\n echo \"🍪 Cookies: 无\"\nfi\n\n# 检查环境变量(自动推送)\nif [[ \"$AUTO_PUSH\" == \"true\" ]]; then\n if [[ -z \"$NOTION_VIDEO_SUMMARY_DATABASE_ID\" ]]; then\n NOTION_VIDEO_SUMMARY_DATABASE_ID=$(grep \"^NOTION_VIDEO_SUMMARY_DATABASE_ID=\" \"$HOME/.openclaw/.env\" 2>/dev/null | cut -d'=' -f2- | tr -d '\"' | tr -d \"'\")\n fi\nfi\n\n[[ \"$VERBOSE\" == \"true\" ]] && echo \"🔍 详细模式 | \"\n[[ \"$KEEP_VIDEO\" == \"true\" ]] && echo \"💾 保留视频 | \"\n[[ \"$AUTO_PUSH\" == \"true\" ]] && echo \"📤 自动推送 | \"\n[[ \"$RESUME\" == \"true\" ]] && echo \"♻️ 恢复模式\"\necho \"\"\n\n# 进度保存函数(环境变量传递,无 shell 注入)\nsave_progress() {\n local step=$1\n local status=$2\n local timestamp\n timestamp=$(date -Iseconds)\n PROGRESS_STEP=\"$step\" \\\n PROGRESS_STATUS=\"$status\" \\\n PROGRESS_TIMESTAMP=\"$timestamp\" \\\n VIDEO_URL=\"$VIDEO_URL\" \\\n OUTPUT_DIR=\"$OUTPUT_DIR\" \\\n PROGRESS_FILE=\"$PROGRESS_FILE\" \\\n python3 -c \"\nimport os, json\ndata = {\n 'video_url': os.environ['VIDEO_URL'],\n 'current_step': os.environ['PROGRESS_STEP'],\n 'status': os.environ['PROGRESS_STATUS'],\n 'timestamp': os.environ['PROGRESS_TIMESTAMP'],\n 'output_dir': os.environ['OUTPUT_DIR']\n}\nwith open(os.environ['PROGRESS_FILE'], 'w') as f:\n json.dump(data, f, indent=2)\n\"\n}\n\n# 检查进度(恢复模式,环境变量传递)\ncheck_progress() {\n if [[ \"$RESUME\" == \"true\" && -f \"$PROGRESS_FILE\" ]]; then\n local last_step\n last_step=$(PROGRESS_FILE=\"$PROGRESS_FILE\" python3 -c \"\nimport os, json\nwith open(os.environ['PROGRESS_FILE']) as f:\n print(json.load(f).get('current_step', ''))\n\" 2>/dev/null)\n if [[ -n \"$last_step\" ]]; then\n echo \"♻️ 检测到上次运行到:Step $last_step\"\n echo \" 将跳过已完成的步骤...\"\n return 0\n fi\n fi\n return 1\n}\n\necho \"🎬 Video Summarizer v1.0.13\"\necho \"\"\n\n# Step 1: 元数据\nif check_progress && [[ -f \"$OUTPUT_DIR/metadata.json\" && -s \"$OUTPUT_DIR/metadata.json\" ]]; then\n echo \"⏭️ Step 1 跳过\"\nelse\n echo \"📥 Step 1: 元数据...\"\n save_progress \"1\" \"running\"\n \n # 抖音平台特殊处理:使用专用工具获取元数据\n if [[ \"$PLATFORM\" == \"douyin\" ]]; then\n DOUYIN_SCRIPT=\"$SCRIPT_DIR/douyin_downloader.py\"\n \n if [[ -f \"$DOUYIN_SCRIPT\" ]]; then\n log_info \"抖音平台:使用专用工具获取元数据...\"\n \n # 获取视频信息(JSON 格式,便于解析)\n VIDEO_JSON=$(python3 \"$DOUYIN_SCRIPT\" --link \"$VIDEO_URL\" --action info --json 2>/dev/null)\n \n # 使用 stdin 管道传递 JSON,环境变量传递路径(无 shell 注入)\n echo \"$VIDEO_JSON\" | VIDEO_URL=\"$VIDEO_URL\" OUTPUT_DIR=\"$OUTPUT_DIR\" python3 -c \"\nimport sys, json, os\n\ntry:\n data = json.loads(sys.stdin.read())\n \n metadata = {\n 'title': str(data.get('title', '')).replace('\\\\n', ' ').replace('\\\\r', '').strip(),\n 'uploader': str(data.get('author', '')),\n 'uploader_id': str(data.get('video_id', '')),\n 'duration': 0,\n 'duration_string': str(data.get('duration_string', '')),\n 'thumbnail': str(data.get('cover', '')),\n 'webpage_url': os.environ['VIDEO_URL'],\n 'platform': 'douyin',\n 'download_url': str(data.get('url', '')),\n 'upload_date': str(data.get('upload_date', ''))\n }\n \n with open(os.path.join(os.environ['OUTPUT_DIR'], 'metadata.json'), 'w', encoding='utf-8') as f:\n json.dump(metadata, f, indent=2, ensure_ascii=False)\n \n print(f\\\"✅ 元数据完成 | 标题:{metadata['title']} | 视频 ID: {metadata['uploader_id']}\\\")\nexcept Exception as e:\n print(f'❌ 元数据解析失败:{e}', file=sys.stderr)\n sys.exit(1)\n\"\n else\n log_warn \"抖音下载脚本不存在,使用 yt-dlp\"\n if [[ -f \"$COOKIES_FILE\" ]]; then\n yt-dlp --cookies \"$COOKIES_FILE\" --dump-json \"$VIDEO_URL\" > \"$OUTPUT_DIR/metadata.json\" 2>/dev/null || echo '{}' > \"$OUTPUT_DIR/metadata.json\"\n else\n yt-dlp --dump-json \"$VIDEO_URL\" > \"$OUTPUT_DIR/metadata.json\" 2>/dev/null || echo '{}' > \"$OUTPUT_DIR/metadata.json\"\n fi\n fi\n else\n # 非抖音平台:使用 yt-dlp\n if [[ -f \"$COOKIES_FILE\" ]]; then\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n yt-dlp --cookies \"$COOKIES_FILE\" --dump-json \"$VIDEO_URL\" > \"$OUTPUT_DIR/metadata.json\"\n else\n yt-dlp --cookies \"$COOKIES_FILE\" --dump-json \"$VIDEO_URL\" > \"$OUTPUT_DIR/metadata.json\" 2>/dev/null\n fi\n else\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n yt-dlp --dump-json \"$VIDEO_URL\" > \"$OUTPUT_DIR/metadata.json\"\n else\n yt-dlp --dump-json \"$VIDEO_URL\" > \"$OUTPUT_DIR/metadata.json\" 2>/dev/null\n fi\n fi\n \n # 为小红书提取 upload_date(从笔记 ID 解析,环境变量传递路径)\n if [[ \"$PLATFORM\" == \"xhs\" ]]; then\n OUTPUT_DIR=\"$OUTPUT_DIR\" python3 -c \"\nimport json, os\nfrom datetime import datetime\n\noutput_dir = os.environ['OUTPUT_DIR']\nwith open(os.path.join(output_dir, 'metadata.json'), 'r') as f:\n meta = json.load(f)\n\nnote_id = meta.get('id', '') or meta.get('display_id', '') or meta.get('webpage_url', '').split('/')[-1].split('?')[0]\nif len(note_id) >= 8:\n try:\n ts = int(note_id[:8], 16)\n upload_date = datetime.fromtimestamp(ts).strftime('%Y%m%d')\n meta['upload_date'] = upload_date\n except:\n meta['upload_date'] = ''\nelse:\n meta['upload_date'] = meta.get('upload_date', '')\n\nwith open(os.path.join(output_dir, 'metadata.json'), 'w') as f:\n json.dump(meta, f, indent=2, ensure_ascii=False)\n\"\n fi\n fi\n \n TITLE=$(python3 -c \"import json; print(json.load(open('$OUTPUT_DIR/metadata.json')).get('title', 'Unknown'))\" 2>/dev/null || echo \"Unknown\")\n UPLOADER=$(python3 -c \"import json; print(json.load(open('$OUTPUT_DIR/metadata.json')).get('uploader', 'Unknown'))\" 2>/dev/null || echo \"Unknown\")\n DURATION=$(python3 -c \"import json; print(json.load(open('$OUTPUT_DIR/metadata.json')).get('duration_string', 'Unknown'))\" 2>/dev/null || echo \"Unknown\")\n DURATION_SEC=$(python3 -c \"import json; print(int(json.load(open('$OUTPUT_DIR/metadata.json')).get('duration', 0)))\" 2>/dev/null || echo \"0\")\n THUMBNAIL=$(python3 -c \"import json; print(json.load(open('$OUTPUT_DIR/metadata.json')).get('thumbnail', ''))\" 2>/dev/null || echo \"\")\n \n echo \"✅ 元数据完成 | 标题:$TITLE | UP 主:$UPLOADER | 时长:$DURATION\"\n [[ \"$VERBOSE\" == \"true\" ]] && echo \" 📄 $OUTPUT_DIR/metadata.json\"\n save_progress \"1\" \"done\"\nfi\n\n# Step 2 & 3: 并行执行 - 视频下载 + 字幕处理\n# 两者互不依赖,可以并行执行以节省时间\n\n# 检查是否可以跳过\nif check_progress && [[ -f \"$OUTPUT_DIR/video.mp4\" && -n \"$(find \"$OUTPUT_DIR\" -name \"*.vtt\" -o -name \"audio.txt\" 2>/dev/null | head -1)\" ]]; then\n echo \"⏭️ Step 2-3 跳过\"\n VIDEO_FILE=\"$OUTPUT_DIR/video.mp4\"\n SUBTITLE_FILE=$(find \"$OUTPUT_DIR\" -name \"*.vtt\" -o -name \"audio.txt\" 2>/dev/null | head -1)\n SUBTITLE_SOURCE=\"已存在\"\nelse\n echo \"🚀 Step 2-3: 并行执行 - 视频下载 + 字幕处理...\"\n save_progress \"2\" \"running\"\n \n # 初始化变量\n SUBTITLE_FILE=\"\"\n SUBTITLE_SOURCE=\"\"\n SUBTITLE_LOG=\"$OUTPUT_DIR/subtitle_download.log\"\n VIDEO_FILE=\"$OUTPUT_DIR/video.mp4\"\n VIDEO_LOG=\"$OUTPUT_DIR/video_download.log\"\n \n # ========== 任务 A: 视频下载(后台)==========\n (\n # 子进程中重新定义日志函数\n log_info() { echo \"ℹ️ $*\"; }\n log_warn() { echo \"⚠️ $*\"; }\n log_error() { echo \"❌ $*\"; }\n log_success() { echo \"✅ $*\"; }\n \n log_info \"[视频任务] 开始下载视频...\"\n DOWNLOAD_SUCCESS=false\n \n # 抖音平台特殊处理\n if [[ \"$PLATFORM\" == \"douyin\" ]]; then\n log_info \"[视频任务] 抖音平台:使用专用下载工具...\"\n DOUYIN_SCRIPT=\"$SCRIPT_DIR/douyin_downloader.py\"\n \n if [[ -f \"$DOUYIN_SCRIPT\" ]]; then\n python3 \"$DOUYIN_SCRIPT\" --link \"$VIDEO_URL\" --action info > \"$VIDEO_LOG\" 2>&1\n DOWNLOAD_URL=$(grep \"下载链接\" \"$VIDEO_LOG\" | sed 's/下载链接://')\n \n if [[ -n \"$DOWNLOAD_URL\" ]]; then\n log_info \"[视频任务] 下载链接已获取\"\n log_info \"[视频任务] 开始 curl 下载...\" >> \"$VIDEO_LOG\"\n curl -L -o \"$VIDEO_FILE\" \"$DOWNLOAD_URL\" 2>&1 | tee -a \"$VIDEO_LOG\"\n CURL_EXIT=$?\n log_info \"[视频任务] curl 退出码:$CURL_EXIT\" >> \"$VIDEO_LOG\"\n log_info \"[视频任务] 检查文件:$VIDEO_FILE\" >> \"$VIDEO_LOG\"\n ls -lh \"$VIDEO_FILE\" >> \"$VIDEO_LOG\" 2>&1\n # 检查文件是否存在且大小 > 0\n if [[ -f \"$VIDEO_FILE\" && -s \"$VIDEO_FILE\" ]]; then\n log_info \"[视频任务] 文件检查通过,设置 DOWNLOAD_SUCCESS=true\" >> \"$VIDEO_LOG\"\n DOWNLOAD_SUCCESS=true\n FILE_SIZE=$(ls -lh \"$VIDEO_FILE\" 2>/dev/null | awk '{print $5}')\n log_success \"[视频任务] 抖音视频下载成功 | $FILE_SIZE\" >> \"$VIDEO_LOG\"\n else\n log_warn \"[视频任务] 文件检查失败\" >> \"$VIDEO_LOG\"\n fi\n log_info \"[视频任务] DOWNLOAD_SUCCESS=$DOWNLOAD_SUCCESS\" >> \"$VIDEO_LOG\"\n fi\n else\n log_warn \"[视频任务] 抖音下载脚本不存在,回退到 yt-dlp\"\n fi\n fi\n \n # 非抖音平台或抖音下载失败\n if [[ \"$DOWNLOAD_SUCCESS\" != \"true\" ]]; then\n for i in 1 2 3; do\n log_info \"[视频任务] 尝试 $i/3...\"\n if [[ -n \"$COOKIES_FILE\" && -f \"$COOKIES_FILE\" ]]; then\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n yt-dlp --cookies \"$COOKIES_FILE\" -f \"bestvideo[height\u003c=720]+bestaudio/best[height\u003c=720]\" \\\n --merge-output-format mp4 \\\n -o \"$VIDEO_FILE\" \"$VIDEO_URL\" 2>&1 | tee -a \"$VIDEO_LOG\" && { DOWNLOAD_SUCCESS=true; break; } || {\n rm -f \"$VIDEO_FILE\"* 2>/dev/null\n }\n else\n yt-dlp --cookies \"$COOKIES_FILE\" -f \"bestvideo[height\u003c=720]+bestaudio/best[height\u003c=720]\" \\\n --merge-output-format mp4 \\\n -o \"$VIDEO_FILE\" \"$VIDEO_URL\" 2>/dev/null && { DOWNLOAD_SUCCESS=true; break; } || {\n rm -f \"$VIDEO_FILE\"* 2>/dev/null\n }\n fi\n else\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n yt-dlp -f \"bestvideo[height\u003c=720]+bestaudio/best[height\u003c=720]\" \\\n --merge-output-format mp4 \\\n -o \"$VIDEO_FILE\" \"$VIDEO_URL\" 2>&1 | tee -a \"$VIDEO_LOG\" && { DOWNLOAD_SUCCESS=true; break; } || {\n rm -f \"$VIDEO_FILE\"* 2>/dev/null\n }\n else\n yt-dlp -f \"bestvideo[height\u003c=720]+bestaudio/best[height\u003c=720]\" \\\n --merge-output-format mp4 \\\n -o \"$VIDEO_FILE\" \"$VIDEO_URL\" 2>/dev/null && { DOWNLOAD_SUCCESS=true; break; } || {\n rm -f \"$VIDEO_FILE\"* 2>/dev/null\n }\n fi\n fi\n done\n \n if [[ \"$DOWNLOAD_SUCCESS\" != \"true\" ]]; then\n log_warn \"[视频任务] 降级尝试...\"\n if [[ -n \"$COOKIES_FILE\" && -f \"$COOKIES_FILE\" ]]; then\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n yt-dlp --cookies \"$COOKIES_FILE\" -f \"best\" --merge-output-format mp4 -o \"$VIDEO_FILE\" \"$VIDEO_URL\" 2>&1 | tee -a \"$VIDEO_LOG\" || {\n log_error \"[视频任务] 视频下载失败\"\n exit 1\n }\n else\n yt-dlp --cookies \"$COOKIES_FILE\" -f \"best\" --merge-output-format mp4 -o \"$VIDEO_FILE\" \"$VIDEO_URL\" 2>/dev/null || {\n log_error \"[视频任务] 视频下载失败\"\n exit 1\n }\n fi\n else\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n yt-dlp -f \"best\" --merge-output-format mp4 -o \"$VIDEO_FILE\" \"$VIDEO_URL\" 2>&1 | tee -a \"$VIDEO_LOG\" || {\n log_error \"[视频任务] 视频下载失败\"\n exit 1\n }\n else\n yt-dlp -f \"best\" --merge-output-format mp4 -o \"$VIDEO_FILE\" \"$VIDEO_URL\" 2>/dev/null || {\n log_error \"[视频任务] 视频下载失败\"\n exit 1\n }\n fi\n fi\n fi\n fi\n \n log_success \"[视频任务] 视频下载完成 | $(ls -lh \"$VIDEO_FILE\" 2>/dev/null | awk '{print $5}')\"\n echo \"VIDEO_DONE\" > \"$OUTPUT_DIR/.video_done\"\n ) &\n VIDEO_PID=$!\n \n # ========== 任务 B: 字幕处理(后台)==========\n (\n # 子进程中重新定义日志函数\n log_info() { echo \"ℹ️ $*\"; }\n log_warn() { echo \"⚠️ $*\"; }\n log_error() { echo \"❌ $*\"; }\n log_success() { echo \"✅ $*\"; }\n \n log_info \"[字幕任务] 开始处理字幕...\"\n \n # 尝试 1: Cookies + 官方字幕\n if [[ -f \"$COOKIES_FILE\" ]]; then\n log_info \"[字幕任务] 尝试使用 Cookies 下载官方字幕...\"\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n yt-dlp --cookies \"$COOKIES_FILE\" \\\n --write-sub --write-auto-sub \\\n --sub-lang \"ai-zh,zh-Hans,zh,en\" \\\n --skip-download \\\n --convert-subs vtt \\\n -o \"$OUTPUT_DIR/video\" \"$VIDEO_URL\" 2>&1 | tee -a \"$SUBTITLE_LOG\" || true\n else\n yt-dlp --cookies \"$COOKIES_FILE\" \\\n --write-sub --write-auto-sub \\\n --sub-lang \"ai-zh,zh-Hans,zh,en\" \\\n --skip-download \\\n --convert-subs vtt \\\n -o \"$OUTPUT_DIR/video\" \"$VIDEO_URL\" 2>/dev/null || true\n fi\n \n SUBTITLE_FILE=$(find \"$OUTPUT_DIR\" -name \"*.vtt\" 2>/dev/null | head -1)\n [[ -n \"$SUBTITLE_FILE\" && -s \"$SUBTITLE_FILE\" ]] && SUBTITLE_SOURCE=\"官方字幕\"\n fi\n \n # 尝试 2: 自动字幕\n if [[ -z \"$SUBTITLE_FILE\" ]]; then\n log_info \"[字幕任务] 尝试下载自动字幕...\"\n if [[ -n \"$COOKIES_FILE\" && -f \"$COOKIES_FILE\" ]]; then\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n yt-dlp --cookies \"$COOKIES_FILE\" --write-auto-sub \\\n --sub-lang \"zh-Hans,zh,en\" \\\n --skip-download \\\n --convert-subs vtt \\\n -o \"$OUTPUT_DIR/video\" \"$VIDEO_URL\" 2>&1 | tee -a \"$SUBTITLE_LOG\" || true\n else\n yt-dlp --cookies \"$COOKIES_FILE\" --write-auto-sub \\\n --sub-lang \"zh-Hans,zh,en\" \\\n --skip-download \\\n --convert-subs vtt \\\n -o \"$OUTPUT_DIR/video\" \"$VIDEO_URL\" 2>/dev/null || true\n fi\n else\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n yt-dlp --write-auto-sub \\\n --sub-lang \"zh-Hans,zh,en\" \\\n --skip-download \\\n --convert-subs vtt \\\n -o \"$OUTPUT_DIR/video\" \"$VIDEO_URL\" 2>&1 | tee -a \"$SUBTITLE_LOG\" || true\n else\n yt-dlp --write-auto-sub \\\n --sub-lang \"zh-Hans,zh,en\" \\\n --skip-download \\\n --convert-subs vtt \\\n -o \"$OUTPUT_DIR/video\" \"$VIDEO_URL\" 2>/dev/null || true\n fi\n fi\n \n SUBTITLE_FILE=$(find \"$OUTPUT_DIR\" -name \"*.vtt\" 2>/dev/null | head -1)\n [[ -n \"$SUBTITLE_FILE\" && -s \"$SUBTITLE_FILE\" ]] && SUBTITLE_SOURCE=\"自动字幕\"\n fi\n \n # Plan B: 语音转录\n if [[ -z \"$SUBTITLE_FILE\" ]]; then\n log_warn \"[字幕任务] 未找到可用字幕,启动 Plan B 语音转录...\"\n \n AUDIO_FILE=\"$OUTPUT_DIR/audio.mp3\"\n SUBTITLE_FILE=\"$OUTPUT_DIR/audio.txt\"\n \n # 下载音频\n \"$SCRIPT_DIR/download-audio.sh\" \"$VIDEO_URL\" \"$AUDIO_FILE\"\n \n # 语音转录\n python3 \"$SCRIPT_DIR/transcribe-audio.py\" \"$AUDIO_FILE\" \"$SUBTITLE_FILE\"\n \n SUBTITLE_SOURCE=\"语音转录 (Plan B)\"\n log_success \"[字幕任务] 语音转录完成\"\n fi\n \n log_success \"[字幕任务] 字幕处理完成 | 来源:$SUBTITLE_SOURCE\"\n echo \"SUBTITLE_DONE\" > \"$OUTPUT_DIR/.subtitle_done\"\n ) &\n SUBTITLE_PID=$!\n \n # ========== 等待两个任务完成 ==========\n echo \"⏳ 等待视频下载和字幕处理完成...\"\n wait $VIDEO_PID\n VIDEO_EXIT=$?\n wait $SUBTITLE_PID\n SUBTITLE_EXIT=$?\n \n # 检查任务结果\n if [[ $VIDEO_EXIT -ne 0 ]]; then\n log_error \"视频下载任务失败\"\n exit 1\n fi\n \n # 检查视频文件是否真的存在(修复:yt-dlp 可能报错但仍返回 0)\n if [[ ! -f \"$VIDEO_FILE\" ]]; then\n log_warn \"视频文件不存在,检查是否仅有音频\"\n # 如果有音频文件,继续使用(截图将使用封面图代替)\n if [[ -f \"$OUTPUT_DIR/audio.mp3\" ]]; then\n log_info \"检测到音频文件,继续处理(截图将使用封面图)\"\n else\n log_error \"视频和音频文件都不存在,无法继续\"\n exit 1\n fi\n fi\n \n if [[ $SUBTITLE_EXIT -ne 0 ]]; then\n log_error \"字幕处理任务失败\"\n exit 1\n fi\n \n # 重新查找字幕文件(可能在子进程中生成)\n SUBTITLE_FILE=$(find \"$OUTPUT_DIR\" -name \"*.vtt\" -o -name \"audio.txt\" 2>/dev/null | head -1)\n \n echo \"✅ 视频下载完成 | $(ls -lh \"$VIDEO_FILE\" 2>/dev/null | awk '{print $5}')\"\n echo \"✅ 字幕处理完成 | 来源:${SUBTITLE_SOURCE:-未知}\"\n save_progress \"2\" \"done\"\nfi\n\n# Step 3: 文本提取(原 Step 3,现在是 Step 3)\nif check_progress && [[ -f \"$OUTPUT_DIR/transcript.txt\" ]]; then\n echo \"⏭️ Step 3 跳过\"\n WORD_COUNT=$(wc -w \u003c \"$OUTPUT_DIR/transcript.txt\")\nelse\n echo \"📝 Step 3: 文本提取...\"\n save_progress \"3\" \"running\"\n \n # 重新查找字幕文件(可能在并行任务中生成)\n if [[ -z \"$SUBTITLE_FILE\" ]]; then\n SUBTITLE_FILE=$(find \"$OUTPUT_DIR\" -name \"*.vtt\" -o -name \"audio.txt\" 2>/dev/null | head -1)\n fi\n \n # 检查是否有 VTT 字幕文件\n if [[ \"$SUBTITLE_FILE\" =~ \\.vtt$ ]]; then\n # VTT 格式:提取纯文本\n awk '/^WEBVTT/{next} /^[0-9]/{next} /^$/{next} /-->/ {next} {print}' \"$SUBTITLE_FILE\" | \\\n sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v \"^$\" > \"$OUTPUT_DIR/transcript.txt\"\n else\n # 纯文本格式(硅基流动等 API 返回)\n cp \"$SUBTITLE_FILE\" \"$OUTPUT_DIR/transcript.txt\"\n fi\n \n WORD_COUNT=$(wc -w \u003c \"$OUTPUT_DIR/transcript.txt\")\n LINE_COUNT=$(wc -l \u003c \"$OUTPUT_DIR/transcript.txt\")\n \n if [[ $WORD_COUNT -eq 0 ]]; then\n echo \" ❌ 字幕文本为空\"\n exit 1\n fi\n \n echo \"✅ 文本提取完成 | $LINE_COUNT 行 | $WORD_COUNT 字\"\n save_progress \"3\" \"done\"\nfi\n\n# Step 4: AI 分析\nif check_progress && [[ -f \"$OUTPUT_DIR/ai_result.json\" ]]; then\n echo \"⏭️ Step 4 跳过(AI 结果已存在)\"\nelse\n echo \"🤖 Step 4: AI 分析(生成 JSON)...\"\n save_progress \"4\" \"running\"\n \n AI_SCRIPT=\"$SCRIPT_DIR/analyze-subtitles-ai.py\"\n AI_JSON_FILE=\"$OUTPUT_DIR/ai_result.json\"\n AI_LOG=\"$OUTPUT_DIR/ai_analysis.log\"\n TEMP_SUMMARY=\"$OUTPUT_DIR/summary_temp.md\"\n \n if [[ -f \"$AI_SCRIPT\" ]]; then\n if [[ \"$VERBOSE\" == \"true\" ]]; then\n python3 \"$AI_SCRIPT\" \"$SUBTITLE_FILE\" \"$OUTPUT_DIR/metadata.json\" \"$TEMP_SUMMARY\" 2>&1 | tee -a \"$AI_LOG\"\n else\n python3 \"$AI_SCRIPT\" \"$SUBTITLE_FILE\" \"$OUTPUT_DIR/metadata.json\" \"$TEMP_SUMMARY\" 2>/dev/null\n fi\n \n if [[ -f \"$AI_JSON_FILE\" ]]; then\n echo \"✅ AI 分析完成 | JSON: $AI_JSON_FILE\"\n AI_SUCCESS=\"true\"\n else\n log_warn \"AI 分析失败,使用基础版总结(无 AI 分析)\"\n [[ \"$VERBOSE\" == \"true\" ]] && cat \"$AI_LOG\" | tail -10\n AI_SUCCESS=\"false\"\n # 创建空的 AI 结果文件,供后续步骤使用\n cat > \"$AI_JSON_FILE\" \u003c\u003c 'AIJSON'\n{\n \"note\": \"AI 分析失败,无法生成概述。\",\n \"key_points\": [],\n \"concepts\": [],\n \"warnings\": [],\n \"summary\": \"AI 分析失败,无法生成总结。\"\n}\nAIJSON\n fi\n else\n log_warn \"AI 脚本不存在,使用基础版总结\"\n AI_SUCCESS=\"false\"\n fi\n save_progress \"4\" \"done\"\nfi\n\n# Step 5: 截图(基于 AI 分析的时间戳)\nif check_progress && [[ -d \"$OUTPUT_DIR/screenshots\" && -n \"$(ls -A \"$OUTPUT_DIR/screenshots\" 2>/dev/null)\" ]]; then\n echo \"⏭️ Step 5 跳过\"\nelse\n echo \"🎬 Step 5: 截图(基于 AI 分析结果)...\"\n save_progress \"5\" \"running\"\n \n mkdir -p \"$OUTPUT_DIR/screenshots\"\n \n # 从 AI 分析结果中提取时间戳(核心要点 + 注意事项,支持最多 30 张)\n SCREENSHOT_TIMES=()\n AI_JSON=\"$OUTPUT_DIR/ai_result.json\"\n MAX_SCREENSHOTS=30\n \n if [[ -f \"$AI_JSON\" ]]; then\n echo \" 📊 从 AI 分析结果提取时间戳...\"\n # DURATION_SEC 数字校验\n if [[ ! \"$DURATION_SEC\" =~ ^[0-9]+$ ]]; then\n log_warn \"DURATION_SEC 异常,设为默认值 600\"\n DURATION_SEC=600\n fi\n # 使用环境变量传递路径和数值(无 shell 注入)\n SCREENSHOT_TIMES=($(AI_JSON=\"$AI_JSON\" DURATION_SEC=\"$DURATION_SEC\" MAX_SCREENSHOTS=\"$MAX_SCREENSHOTS\" python3 -c \"\nimport json, os, sys\n\ntry:\n with open(os.environ['AI_JSON'], 'r', encoding='utf-8') as f:\n data = json.load(f)\n \n times = []\n for point in data.get('key_points', []):\n time_str = point.get('time', '')\n if time_str:\n times.append(time_str)\n \n for warning in data.get('warnings', []):\n time_str = warning.get('time', '')\n if time_str:\n times.append(time_str)\n \n max_ss = int(os.environ.get('MAX_SCREENSHOTS', '30'))\n unique_times = list(dict.fromkeys(times))[:max_ss]\n \n if len(unique_times) \u003c 10:\n duration = int(os.environ['DURATION_SEC'])\n interval = duration // 11\n for i in range(1, 11):\n t = interval * i\n mm = t // 60\n ss = t % 60\n fallback = f'{mm:02d}:{ss:02d}'\n if fallback not in unique_times:\n unique_times.append(fallback)\n if len(unique_times) >= 10:\n break\n \n for t in unique_times[:max_ss]:\n print(t)\nexcept Exception as e:\n duration = int(os.environ['DURATION_SEC'])\n interval = duration // 11\n for i in range(1, 11):\n t = interval * i\n mm = t // 60\n ss = t % 60\n print(f'{mm:02d}:{ss:02d}')\n\"\n))\n echo \" ✅ 提取到 ${#SCREENSHOT_TIMES[@]} 个时间点\"\n else\n echo \" ⚠️ AI 结果不存在,使用均匀分布兜底\"\n fi\n \n # 如果提取失败,使用均匀分布\n if [[ ${#SCREENSHOT_TIMES[@]} -eq 0 ]]; then\n if [[ $DURATION_SEC -lt 120 ]]; then\n SCREENSHOT_TIMES=(\"00:02\" \"00:30\" \"01:00\" \"01:30\" \"02:00\")\n elif [[ $DURATION_SEC -lt 300 ]]; then\n SCREENSHOT_TIMES=(\"00:02\" \"00:30\" \"01:00\" \"01:30\" \"02:00\" \"02:30\" \"03:00\" \"03:30\" \"04:00\" \"04:30\")\n else\n INTERVAL=$((DURATION_SEC / 11))\n SCREENSHOT_TIMES=()\n for i in {1..10}; do\n T=$((INTERVAL * i))\n SCREENSHOT_TIMES+=($(printf \"%02d:%02d\" $((T/60)) $((T%60))))\n done\n fi\n echo \" 📊 使用均匀分布:${#SCREENSHOT_TIMES[@]} 个时间点\"\n fi\n \n # 执行截图\n SUCCESS_COUNT=0\n \n # 检查是否有视频文件\n if [[ ! -f \"$VIDEO_FILE\" ]]; then\n log_warn \"视频文件不存在,使用封面图代替截图\"\n # 从元数据获取封面 URL\n COVER_URL=$(python3 -c \"import json; print(json.load(open('$OUTPUT_DIR/metadata.json')).get('thumbnail', ''))\" 2>/dev/null)\n \n if [[ -n \"$COVER_URL\" ]]; then\n # 下载封面图作为所有截图\n for i in \"${!SCREENSHOT_TIMES[@]}\"; do\n OUT=\"$OUTPUT_DIR/screenshots/screenshot_$(printf \"%02d\" $((i+1))).jpg\"\n if curl -L -o \"$OUT\" \"$COVER_URL\" 2>/dev/null && [[ -s \"$OUT\" ]]; then\n echo \" 📸 ${SCREENSHOT_TIMES[$i]} → 封面图 (视频不可用)\"\n SUCCESS_COUNT=$((SUCCESS_COUNT + 1))\n fi\n done\n else\n log_warn \"封面 URL 也不存在,截图将为空\"\n fi\n else\n # 正常截图流程\n for i in \"${!SCREENSHOT_TIMES[@]}\"; do\n TIME=\"${SCREENSHOT_TIMES[$i]}\"\n # 转换为 HH:MM:SS 格式(ffmpeg 需要)\n if [[ \"$TIME\" =~ ^([0-9]+):([0-9]+)$ ]]; then\n FFMPEG_TIME=\"00:${BASH_REMATCH[1]}:${BASH_REMATCH[2]}\"\n elif [[ \"$TIME\" =~ ^([0-9]+):([0-9]+):([0-9]+)$ ]]; then\n FFMPEG_TIME=\"${BASH_REMATCH[1]}:${BASH_REMATCH[2]}:${BASH_REMATCH[3]}\"\n else\n FFMPEG_TIME=\"00:$TIME\"\n fi\n \n OUT=\"$OUTPUT_DIR/screenshots/screenshot_$(printf \"%02d\" $((i+1))).jpg\"\n ffmpeg -ss \"$FFMPEG_TIME\" -i \"$VIDEO_FILE\" -vframes 1 -update 1 -q:v 2 \"$OUT\" -y 2>/dev/null && {\n echo \" 📸 $TIME → screenshot_$(printf \"%02d\" $((i+1))).jpg\"\n SUCCESS_COUNT=$((SUCCESS_COUNT + 1))\n }\n done\n fi\n \n [[ $SUCCESS_COUNT -eq 0 ]] && { echo \"❌ 截图失败\"; exit 1; }\n echo \"✅ 截图完成 | $SUCCESS_COUNT 张\"\n \n # 保存截图时间戳(供 Markdown 渲染使用)\n SCREENSHOT_TIMES_FILE=\"$OUTPUT_DIR/screenshot_times.txt\"\n printf '%s\\n' \"${SCREENSHOT_TIMES[@]}\" > \"$SCREENSHOT_TIMES_FILE\"\n echo \" 💾 截图时间戳已保存:$SCREENSHOT_TIMES_FILE\"\n \n save_progress \"5\" \"done\"\nfi\n\n# Step 6: OSS 上传\nif check_progress && [[ -f \"$OUTPUT_DIR/screenshot_urls.txt\" ]]; then\n echo \"⏭️ Step 6 跳过\"\nelse\n echo \"☁️ Step 6: OSS 上传...\"\n save_progress \"6\" \"running\"\n \n OSS_SCRIPT=\"$SCRIPT_DIR/upload-to-oss.py\"\n OSS_URLS_FILE=\"$OUTPUT_DIR/screenshot_urls.txt\"\n OSS_LOG_FILE=\"$OUTPUT_DIR/oss_upload.log\"\n\nif [[ -f \"$OSS_SCRIPT\" ]]; then\n # 使用 auto 模式,自动识别平台并生成规范路径\n # 错误日志保存到 oss_upload.log\n python3 \"$OSS_SCRIPT\" auto \"$OUTPUT_DIR/screenshots\" \\\n --video-url \"$VIDEO_URL\" --metadata \"$OUTPUT_DIR/metadata.json\" \\\n --public --format json > \"$OSS_URLS_FILE\" 2> \"$OSS_LOG_FILE\"\n \n EXIT_CODE=$?\n \n if [[ -s \"$OSS_URLS_FILE\" ]]; then\n URL_COUNT=$(python3 -c \"import json; print(len([x for x in json.load(open('$OSS_URLS_FILE')) if x.get('success')]))\" 2>/dev/null || echo \"0\")\n [[ \"$URL_COUNT\" -gt 0 ]] && echo \"✅ OSS 上传成功 | $URL_COUNT 张\" || { echo \"⚠️ OSS 上传失败\"; echo \"[]\" > \"$OSS_URLS_FILE\"; }\n else\n echo \"⚠️ OSS 上传失败,使用本地路径\"\n echo \"[]\" > \"$OSS_URLS_FILE\"\n fi\n \n # 上传封面图\n COVER_FILE=\"$OUTPUT_DIR/cover_url.txt\"\n echo \"🖼️ 上传封面图...\" >> \"$OSS_LOG_FILE\"\n python3 \"$OSS_SCRIPT\" thumbnail \"$OUTPUT_DIR/metadata.json\" \\\n --public --format json > \"$COVER_FILE\" 2>> \"$OSS_LOG_FILE\"\n \n if [[ -f \"$COVER_FILE\" ]]; then\n COVER_URL=$(python3 -c \"import json; print(json.load(open('$COVER_FILE')).get('oss_url', ''))\" 2>/dev/null)\n if [[ -n \"$COVER_URL\" ]]; then\n echo \"✅ 封面上传成功:$COVER_URL\" >> \"$OSS_LOG_FILE\"\n # 更新元数据中的 thumbnail 字段(环境变量传递,无 shell 注入)\n OUTPUT_DIR=\"$OUTPUT_DIR\" COVER_URL=\"$COVER_URL\" python3 -c \"\nimport json, os\nwith open(os.path.join(os.environ['OUTPUT_DIR'], 'metadata.json'), 'r+', encoding='utf-8') as f:\n data = json.load(f)\n data['thumbnail'] = os.environ['COVER_URL']\n f.seek(0)\n json.dump(data, f, ensure_ascii=False, indent=2)\n f.truncate()\nprint('✅ 元数据已更新')\n\" >> \"$OSS_LOG_FILE\" 2>&1\n else\n echo \"⚠️ 封面上传失败,使用原始 URL\" >> \"$OSS_LOG_FILE\"\n fi\n fi\nelse\n echo \"⚠️ OSS 脚本不存在,使用本地路径\"\n echo \"[]\" > \"$OSS_URLS_FILE\"\nfi\nsave_progress \"6\" \"done\"\nfi\n\n# Step 7: 渲染最终 Markdown(截图和 OSS 完成后)\necho \"📝 Step 7: 渲染 Markdown...\"\nSUMMARY_FILE=\"$OUTPUT_DIR/summary.md\"\n\n# 重新调用 AI 脚本,让它读取已上传的截图 URL 并渲染最终 Markdown\npython3 \"$AI_SCRIPT\" \"$SUBTITLE_FILE\" \"$OUTPUT_DIR/metadata.json\" \"$SUMMARY_FILE\" 2>/dev/null || true\n\nif [[ -f \"$SUMMARY_FILE\" ]]; then\n echo \"✅ Markdown 渲染完成\"\n rm -f \"$TEMP_SUMMARY\" 2>/dev/null\nelse\n echo \"⚠️ Markdown 渲染失败,使用临时文件\"\n [[ -f \"$TEMP_SUMMARY\" ]] && mv \"$TEMP_SUMMARY\" \"$SUMMARY_FILE\"\nfi\n\n# Step 8: 输出\necho \"📁 Step 8: 整理输出...\"\nsave_progress \"8\" \"done\"\n\necho \"\"\necho \"================================\"\necho \"✅ 处理完成!\"\necho \"================================\"\necho \"📁 $OUTPUT_DIR\"\necho \"\"\n\n[[ \"$VERBOSE\" == \"true\" ]] && { echo \"📄 文件:\"; ls -lh \"$OUTPUT_DIR\"; echo \"📸 截图:\"; ls \"$OUTPUT_DIR/screenshots/\"; } || echo \"📄 总结:$SUMMARY_FILE | 截图:$(ls \"$OUTPUT_DIR/screenshots/\" 2>/dev/null | wc -l) 张\"\n\n# 安全清理(限制目录范围)\nif [[ \"$KEEP_VIDEO\" != \"true\" ]]; then\n if [[ \"$OUTPUT_DIR\" == /tmp/video-summarizer/* ]]; then\n rm -f \"$OUTPUT_DIR/video.mp4\" \"$OUTPUT_DIR/audio.mp3\" \"$OUTPUT_DIR/audio.webm\" \"$OUTPUT_DIR/video.f\"* 2>/dev/null\n echo \"🧹 已清理视频/音频\"\n else\n log_warn \"跳过清理:输出目录不在预期范围内\"\n fi\nelse\n echo \"💾 保留视频/音频\"\nfi\n\n# 截图状态\n[[ $(python3 -c \"import json; print(len([x for x in json.load(open('$OSS_URLS_FILE')) if x.get('success')]))\" 2>/dev/null || echo 0) -gt 0 ]] && echo \"📸 截图:✅ 已上传\" || echo \"📸 截图:⚠️ 本地\"\necho \"\"\n\n# Step 9: 推送到 Notion(自动检测配置)\necho \"📤 Step 9: 检查 Notion 配置...\"\n\n# 检查 Notion 配置(环境变量或 .env 文件)\nNOTION_CONFIGURED=\"false\"\nNOTION_DB_ID=\"\"\nNOTION_KEY=\"\"\n\n# 1. 优先使用环境变量\nif [[ -n \"$NOTION_VIDEO_SUMMARY_DATABASE_ID\" && -n \"$NOTION_API_KEY\" ]]; then\n NOTION_CONFIGURED=\"true\"\n NOTION_DB_ID=\"$NOTION_VIDEO_SUMMARY_DATABASE_ID\"\n NOTION_KEY=\"$NOTION_API_KEY\"\n log_info \" 使用环境变量配置\"\nelse\n # 2. 尝试从 .env 文件读取\n ENV_FILE=\"$HOME/.openclaw/.env\"\n if [[ -f \"$ENV_FILE\" ]]; then\n ENV_DB_ID=$(grep -E \"^NOTION_VIDEO_SUMMARY_DATABASE_ID=\" \"$ENV_FILE\" 2>/dev/null | cut -d'=' -f2 | tr -d '\"' | tr -d \"'\")\n ENV_KEY=$(grep -E \"^NOTION_API_KEY=\" \"$ENV_FILE\" 2>/dev/null | cut -d'=' -f2 | tr -d '\"' | tr -d \"'\")\n \n if [[ -n \"$ENV_DB_ID\" && -n \"$ENV_KEY\" ]]; then\n NOTION_CONFIGURED=\"true\"\n NOTION_DB_ID=\"$ENV_DB_ID\"\n NOTION_KEY=\"$ENV_KEY\"\n log_info \" 使用 .env 文件配置 ($ENV_FILE)\"\n fi\n fi\nfi\n\n# 执行推送或跳过\nif [[ \"$NOTION_CONFIGURED\" == \"true\" ]]; then\n echo \"ℹ️ Notion 配置已检测,开始推送...\"\n save_progress \"9\" \"running\"\n \n NOTION_VIDEO_SUMMARY_DATABASE_ID=\"$NOTION_DB_ID\" \\\n python3 \"$SCRIPT_DIR/push-to-notion.py\" \"$SUMMARY_FILE\"\n PUSH_EXIT=$?\n \n if [[ $PUSH_EXIT -eq 0 ]]; then\n echo \"✅ Notion 推送成功\"\n save_progress \"9\" \"done\"\n else\n echo \"❌ Notion 推送失败\"\n save_progress \"9\" \"failed\"\n fi\nelse\n echo \"⚠️ 未检测到 Notion 配置,跳过推送\"\n echo \" 💡 提示:如需自动推送,请配置以下任一方式:\"\n echo \" 方式 1 - 环境变量:\"\n echo \" export NOTION_VIDEO_SUMMARY_DATABASE_ID=your_database_id\"\n echo \" export NOTION_API_KEY=your_api_key\"\n echo \" 方式 2 - .env 文件:\"\n echo \" 在 $HOME/.openclaw/.env 中添加:\"\n echo \" NOTION_VIDEO_SUMMARY_DATABASE_ID=your_database_id\"\n echo \" NOTION_API_KEY=your_api_key\"\nfi\necho \"\"\n","content_type":"application/x-sh; charset=utf-8","language":"bash","size":42869,"content_sha256":"5ce6b5711321f56792fc498743e875ae00afdcd0e80efba3f0549286e70cb6d3"},{"filename":"templates/README.md","content":"# 视频总结模板\n\n## 用途\n\n此模板用于生成视频总结的 Markdown 文档,由 `analyze-subtitles-ai.py` 脚本调用。\n\n## 模板变量\n\n| 变量 | 说明 | 来源 |\n|------|------|------|\n| `{title}` | 视频标题 | 元数据 |\n| `{tags}` | 标签列表 | AI 生成 |\n| `{author}` | UP 主名称 | 元数据 |\n| `{thumbnail}` | 封面图 URL | 元数据 |\n| `{note}` | 100-200 字概述 | AI 生成 |\n| `{video_url}` | 视频链接 | 元数据 |\n| `{duration}` | 视频时长 | 元数据 |\n| `{uploader}` | UP 主 | 元数据 |\n| `{subtitle_source}` | 字幕来源 | 元数据 |\n| `{key_points}` | 核心要点 | AI 生成 |\n| `{concepts}` | 关键概念 | AI 生成 |\n| `{warnings}` | 注意事项 | AI 生成 |\n| `{screenshots}` | 视频帧截图 | OSS 上传结果 |\n| `{summary}` | 最终总结 | AI 生成 |\n| `{timestamp}` | 生成时间 | 自动生成 |\n| `{version}` | 技能版本 | 硬编码 |\n\n## 修改模板\n\n直接编辑 `summary.md` 文件即可修改输出格式。\n\n**示例:添加新章节**\n\n```markdown\n## 📚 关键概念\n\n{concepts}\n\n## 🎯 核心要点\n\n{key_points}\n\n## 🆕 新增章节\n\n这里是新章节内容...\n\n```\n\n## 测试模板修改\n\n```bash\n# 处理一个视频测试新模板\n./scripts/video-summarize.sh \"视频 URL\" /tmp/test-output\n\n# 查看生成的总结\ncat /tmp/test-output/summary.md\n```\n\n## 注意事项\n\n1. **保留变量名** - `{xxx}` 格式的变量不要删除,否则对应内容不会显示\n2. **Markdown 格式** - 保持标准 Markdown 语法,确保 Notion 兼容\n3. **变量位置** - 变量可以放在模板任意位置,按逻辑组织即可\n\n---\n\n**版本:** v1.0.13 \n**最后更新:** 2026-04-06\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":1684,"content_sha256":"324c41e4a0d2086fbe7f454677626e748761e874a41a46a6af04e309452f5b50"},{"filename":"templates/summary.md","content":"# {视频标题}\n\n**Tags:** `{标签 1}` `{标签 2}` `{标签 3}` `{标签 4}` `{标签 5}`\n\n**Author:** {UP 主名称}\n\n**Cover:**\n![视频封面]({封面 URL})\n\n---\n\n## 📝 Note\n\n{100-200 字概述}\n\n---\n\n## 📺 视频信息\n\n**来源:** {视频来源}\n**链接:** {视频 URL}\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*技能版本:video-summarizer v1.0.13*\n*维护人:Ajay Hao*\n","content_type":"text/markdown; charset=utf-8","language":"markdown","size":914,"content_sha256":"a152ee3f9b15dc62e042fc38cf894b99a46ddd9f66415482c779bb0baab54e2c"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Video Summarizer — OpenClaw Skill","type":"text"}]},{"type":"paragraph","content":[{"text":"将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。","type":"text"}]},{"type":"paragraph","content":[{"text":"版本","type":"text","marks":[{"type":"strong"}]},{"text":": 1.0.13","type":"text"},{"type":"br"},{"text":"发布","type":"text","marks":[{"type":"strong"}]},{"text":": 2026-05-10","type":"text"},{"type":"br"},{"text":"许可","type":"text","marks":[{"type":"strong"}]},{"text":": MIT","type":"text"},{"type":"br"},{"text":"作者","type":"text","marks":[{"type":"strong"}]},{"text":": Ajay Hao","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ ","type":"text"},{"text":"安全提示","type":"text","marks":[{"type":"strong"}]}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"本技能会将视频内容发送至第三方 AI 服务进行分析","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"建议使用专用 API Key(非生产环境)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OSS Bucket 请配置最小权限(仅写入/读取)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"B 站 Cookies 仅在你控制的设备上使用","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"📖 技能描述","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"核心能力","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"🎬 ","type":"text"},{"text":"多平台支持","type":"text","marks":[{"type":"strong"}]},{"text":": B 站、YouTube、小红书、抖音","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"📝 ","type":"text"},{"text":"智能分析","type":"text","marks":[{"type":"strong"}]},{"text":": AI(平台可配置)提取关键概念、核心要点、注意事项","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"📸 ","type":"text"},{"text":"截图嵌入","type":"text","marks":[{"type":"strong"}]},{"text":": 基于 AI 分析结果自动生成关键帧截图","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"☁️ ","type":"text"},{"text":"图床集成","type":"text","marks":[{"type":"strong"}]},{"text":": 阿里云 OSS 自动上传,永久链接","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"🚀 ","type":"text"},{"text":"一键推送","type":"text","marks":[{"type":"strong"}]},{"text":": 自动推送 Notion 数据库","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"技术特性","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"双模式转录","type":"text","marks":[{"type":"strong"}]},{"text":": Plan A(官方字幕)优先,Plan B(语音转录)兜底","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"并行优化","type":"text","marks":[{"type":"strong"}]},{"text":": 字幕下载与视频下载并行执行,节省 32% 时间","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"GPU 自适应","type":"text","marks":[{"type":"strong"}]},{"text":": 自动检测显存,选择最优 Whisper 模型","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"断点续跑","type":"text","marks":[{"type":"strong"}]},{"text":": 支持从中断点恢复,避免重复处理","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"四层标签","type":"text","marks":[{"type":"strong"}]},{"text":": 标题 hashtag → 元数据 → AI 关键词 → 默认值","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"🔐 安全与隐私说明","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"敏感数据处理","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"文件/路径","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用途","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"敏感性","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户控制","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~/.cookies/bilibili_cookies.txt","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"B 站官方字幕获取","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"高(Session Token)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户主动扫码生成,可随时删除","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~/.openclaw/.env","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API Keys 存储","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"高","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用户自行配置,skill 不修改","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"/tmp/video-summarizer-*/","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"临时输出","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"低","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"处理完成后可手动清理","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"外部服务端点","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"服务","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"域名","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用途","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"传输数据","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"LLM 平台 (可配置)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"通过 LLM_BASE_URL 指定","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AI 分析","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"字幕文本、元数据","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"阿里云 OSS","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"oss-cn-shanghai.aliyuncs.com","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"图床上传","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"截图、封面图","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Groq","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"api.groq.com","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"备用转录(可选,需代理)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"音频片段","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bilibili","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"bilibili.com","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"视频下载/字幕","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"无(仅下载)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"YouTube","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"youtube.com","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"视频下载/字幕","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"无(仅下载)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"抖音","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"douyin.com","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"iesdouyin.com","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"视频下载","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"无(仅下载)","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"说明","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"LLM 平台由用户通过 ","type":"text"},{"text":"LLM_BASE_URL","type":"text","marks":[{"type":"code_inline"}]},{"text":" 控制,支持任意 OpenAI 兼容接口(DeepSeek / DashScope / OpenAI / Groq 等)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Groq API 为可选加速方案,未配置时自动降级到本地 Faster-Whisper","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"抖音专用下载器 ","type":"text"},{"text":"douyin_downloader.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" 已移除硅基流动依赖,使用与主流程一致的 Groq API + 本地降级方案","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"最小权限建议","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"OSS Bucket","type":"text","marks":[{"type":"strong"}]},{"text":": 创建专用 Bucket,仅授予 PutObject/GetObject 权限","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"API Keys","type":"text","marks":[{"type":"strong"}]},{"text":": 使用子账号 Key,设置 IP 白名单","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"测试环境","type":"text","marks":[{"type":"strong"}]},{"text":": 首次使用建议在隔离环境测试","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"⚠️ 数据流向提醒","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"LLM 平台(可配置)","type":"text","marks":[{"type":"strong"}]},{"text":": 字幕文本和视频元数据会发送至用户配置的 LLM 服务(通过 ","type":"text"},{"text":"LLM_BASE_URL","type":"text","marks":[{"type":"code_inline"}]},{"text":" 指定),请勿处理包含敏感信息的视频","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Groq(可选)","type":"text","marks":[{"type":"strong"}]},{"text":": 音频片段会发送至 Groq API(加速转录),未配置时自动降级为本地 Faster-Whisper","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"阿里云 OSS","type":"text","marks":[{"type":"strong"}]},{"text":": 截图和封面图上传至 OSS Bucket,建议配置为专用 Bucket 并设置为私有读/写","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Notion","type":"text","marks":[{"type":"strong"}]},{"text":": 总结结果推送至 Notion 数据库,确保 Integration 仅授予目标数据库权限","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"B 站 Cookies","type":"text","marks":[{"type":"strong"}]},{"text":": 扫码登录后存储于 ","type":"text"},{"text":"~/.cookies/bilibili_cookies.txt","type":"text","marks":[{"type":"code_inline"}]},{"text":",权限已限制为 600,仅限本机访问","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cookie 安全","type":"text","marks":[{"type":"strong"}]},{"text":": 登录完成后临时 ","type":"text"},{"text":"cookies.json","type":"text","marks":[{"type":"code_inline"}]},{"text":" 会自动清理,不残留 skill 目录内","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"🎯 平台支持详情","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Bilibili(完整支持)","type":"text"}]},{"type":"paragraph","content":[{"text":"字幕","type":"text","marks":[{"type":"strong"}]},{"text":": ✅ 官方字幕 + 自动字幕","type":"text"},{"type":"br"},{"text":"语音转录","type":"text","marks":[{"type":"strong"}]},{"text":": ✅ 支持(Plan B)","type":"text"},{"type":"br"},{"text":"Cookies","type":"text","marks":[{"type":"strong"}]},{"text":": 推荐(获取官方字幕)","type":"text"},{"type":"br"},{"text":"下载工具","type":"text","marks":[{"type":"strong"}]},{"text":": yt-dlp (>=2026.03.17)","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"操作步骤","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 1. 扫码登录(首次使用,获取官方字幕)\ncd ~/.openclaw/skills/video-summarizer/scripts\n./bili-login.sh\n\n# 2. 处理视频\n./video-summarize.sh \"https://www.bilibili.com/video/BV1xxxx\"\n\n# 3. 查看结果\ncat /tmp/video-summarizer-*/summary.md","type":"text"}]},{"type":"paragraph","content":[{"text":"说明","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Cookies 文件:","type":"text"},{"text":"~/.cookies/bilibili_cookies.txt","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"无 Cookies 时使用 Plan B 语音转录","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"支持 b23.tv 短链","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"YouTube(完整支持)","type":"text"}]},{"type":"paragraph","content":[{"text":"字幕","type":"text","marks":[{"type":"strong"}]},{"text":": ✅ 自动字幕(多语言)","type":"text"},{"type":"br"},{"text":"语音转录","type":"text","marks":[{"type":"strong"}]},{"text":": ✅ 支持(Plan B)","type":"text"},{"type":"br"},{"text":"Cookies","type":"text","marks":[{"type":"strong"}]},{"text":": ❌ 不需要","type":"text"},{"type":"br"},{"text":"下载工具","type":"text","marks":[{"type":"strong"}]},{"text":": yt-dlp (>=2026.03.17)","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"操作步骤","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 直接处理(无需登录)\n./video-summarize.sh \"https://www.youtube.com/watch?v=dQw4w9WgXcQ\"\n\n# 指定输出目录\n./video-summarize.sh \"https://youtu.be/dQw4w9WgXcQ\" /tmp/output","type":"text"}]},{"type":"paragraph","content":[{"text":"说明","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"需网络可达(可能需要代理)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"优先下载英文字幕,无则用语音转录","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"支持 youtu.be 短链","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"小红书(基本支持)","type":"text"}]},{"type":"paragraph","content":[{"text":"字幕","type":"text","marks":[{"type":"strong"}]},{"text":": ❌ 无字幕","type":"text"},{"type":"br"},{"text":"语音转录","type":"text","marks":[{"type":"strong"}]},{"text":": ✅ 唯一方式(Plan B)","type":"text"},{"type":"br"},{"text":"Cookies","type":"text","marks":[{"type":"strong"}]},{"text":": ❌ 不需要","type":"text"},{"type":"br"},{"text":"下载工具","type":"text","marks":[{"type":"strong"}]},{"text":": yt-dlp (>=2026.03.17)","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"操作步骤","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 直接处理(自动使用 Plan B 语音转录)\n./video-summarize.sh \"https://www.xiaohongshu.com/explore/xxxx\"\n\n# 或短链\n./video-summarize.sh \"https://xhslink.com/o/xxxx\"","type":"text"}]},{"type":"paragraph","content":[{"text":"说明","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"必须使用 Plan B 语音转录","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"推荐配置 GROQ_API_KEY 加速转录","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"自动上传封面图到 OSS","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"抖音(完整支持)","type":"text"}]},{"type":"paragraph","content":[{"text":"字幕","type":"text","marks":[{"type":"strong"}]},{"text":": ❌ 无字幕","type":"text"},{"type":"br"},{"text":"语音转录","type":"text","marks":[{"type":"strong"}]},{"text":": ✅ 唯一方式(Plan B)","type":"text"},{"type":"br"},{"text":"Cookies","type":"text","marks":[{"type":"strong"}]},{"text":": ❌ 不需要(专用下载器)","type":"text"},{"type":"br"},{"text":"下载工具","type":"text","marks":[{"type":"strong"}]},{"text":": douyin_downloader.py","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"操作步骤","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 直接处理(专用下载器,无需 Cookies)\n./video-summarize.sh \"https://www.douyin.com/video/7234567890\"\n\n# 支持短链\n./video-summarize.sh \"https://v.douyin.com/abc123/\"","type":"text"}]},{"type":"paragraph","content":[{"text":"说明","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"使用专用下载器 ","type":"text"},{"text":"douyin_downloader.py","type":"text","marks":[{"type":"code_inline"}]},{"text":"(仅用于获取元数据和下载视频)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"无反爬限制,无需 Cookies","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"语音转录使用主流程的 ","type":"text"},{"text":"transcribe-audio.py","type":"text","marks":[{"type":"code_inline"}]},{"text":"(Groq API + 本地降级)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"douyin_downloader.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" 的 extract 模式已移除硅基流动依赖,改用 Groq API + 本地降级","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"⚙️ 配置详解","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"环境变量(~/.openclaw/.env)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# ========== 必需配置 ==========\n\n# LLM AI 分析(OpenAI 兼容接口,三者缺一不可)\nLLM_API_KEY=sk-your-api-key\nLLM_BASE_URL=https://api.deepseek.com\nLLM_MODEL=deepseek-v4-pro\n\n# 其他平台示例:\n# LLM_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1 # DashScope\n# LLM_BASE_URL=https://api.openai.com/v1 # OpenAI\n# LLM_BASE_URL=https://api.groq.com/openai/v1 # Groq\n\n# 阿里云 OSS 图床\nALIYUN_OSS_AK=your_access_key_id\nALIYUN_OSS_SK=your_access_key_secret\nALIYUN_OSS_BUCKET_ID=your_bucket_name\nALIYUN_OSS_ENDPOINT=oss-cn-shanghai.aliyuncs.com\n\n# ========== 可选配置 ==========\n\n# Notion 自动推送(可选)\nNOTION_API_KEY=\u003cyour_notion_api_key>\nNOTION_VIDEO_SUMMARY_DATABASE_ID=\u003cyour_database_id> # 单个数据库 ID\n\n# 语音转录加速(Groq API,可选)\n# 国内需代理访问,如未配置自动降级到本地 Faster-Whisper\n# 不配置此项不影响使用\n# 注意:douyin_downloader.py 也使用此变量(已移除硅基流动依赖)\nGROQ_API_KEY=\u003cyour_groq_api_key>\n\n# 本地 Whisper 模型(有 GPU 时自动检测)\nWHISPER_MODEL=base # tiny/base/small/medium/large","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"OSS Bucket 要求","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"访问权限","type":"text","marks":[{"type":"strong"}]},{"text":": 公开可读(直接 URL 访问)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"CORS 配置","type":"text","marks":[{"type":"strong"}]},{"text":": 允许跨域访问(Notion 嵌入需要)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"存储类型","type":"text","marks":[{"type":"strong"}]},{"text":": 标准存储(低频访问会影响加载速度)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Notion 数据库配置","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"数据库属性(Properties)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"属性名","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"类型","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"说明","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"来源","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Title","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"title","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"视频标题(≤200 字符)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Markdown ","type":"text"},{"text":"# 标题","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Source","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rich_text","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"平台来源(Bilibili/YouTube/小红书/抖音)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"metadata.platform / URL 推断","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Author","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rich_text","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"UP 主/作者名称","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Markdown ","type":"text"},{"text":"**UP 主:**","type":"text","marks":[{"type":"code_inline"}]},{"text":" / metadata.uploader","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Url","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"url","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"视频原始链接","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"metadata.webpage_url / Markdown ","type":"text"},{"text":"**链接:**","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tags","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"multi_select","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"标签(最多 5 个)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"三层策略提取","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PubDate","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"date","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"发布日期","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"metadata.upload_date","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Length","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rich_text","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"视频时长(MM:SS 格式)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"metadata.duration_string","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cover","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"files","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"封面图片(可选,外部 URL)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"metadata.thumbnail","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ts","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"date","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"创建时间戳(ISO 8601,东八区 +08:00)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"当前时间","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"配置步骤","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"创建数据库","type":"text","marks":[{"type":"strong"}]},{"text":"(Table 视图)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"添加上述 9 个属性","type":"text","marks":[{"type":"strong"}]},{"text":"(字段名必须完全匹配)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"获取 Database ID","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"打开数据库 → 复制 URL 中 ","type":"text"},{"text":"?v=","type":"text","marks":[{"type":"code_inline"}]},{"text":" 后的 ID","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"示例:","type":"text"},{"text":"https://notion.so/your-workspace/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx?v=yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Database ID = ","type":"text"},{"text":"yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy","type":"text","marks":[{"type":"code_inline"}]}]}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"配置环境变量","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"NOTION_API_KEY=\u003cyour_notion_api_key>\nNOTION_VIDEO_SUMMARY_DATABASE_ID=\u003cyour_database_id> # 单个数据库","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"数据库视图示例","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"┌─────────┬──────────┬─────────┬──────┬──────┬─────────┬────────┬───────┬─────────────┐\n│ Title │ Source │ Author │ Url │ Tags │ PubDate │ Length │ Cover │ ts │\n│ title │ text │ text │ url │ multi│ date │ text │ files │ date │\n└─────────┴──────────┴─────────┴──────┴──────┴─────────┴────────┴───────┴─────────────┘","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"🏗️ 系统架构","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"处理流程","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"用户输入 (视频 URL)\n ↓\nStep 1: 平台识别 + 元数据\n ↓\n┌──────┴──────┐\n↓ ↓\nStep 2: 字幕 Step 3: 视频下载 ← 并行执行\n ↓ ↓\n └──────┬──────┘\n ↓\nStep 4: 文本提取 (VTT → TXT / Plan B 转录)\n ↓\nStep 5: AI 分析 (OpenAI 兼容接口,通过 LLM_API_KEY + LLM_BASE_URL + LLM_MODEL 配置)\n ↓\nStep 6: 截图生成 (ffmpeg, 基于 AI 时间戳)\n ↓\nStep 7: OSS 上传 (截图 + 封面)\n ↓\nStep 8: Markdown 渲染\n ↓\nStep 9: Notion 推送 (可选)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"技术栈","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"层级","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"技术","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"编排层","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bash (video-summarize.sh)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"分析层","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python + OpenAI 兼容 LLM API","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"转录层","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Groq API (可选) / Faster-Whisper (本地)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"工具层","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"yt-dlp (>=2026.03.17), ffmpeg (>=6.1), oss2, requests","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"数据文件","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"文件","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"来源","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"用途","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"metadata.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"yt-dlp / douyin_downloader","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"视频元数据","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"transcript.txt","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"VTT 提取 / Plan B 转录","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"纯文本字幕","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"ai_result.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"analyze-subtitles-ai.py","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AI 分析结果","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"screenshot_urls.txt","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload-to-oss.py","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"截图 OSS 链接","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"summary.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"analyze-subtitles-ai.py","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"最终总结","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"📋 脚本清单","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"核心脚本(4 个)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"脚本","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"功能","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"video-summarize.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"主流程编排(Plan A/B 自动选择)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"analyze-subtitles-ai.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"AI 分析 + Markdown 渲染","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"upload-to-oss.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OSS 图床上传(截图 + 封面)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"push-to-notion.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Notion 推送(优先使用环境变量 ","type":"text"},{"text":"NOTION_VIDEO_SUMMARY_DATABASE_ID","type":"text","marks":[{"type":"code_inline"}]},{"text":",其次 CLI 参数)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"平台工具(2 个)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"脚本","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"功能","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"bili-login.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"B 站扫码登录(获取 Cookies)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"douyin_downloader.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"抖音专用下载器(无需 Cookies,info/download 操作无需 API;extract 模式使用 Groq API + 本地降级转录音频)","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"辅助工具(4 个)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"脚本","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"功能","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"download-audio.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan B: 音频下载","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"transcribe-audio.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan B: 语音转录(GPU 自适应)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"check-config.sh","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"配置检查","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"convert-bili-cookie.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cookies 格式转换","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"🎯 Plan A vs Plan B","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"对比表","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"项目","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan A","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan B","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"字幕来源","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"平台官方字幕","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"语音转录","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"准确率","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"90%+","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"80-90%","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"速度","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"快 (1-2 分钟)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"较慢 (3-5 分钟)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"依赖","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cookies(B 站)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"GPU 或 API Key","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"各平台使用情况","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"平台","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan A","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan B","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"默认","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bilibili","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✅ 官方 + 自动","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✅ 备用","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan A","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"YouTube","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✅ 自动字幕","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✅ 备用","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan A","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"小红书","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"❌ 无","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✅ 唯一","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan B","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"抖音","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"❌ 无","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"✅ 唯一","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan B","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Plan B 三层降级方案","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"1. Groq API (whisper-large-v3) → 云端高速(可选,需配置 GROQ_API_KEY 且网络可达)\n └─ 失败/未配置 → 降级到本地\n\n2. Faster-Whisper (本地) → GPU/CPU 自适应\n ├─ GPU ≥8GB → large-v2 模型\n ├─ GPU ≥4GB → medium 模型\n ├─ GPU ≥2GB → small 模型\n ├─ GPU ≥1GB → base 模型 (GPU)\n └─ 无 GPU → base 模型 (CPU)\n └─ 失败 → 降级到方案 3\n\n3. Whisper.cpp / OpenAI Whisper (本地保底)\n └─ 完全离线,作为最终兜底","type":"text"}]},{"type":"paragraph","content":[{"text":"说明","type":"text","marks":[{"type":"strong"}]},{"text":":","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Groq API 为可选配置,未配置时直接使用本地 Faster-Whisper","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"本地转录无需任何 API Key,完全离线运行","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"国内使用 Groq 需配置代理","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"抖音专用下载器","type":"text","marks":[{"type":"strong"}]},{"text":" ","type":"text"},{"text":"douyin_downloader.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" 也使用 Groq API + 本地降级方案(已移除硅基流动依赖),extract 模式用于音频转录","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"📁 输出文件结构","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"output/\n├── summary.md # 📝 最终总结(主要成果)\n├── ai_result.json # 🧠 AI 分析原始结果(结构化 JSON)\n├── screenshot_urls.txt # 🔗 截图 OSS 链接\n├── metadata.json # 📊 视频元数据\n├── transcript.txt # 📄 纯文本字幕\n├── audio.txt # 🎤 语音转录原始文本(Plan B 时使用)\n├── cover_url.txt # ☁️ 封面 OSS 上传结果\n├── screenshot_times.txt # ⏱️ 截图时间戳记录\n├── screenshots/ # 📸 截图原图(本地备份)\n├── cover.jpg # 🖼️ 封面图(本地备份)\n└── *.log # 📋 日志文件(verbose 模式)","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"OSS 路径规范","type":"text"}]},{"type":"paragraph","content":[{"text":"截图路径","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"/screenshots/\u003c平台>/\u003c视频 ID>_\u003c时间戳>/\u003c截图文件>","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"screenshots/bilibili/BV1eTPEzNEqf_20260405_053203/screenshot_01.jpg\nscreenshots/douyin/7234567890_20260405_175010/chapter_01.jpg\nscreenshots/xhs/69c1493b000000002003b3ce_20260405_152852/screenshot_01.jpg\nscreenshots/youtube/dQw4w9WgXcQ_20260405_120000/screenshot_01.jpg","type":"text"}]},{"type":"paragraph","content":[{"text":"封面路径","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"thumbnails/\u003c平台>/\u003c视频 ID>/cover.jpg","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"thumbnails/bilibili/BV1eTPEzNEqf/cover.jpg\nthumbnails/douyin/7234567890/cover.jpg","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"🏷️ 标签策略","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"四层提取策略","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":""},"content":[{"text":"1. 标题 hashtag → #([\\w\\u4e00-\\u9fa5]+)\n 示例:\"#AI 教程 #大模型原理\" → [\"AI 教程\", \"大模型原理\"]\n\n2. 元数据 tags → yt-dlp 提取的原始标签\n 示例:[\"原理\", \"AI\", \"教程\", \"claude\", \"大模型\"]\n\n3. AI 关键词 → AI 分析提取的核心概念\n 示例:[\"Transformer\", \"注意力机制\", \"深度学习\"]\n\n4. 默认值 → 视频总结/知识分享/学习\n 当前三层都为空时使用","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"标签规则","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"长度","type":"text","marks":[{"type":"strong"}]},{"text":": 2-15 字符(兼容英文如 \"openclaw\")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"数量","type":"text","marks":[{"type":"strong"}]},{"text":": 最多 5 个","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"去重","type":"text","marks":[{"type":"strong"}]},{"text":": 自动去重,保留唯一值","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"优先级","type":"text","marks":[{"type":"strong"}]},{"text":": 1 → 2 → 3 → 4","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"🔧 故障排查","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Cookies 过期(B 站)","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 重新扫码登录\n./bili-login.sh","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"配置检查","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 运行检查脚本\n./check-config.sh","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"查看详细日志","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"# 使用 verbose 模式\n./video-summarize.sh \"URL\" --verbose\n\n# 查看错误日志\ncat /tmp/output/error.log","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"常见问题","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"问题","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"原因","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"解决方案","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"截图 404","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"OSS 路径不匹配","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"python3 upload-to-oss.py auto /tmp/output","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"标签默认值","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"标签提取失败","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"检查标题 hashtag 格式 ","type":"text"},{"text":"#标签","type":"text","marks":[{"type":"code_inline"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"转录失败","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"无 GPU/API 配额","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"检查 ","type":"text"},{"text":"GROQ_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":",或确保 ","type":"text"},{"text":"faster-whisper","type":"text","marks":[{"type":"code_inline"}]},{"text":" 已安装","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Notion 推送失败","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API Key 过期","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"更新 ","type":"text"},{"text":"NOTION_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":"(可选功能,仅 --push 需要)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"并行任务失败","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"依赖缺失","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"检查 ","type":"text"},{"text":"ffmpeg","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"yt-dlp","type":"text","marks":[{"type":"code_inline"}]},{"text":" 安装(版本要求:ffmpeg >= 6.1, yt-dlp >= 2026.03.17)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"抖音下载失败","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"链接格式错误","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"使用完整 URL 或 v.douyin.com 短链","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"抖音文案提取失败","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"无 API Key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"douyin_downloader.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" 会自动降级到本地 Faster-Whisper,无需单独配置","type":"text"}]}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"📊 性能基准","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"处理时间(10 分钟视频)","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"平台","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan A","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan B(本地)","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Plan B(Groq)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Bilibili","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~90 秒","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~150 秒","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~120 秒","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"YouTube","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~90 秒","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~150 秒","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~120 秒","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"小红书","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~150 秒","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~120 秒","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"抖音","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"-","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~150 秒","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"~120 秒","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"优化效果","type":"text","marks":[{"type":"strong"}]},{"text":": 并行优化后节省约 30 秒(32%↓)","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"📝 输出格式(summary.md)","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"标题 + Tags + Author","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"📝 Note","type":"text","marks":[{"type":"strong"}]},{"text":" — AI 概述(150-250 字)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"📺 视频信息","type":"text","marks":[{"type":"strong"}]},{"text":" — 链接/时长/播放数据","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"📚 关键概念","type":"text","marks":[{"type":"strong"}]},{"text":" — 术语表格(3-5 个,按时间排序)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"🎯 核心要点","type":"text","marks":[{"type":"strong"}]},{"text":" — emoji+ 描述 + 时间戳(5-8 个)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"🎬 视频章节","type":"text","marks":[{"type":"strong"}]},{"text":" — 标题 + 时间轴 + 截图","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"⚠️ 注意事项","type":"text","marks":[{"type":"strong"}]},{"text":" — 特别提醒(2-4 个)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"💡 总结","type":"text","marks":[{"type":"strong"}]},{"text":" — AI 归纳(200-300 字)","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"🔜 后续优化","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"计划中","type":"text"}]},{"type":"checkbox_list","attrs":{"id":null},"content":[{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"代码重构(提取公共函数)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"单元测试(核心函数覆盖率 80%+)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"性能优化(截图并行上传、结果缓存)","type":"text"}]}]},{"type":"checkbox_item","attrs":{"checked":false},"content":[{"type":"paragraph","content":[{"text":"支持更多平台(TikTok、Instagram Reels)","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"📞 更多文档","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"快速入门","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"README.md","type":"text","marks":[{"type":"link","attrs":{"href":"README.md","title":null}}]},{"text":" - 5 分钟上手","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"变更历史","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"CHANGELOG.md","type":"text","marks":[{"type":"link","attrs":{"href":"CHANGELOG.md","title":null}}]},{"text":" - 版本演进","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"提示词配置","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"prompt.json","type":"text","marks":[{"type":"link","attrs":{"href":"prompt.json","title":null}}]},{"text":" - AI 分析参数","type":"text"}]}]}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":2},"content":[{"text":"维护人","type":"text","marks":[{"type":"strong"}]},{"text":": Ajay Hao","type":"text"},{"type":"br"},{"text":"项目地址","type":"text","marks":[{"type":"strong"}]},{"text":": https://github.com/AjayHao/video-summarizer","type":"text"},{"type":"br"},{"text":"OpenClaw Skill","type":"text","marks":[{"type":"strong"}]},{"text":": 已发布到 clawdhub","type":"text"}]}]},"metadata":{"date":"2026-06-05","name":"video-summarizer","author":"@skillopedia","source":{"stars":1,"repo_name":"video-summarizer","origin_url":"https://github.com/ajayhao/video-summarizer/blob/HEAD/SKILL.md","repo_owner":"ajayhao","body_sha256":"020ac011d1d92f3e7bbcc4015d2b67b37ce94957d7f61346d8621f356d9e088e","cluster_key":"609d4837b99bf6b2f14634e8809199a364d544053a23a08a4993063c0165114e","clean_bundle":{"format":"clean-skill-bundle-v1","source":"ajayhao/video-summarizer/SKILL.md","attachments":[{"id":"39c56409-8621-51ce-874e-f909fccf9d87","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/39c56409-8621-51ce-874e-f909fccf9d87/attachment","path":".gitignore","size":462,"sha256":"645c10da526f573ca201c33b9acc29427f9b7f0cdf55d7b03b632ee7e12407d7","contentType":"text/plain; charset=utf-8"},{"id":"28a6bd86-ab17-5236-ba76-88b73e1afa8f","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/28a6bd86-ab17-5236-ba76-88b73e1afa8f/attachment.md","path":"CHANGELOG.md","size":29149,"sha256":"180a2ae3ff18033255d713fb9cd021dd4e5c5fad448073cac74a330f38bd7c2e","contentType":"text/markdown; charset=utf-8"},{"id":"18f325ca-d052-5e9a-a5c6-0d61fb9bec26","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/18f325ca-d052-5e9a-a5c6-0d61fb9bec26/attachment.md","path":"README.md","size":3603,"sha256":"c7c295cc8f7f5934db4d59ff6c1bb089693e94190cebe65029bc06d691cb51fa","contentType":"text/markdown; charset=utf-8"},{"id":"f60e054c-e173-5038-84eb-1017b436ca50","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f60e054c-e173-5038-84eb-1017b436ca50/attachment.json","path":"prompt.json","size":3731,"sha256":"f63cec7d58ee4b5aba3772546a109142465f8a78709551c00ca7bbf62a96de49","contentType":"application/json; charset=utf-8"},{"id":"8b82d0f5-b99d-5542-916a-9e12c1bef694","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/8b82d0f5-b99d-5542-916a-9e12c1bef694/attachment.py","path":"scripts/analyze-subtitles-ai.py","size":31115,"sha256":"dce780d48023277a8e7e7e319c5d65095ca7061e3779da5469d9695dfe0c6af5","contentType":"text/x-python; charset=utf-8"},{"id":"f2e7db7e-9e6d-5eaf-86ac-1ed2583df8de","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f2e7db7e-9e6d-5eaf-86ac-1ed2583df8de/attachment.sh","path":"scripts/bili-login.sh","size":2014,"sha256":"7c71cca64f372fd6ec35e063840e2fb4c0ac63b9c8801f501cae3bb3e2329533","contentType":"application/x-sh; charset=utf-8"},{"id":"21cf804a-b0e4-5e21-b807-83941cab4fac","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/21cf804a-b0e4-5e21-b807-83941cab4fac/attachment.sh","path":"scripts/check-config.sh","size":5843,"sha256":"a6d44db2288664151202e2e04de7ca251bdb0e1c7f6805fe56955e406f08678c","contentType":"application/x-sh; charset=utf-8"},{"id":"b9ffb822-8172-5ba8-b7bb-9ffa4bbb9f37","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b9ffb822-8172-5ba8-b7bb-9ffa4bbb9f37/attachment.py","path":"scripts/convert-bili-cookie.py","size":3174,"sha256":"298a5f6c7a8ecdb916aae9f05378ceef62c933cae611802925998cae5aeaaa7a","contentType":"text/x-python; charset=utf-8"},{"id":"5a78fb6d-cc5f-54f8-a775-21bc272d7834","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/5a78fb6d-cc5f-54f8-a775-21bc272d7834/attachment.py","path":"scripts/douyin_downloader.py","size":23028,"sha256":"3a1cdb034f4e6c80a75db8064b26424e63edfa65008dbf307505897da26f357f","contentType":"text/x-python; charset=utf-8"},{"id":"c1801610-1403-545c-9a4c-4166f1cd94fb","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/c1801610-1403-545c-9a4c-4166f1cd94fb/attachment.sh","path":"scripts/download-audio.sh","size":4656,"sha256":"db8cf14b636fba10c24215fce7ea87fcee9d6cd475c8a1646b26153f03a6626d","contentType":"application/x-sh; charset=utf-8"},{"id":"f6411fe5-e434-5fcb-ae88-4ffbc6f463d8","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f6411fe5-e434-5fcb-ae88-4ffbc6f463d8/attachment.py","path":"scripts/llm_client.py","size":5417,"sha256":"8c58c00a33df8941355c3805ca9a9171f7039781a3b4ae60cb95c91d71ffdb31","contentType":"text/x-python; charset=utf-8"},{"id":"23925667-1965-55ef-b27a-ffc0eb55e265","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/23925667-1965-55ef-b27a-ffc0eb55e265/attachment.py","path":"scripts/push-to-notion.py","size":37250,"sha256":"1efa0559affddc2a4ac3c94baef07d78ca5ff9ac14f3aa33b456b02e02abd478","contentType":"text/x-python; charset=utf-8"},{"id":"458803bd-9437-5853-bdb5-bc78f9df0e77","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/458803bd-9437-5853-bdb5-bc78f9df0e77/attachment.png","path":"scripts/qrcode.png","size":3444,"sha256":"d452e1590983c43e47c495b603bdd7f583d3ce507526a7148078307bcc519bf0","contentType":"image/png"},{"id":"4ac2f9c1-ebea-505a-a5a7-c22f07e5c2c9","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/4ac2f9c1-ebea-505a-a5a7-c22f07e5c2c9/attachment.py","path":"scripts/transcribe-audio.py","size":10818,"sha256":"91d54364b7d4521bfc8e07f8e5775646129f0935debabc4a4353530b5aa46f78","contentType":"text/x-python; charset=utf-8"},{"id":"260f489c-242c-5f6b-bf59-d10d4d789d88","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/260f489c-242c-5f6b-bf59-d10d4d789d88/attachment.py","path":"scripts/upload-to-oss.py","size":15851,"sha256":"984f154ba836db2a1022d7befd183e3a38faf588d3534a728511fcc0bda2086a","contentType":"text/x-python; charset=utf-8"},{"id":"b4dab6c3-13ba-5e70-9148-1de5f287df6d","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/b4dab6c3-13ba-5e70-9148-1de5f287df6d/attachment.sh","path":"scripts/video-summarize.sh","size":42869,"sha256":"5ce6b5711321f56792fc498743e875ae00afdcd0e80efba3f0549286e70cb6d3","contentType":"application/x-sh; charset=utf-8"},{"id":"a3045d2e-e72e-5072-9832-a49363c59821","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/a3045d2e-e72e-5072-9832-a49363c59821/attachment.md","path":"templates/README.md","size":1684,"sha256":"324c41e4a0d2086fbe7f454677626e748761e874a41a46a6af04e309452f5b50","contentType":"text/markdown; charset=utf-8"},{"id":"f8c07481-8a12-5014-a387-b8def94c8367","key":"uploads/10433ee7-ad12-4ae0-b34e-97553e46c6c8/f8c07481-8a12-5014-a387-b8def94c8367/attachment.md","path":"templates/summary.md","size":914,"sha256":"a152ee3f9b15dc62e042fc38cf894b99a46ddd9f66415482c779bb0baab54e2c","contentType":"text/markdown; charset=utf-8"}],"bundle_sha256":"a67293cba4debaf2de66e4ec8938fdf75172ba698f9f3cce7c1ce35aa476b79c","attachment_count":18,"text_attachments":16,"attachment_storage":"skillopedia-attachments-v1","binary_attachments":2,"excluded_attachments":[]},"cluster_size":1,"skill_md_path":"SKILL.md","import_metadata":{"date":"2026-06-05","author":"@skillopedia","version":"v1","category":"integrations-apis","category_label":"Integrations"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"integrations-apis","metadata":{"openclaw":{"emoji":"🎬","install":[{"id":"brew","bins":["ffmpeg","yt-dlp"],"kind":"brew","label":"Install ffmpeg and yt-dlp (brew)","formula":"ffmpeg yt-dlp"},{"id":"apt","bins":["ffmpeg","yt-dlp"],"kind":"apt","label":"Install ffmpeg and yt-dlp (apt)","packages":"ffmpeg yt-dlp"},{"id":"pip","kind":"pip","label":"Install Python dependencies","packages":"requests==2.31.0 oss2==2.18.4 python-dotenv==1.0.1 biliup==0.4.86"}],"requires":{"env":["LLM_API_KEY","LLM_BASE_URL","ALIYUN_OSS_AK","ALIYUN_OSS_SK","ALIYUN_OSS_BUCKET_ID","ALIYUN_OSS_ENDPOINT"],"bins":["ffmpeg (>=6.1)","yt-dlp (>=2026.03.17)"]}}},"import_tag":"clean-skills-v1","description":"将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion"}},"renderedAt":1782981952954}

Video Summarizer — OpenClaw Skill 将 B 站/YouTube/小红书/抖音视频转换为结构化 Notion 总结文档,自动上传截图,一键推送 Notion。 版本 : 1.0.13 发布 : 2026-05-10 许可 : MIT 作者 : Ajay Hao --- ⚠️ 安全提示 - 本技能会将视频内容发送至第三方 AI 服务进行分析 - 建议使用专用 API Key(非生产环境) - OSS Bucket 请配置最小权限(仅写入/读取) - B 站 Cookies 仅在你控制的设备上使用 --- 📖 技能描述 核心能力 - 🎬 多平台支持 : B 站、YouTube、小红书、抖音 - 📝 智能分析 : AI(平台可配置)提取关键概念、核心要点、注意事项 - 📸 截图嵌入 : 基于 AI 分析结果自动生成关键帧截图 - ☁️ 图床集成 : 阿里云 OSS 自动上传,永久链接 - 🚀 一键推送 : 自动推送 Notion 数据库 技术特性 - 双模式转录 : Plan A(官方字幕)优先,Plan B(语音转录)兜底 - 并行优化 : 字幕下载与视频下载并行执行,节省 32% 时间 - GPU 自适应 : 自动检测显存,选择最优 Whisper 模型 - 断点续跑 : 支持从中断点恢复,避免重复处理 - 四层标签 : 标题 hashtag…