1 line
72 KiB
JSON
1 line
72 KiB
JSON
{"repo": "chubbyguan/chubbyskills", "files": {"youtube-transcribe/scripts/transcribe.py": {"56c434bd9f8f5d15d540038073aa44c24df39dbd": {"t": 1785745410.2108395, "n": 10625, "h": "d816535a31e0d0de", "c": "#!/usr/bin/env python3\n\"\"\"\nYouTube 视频转录 + 翻译工具\n\n用法:\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\"\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\" --no-translate\n\"\"\"\n\nimport sys\nimport os\nimport re\nimport time\nimport json\nimport subprocess\nimport tempfile\nimport shutil\nimport glob\nimport urllib.request\nfrom datetime import datetime\n\n\nSUB_LANGS = \"en,en-US,en-orig,zh-Hans,zh-CN,zh,zh-Hant\"\n\n\ndef get_video_info(url: str) -> dict:\n \"\"\"Get video title and duration.\"\"\"\n result = subprocess.run(\n [\"yt-dlp\", \"--get-title\", \"--get-duration\", \"--no-check-certificates\", url],\n capture_output=True, text=True, timeout=30\n )\n lines = result.stdout.strip().split('\\n')\n return {\n 'title': lines[0] if lines else 'Untitled',\n 'duration': lines[1] if len(lines) > 1 else '',\n }\n\n\ndef download_audio(url: str, output_dir: str) -> str:\n \"\"\"Download audio from YouTube.\"\"\"\n print(\" ⬇️ Downloading audio...\", file=sys.stderr)\n\n audio_path = os.path.join(output_dir, \"audio.mp3\")\n subprocess.run(\n [\"yt-dlp\",\n \"--extract-audio\", \"--audio-format\", \"mp3\", \"--audio-quality\", \"128K\",\n \"-o\", audio_path,\n \"--no-check-certificates\",\n url],\n timeout=600, check=True\n )\n\n size_mb = os.path.getsize(audio_path) / (1024 * 1024)\n print(f\" ✅ Audio: {size_mb:.1f} MB\", file=sys.stderr)\n return audio_path\n\n\ndef parse_vtt(path: str) -> str:\n \"\"\"把 vtt 字幕解析成纯文本,去时间轴、去标签、去连续重复行。\"\"\"\n with open(path, encoding=\"utf-8\", errors=\"replace\") as f:\n lines = f.read().splitlines()\n out = []\n for line in lines:\n line = line.strip()\n if not line or line == \"WEBVTT\" or \"-->\" in line:\n continue\n if line.isdigit() or line.startswith((\"Kind:\", \"Language:\", \"NOTE\")):\n continue\n line = re.sub(r\"<[^>]+>\", \"\", line).replace(\" \", \" \").strip()\n if not line or (out and out[-1] == line):\n continue\n out.append(line)\n return \"\\n\".join(out)\n\n\ndef try_subtitles(url: str, tmpdir: str):\n \"\"\"字幕优先:抓官方/自动字幕,免下载+转录。返回 (text, language) 或 None。\"\"\"\n print(\" 💬 尝试抓取字幕...\", file=sys.stderr)\n try:\n subprocess.run(\n [\"yt-dlp\", \"--skip-download\", \"--write-subs\", \"--write-auto-subs\",\n \"--sub-langs\", SUB_LANGS, \"--sub-format\", \"vtt\",\n \"--no-check-certificates\",\n \"-o\", os.path.join(tmpdir, \"sub.%(ext)s\"), url],\n capture_output=True, text=True, timeout=120,\n )\n except Exception:\n return None\n files = glob.glob(os.path.join(tmpdir, \"*.vtt\"))\n if not files:\n print(\" 💬 无字幕,回退音频转录\", file=sys.stderr)\n return None\n\n def rank(p):\n name = os.path.basename(p).lower()\n for i, tag in enumerate([\"en\", \"zh-hans\", \"zh-cn\", \"zh\"]):\n if tag in name:\n return i\n return 99\n chosen = sorted(files, key=rank)[0]\n text = parse_vtt(chosen)\n if len(text) < 50:\n return None\n chinese = len(re.findall(r\"[一-鿿]\", text))\n language = \"zh\" if chinese / max(len(text), 1) > 0.3 else \"en\"\n print(f\" ✅ 命中字幕({language},{len(text)} 字)\", file=sys.stderr)\n return text, language\n\n\ndef transcribe_audio(audio_path: str) -> tuple:\n \"\"\"Transcribe audio using SenseVoice-Small. Returns (text, language).\"\"\"\n from funasr import AutoModel\n from funasr.utils.postprocess_utils import rich_transcription_postprocess\n\n print(\" 🎙️ Loading model...\", file=sys.stderr)\n model = AutoModel(\n model=\"iic/SenseVoiceSmall\",\n trust_remote_code=True,\n vad_model=\"fsmn-vad\",\n vad_kwargs={\"max_single_segment_time\": 30000},\n device=\"cpu\",\n )\n\n print(\" 🎙️ Transcribing...\", file=sys.stderr)\n start = time.time()\n\n # Auto detect language\n result = model.generate(\n input=audio_path,\n language=\"auto\", # Auto detect\n use_itn=True,\n batch_size_s=60\n )\n elapsed = time.time() - start\n\n # Extract text\n text = \"\"\n if result and len(result) > 0:\n for r in result:\n if \"text\" in r:\n text += rich_transcription_postprocess(r[\"text\"]) + \"\\n\\n\"\n\n # Detect language (simple heuristic)\n chinese_chars = len(re.findall(r'[\\u4e00-\\u9fff]', text))\n total_chars = len(text.strip())\n language = \"zh\" if chinese_chars / max(total_chars, 1) > 0.3 else \"en\"\n\n print(f\" ✅ Transcribed in {elapsed:.1f}s (detected: {language})\", file=sys.stderr)\n return text.strip(), language\n\n\ndef translate_text(text: str, api_key: str = None) -> str:\n \"\"\"Translate English to Chinese using DeepSeek API.\"\"\"\n if not api_key:\n api_key = os.environ.get(\"DEEPSEEK_API_KEY\")\n\n if not api_key:\n print(\" ⚠️ No DEEPSEEK_API_KEY, skipping translation\", file=sys.stderr)\n return None\n\n print(\" 🌐 Translating to Chinese...\", file=sys.stderr)\n\n # Split into chunks if too long\n chunks = []\n if len(text) > 4000:\n paragraphs = text.split('\\n\\n')\n current_chunk = \"\"\n for para in paragraphs:\n if len(current_chunk) + len(para) > 3500:\n chunks.append(current_chunk)\n current_chunk = para\n else:\n current_chunk += \"\\n\\n\" + para if current_chunk else para\n if current_chunk:\n chunks.append(current_chunk)\n else:\n chunks = [text]\n\n translated_chunks = []\n for i, chunk in enumerate(chunks):\n if len(chunks) > 1:\n print(f\" 🌐 Translating chunk {i+1}/{len(chunks)}...\", file=sys.stderr)\n\n try:\n payload = json.dumps({\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\"role\": \"system\", \"content\": \"你是专业翻译。将英文翻译成中文,保持原文风格和格式。不要添加额外解释。\"},\n {\"role\": \"user\", \"content\": f\"翻译以下英文为中文:\\n\\n{chunk}\"}\n ],\n \"temperature\": 0.3\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.deepseek.com/v1/chat/completions\",\n data=payload,\n headers={\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n },\n )\n with urllib.request.urlopen(req, timeout=120) as resp:\n response = json.loads(resp.read())\n\n translated = response['choices'][0]['message']['content']\n translated_chunks.append(translated)\n except Exception as e:\n print(f\" ⚠️ Translation failed: {e}\", file=sys.stderr)\n return None\n\n print(\" ✅ Translation done\", file=sys.stderr)\n return \"\\n\\n\".join(translated_chunks)\n\n\ndef generate_markdown(title: str, original: str, translated: str, language: str, url: str, transcriber: str = \"SenseVoice-Small\") -> str:\n \"\"\"Generate bilingual Markdown.\"\"\"\n now = datetime.now().strftime(\"%Y-%m-%d\")\n\n if language == \"en\" and translated:\n return f\"\"\"---\ntitle: {title}\ntype: note\nplatform: youtube\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nauthor:\nlanguage: en\ntranslated: true\ntranscriber: {transcriber}\n---\n\n# {title}\n\n> 🌐 英文视频,已翻译为中文\n\n---\n\n## 中文翻译\n\n{translated}\n\n---\n\n## English Original\n\n{original}\n\"\"\"\n else:\n return f\"\"\"---\ntitle: {title}\ntype: note\nplatform: youtube\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nauthor:\nlanguage: {language}\ntranscriber: {transcriber}\n---\n\n# {title}\n\n{original}\n\"\"\"\n\n\ndef sanitize_filename(name: str) -> str:\n \"\"\"Clean filename.\"\"\"\n s = re.sub(r'[<>:\"/\\\\|?*]', '', name)\n s = re.sub(r'\\s+', '-', s)\n return s[:50]\n\n\ndef main():\n import argparse\n parser = argparse.ArgumentParser(description='YouTube 视频转录 + 翻译')\n parser.add_argument('url', help='YouTube URL')\n parser.add_argument('--output', '-o', default='.', help='输出目录')\n parser.add_argument('--no-translate', action='store_true', help='不翻译')\n parser.add_argument('--no-subtitle', action='store_true', help='跳过字幕,强制音频转录')\n args = parser.parse_args()\n\n # Step 1: Get video info\n print(\"=\" * 50, file=sys.stderr)\n print(\"Step 1: Getting video info...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n\n info = get_video_info(args.url)\n title = info['title']\n print(f\" 📺 Title: {title}\", file=sys.stderr)\n print(f\" ⏱️ Duration: {info['duration']}\", file=sys.stderr)\n\n # Step 2: Get transcript (subtitle-first, fallback to audio)\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 2: Getting transcript...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n\n tmpdir = tempfile.mkdtemp(prefix=\"youtube-\")\n try:\n transcriber = \"SenseVoice-Small\"\n sub = None if args.no_subtitle else try_subtitles(args.url, tmpdir)\n if sub:\n transcriber = \"字幕\"\n else:\n audio_path = download_audio(args.url, tmpdir)\n\n # Step 3: Transcribe\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 3: Building transcript...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n\n if sub:\n text, language = sub\n else:\n text, language = transcribe_audio(audio_path)\n\n # Step 4: Translate if English\n translated = None\n if language == \"en\" and not args.no_translate:\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 4: Translating...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n translated = translate_text(text)\n\n # Step 5: Generate Markdown\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 5: Generating Markdown...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n\n markdown = generate_markdown(title, text, translated, language, args.url, transcriber)\n\n # Save\n safe_title = sanitize_filename(title)\n output_path = os.path.join(args.output, f\"{safe_title}.md\")\n os.makedirs(args.output, exist_ok=True)\n\n with open(output_path, 'w', encoding='utf-8') as f:\n f.write(markdown)\n\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"✅ Done!\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n print(f\" Title: {title}\", file=sys.stderr)\n print(f\" Language: {language}\", file=sys.stderr)\n print(f\" Translated: {'Yes' if translated else 'No'}\", file=sys.stderr)\n print(f\" Output: {output_path}\", file=sys.stderr)\n\n print(output_path)\n\n finally:\n shutil.rmtree(tmpdir, ignore_errors=True)\n\n\nif __name__ == \"__main__\":\n main()\n"}, "820b293dc0df12270e8f6ab23c28fae1b4f11f02": {"t": 1785752935.469398, "n": 10625, "h": "d816535a31e0d0de", "c": "#!/usr/bin/env python3\n\"\"\"\nYouTube 视频转录 + 翻译工具\n\n用法:\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\"\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\" --no-translate\n\"\"\"\n\nimport sys\nimport os\nimport re\nimport time\nimport json\nimport subprocess\nimport tempfile\nimport shutil\nimport glob\nimport urllib.request\nfrom datetime import datetime\n\n\nSUB_LANGS = \"en,en-US,en-orig,zh-Hans,zh-CN,zh,zh-Hant\"\n\n\ndef get_video_info(url: str) -> dict:\n \"\"\"Get video title and duration.\"\"\"\n result = subprocess.run(\n [\"yt-dlp\", \"--get-title\", \"--get-duration\", \"--no-check-certificates\", url],\n capture_output=True, text=True, timeout=30\n )\n lines = result.stdout.strip().split('\\n')\n return {\n 'title': lines[0] if lines else 'Untitled',\n 'duration': lines[1] if len(lines) > 1 else '',\n }\n\n\ndef download_audio(url: str, output_dir: str) -> str:\n \"\"\"Download audio from YouTube.\"\"\"\n print(\" ⬇️ Downloading audio...\", file=sys.stderr)\n\n audio_path = os.path.join(output_dir, \"audio.mp3\")\n subprocess.run(\n [\"yt-dlp\",\n \"--extract-audio\", \"--audio-format\", \"mp3\", \"--audio-quality\", \"128K\",\n \"-o\", audio_path,\n \"--no-check-certificates\",\n url],\n timeout=600, check=True\n )\n\n size_mb = os.path.getsize(audio_path) / (1024 * 1024)\n print(f\" ✅ Audio: {size_mb:.1f} MB\", file=sys.stderr)\n return audio_path\n\n\ndef parse_vtt(path: str) -> str:\n \"\"\"把 vtt 字幕解析成纯文本,去时间轴、去标签、去连续重复行。\"\"\"\n with open(path, encoding=\"utf-8\", errors=\"replace\") as f:\n lines = f.read().splitlines()\n out = []\n for line in lines:\n line = line.strip()\n if not line or line == \"WEBVTT\" or \"-->\" in line:\n continue\n if line.isdigit() or line.startswith((\"Kind:\", \"Language:\", \"NOTE\")):\n continue\n line = re.sub(r\"<[^>]+>\", \"\", line).replace(\" \", \" \").strip()\n if not line or (out and out[-1] == line):\n continue\n out.append(line)\n return \"\\n\".join(out)\n\n\ndef try_subtitles(url: str, tmpdir: str):\n \"\"\"字幕优先:抓官方/自动字幕,免下载+转录。返回 (text, language) 或 None。\"\"\"\n print(\" 💬 尝试抓取字幕...\", file=sys.stderr)\n try:\n subprocess.run(\n [\"yt-dlp\", \"--skip-download\", \"--write-subs\", \"--write-auto-subs\",\n \"--sub-langs\", SUB_LANGS, \"--sub-format\", \"vtt\",\n \"--no-check-certificates\",\n \"-o\", os.path.join(tmpdir, \"sub.%(ext)s\"), url],\n capture_output=True, text=True, timeout=120,\n )\n except Exception:\n return None\n files = glob.glob(os.path.join(tmpdir, \"*.vtt\"))\n if not files:\n print(\" 💬 无字幕,回退音频转录\", file=sys.stderr)\n return None\n\n def rank(p):\n name = os.path.basename(p).lower()\n for i, tag in enumerate([\"en\", \"zh-hans\", \"zh-cn\", \"zh\"]):\n if tag in name:\n return i\n return 99\n chosen = sorted(files, key=rank)[0]\n text = parse_vtt(chosen)\n if len(text) < 50:\n return None\n chinese = len(re.findall(r\"[一-鿿]\", text))\n language = \"zh\" if chinese / max(len(text), 1) > 0.3 else \"en\"\n print(f\" ✅ 命中字幕({language},{len(text)} 字)\", file=sys.stderr)\n return text, language\n\n\ndef transcribe_audio(audio_path: str) -> tuple:\n \"\"\"Transcribe audio using SenseVoice-Small. Returns (text, language).\"\"\"\n from funasr import AutoModel\n from funasr.utils.postprocess_utils import rich_transcription_postprocess\n\n print(\" 🎙️ Loading model...\", file=sys.stderr)\n model = AutoModel(\n model=\"iic/SenseVoiceSmall\",\n trust_remote_code=True,\n vad_model=\"fsmn-vad\",\n vad_kwargs={\"max_single_segment_time\": 30000},\n device=\"cpu\",\n )\n\n print(\" 🎙️ Transcribing...\", file=sys.stderr)\n start = time.time()\n\n # Auto detect language\n result = model.generate(\n input=audio_path,\n language=\"auto\", # Auto detect\n use_itn=True,\n batch_size_s=60\n )\n elapsed = time.time() - start\n\n # Extract text\n text = \"\"\n if result and len(result) > 0:\n for r in result:\n if \"text\" in r:\n text += rich_transcription_postprocess(r[\"text\"]) + \"\\n\\n\"\n\n # Detect language (simple heuristic)\n chinese_chars = len(re.findall(r'[\\u4e00-\\u9fff]', text))\n total_chars = len(text.strip())\n language = \"zh\" if chinese_chars / max(total_chars, 1) > 0.3 else \"en\"\n\n print(f\" ✅ Transcribed in {elapsed:.1f}s (detected: {language})\", file=sys.stderr)\n return text.strip(), language\n\n\ndef translate_text(text: str, api_key: str = None) -> str:\n \"\"\"Translate English to Chinese using DeepSeek API.\"\"\"\n if not api_key:\n api_key = os.environ.get(\"DEEPSEEK_API_KEY\")\n\n if not api_key:\n print(\" ⚠️ No DEEPSEEK_API_KEY, skipping translation\", file=sys.stderr)\n return None\n\n print(\" 🌐 Translating to Chinese...\", file=sys.stderr)\n\n # Split into chunks if too long\n chunks = []\n if len(text) > 4000:\n paragraphs = text.split('\\n\\n')\n current_chunk = \"\"\n for para in paragraphs:\n if len(current_chunk) + len(para) > 3500:\n chunks.append(current_chunk)\n current_chunk = para\n else:\n current_chunk += \"\\n\\n\" + para if current_chunk else para\n if current_chunk:\n chunks.append(current_chunk)\n else:\n chunks = [text]\n\n translated_chunks = []\n for i, chunk in enumerate(chunks):\n if len(chunks) > 1:\n print(f\" 🌐 Translating chunk {i+1}/{len(chunks)}...\", file=sys.stderr)\n\n try:\n payload = json.dumps({\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\"role\": \"system\", \"content\": \"你是专业翻译。将英文翻译成中文,保持原文风格和格式。不要添加额外解释。\"},\n {\"role\": \"user\", \"content\": f\"翻译以下英文为中文:\\n\\n{chunk}\"}\n ],\n \"temperature\": 0.3\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.deepseek.com/v1/chat/completions\",\n data=payload,\n headers={\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n },\n )\n with urllib.request.urlopen(req, timeout=120) as resp:\n response = json.loads(resp.read())\n\n translated = response['choices'][0]['message']['content']\n translated_chunks.append(translated)\n except Exception as e:\n print(f\" ⚠️ Translation failed: {e}\", file=sys.stderr)\n return None\n\n print(\" ✅ Translation done\", file=sys.stderr)\n return \"\\n\\n\".join(translated_chunks)\n\n\ndef generate_markdown(title: str, original: str, translated: str, language: str, url: str, transcriber: str = \"SenseVoice-Small\") -> str:\n \"\"\"Generate bilingual Markdown.\"\"\"\n now = datetime.now().strftime(\"%Y-%m-%d\")\n\n if language == \"en\" and translated:\n return f\"\"\"---\ntitle: {title}\ntype: note\nplatform: youtube\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nauthor:\nlanguage: en\ntranslated: true\ntranscriber: {transcriber}\n---\n\n# {title}\n\n> 🌐 英文视频,已翻译为中文\n\n---\n\n## 中文翻译\n\n{translated}\n\n---\n\n## English Original\n\n{original}\n\"\"\"\n else:\n return f\"\"\"---\ntitle: {title}\ntype: note\nplatform: youtube\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nauthor:\nlanguage: {language}\ntranscriber: {transcriber}\n---\n\n# {title}\n\n{original}\n\"\"\"\n\n\ndef sanitize_filename(name: str) -> str:\n \"\"\"Clean filename.\"\"\"\n s = re.sub(r'[<>:\"/\\\\|?*]', '', name)\n s = re.sub(r'\\s+', '-', s)\n return s[:50]\n\n\ndef main():\n import argparse\n parser = argparse.ArgumentParser(description='YouTube 视频转录 + 翻译')\n parser.add_argument('url', help='YouTube URL')\n parser.add_argument('--output', '-o', default='.', help='输出目录')\n parser.add_argument('--no-translate', action='store_true', help='不翻译')\n parser.add_argument('--no-subtitle', action='store_true', help='跳过字幕,强制音频转录')\n args = parser.parse_args()\n\n # Step 1: Get video info\n print(\"=\" * 50, file=sys.stderr)\n print(\"Step 1: Getting video info...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n\n info = get_video_info(args.url)\n title = info['title']\n print(f\" 📺 Title: {title}\", file=sys.stderr)\n print(f\" ⏱️ Duration: {info['duration']}\", file=sys.stderr)\n\n # Step 2: Get transcript (subtitle-first, fallback to audio)\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 2: Getting transcript...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n\n tmpdir = tempfile.mkdtemp(prefix=\"youtube-\")\n try:\n transcriber = \"SenseVoice-Small\"\n sub = None if args.no_subtitle else try_subtitles(args.url, tmpdir)\n if sub:\n transcriber = \"字幕\"\n else:\n audio_path = download_audio(args.url, tmpdir)\n\n # Step 3: Transcribe\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 3: Building transcript...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n\n if sub:\n text, language = sub\n else:\n text, language = transcribe_audio(audio_path)\n\n # Step 4: Translate if English\n translated = None\n if language == \"en\" and not args.no_translate:\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 4: Translating...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n translated = translate_text(text)\n\n # Step 5: Generate Markdown\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 5: Generating Markdown...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n\n markdown = generate_markdown(title, text, translated, language, args.url, transcriber)\n\n # Save\n safe_title = sanitize_filename(title)\n output_path = os.path.join(args.output, f\"{safe_title}.md\")\n os.makedirs(args.output, exist_ok=True)\n\n with open(output_path, 'w', encoding='utf-8') as f:\n f.write(markdown)\n\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"✅ Done!\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n print(f\" Title: {title}\", file=sys.stderr)\n print(f\" Language: {language}\", file=sys.stderr)\n print(f\" Translated: {'Yes' if translated else 'No'}\", file=sys.stderr)\n print(f\" Output: {output_path}\", file=sys.stderr)\n\n print(output_path)\n\n finally:\n shutil.rmtree(tmpdir, ignore_errors=True)\n\n\nif __name__ == \"__main__\":\n main()\n"}, "cdbe58ee3f989712c208082d4938504fc7cc08c3": {"t": 1785752937.4726965, "n": 10757, "h": "d924c2cd574214dc", "c": "#!/usr/bin/env python3\n\"\"\"\nYouTube 视频转录 + 翻译工具\n\n用法:\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\"\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\" --no-translate\n\"\"\"\n\nimport sys\nimport os\nimport re\nimport time\nimport json\nimport subprocess\nimport tempfile\nimport shutil\nimport glob\nimport urllib.request\nfrom datetime import datetime\n\n\nSUB_LANGS = \"en,en-US,en-orig,zh-Hans,zh-CN,zh,zh-Hant\"\n\n\ndef get_video_info(url: str) -> dict:\n \"\"\"Get video title and duration.\"\"\"\n result = subprocess.run(\n [\"yt-dlp\", \"--get-title\", \"--get-duration\", \"--no-check-certificates\", url],\n capture_output=True, text=True, timeout=30\n )\n lines = result.stdout.strip().split('\\n')\n return {\n 'title': lines[0] if lines else 'Untitled',\n 'duration': lines[1] if len(lines) > 1 else '',\n }\n\n\ndef download_audio(url: str, output_dir: str) -> str:\n \"\"\"Download audio from YouTube.\"\"\"\n print(f\" ⬇️ Downloading audio...\", file=sys.stderr)\n \n audio_path = os.path.join(output_dir, \"audio.mp3\")\n subprocess.run(\n [\"yt-dlp\",\n \"--extract-audio\", \"--audio-format\", \"mp3\", \"--audio-quality\", \"128K\",\n \"-o\", audio_path,\n \"--no-check-certificates\",\n url],\n timeout=600, check=True\n )\n \n size_mb = os.path.getsize(audio_path) / (1024 * 1024)\n print(f\" ✅ Audio: {size_mb:.1f} MB\", file=sys.stderr)\n return audio_path\n\n\ndef parse_vtt(path: str) -> str:\n \"\"\"把 vtt 字幕解析成纯文本,去时间轴、去标签、去连续重复行。\"\"\"\n with open(path, encoding=\"utf-8\", errors=\"replace\") as f:\n lines = f.read().splitlines()\n out = []\n for line in lines:\n line = line.strip()\n if not line or line == \"WEBVTT\" or \"-->\" in line:\n continue\n if line.isdigit() or line.startswith((\"Kind:\", \"Language:\", \"NOTE\")):\n continue\n line = re.sub(r\"<[^>]+>\", \"\", line).replace(\" \", \" \").strip()\n if not line or (out and out[-1] == line):\n continue\n out.append(line)\n return \"\\n\".join(out)\n\n\ndef try_subtitles(url: str, tmpdir: str):\n \"\"\"字幕优先:抓官方/自动字幕,免下载+转录。返回 (text, language) 或 None。\"\"\"\n print(\" 💬 尝试抓取字幕...\", file=sys.stderr)\n try:\n subprocess.run(\n [\"yt-dlp\", \"--skip-download\", \"--write-subs\", \"--write-auto-subs\",\n \"--sub-langs\", SUB_LANGS, \"--sub-format\", \"vtt\",\n \"--no-check-certificates\",\n \"-o\", os.path.join(tmpdir, \"sub.%(ext)s\"), url],\n capture_output=True, text=True, timeout=120,\n )\n except Exception:\n return None\n files = glob.glob(os.path.join(tmpdir, \"*.vtt\"))\n if not files:\n print(\" 💬 无字幕,回退音频转录\", file=sys.stderr)\n return None\n\n def rank(p):\n name = os.path.basename(p).lower()\n for i, tag in enumerate([\"en\", \"zh-hans\", \"zh-cn\", \"zh\"]):\n if tag in name:\n return i\n return 99\n chosen = sorted(files, key=rank)[0]\n text = parse_vtt(chosen)\n if len(text) < 50:\n return None\n chinese = len(re.findall(r\"[一-鿿]\", text))\n language = \"zh\" if chinese / max(len(text), 1) > 0.3 else \"en\"\n print(f\" ✅ 命中字幕({language},{len(text)} 字)\", file=sys.stderr)\n return text, language\n\n\ndef transcribe_audio(audio_path: str) -> tuple:\n \"\"\"Transcribe audio using SenseVoice-Small. Returns (text, language).\"\"\"\n from funasr import AutoModel\n from funasr.utils.postprocess_utils import rich_transcription_postprocess\n\n print(\" 🎙️ Loading model...\", file=sys.stderr)\n model = AutoModel(\n model=\"iic/SenseVoiceSmall\",\n trust_remote_code=True,\n vad_model=\"fsmn-vad\",\n vad_kwargs={\"max_single_segment_time\": 30000},\n device=\"cpu\",\n )\n\n print(\" 🎙️ Transcribing...\", file=sys.stderr)\n start = time.time()\n \n # Auto detect language\n result = model.generate(\n input=audio_path,\n language=\"auto\", # Auto detect\n use_itn=True,\n batch_size_s=60\n )\n elapsed = time.time() - start\n\n # Extract text\n text = \"\"\n if result and len(result) > 0:\n for r in result:\n if \"text\" in r:\n text += rich_transcription_postprocess(r[\"text\"]) + \"\\n\\n\"\n \n # Detect language (simple heuristic)\n chinese_chars = len(re.findall(r'[\\u4e00-\\u9fff]', text))\n total_chars = len(text.strip())\n language = \"zh\" if chinese_chars / max(total_chars, 1) > 0.3 else \"en\"\n \n print(f\" ✅ Transcribed in {elapsed:.1f}s (detected: {language})\", file=sys.stderr)\n return text.strip(), language\n\n\ndef translate_text(text: str, api_key: str = None) -> str:\n \"\"\"Translate English to Chinese using DeepSeek API.\"\"\"\n if not api_key:\n api_key = os.environ.get(\"DEEPSEEK_API_KEY\")\n\n if not api_key:\n print(\" ⚠️ No DEEPSEEK_API_KEY, skipping translation\", file=sys.stderr)\n return None\n\n print(\" 🌐 Translating to Chinese...\", file=sys.stderr)\n\n # Split into chunks if too long\n chunks = []\n if len(text) > 4000:\n paragraphs = text.split('\\n\\n')\n current_chunk = \"\"\n for para in paragraphs:\n if len(current_chunk) + len(para) > 3500:\n chunks.append(current_chunk)\n current_chunk = para\n else:\n current_chunk += \"\\n\\n\" + para if current_chunk else para\n if current_chunk:\n chunks.append(current_chunk)\n else:\n chunks = [text]\n\n translated_chunks = []\n for i, chunk in enumerate(chunks):\n if len(chunks) > 1:\n print(f\" 🌐 Translating chunk {i+1}/{len(chunks)}...\", file=sys.stderr)\n\n try:\n payload = json.dumps({\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\"role\": \"system\", \"content\": \"你是专业翻译。将英文翻译成中文,保持原文风格和格式。不要添加额外解释。\"},\n {\"role\": \"user\", \"content\": f\"翻译以下英文为中文:\\n\\n{chunk}\"}\n ],\n \"temperature\": 0.3\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.deepseek.com/v1/chat/completions\",\n data=payload,\n headers={\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n },\n )\n with urllib.request.urlopen(req, timeout=120) as resp:\n response = json.loads(resp.read())\n\n translated = response['choices'][0]['message']['content']\n translated_chunks.append(translated)\n except Exception as e:\n print(f\" ⚠️ Translation failed: {e}\", file=sys.stderr)\n return None\n\n print(f\" ✅ Translation done\", file=sys.stderr)\n return \"\\n\\n\".join(translated_chunks)\n\n\ndef generate_markdown(title: str, original: str, translated: str, language: str, url: str, transcriber: str = \"SenseVoice-Small\") -> str:\n \"\"\"Generate bilingual Markdown.\"\"\"\n now = datetime.now().strftime(\"%Y-%m-%d\")\n elapsed_note = \"\"\n \n if language == \"en\" and translated:\n return f\"\"\"---\ntitle: {title}\ntype: note\nplatform: youtube\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nauthor:\nlanguage: en\ntranslated: true\ntranscriber: {transcriber}\n---\n\n# {title}\n\n> 🌐 英文视频,已翻译为中文\n\n---\n\n## 中文翻译\n\n{translated}\n\n---\n\n## English Original\n\n{original}\n\"\"\"\n else:\n return f\"\"\"---\ntitle: {title}\ntype: note\nplatform: youtube\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nauthor:\nlanguage: {language}\ntranscriber: {transcriber}\n---\n\n# {title}\n\n{original}\n\"\"\"\n\n\ndef sanitize_filename(name: str) -> str:\n \"\"\"Clean filename.\"\"\"\n s = re.sub(r'[<>:\"/\\\\|?*]', '', name)\n s = re.sub(r'\\s+', '-', s)\n return s[:50]\n\n\ndef main():\n import argparse\n parser = argparse.ArgumentParser(description='YouTube 视频转录 + 翻译')\n parser.add_argument('url', help='YouTube URL')\n parser.add_argument('--output', '-o', default='.', help='输出目录')\n parser.add_argument('--no-translate', action='store_true', help='不翻译')\n parser.add_argument('--no-subtitle', action='store_true', help='跳过字幕,强制音频转录')\n args = parser.parse_args()\n \n # Step 1: Get video info\n print(\"=\" * 50, file=sys.stderr)\n print(\"Step 1: Getting video info...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n info = get_video_info(args.url)\n title = info['title']\n print(f\" 📺 Title: {title}\", file=sys.stderr)\n print(f\" ⏱️ Duration: {info['duration']}\", file=sys.stderr)\n \n # Step 2: Get transcript (subtitle-first, fallback to audio)\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 2: Getting transcript...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n tmpdir = tempfile.mkdtemp(prefix=\"youtube-\")\n try:\n transcriber = \"SenseVoice-Small\"\n sub = None if args.no_subtitle else try_subtitles(args.url, tmpdir)\n if sub:\n transcriber = \"字幕\"\n else:\n audio_path = download_audio(args.url, tmpdir)\n \n # Step 3: Transcribe\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 3: Building transcript...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n\n if sub:\n text, language = sub\n else:\n text, language = transcribe_audio(audio_path)\n \n # Step 4: Translate if English\n translated = None\n if language == \"en\" and not args.no_translate:\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 4: Translating...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n translated = translate_text(text)\n \n # Step 5: Generate Markdown\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 5: Generating Markdown...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n markdown = generate_markdown(title, text, translated, language, args.url, transcriber)\n \n # Save\n safe_title = sanitize_filename(title)\n output_path = os.path.join(args.output, f\"{safe_title}.md\")\n os.makedirs(args.output, exist_ok=True)\n \n with open(output_path, 'w', encoding='utf-8') as f:\n f.write(markdown)\n \n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"✅ Done!\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n print(f\" Title: {title}\", file=sys.stderr)\n print(f\" Language: {language}\", file=sys.stderr)\n print(f\" Translated: {'Yes' if translated else 'No'}\", file=sys.stderr)\n print(f\" Output: {output_path}\", file=sys.stderr)\n \n print(output_path)\n \n finally:\n shutil.rmtree(tmpdir, ignore_errors=True)\n\n\nif __name__ == \"__main__\":\n main()\n"}, "7f3f850580da96b43752127d5e17abb4848c7cde": {"t": 1785752938.856805, "n": 8358, "h": "8332f69a9ac928cd", "c": "#!/usr/bin/env python3\n\"\"\"\nYouTube 视频转录 + 翻译工具\n\n用法:\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\"\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\" --no-translate\n\"\"\"\n\nimport sys\nimport os\nimport re\nimport time\nimport json\nimport subprocess\nimport tempfile\nimport shutil\nimport urllib.request\nfrom datetime import datetime\n\n\ndef get_video_info(url: str) -> dict:\n \"\"\"Get video title and duration.\"\"\"\n result = subprocess.run(\n [\"yt-dlp\", \"--get-title\", \"--get-duration\", \"--no-check-certificates\", url],\n capture_output=True, text=True, timeout=30\n )\n lines = result.stdout.strip().split('\\n')\n return {\n 'title': lines[0] if lines else 'Untitled',\n 'duration': lines[1] if len(lines) > 1 else '',\n }\n\n\ndef download_audio(url: str, output_dir: str) -> str:\n \"\"\"Download audio from YouTube.\"\"\"\n print(f\" ⬇️ Downloading audio...\", file=sys.stderr)\n \n audio_path = os.path.join(output_dir, \"audio.mp3\")\n subprocess.run(\n [\"yt-dlp\",\n \"--extract-audio\", \"--audio-format\", \"mp3\", \"--audio-quality\", \"128K\",\n \"-o\", audio_path,\n \"--no-check-certificates\",\n url],\n timeout=600, check=True\n )\n \n size_mb = os.path.getsize(audio_path) / (1024 * 1024)\n print(f\" ✅ Audio: {size_mb:.1f} MB\", file=sys.stderr)\n return audio_path\n\n\ndef transcribe_audio(audio_path: str) -> tuple:\n \"\"\"Transcribe audio using SenseVoice-Small. Returns (text, language).\"\"\"\n from funasr import AutoModel\n from funasr.utils.postprocess_utils import rich_transcription_postprocess\n\n print(\" 🎙️ Loading model...\", file=sys.stderr)\n model = AutoModel(\n model=\"iic/SenseVoiceSmall\",\n trust_remote_code=True,\n vad_model=\"fsmn-vad\",\n vad_kwargs={\"max_single_segment_time\": 30000},\n device=\"cpu\",\n )\n\n print(\" 🎙️ Transcribing...\", file=sys.stderr)\n start = time.time()\n \n # Auto detect language\n result = model.generate(\n input=audio_path,\n language=\"auto\", # Auto detect\n use_itn=True,\n batch_size_s=60\n )\n elapsed = time.time() - start\n\n # Extract text\n text = \"\"\n if result and len(result) > 0:\n for r in result:\n if \"text\" in r:\n text += rich_transcription_postprocess(r[\"text\"]) + \"\\n\\n\"\n \n # Detect language (simple heuristic)\n chinese_chars = len(re.findall(r'[\\u4e00-\\u9fff]', text))\n total_chars = len(text.strip())\n language = \"zh\" if chinese_chars / max(total_chars, 1) > 0.3 else \"en\"\n \n print(f\" ✅ Transcribed in {elapsed:.1f}s (detected: {language})\", file=sys.stderr)\n return text.strip(), language\n\n\ndef translate_text(text: str, api_key: str = None) -> str:\n \"\"\"Translate English to Chinese using DeepSeek API.\"\"\"\n if not api_key:\n api_key = os.environ.get(\"DEEPSEEK_API_KEY\")\n\n if not api_key:\n print(\" ⚠️ No DEEPSEEK_API_KEY, skipping translation\", file=sys.stderr)\n return None\n\n print(\" 🌐 Translating to Chinese...\", file=sys.stderr)\n\n # Split into chunks if too long\n chunks = []\n if len(text) > 4000:\n paragraphs = text.split('\\n\\n')\n current_chunk = \"\"\n for para in paragraphs:\n if len(current_chunk) + len(para) > 3500:\n chunks.append(current_chunk)\n current_chunk = para\n else:\n current_chunk += \"\\n\\n\" + para if current_chunk else para\n if current_chunk:\n chunks.append(current_chunk)\n else:\n chunks = [text]\n\n translated_chunks = []\n for i, chunk in enumerate(chunks):\n if len(chunks) > 1:\n print(f\" 🌐 Translating chunk {i+1}/{len(chunks)}...\", file=sys.stderr)\n\n try:\n payload = json.dumps({\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\"role\": \"system\", \"content\": \"你是专业翻译。将英文翻译成中文,保持原文风格和格式。不要添加额外解释。\"},\n {\"role\": \"user\", \"content\": f\"翻译以下英文为中文:\\n\\n{chunk}\"}\n ],\n \"temperature\": 0.3\n }).encode(\"utf-8\")\n\n req = urllib.request.Request(\n \"https://api.deepseek.com/v1/chat/completions\",\n data=payload,\n headers={\n \"Content-Type\": \"application/json\",\n \"Authorization\": f\"Bearer {api_key}\",\n },\n )\n with urllib.request.urlopen(req, timeout=120) as resp:\n response = json.loads(resp.read())\n\n translated = response['choices'][0]['message']['content']\n translated_chunks.append(translated)\n except Exception as e:\n print(f\" ⚠️ Translation failed: {e}\", file=sys.stderr)\n return None\n\n print(f\" ✅ Translation done\", file=sys.stderr)\n return \"\\n\\n\".join(translated_chunks)\n\n\ndef generate_markdown(title: str, original: str, translated: str, language: str, url: str) -> str:\n \"\"\"Generate bilingual Markdown.\"\"\"\n now = datetime.now().strftime(\"%Y-%m-%d\")\n elapsed_note = \"\"\n \n if language == \"en\" and translated:\n return f\"\"\"---\ntitle: {title}\ntype: note\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nlanguage: en\ntranslated: true\n---\n\n# {title}\n\n> 🌐 英文视频,已翻译为中文\n\n---\n\n## 中文翻译\n\n{translated}\n\n---\n\n## English Original\n\n{original}\n\"\"\"\n else:\n return f\"\"\"---\ntitle: {title}\ntype: note\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nlanguage: {language}\n---\n\n# {title}\n\n{original}\n\"\"\"\n\n\ndef sanitize_filename(name: str) -> str:\n \"\"\"Clean filename.\"\"\"\n s = re.sub(r'[<>:\"/\\\\|?*]', '', name)\n s = re.sub(r'\\s+', '-', s)\n return s[:50]\n\n\ndef main():\n import argparse\n parser = argparse.ArgumentParser(description='YouTube 视频转录 + 翻译')\n parser.add_argument('url', help='YouTube URL')\n parser.add_argument('--output', '-o', default='.', help='输出目录')\n parser.add_argument('--no-translate', action='store_true', help='不翻译')\n args = parser.parse_args()\n \n # Step 1: Get video info\n print(\"=\" * 50, file=sys.stderr)\n print(\"Step 1: Getting video info...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n info = get_video_info(args.url)\n title = info['title']\n print(f\" 📺 Title: {title}\", file=sys.stderr)\n print(f\" ⏱️ Duration: {info['duration']}\", file=sys.stderr)\n \n # Step 2: Download audio\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 2: Downloading audio...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n tmpdir = tempfile.mkdtemp(prefix=\"youtube-\")\n try:\n audio_path = download_audio(args.url, tmpdir)\n \n # Step 3: Transcribe\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 3: Transcribing...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n text, language = transcribe_audio(audio_path)\n \n # Step 4: Translate if English\n translated = None\n if language == \"en\" and not args.no_translate:\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 4: Translating...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n translated = translate_text(text)\n \n # Step 5: Generate Markdown\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 5: Generating Markdown...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n markdown = generate_markdown(title, text, translated, language, args.url)\n \n # Save\n safe_title = sanitize_filename(title)\n output_path = os.path.join(args.output, f\"{safe_title}.md\")\n os.makedirs(args.output, exist_ok=True)\n \n with open(output_path, 'w', encoding='utf-8') as f:\n f.write(markdown)\n \n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"✅ Done!\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n print(f\" Title: {title}\", file=sys.stderr)\n print(f\" Language: {language}\", file=sys.stderr)\n print(f\" Translated: {'Yes' if translated else 'No'}\", file=sys.stderr)\n print(f\" Output: {output_path}\", file=sys.stderr)\n \n print(output_path)\n \n finally:\n shutil.rmtree(tmpdir, ignore_errors=True)\n\n\nif __name__ == \"__main__\":\n main()\n"}, "53000c11c2758c068cfb9564b1c3e4332a782999": {"t": 1785752940.1471877, "n": 8397, "h": "6b26965692b7cd0c", "c": "#!/usr/bin/env python3\n\"\"\"\nYouTube 视频转录 + 翻译工具\n\n用法:\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\"\n python transcribe.py \"https://www.youtube.com/watch?v=xxxxx\" --no-translate\n\"\"\"\n\nimport sys\nimport os\nimport re\nimport time\nimport json\nimport subprocess\nimport tempfile\nimport shutil\nfrom datetime import datetime\n\n\ndef get_video_info(url: str) -> dict:\n \"\"\"Get video title and duration.\"\"\"\n result = subprocess.run(\n [\"yt-dlp\", \"--get-title\", \"--get-duration\", \"--no-check-certificates\", url],\n capture_output=True, text=True, timeout=30\n )\n lines = result.stdout.strip().split('\\n')\n return {\n 'title': lines[0] if lines else 'Untitled',\n 'duration': lines[1] if len(lines) > 1 else '',\n }\n\n\ndef download_audio(url: str, output_dir: str) -> str:\n \"\"\"Download audio from YouTube.\"\"\"\n print(f\" ⬇️ Downloading audio...\", file=sys.stderr)\n \n audio_path = os.path.join(output_dir, \"audio.mp3\")\n subprocess.run(\n [\"yt-dlp\",\n \"--extract-audio\", \"--audio-format\", \"mp3\", \"--audio-quality\", \"128K\",\n \"-o\", audio_path,\n \"--no-check-certificates\",\n url],\n timeout=600, check=True\n )\n \n size_mb = os.path.getsize(audio_path) / (1024 * 1024)\n print(f\" ✅ Audio: {size_mb:.1f} MB\", file=sys.stderr)\n return audio_path\n\n\ndef transcribe_audio(audio_path: str) -> tuple:\n \"\"\"Transcribe audio using SenseVoice-Small. Returns (text, language).\"\"\"\n from funasr import AutoModel\n from funasr.utils.postprocess_utils import rich_transcription_postprocess\n\n print(\" 🎙️ Loading model...\", file=sys.stderr)\n model = AutoModel(\n model=\"iic/SenseVoiceSmall\",\n trust_remote_code=True,\n vad_model=\"fsmn-vad\",\n vad_kwargs={\"max_single_segment_time\": 30000},\n device=\"cpu\",\n )\n\n print(\" 🎙️ Transcribing...\", file=sys.stderr)\n start = time.time()\n \n # Auto detect language\n result = model.generate(\n input=audio_path,\n language=\"auto\", # Auto detect\n use_itn=True,\n batch_size_s=60\n )\n elapsed = time.time() - start\n\n # Extract text\n text = \"\"\n if result and len(result) > 0:\n for r in result:\n if \"text\" in r:\n text += rich_transcription_postprocess(r[\"text\"]) + \"\\n\\n\"\n \n # Detect language (simple heuristic)\n chinese_chars = len(re.findall(r'[\\u4e00-\\u9fff]', text))\n total_chars = len(text.strip())\n language = \"zh\" if chinese_chars / max(total_chars, 1) > 0.3 else \"en\"\n \n print(f\" ✅ Transcribed in {elapsed:.1f}s (detected: {language})\", file=sys.stderr)\n return text.strip(), language\n\n\ndef translate_text(text: str, api_key: str = None) -> str:\n \"\"\"Translate English to Chinese using DeepSeek API.\"\"\"\n # Try to get API key from environment\n if not api_key:\n api_key = os.environ.get(\"DEEPSEEK_API_KEY\")\n \n if not api_key:\n print(\" ⚠️ No DEEPSEEK_API_KEY, skipping translation\", file=sys.stderr)\n return None\n \n print(\" 🌐 Translating to Chinese...\", file=sys.stderr)\n \n # Split into chunks if too long\n chunks = []\n if len(text) > 4000:\n # Split by paragraphs\n paragraphs = text.split('\\n\\n')\n current_chunk = \"\"\n for para in paragraphs:\n if len(current_chunk) + len(para) > 3500:\n chunks.append(current_chunk)\n current_chunk = para\n else:\n current_chunk += \"\\n\\n\" + para if current_chunk else para\n if current_chunk:\n chunks.append(current_chunk)\n else:\n chunks = [text]\n \n translated_chunks = []\n for i, chunk in enumerate(chunks):\n if len(chunks) > 1:\n print(f\" 🌐 Translating chunk {i+1}/{len(chunks)}...\", file=sys.stderr)\n \n try:\n result = subprocess.run(\n [\"curl\", \"-s\", \"https://api.deepseek.com/v1/chat/completions\",\n \"-H\", \"Content-Type: application/json\",\n \"-H\", f\"Authorization: Bearer {api_key}\",\n \"-d\", json.dumps({\n \"model\": \"deepseek-chat\",\n \"messages\": [\n {\"role\": \"system\", \"content\": \"你是专业翻译。将英文翻译成中文,保持原文风格和格式。不要添加额外解释。\"},\n {\"role\": \"user\", \"content\": f\"翻译以下英文为中文:\\n\\n{chunk}\"}\n ],\n \"temperature\": 0.3\n })],\n capture_output=True, text=True, timeout=120\n )\n \n response = json.loads(result.stdout)\n translated = response['choices'][0]['message']['content']\n translated_chunks.append(translated)\n except Exception as e:\n print(f\" ⚠️ Translation failed: {e}\", file=sys.stderr)\n return None\n \n print(f\" ✅ Translation done\", file=sys.stderr)\n return \"\\n\\n\".join(translated_chunks)\n\n\ndef generate_markdown(title: str, original: str, translated: str, language: str, url: str) -> str:\n \"\"\"Generate bilingual Markdown.\"\"\"\n now = datetime.now().strftime(\"%Y-%m-%d\")\n elapsed_note = \"\"\n \n if language == \"en\" and translated:\n return f\"\"\"---\ntitle: {title}\ntype: note\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nlanguage: en\ntranslated: true\n---\n\n# {title}\n\n> 🌐 英文视频,已翻译为中文\n\n---\n\n## 中文翻译\n\n{translated}\n\n---\n\n## English Original\n\n{original}\n\"\"\"\n else:\n return f\"\"\"---\ntitle: {title}\ntype: note\ntags: [YouTube]\ncreated: {now}\nsource: {url}\nlanguage: {language}\n---\n\n# {title}\n\n{original}\n\"\"\"\n\n\ndef sanitize_filename(name: str) -> str:\n \"\"\"Clean filename.\"\"\"\n s = re.sub(r'[<>:\"/\\\\|?*]', '', name)\n s = re.sub(r'\\s+', '-', s)\n return s[:50]\n\n\ndef main():\n import argparse\n parser = argparse.ArgumentParser(description='YouTube 视频转录 + 翻译')\n parser.add_argument('url', help='YouTube URL')\n parser.add_argument('--output', '-o', default='.', help='输出目录')\n parser.add_argument('--no-translate', action='store_true', help='不翻译')\n args = parser.parse_args()\n \n # Step 1: Get video info\n print(\"=\" * 50, file=sys.stderr)\n print(\"Step 1: Getting video info...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n info = get_video_info(args.url)\n title = info['title']\n print(f\" 📺 Title: {title}\", file=sys.stderr)\n print(f\" ⏱️ Duration: {info['duration']}\", file=sys.stderr)\n \n # Step 2: Download audio\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 2: Downloading audio...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n tmpdir = tempfile.mkdtemp(prefix=\"youtube-\")\n try:\n audio_path = download_audio(args.url, tmpdir)\n \n # Step 3: Transcribe\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 3: Transcribing...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n text, language = transcribe_audio(audio_path)\n \n # Step 4: Translate if English\n translated = None\n if language == \"en\" and not args.no_translate:\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 4: Translating...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n translated = translate_text(text)\n \n # Step 5: Generate Markdown\n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"Step 5: Generating Markdown...\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n \n markdown = generate_markdown(title, text, translated, language, args.url)\n \n # Save\n safe_title = sanitize_filename(title)\n output_path = os.path.join(args.output, f\"{safe_title}.md\")\n os.makedirs(args.output, exist_ok=True)\n \n with open(output_path, 'w', encoding='utf-8') as f:\n f.write(markdown)\n \n print(\"\\n\" + \"=\" * 50, file=sys.stderr)\n print(\"✅ Done!\", file=sys.stderr)\n print(\"=\" * 50, file=sys.stderr)\n print(f\" Title: {title}\", file=sys.stderr)\n print(f\" Language: {language}\", file=sys.stderr)\n print(f\" Translated: {'Yes' if translated else 'No'}\", file=sys.stderr)\n print(f\" Output: {output_path}\", file=sys.stderr)\n \n print(output_path)\n \n finally:\n shutil.rmtree(tmpdir, ignore_errors=True)\n\n\nif __name__ == \"__main__\":\n main()\n"}}, "setup.sh": {"56c434bd9f8f5d15d540038073aa44c24df39dbd": {"t": 1785745936.3126185, "n": 4234, "h": "15e582c45be8a9db", "c": "#!/usr/bin/env bash\n# Chubby Skills - staged installer\n#\n# Usage:\n# bash setup.sh # light mode: zero/low-dependency tools\n# bash setup.sh light # same as default\n# bash setup.sh video # video transcription stack\n# bash setup.sh podcast # podcast transcription stack\n# bash setup.sh wechat # WeChat/PDF extraction stack\n# bash setup.sh all # everything\n# bash setup.sh doctor # environment check only\n# bash setup.sh bilibili xhs # aliases are accepted\n\nset -euo pipefail\n\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m'\n\ninfo() { echo -e \"${GREEN}[✓]${NC} $*\"; }\nwarn() { echo -e \"${YELLOW}[!]${NC} $*\"; }\nerror() { echo -e \"${RED}[✗]${NC} $*\"; }\n\nhas_cmd() {\n command -v \"$1\" >/dev/null 2>&1\n}\n\nrequire_cmd() {\n if has_cmd \"$1\"; then\n info \"已安装 $1\"\n else\n error \"未找到 $1:$2\"\n return 1\n fi\n}\n\nrun_doctor() {\n python3 tools/check_env.py\n}\n\npip_install() {\n python3 -m pip install \"$@\"\n}\n\ninstall_ytdlp() {\n if has_cmd yt-dlp; then\n info \"yt-dlp 已安装\"\n return\n fi\n warn \"未找到 yt-dlp,正在安装...\"\n if [[ \"$(uname)\" == \"Darwin\" ]] && has_cmd brew; then\n brew install yt-dlp || pip_install yt-dlp\n else\n pip_install yt-dlp\n fi\n}\n\ninstall_light() {\n echo \"\"\n echo \"--- light:零/轻依赖能力 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n info \"可直接使用:X 图文、小红书图文、情报雷达、知识库健康检查、content-enrich\"\n warn \"content-enrich / 学习笔记 / 爆款拆解需要按需设置 DEEPSEEK_API_KEY\"\n warn \"小红书采集建议按需设置 XHS_COOKIE\"\n}\n\ninstall_video() {\n echo \"\"\n echo \"--- video:视频转录依赖 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n require_cmd ffmpeg \"macOS: brew install ffmpeg | Ubuntu: sudo apt install ffmpeg\"\n install_ytdlp\n warn \"即将安装 funasr / modelscope / torch / torchaudio,首次安装体积较大。\"\n pip_install funasr modelscope torch torchaudio\n info \"视频转录依赖安装完成\"\n}\n\ninstall_podcast() {\n echo \"\"\n echo \"--- podcast:播客转录依赖 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n require_cmd ffmpeg \"macOS: brew install ffmpeg | Ubuntu: sudo apt install ffmpeg\"\n pip_install faster-whisper\n info \"播客转录依赖安装完成\"\n}\n\ninstall_wechat() {\n echo \"\"\n echo \"--- wechat:公众号/PDF 处理依赖 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n pip_install beautifulsoup4 markitdown pymupdf\n info \"公众号/PDF 处理依赖安装完成\"\n}\n\nnormalize_target() {\n case \"$1\" in\n light|x|twitter|xhs|xiaohongshu|content|radar|knowledge|kb)\n echo \"light\" ;;\n video|douyin|bilibili|youtube|tiktok|weibo|zhihu)\n echo \"video\" ;;\n podcast|rss)\n echo \"podcast\" ;;\n wechat|pdf|article)\n echo \"wechat\" ;;\n all)\n echo \"all\" ;;\n doctor|check)\n echo \"doctor\" ;;\n *)\n echo \"unknown\" ;;\n esac\n}\n\necho \"\"\necho \"=========================================\"\necho \" Chubby Skills 安装助手\"\necho \"=========================================\"\n\nif [[ $# -eq 0 ]]; then\n set -- light\nfi\n\nwant_light=false\nwant_video=false\nwant_podcast=false\nwant_wechat=false\nwant_all=false\nwant_doctor=false\n\nfor raw in \"$@\"; do\n target=\"$(normalize_target \"$raw\")\"\n if [[ \"$target\" == \"unknown\" ]]; then\n error \"未知安装目标:$raw\"\n echo \"可用目标:light / video / podcast / wechat / all / doctor\"\n exit 2\n fi\n case \"$target\" in\n light) want_light=true ;;\n video) want_video=true ;;\n podcast) want_podcast=true ;;\n wechat) want_wechat=true ;;\n all) want_all=true ;;\n doctor) want_doctor=true ;;\n esac\ndone\n\nif $want_doctor; then\n run_doctor\n exit 0\nfi\n\nif $want_all; then\n install_light\n install_video\n install_podcast\n install_wechat\nelse\n $want_light && install_light\n $want_video && install_video\n $want_podcast && install_podcast\n $want_wechat && install_wechat\nfi\n\necho \"\"\necho \"=========================================\"\necho \" 安装步骤完成\"\necho \"=========================================\"\necho \"\"\necho \"下一步建议:\"\necho \" python3 tools/check_env.py\"\necho \" python3 tools/chubby.py quickstart\"\necho \" python3 tools/platform_smoke.py --mode all --check\"\necho \" python3 tools/golden_outputs.py examples/outputs\"\n"}, "88701e7ef4cac72c606bc5d356011c2e08810cfc": {"t": 1785752935.4563684, "n": 4234, "h": "15e582c45be8a9db", "c": "#!/usr/bin/env bash\n# Chubby Skills - staged installer\n#\n# Usage:\n# bash setup.sh # light mode: zero/low-dependency tools\n# bash setup.sh light # same as default\n# bash setup.sh video # video transcription stack\n# bash setup.sh podcast # podcast transcription stack\n# bash setup.sh wechat # WeChat/PDF extraction stack\n# bash setup.sh all # everything\n# bash setup.sh doctor # environment check only\n# bash setup.sh bilibili xhs # aliases are accepted\n\nset -euo pipefail\n\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m'\n\ninfo() { echo -e \"${GREEN}[✓]${NC} $*\"; }\nwarn() { echo -e \"${YELLOW}[!]${NC} $*\"; }\nerror() { echo -e \"${RED}[✗]${NC} $*\"; }\n\nhas_cmd() {\n command -v \"$1\" >/dev/null 2>&1\n}\n\nrequire_cmd() {\n if has_cmd \"$1\"; then\n info \"已安装 $1\"\n else\n error \"未找到 $1:$2\"\n return 1\n fi\n}\n\nrun_doctor() {\n python3 tools/check_env.py\n}\n\npip_install() {\n python3 -m pip install \"$@\"\n}\n\ninstall_ytdlp() {\n if has_cmd yt-dlp; then\n info \"yt-dlp 已安装\"\n return\n fi\n warn \"未找到 yt-dlp,正在安装...\"\n if [[ \"$(uname)\" == \"Darwin\" ]] && has_cmd brew; then\n brew install yt-dlp || pip_install yt-dlp\n else\n pip_install yt-dlp\n fi\n}\n\ninstall_light() {\n echo \"\"\n echo \"--- light:零/轻依赖能力 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n info \"可直接使用:X 图文、小红书图文、情报雷达、知识库健康检查、content-enrich\"\n warn \"content-enrich / 学习笔记 / 爆款拆解需要按需设置 DEEPSEEK_API_KEY\"\n warn \"小红书采集建议按需设置 XHS_COOKIE\"\n}\n\ninstall_video() {\n echo \"\"\n echo \"--- video:视频转录依赖 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n require_cmd ffmpeg \"macOS: brew install ffmpeg | Ubuntu: sudo apt install ffmpeg\"\n install_ytdlp\n warn \"即将安装 funasr / modelscope / torch / torchaudio,首次安装体积较大。\"\n pip_install funasr modelscope torch torchaudio\n info \"视频转录依赖安装完成\"\n}\n\ninstall_podcast() {\n echo \"\"\n echo \"--- podcast:播客转录依赖 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n require_cmd ffmpeg \"macOS: brew install ffmpeg | Ubuntu: sudo apt install ffmpeg\"\n pip_install faster-whisper\n info \"播客转录依赖安装完成\"\n}\n\ninstall_wechat() {\n echo \"\"\n echo \"--- wechat:公众号/PDF 处理依赖 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n pip_install beautifulsoup4 markitdown pymupdf\n info \"公众号/PDF 处理依赖安装完成\"\n}\n\nnormalize_target() {\n case \"$1\" in\n light|x|twitter|xhs|xiaohongshu|content|radar|knowledge|kb)\n echo \"light\" ;;\n video|douyin|bilibili|youtube|tiktok|weibo|zhihu)\n echo \"video\" ;;\n podcast|rss)\n echo \"podcast\" ;;\n wechat|pdf|article)\n echo \"wechat\" ;;\n all)\n echo \"all\" ;;\n doctor|check)\n echo \"doctor\" ;;\n *)\n echo \"unknown\" ;;\n esac\n}\n\necho \"\"\necho \"=========================================\"\necho \" Chubby Skills 安装助手\"\necho \"=========================================\"\n\nif [[ $# -eq 0 ]]; then\n set -- light\nfi\n\nwant_light=false\nwant_video=false\nwant_podcast=false\nwant_wechat=false\nwant_all=false\nwant_doctor=false\n\nfor raw in \"$@\"; do\n target=\"$(normalize_target \"$raw\")\"\n if [[ \"$target\" == \"unknown\" ]]; then\n error \"未知安装目标:$raw\"\n echo \"可用目标:light / video / podcast / wechat / all / doctor\"\n exit 2\n fi\n case \"$target\" in\n light) want_light=true ;;\n video) want_video=true ;;\n podcast) want_podcast=true ;;\n wechat) want_wechat=true ;;\n all) want_all=true ;;\n doctor) want_doctor=true ;;\n esac\ndone\n\nif $want_doctor; then\n run_doctor\n exit 0\nfi\n\nif $want_all; then\n install_light\n install_video\n install_podcast\n install_wechat\nelse\n $want_light && install_light\n $want_video && install_video\n $want_podcast && install_podcast\n $want_wechat && install_wechat\nfi\n\necho \"\"\necho \"=========================================\"\necho \" 安装步骤完成\"\necho \"=========================================\"\necho \"\"\necho \"下一步建议:\"\necho \" python3 tools/check_env.py\"\necho \" python3 tools/chubby.py quickstart\"\necho \" python3 tools/platform_smoke.py --mode all --check\"\necho \" python3 tools/golden_outputs.py examples/outputs\"\n"}, "820b293dc0df12270e8f6ab23c28fae1b4f11f02": {"t": 1785752936.7973084, "n": 4190, "h": "23b32d620771dcae", "c": "#!/usr/bin/env bash\n# Chubby Skills - staged installer\n#\n# Usage:\n# bash setup.sh # light mode: zero/low-dependency tools\n# bash setup.sh light # same as default\n# bash setup.sh video # video transcription stack\n# bash setup.sh podcast # podcast transcription stack\n# bash setup.sh wechat # WeChat/PDF extraction stack\n# bash setup.sh all # everything\n# bash setup.sh doctor # environment check only\n# bash setup.sh bilibili xhs # aliases are accepted\n\nset -euo pipefail\n\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m'\n\ninfo() { echo -e \"${GREEN}[✓]${NC} $*\"; }\nwarn() { echo -e \"${YELLOW}[!]${NC} $*\"; }\nerror() { echo -e \"${RED}[✗]${NC} $*\"; }\n\nhas_cmd() {\n command -v \"$1\" >/dev/null 2>&1\n}\n\nrequire_cmd() {\n if has_cmd \"$1\"; then\n info \"已安装 $1\"\n else\n error \"未找到 $1:$2\"\n return 1\n fi\n}\n\nrun_doctor() {\n python3 tools/check_env.py\n}\n\npip_install() {\n python3 -m pip install \"$@\"\n}\n\ninstall_ytdlp() {\n if has_cmd yt-dlp; then\n info \"yt-dlp 已安装\"\n return\n fi\n warn \"未找到 yt-dlp,正在安装...\"\n if [[ \"$(uname)\" == \"Darwin\" ]] && has_cmd brew; then\n brew install yt-dlp || pip_install yt-dlp\n else\n pip_install yt-dlp\n fi\n}\n\ninstall_light() {\n echo \"\"\n echo \"--- light:零/轻依赖能力 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n info \"可直接使用:X 图文、小红书图文、情报雷达、知识库健康检查、content-enrich\"\n warn \"content-enrich / 学习笔记 / 爆款拆解需要按需设置 DEEPSEEK_API_KEY\"\n warn \"小红书采集建议按需设置 XHS_COOKIE\"\n}\n\ninstall_video() {\n echo \"\"\n echo \"--- video:视频转录依赖 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n require_cmd ffmpeg \"macOS: brew install ffmpeg | Ubuntu: sudo apt install ffmpeg\"\n install_ytdlp\n warn \"即将安装 funasr / modelscope / torch / torchaudio,首次安装体积较大。\"\n pip_install funasr modelscope torch torchaudio\n info \"视频转录依赖安装完成\"\n}\n\ninstall_podcast() {\n echo \"\"\n echo \"--- podcast:播客转录依赖 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n require_cmd ffmpeg \"macOS: brew install ffmpeg | Ubuntu: sudo apt install ffmpeg\"\n pip_install faster-whisper\n info \"播客转录依赖安装完成\"\n}\n\ninstall_wechat() {\n echo \"\"\n echo \"--- wechat:公众号/PDF 处理依赖 ---\"\n require_cmd python3 \"请先安装 Python 3.9+\"\n pip_install beautifulsoup4 markitdown pymupdf\n info \"公众号/PDF 处理依赖安装完成\"\n}\n\nnormalize_target() {\n case \"$1\" in\n light|x|twitter|xhs|xiaohongshu|content|radar|knowledge|kb)\n echo \"light\" ;;\n video|douyin|bilibili|youtube|tiktok|weibo|zhihu)\n echo \"video\" ;;\n podcast|rss)\n echo \"podcast\" ;;\n wechat|pdf|article)\n echo \"wechat\" ;;\n all)\n echo \"all\" ;;\n doctor|check)\n echo \"doctor\" ;;\n *)\n echo \"unknown\" ;;\n esac\n}\n\necho \"\"\necho \"=========================================\"\necho \" Chubby Skills 安装助手\"\necho \"=========================================\"\n\nif [[ $# -eq 0 ]]; then\n set -- light\nfi\n\nwant_light=false\nwant_video=false\nwant_podcast=false\nwant_wechat=false\nwant_all=false\nwant_doctor=false\n\nfor raw in \"$@\"; do\n target=\"$(normalize_target \"$raw\")\"\n if [[ \"$target\" == \"unknown\" ]]; then\n error \"未知安装目标:$raw\"\n echo \"可用目标:light / video / podcast / wechat / all / doctor\"\n exit 2\n fi\n case \"$target\" in\n light) want_light=true ;;\n video) want_video=true ;;\n podcast) want_podcast=true ;;\n wechat) want_wechat=true ;;\n all) want_all=true ;;\n doctor) want_doctor=true ;;\n esac\ndone\n\nif $want_doctor; then\n run_doctor\n exit 0\nfi\n\nif $want_all; then\n install_light\n install_video\n install_podcast\n install_wechat\nelse\n $want_light && install_light\n $want_video && install_video\n $want_podcast && install_podcast\n $want_wechat && install_wechat\nfi\n\necho \"\"\necho \"=========================================\"\necho \" 安装步骤完成\"\necho \"=========================================\"\necho \"\"\necho \"下一步建议:\"\necho \" python3 tools/check_env.py\"\necho \" python3 tools/chubby_ingest.py '<链接>' -o output/\"\necho \" python3 tools/validate_outputs.py examples/outputs\"\n"}, "7f3f850580da96b43752127d5e17abb4848c7cde": {"t": 1785752938.2242327, "n": 3471, "h": "f22481f19ef3f8dd", "c": "#!/usr/bin/env bash\n# Chubby Skills - 一键安装脚本\n# 用法: bash setup.sh [skill-name ...]\n# 不带参数 → 安装所有依赖\n# 带参数 → 只安装指定 skill 的依赖\n#\n# 示例:\n# bash setup.sh # 全部安装\n# bash setup.sh douyin-transcribe # 只装抖音转录\n# bash setup.sh podcast wechat # 装播客 + 公众号\n\nset -euo pipefail\n\nRED='\\033[0;31m'\nGREEN='\\033[0;32m'\nYELLOW='\\033[1;33m'\nNC='\\033[0m'\n\ninfo() { echo -e \"${GREEN}[✓]${NC} $*\"; }\nwarn() { echo -e \"${YELLOW}[!]${NC} $*\"; }\nerror() { echo -e \"${RED}[✗]${NC} $*\"; }\n\ncheck_cmd() {\n if ! command -v \"$1\" &>/dev/null; then\n error \"未找到 $1,请先安装\"\n return 1\n fi\n info \"已安装 $1\"\n}\n\n# ── 基础检查 ──────────────────────────────────────────\necho \"\"\necho \"=========================================\"\necho \" Chubby Skills 安装助手\"\necho \"=========================================\"\necho \"\"\n\ncheck_cmd python3\ncheck_cmd ffmpeg\ncheck_cmd curl\n\n# ── 解析参数 ──────────────────────────────────────────\nINSTALL_ALL=true\nSKILLS=()\nif [[ $# -gt 0 ]]; then\n INSTALL_ALL=false\n SKILLS=(\"$@\")\nfi\n\nneeds() {\n # 检查是否需要安装某个 skill 的依赖\n if $INSTALL_ALL; then return 0; fi\n for s in \"${SKILLS[@]}\"; do\n case \"$1\" in\n *\"$s\"*) return 0 ;;\n esac\n done\n return 1\n}\n\n# ── 视频转录依赖 (funasr + torch) ────────────────────\ninstall_video_deps() {\n echo \"\"\n echo \"--- 视频转录依赖 (funasr / torch) ---\"\n if needs \"douyin\" || needs \"bilibili\" || needs \"tiktok\" || needs \"weibo\" || needs \"zhihu\" || needs \"youtube\" || needs \"video\"; then\n info \"安装 funasr + torch(首次较慢,约 2-3 GB)...\"\n pip install funasr modelscope torch torchaudio\n info \"视频转录依赖安装完成\"\n fi\n}\n\n# ── yt-dlp ────────────────────────────────────────────\ninstall_ytdlp() {\n echo \"\"\n echo \"--- yt-dlp ---\"\n if needs \"bilibili\" || needs \"tiktok\" || needs \"weibo\" || needs \"zhihu\" || needs \"youtube\"; then\n if command -v yt-dlp &>/dev/null; then\n info \"yt-dlp 已安装\"\n else\n warn \"未找到 yt-dlp,正在安装...\"\n if [[ \"$(uname)\" == \"Darwin\" ]]; then\n brew install yt-dlp 2>/dev/null || pip install yt-dlp\n else\n pip install yt-dlp\n fi\n info \"yt-dlp 安装完成\"\n fi\n fi\n}\n\n# ── 播客转录依赖 (faster-whisper) ─────────────────────\ninstall_podcast_deps() {\n echo \"\"\n echo \"--- 播客转录依赖 (faster-whisper) ---\"\n if needs \"podcast\"; then\n pip install faster-whisper\n info \"播客转录依赖安装完成\"\n fi\n}\n\n# ── 公众号处理依赖 ────────────────────────────────────\ninstall_wechat_deps() {\n echo \"\"\n echo \"--- 公众号处理依赖 ---\"\n if needs \"wechat\"; then\n pip install beautifulsoup4 markitdown pymupdf\n info \"公众号处理依赖安装完成\"\n fi\n}\n\n# ── 翻译功能检查 ──────────────────────────────────────\ncheck_translation() {\n if needs \"youtube\" || needs \"learning\"; then\n if [[ -z \"${DEEPSEEK_API_KEY:-}\" ]]; then\n warn \"翻译/闪卡功能需要 DEEPSEEK_API_KEY\"\n warn \" export DEEPSEEK_API_KEY=your-key-here\"\n else\n info \"DEEPSEEK_API_KEY 已设置\"\n fi\n fi\n}\n\n# ── 执行 ──────────────────────────────────────────────\ninstall_video_deps\ninstall_ytdlp\ninstall_podcast_deps\ninstall_wechat_deps\ncheck_translation\n\necho \"\"\necho \"=========================================\"\necho \" 安装完成!\"\necho \"=========================================\"\necho \"\"\necho \"快速验证:\"\necho \" python3 industry-intelligence-radar/scripts/scan.py --hours 1\"\necho \" python3 podcast-transcribe/scripts/transcribe.py --help\"\necho \"\"\n"}}}} |