diff --git a/.gitignore b/.gitignore index 199b9a0..3b2f15f 100644 --- a/.gitignore +++ b/.gitignore @@ -45,4 +45,8 @@ htmlcov/ *.whl .claude -temp/ \ No newline at end of file +temp/ + +# Local +.env +output/ \ No newline at end of file diff --git a/README.md b/README.md index a03c73b..c91ff09 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,41 @@ -# 抖音无水印视频文本提取 MCP 服务器 +# 抖音无水印视频下载与文案提取 [![PyPI version](https://badge.fury.io/py/douyin-mcp-server.svg)](https://badge.fury.io/py/douyin-mcp-server) [![Python version](https://img.shields.io/pypi/pyversions/douyin-mcp-server.svg)](https://pypi.org/project/douyin-mcp-server/) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) -一个基于 Model Context Protocol (MCP) 的服务器,可以从抖音分享链接下载无水印视频,提取音频并转换为文本。 +从抖音分享链接下载无水印视频,提取音频并转换为文本。 douyin-mcp-server MCP server -## 📋 项目声明 +## 📦 两种使用方式 -**官方文档地址:** https://github.com/yzfly/douyin-mcp-server - -请以本项目的 [README.md](https://github.com/yzfly/douyin-mcp-server/blob/main/README.md) 文件为准,了解项目的功能特性、使用方法、API 配置说明等详细信息。 - -**重要提醒:** 第三方平台如因自身 MCP Server 功能支持度限制而无法正常使用,请联系相应平台方。本项目不提供任何形式的技术支持或保证,用户需自行承担使用本项目可能产生的任何损失或损害。 - -**法律声明:** -1. 本项目基于 Apache 2.0 协议发布 -2. 本项目仅供学习和研究使用,不得用于任何违法或违规目的 -3. 本项目的使用必须遵守相关法律法规 -4. 本项目的作者和贡献者不对项目的任何部分承担法律责任 +| 方式 | 适用场景 | 说明 | +|------|----------|------| +| **MCP Server** | Claude Desktop、Cherry Studio 等 | MCP 协议集成 | +| **Claude Code Skill** | Claude Code CLI | 命令行批量提取,Markdown 输出 | ## ✨ 功能特性 -- 🎵 **无水印视频获取** - 从抖音分享链接获取高质量无水印视频 +- 🎵 **无水印视频获取** - 从抖音分享链接获取高质量无水印视频(无需 API 密钥) - 🎧 **智能音频提取** - 自动从视频中提取音频内容 -- 📝 **AI 文本识别** - 使用先进的语音识别技术提取文本内容 +- 📝 **AI 文本识别** - 使用硅基流动 SenseVoice API 提取文案 +- 📄 **Markdown 输出** - 文案自动保存为 Markdown 格式(Skill 模式) - 🧹 **自动清理** - 智能清理处理过程中的临时文件 -- 🔧 **灵活配置** - 支持自定义 API 配置,默认使用 [阿里云百炼 API](https://help.aliyun.com/zh/model-studio/get-api-key?) -## 🚀 快速开始 +--- + +## 🚀 方式一:MCP Server (v1.3.0) + +适用于 Claude Desktop、Cherry Studio 等支持 MCP 协议的应用。 ### 步骤 1:获取 API 密钥 -前往 [阿里云百炼 API](https://help.aliyun.com/zh/model-studio/get-api-key?) 获取您的 `DASHSCOPE_API_KEY`: +前往 [硅基流动](https://cloud.siliconflow.cn/i/TxUlXG3u) 注册并获取 API Key(有免费额度)。 -![获取阿里云百炼API](https://files.mdnice.com/user/43439/36e658be-1ccf-41dd-87cf-d43fefde5c4e.png) - -### 步骤 2:配置环境变量 - -在 Claude Desktop、Cherry Studio 等支持 MCP Server 的应用配置文件中添加以下配置: +### 步骤 2:配置 MCP Server ```json { @@ -51,200 +44,142 @@ "command": "uvx", "args": ["douyin-mcp-server"], "env": { - "DASHSCOPE_API_KEY": "sk-xxxx" + "API_KEY": "sk-xxxx" } } } } ``` -### 步骤 3:开始使用 +### 工具说明 -配置完成后,您就可以在支持的应用中正常调用 MCP 工具了。 +| 工具 | 功能 | 需要 API | +|------|------|----------| +| `get_douyin_download_link` | 获取无水印下载链接 | ❌ | +| `extract_douyin_text` | 提取视频文案 | ✅ | +| `parse_douyin_video_info` | 解析视频信息 | ❌ | -## ⚙️ API 配置说明 +--- -### 当前版本(>= 1.2.0) +## 🛠️ 方式二:Claude Code Skill -最新版本默认使用阿里云百炼 API,具有以下优势: -- ✅ 识别效果更好 -- ✅ 处理速度更快 -- ✅ 本地资源消耗更小 +适用于 Claude Code CLI,支持批量提取文案并保存为 Markdown 文件。 -**配置步骤:** -1. 前往 [阿里云百炼](https://help.aliyun.com/zh/model-studio/get-api-key?) 开通 API 服务 -2. 获取 API Key 并配置到环境变量 `DASHSCOPE_API_KEY` 中 +### 安装 Skill -### 旧版本兼容(<= 1.1.0) - -如果您需要使用旧版本,请使用以下配置: - -```json -{ - "mcpServers": { - "douyin-mcp": { - "command": "uvx", - "args": ["douyin-mcp-server@1.1.0"], - "env": { - "DOUYIN_API_KEY": "your-api-key-here" - } - } - } -} -``` - -**注意:** 旧版本使用硅基流动 API,需要在 [硅基流动](https://cloud.siliconflow.cn/i/TxUlXG3u) 注册账号并获取 API Key。 - -📖 [1.1.0 版本文档](https://pypi.org/project/douyin-mcp-server/1.1.0/) - -## 🛠️ 工具说明 - -### `get_douyin_download_link` - -获取抖音视频的无水印下载链接。 - -**参数:** -- `share_link` (string): 抖音分享链接或包含链接的文本 - -**返回:** -- JSON 格式的下载链接和视频信息 - -**特点:** 无需 API 密钥即可使用 - -### `extract_douyin_text` - -完整的文本提取工具,一站式完成视频到文本的转换。 - -**处理流程:** -1. 解析抖音分享链接 -2. 直接使用视频 URL 进行语音识别 -3. 返回提取的文本内容 - -**参数:** -- `share_link` (string): 抖音分享链接或包含链接的文本 -- `model` (string, 可选): 语音识别模型,默认使用 `paraformer-v2` - -**环境变量要求:** -- `DASHSCOPE_API_KEY`: 阿里云百炼 API 密钥(必需) - -### `parse_douyin_video_info` - -轻量级视频信息解析工具。 - -**参数:** -- `share_link` (string): 抖音分享链接 - -**特点:** 仅解析视频基本信息,不下载视频文件 - -### 资源访问 - -- `douyin://video/{video_id}`: 通过视频 ID 获取详细信息 - -## 📦 系统要求 - -### 运行环境 -- **Python**: 3.10 或更高版本 - -### 依赖库 -- `requests` - HTTP 请求处理 -- `ffmpeg-python` - 音视频处理 -- `tqdm` - 进度条显示 -- `mcp` - Model Context Protocol 支持 -- `dashscope` - 阿里云百炼 API 客户端 - -## ⚠️ 注意事项 - -- 🔑 **API 密钥必需**:文本提取功能需要有效的阿里云百炼 API 密钥 -- 🆓 **部分功能免费**:获取下载链接功能无需 API 密钥 -- 📱 **格式支持**:支持大部分抖音视频格式 -- 🚀 **性能优化**:使用阿里云百炼 API 获得更快更准确的识别效果 - -## 🔧 开发指南 - -### 本地开发环境搭建 +下载 `douyin-video.skill` 文件,解压到 skills 目录: + +```bash +# 全局安装 +unzip douyin-video.skill -d ~/.claude/skills/ + +# 或项目级安装 +unzip douyin-video.skill -d .claude/skills/ +``` + +### 配置环境变量 + +```bash +export API_KEY="your-siliconflow-api-key" +``` + +### 使用方法 + +```bash +# 获取视频信息(无需 API) +python douyin_downloader.py -l "抖音链接" -a info + +# 下载视频 +python douyin_downloader.py -l "抖音链接" -a download -o ./videos + +# 提取文案 +python douyin_downloader.py -l "抖音链接" -a extract -o ./output + +# 提取文案并保存视频 +python douyin_downloader.py -l "抖音链接" -a extract -o ./output --save-video +``` + +### 输出格式 + +``` +output/ +├── 7600361826030865707/ +│ └── transcript.md # Markdown 格式文案 +└── 7581044356631612699/ + ├── transcript.md + └── *.mp4 # --save-video 时保存 +``` + +**transcript.md 示例:** + +```markdown +# 视频标题 + +| 属性 | 值 | +|------|-----| +| 视频ID | `7600361826030865707` | +| 提取时间 | 2026-01-30 14:19:00 | +| 下载链接 | [点击下载](url) | + +--- + +## 文案内容 + +(语音识别文字) +``` + +--- + +## 📋 系统要求 + +- **Python**: 3.8+ +- **FFmpeg**: 必须安装 + - macOS: `brew install ffmpeg` + - Ubuntu: `apt install ffmpeg` + +--- + +## 🔧 本地开发 ```bash -# 克隆项目 git clone https://github.com/yzfly/douyin-mcp-server.git cd douyin-mcp-server - -# 安装依赖(开发模式) pip install -e . -``` - -### 运行测试 - -```bash -# 启动服务器进行测试 python -m douyin_mcp_server.server ``` -### Claude Desktop 本地开发配置 - -在 Claude Desktop 配置文件中添加本地开发配置: - -```json -{ - "mcpServers": { - "douyin-mcp": { - "command": "uv", - "args": [ - "run", - "--directory", - "/path/to/your/douyin-mcp-server", - "python", - "-m", - "douyin_mcp_server" - ], - "env": { - "DASHSCOPE_API_KEY": "your-api-key-here" - } - } - } -} -``` +--- ## ⚠️ 免责声明 -### 使用风险 -- 使用者对本项目的使用完全自主决定,并自行承担所有风险 -- 作者对使用者因使用本项目而产生的任何损失、责任或风险概不负责 +- 本项目仅供学习和研究使用 +- 使用者需遵守相关法律法规 +- 禁止用于侵犯知识产权的行为 +- 作者不对使用本项目产生的损失承担责任 -### 代码质量 -- 本项目基于现有知识和技术开发,作者努力确保代码的正确性和安全性 -- 但不保证代码完全没有错误或缺陷,使用者需自行评估和测试 +--- -### 第三方依赖 -- 本项目依赖的第三方库、插件或服务遵循各自的开源或商业许可 -- 使用者需自行查阅并遵守相应协议 -- 作者不对第三方组件的稳定性、安全性及合规性承担责任 +## 📝 更新日志 -### 法律合规 -- 使用者必须自行研究相关法律法规,确保使用行为合法合规 -- 任何违反法律法规导致的法律责任和风险,均由使用者自行承担 -- 禁止使用本工具从事任何侵犯知识产权的行为 -- 开发者不参与、不支持、不认可任何非法内容的获取或分发 +### v1.3.0 (最新) -### 数据处理 -- 本项目不对使用者的数据收集、存储、传输等处理活动的合规性承担责任 -- 使用者应自行遵守相关法律法规,确保数据处理行为合法正当 +- ✨ **Claude Code Skill**:新增命令行工具,支持批量提取文案 +- 📄 **Markdown 输出**:文案自动保存为 Markdown 格式 +- 🎯 **双 API 支持**:同时支持硅基流动和阿里云百炼 API -### 责任限制 -- 使用者不得将项目作者、贡献者或相关方与使用行为联系起来 -- 不得要求作者对使用项目产生的任何损失或损害负责 -- 基于本项目的二次开发、修改或编译程序与原作者无关 +### v1.2.0 -### 知识产权 -- 本项目不授予使用者任何专利许可 -- 若使用本项目导致专利纠纷或侵权,使用者自行承担全部风险和责任 -- 未经书面授权,不得用于商业宣传、推广或再授权 +- 🔄 **API 切换**:升级为阿里云百炼 API(环境变量 `DASHSCOPE_API_KEY`) -### 服务终止 -- 作者保留随时终止向违反声明的使用者提供服务的权利 -- 可能要求违规使用者销毁已获取的代码及衍生作品 -- 作者保留在不另行通知的情况下更新本声明的权利 +### v1.1.0 -**⚠️ 重要提醒:在使用本项目前,请认真阅读并完全理解上述免责声明。如有疑问或不同意任何条款,请勿使用本项目。继续使用即视为完全接受上述声明并自愿承担所有风险和后果。** +- 🐛 **问题修复**:修复文件名过长导致的错误 + +### v1.0.0 + +- 🎉 首次发布 + +--- ## 📄 许可证 @@ -252,27 +187,4 @@ Apache License 2.0 ## 👨‍💻 作者 -- **yzfly** - [yz.liu.me@gmail.com](mailto:yz.liu.me@gmail.com) -- GitHub: [https://github.com/yzfly](https://github.com/yzfly) - -## 🤝 贡献 - -欢迎提交 Issue 和 Pull Request!我们期待您的参与和贡献。 - -## 📝 更新日志 - -### v1.2.0 (最新) -- 🚀 **性能提升**:更快、更准确的视频文案提取 -- 🔄 **API 升级**:切换到阿里云百炼 API,显著提升识别准确率 -- 🔧 **配置更新**:环境变量从 `DOUYIN_API_KEY` 更新为 `DASHSCOPE_API_KEY` - -### v1.1.0 -- 🐛 **问题修复**:修复提取视频时文件名过长导致的错误 - -### v1.0.0 -- 🎉 **首次发布**:初始版本 -- ✨ **核心功能**:支持抖音视频文本提取 -- 🔗 **链接获取**:支持获取无水印视频下载链接 -- 🔐 **环境配置**:从环境变量读取 API 密钥 -- 🧹 **自动清理**:自动清理临时文件 -- ⚙️ **灵活配置**:支持自定义 API 配置 \ No newline at end of file +**yzfly** - [GitHub](https://github.com/yzfly) | [Email](mailto:yz.liu.me@gmail.com) diff --git a/douyin-video.skill b/douyin-video.skill new file mode 100644 index 0000000..cb95e11 Binary files /dev/null and b/douyin-video.skill differ diff --git a/douyin-video/SKILL.md b/douyin-video/SKILL.md new file mode 100644 index 0000000..81be0ee --- /dev/null +++ b/douyin-video/SKILL.md @@ -0,0 +1,153 @@ +--- +name: douyin-video +description: "抖音无水印视频下载和文案提取工具. 从抖音分享链接获取无水印视频下载链接, 下载视频, 提取视频中的语音文案并自动保存到文件. 适用场景包括获取抖音视频信息, 下载无水印视频, 批量提取视频文案. 当用户需要处理抖音视频链接或提取视频内容时触发." +--- + +# 抖音无水印视频下载和文案提取 + +从抖音分享链接获取无水印视频下载链接, 下载视频, 并使用语音识别提取视频中的文案, 自动保存到文件. + +## 功能概述 + +- **获取下载链接**: 从抖音分享链接解析出无水印视频的直接下载地址 (无需 API 密钥) +- **下载视频**: 将无水印视频下载到本地指定目录 +- **提取文案**: 通过语音识别从视频中提取文字内容 (需要硅基流动 API 密钥) +- **自动保存**: 每个视频的文案自动保存到独立文件夹 (视频ID为文件夹名) + +## 环境要求 + +### 依赖安装 + +```bash +pip install requests ffmpeg-python +``` + +### 系统要求 + +- FFmpeg 必须安装在系统中 (用于音视频处理) +- macOS: `brew install ffmpeg` +- Ubuntu: `apt install ffmpeg` + +### API 密钥配置 (仅文案提取需要) + +文案提取功能使用硅基流动 API, 需要设置环境变量: + +```bash +export API_KEY="your-siliconflow-api-key" +``` + +获取 API 密钥: https://cloud.siliconflow.cn/ + +## 使用方法 + +### 方法一: 使用脚本 (推荐) + +```bash +# 获取视频信息和下载链接 (无需 API 密钥) +python douyin_downloader.py --link "抖音分享链接" --action info + +# 下载视频到指定目录 +python douyin_downloader.py --link "抖音分享链接" --action download --output ./videos + +# 提取视频文案并保存到文件 (需要 API_KEY 环境变量) +python douyin_downloader.py --link "抖音分享链接" --action extract --output ./output + +# 提取文案并同时保存视频 +python douyin_downloader.py --link "抖音分享链接" --action extract --output ./output --save-video + +# 安静模式 (减少输出) +python douyin_downloader.py --link "抖音分享链接" --action extract --output ./output --quiet +``` + +### 输出目录结构 + +提取文案后, 每个视频会保存到独立文件夹: + +``` +output/ +├── 7600361826030865707/ # 视频ID为文件夹名 +│ └── transcript.md # Markdown 格式文案文件 +├── 7581044356631612699/ +│ ├── transcript.md +│ └── 7581044356631612699.mp4 # 使用 --save-video 时保存 +└── ... +``` + +### Markdown 文案格式 + +```markdown +# 视频标题 + +| 属性 | 值 | +|------|-----| +| 视频ID | `7600361826030865707` | +| 提取时间 | 2026-01-30 14:19:00 | +| 下载链接 | [点击下载](url) | + +--- + +## 文案内容 + +(语音识别的文字内容) +``` + +### 方法二: 在 Python 代码中调用 + +```python +from douyin_downloader import get_video_info, download_video, extract_text + +# 获取视频信息 +info = get_video_info("抖音分享链接") +print(f"视频ID: {info['video_id']}") +print(f"标题: {info['title']}") +print(f"下载链接: {info['url']}") + +# 下载视频 +video_path = download_video("抖音分享链接", output_dir="./videos") + +# 提取文案并保存到文件 +result = extract_text("抖音分享链接", output_dir="./output") +print(f"文案已保存到: {result['output_path']}") +print(result['text']) +``` + +## 工作流程 + +### 获取视频信息 + +1. 解析抖音分享链接, 提取真实的视频 URL +2. 模拟移动端请求获取页面数据 +3. 从页面 JSON 数据中提取无水印视频地址 +4. 返回视频 ID, 标题和下载链接 + +### 提取视频文案 + +1. 解析分享链接获取视频信息 +2. 下载视频到临时目录 +3. 使用 FFmpeg 从视频中提取音频 (MP3 格式) +4. 调用硅基流动 SenseVoice API 进行语音识别 +5. 清理临时文件, 返回识别的文本 + +## 常见问题 + +### 无法解析链接 + +- 确保链接是有效的抖音分享链接 +- 链接格式通常为 `https://v.douyin.com/xxxxx/` 或完整的抖音视频 URL + +### 提取文案失败 + +- 检查 `API_KEY` 环境变量是否已设置 +- 确保 API 密钥有效且有足够的配额 +- 确保 FFmpeg 已正确安装 + +### 下载速度慢 + +- 这取决于网络条件和视频大小 +- 脚本会显示下载进度 + +## 注意事项 + +- 本工具仅供学习和研究使用 +- 使用时需遵守相关法律法规 +- 请勿用于任何侵犯版权或违法的目的 diff --git a/douyin-video/scripts/douyin_downloader.py b/douyin-video/scripts/douyin_downloader.py new file mode 100644 index 0000000..71ed8de --- /dev/null +++ b/douyin-video/scripts/douyin_downloader.py @@ -0,0 +1,390 @@ +#!/usr/bin/env python3 +""" +抖音无水印视频下载和文案提取工具 + +功能: +1. 从抖音分享链接获取无水印视频下载链接 +2. 下载视频并提取音频 +3. 使用硅基流动 API 从音频中提取文本 +4. 自动保存文案到文件 (一个视频一个文件夹) + +环境变量: +- API_KEY: 硅基流动 API 密钥 (用于文案提取功能) + +使用示例: + # 获取下载链接 (无需 API 密钥) + python douyin_downloader.py --link "抖音分享链接" --action info + + # 下载视频 + python douyin_downloader.py --link "抖音分享链接" --action download --output ./videos + + # 提取文案并保存到文件 (需要 API_KEY 环境变量) + python douyin_downloader.py --link "抖音分享链接" --action extract --output ./output +""" + +import os +import re +import sys +import json +import argparse +import tempfile +import shutil +from pathlib import Path +from typing import Optional +from datetime import datetime + + +def check_dependencies(): + """检查必要的依赖是否已安装""" + missing = [] + try: + import requests + except ImportError: + missing.append("requests") + try: + import ffmpeg + except ImportError: + missing.append("ffmpeg-python") + + if missing: + print(f"缺少依赖: {', '.join(missing)}") + print(f"请运行: pip install {' '.join(missing)}") + sys.exit(1) + + +check_dependencies() + +import requests +import ffmpeg + +# 请求头,模拟移动端访问 +HEADERS = { + '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' +} + +# 硅基流动 API 配置 +DEFAULT_API_BASE_URL = "https://api.siliconflow.cn/v1/audio/transcriptions" +DEFAULT_MODEL = "FunAudioLLM/SenseVoiceSmall" + + +class DouyinProcessor: + """抖音视频处理器""" + + def __init__(self, api_key: str = "", api_base_url: Optional[str] = None, model: Optional[str] = None): + self.api_key = api_key + self.api_base_url = api_base_url or DEFAULT_API_BASE_URL + self.model = model or DEFAULT_MODEL + self.temp_dir = Path(tempfile.mkdtemp()) + + def __del__(self): + """清理临时目录""" + if hasattr(self, 'temp_dir') and self.temp_dir.exists(): + shutil.rmtree(self.temp_dir, ignore_errors=True) + + def parse_share_url(self, share_text: str) -> dict: + """从分享文本中提取无水印视频链接""" + # 提取分享链接 + urls = re.findall(r'http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+', share_text) + if not urls: + raise ValueError("未找到有效的分享链接") + + share_url = urls[0] + share_response = requests.get(share_url, headers=HEADERS) + video_id = share_response.url.split("?")[0].strip("/").split("/")[-1] + share_url = f'https://www.iesdouyin.com/share/video/{video_id}' + + # 获取视频页面内容 + response = requests.get(share_url, headers=HEADERS) + response.raise_for_status() + + pattern = re.compile( + pattern=r"window\._ROUTER_DATA\s*=\s*(.*?)", + flags=re.DOTALL, + ) + find_res = pattern.search(response.text) + + if not find_res or not find_res.group(1): + raise ValueError("从HTML中解析视频信息失败") + + # 解析JSON数据 + json_data = json.loads(find_res.group(1).strip()) + VIDEO_ID_PAGE_KEY = "video_(id)/page" + NOTE_ID_PAGE_KEY = "note_(id)/page" + + if VIDEO_ID_PAGE_KEY in json_data["loaderData"]: + original_video_info = json_data["loaderData"][VIDEO_ID_PAGE_KEY]["videoInfoRes"] + elif NOTE_ID_PAGE_KEY in json_data["loaderData"]: + original_video_info = json_data["loaderData"][NOTE_ID_PAGE_KEY]["videoInfoRes"] + else: + raise Exception("无法从JSON中解析视频或图集信息") + + data = original_video_info["item_list"][0] + + # 获取视频信息 + video_url = data["video"]["play_addr"]["url_list"][0].replace("playwm", "play") + desc = data.get("desc", "").strip() or f"douyin_{video_id}" + + # 替换文件名中的非法字符 + desc = re.sub(r'[\\/:*?"<>|]', '_', desc) + + return { + "url": video_url, + "title": desc, + "video_id": video_id + } + + def download_video(self, video_info: dict, output_dir: Optional[Path] = None, show_progress: bool = True) -> Path: + """下载视频""" + if output_dir is None: + output_dir = self.temp_dir + else: + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + filename = f"{video_info['video_id']}.mp4" + filepath = output_dir / filename + + if show_progress: + print(f"正在下载视频: {video_info['title']}") + + response = requests.get(video_info['url'], headers=HEADERS, stream=True) + response.raise_for_status() + + # 获取文件大小 + total_size = int(response.headers.get('content-length', 0)) + + # 下载文件 + downloaded = 0 + with open(filepath, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + downloaded += len(chunk) + if show_progress and total_size > 0: + progress = downloaded / total_size * 100 + print(f"\r下载进度: {progress:.1f}%", end="", flush=True) + + if show_progress: + print(f"\n视频下载完成: {filepath}") + return filepath + + def extract_audio(self, video_path: Path, show_progress: bool = True) -> Path: + """从视频文件中提取音频""" + audio_path = video_path.with_suffix('.mp3') + + if show_progress: + print("正在提取音频...") + try: + ( + ffmpeg + .input(str(video_path)) + .output(str(audio_path), acodec='libmp3lame', q=0) + .run(capture_stdout=True, capture_stderr=True, overwrite_output=True) + ) + if show_progress: + print(f"音频提取完成: {audio_path}") + return audio_path + except Exception as e: + raise Exception(f"提取音频时出错: {str(e)}") + + def extract_text_from_audio(self, audio_path: Path, show_progress: bool = True) -> str: + """从音频文件中提取文字""" + if not self.api_key: + raise ValueError("未设置 API 密钥,请设置环境变量 DOUYIN_API_KEY") + + if show_progress: + print("正在识别语音...") + files = { + 'file': (audio_path.name, open(audio_path, 'rb'), 'audio/mpeg'), + 'model': (None, self.model) + } + + headers = { + "Authorization": f"Bearer {self.api_key}" + } + + try: + response = requests.post(self.api_base_url, files=files, headers=headers) + response.raise_for_status() + + # 解析响应 + result = response.json() + if 'text' in result: + return result['text'] + else: + return response.text + + except Exception as e: + raise Exception(f"提取文字时出错: {str(e)}") + finally: + files['file'][1].close() + + def cleanup_files(self, *file_paths: Path): + """清理指定的文件""" + for file_path in file_paths: + if file_path.exists(): + file_path.unlink() + + +def get_video_info(share_link: str) -> dict: + """获取视频信息和下载链接""" + processor = DouyinProcessor() + return processor.parse_share_url(share_link) + + +def download_video(share_link: str, output_dir: str = ".") -> Path: + """下载视频到指定目录""" + processor = DouyinProcessor() + video_info = processor.parse_share_url(share_link) + return processor.download_video(video_info, Path(output_dir)) + + +def extract_text(share_link: str, api_key: Optional[str] = None, output_dir: Optional[str] = None, + save_video: bool = False, show_progress: bool = True) -> dict: + """ + 从视频中提取文案并保存到文件 + + 返回: + dict: 包含 video_info, text, output_path 的字典 + """ + api_key = api_key or os.getenv('API_KEY') + if not api_key: + raise ValueError("未设置环境变量 API_KEY,请先获取硅基流动 API 密钥") + + processor = DouyinProcessor(api_key) + + if show_progress: + print("正在解析抖音分享链接...") + video_info = processor.parse_share_url(share_link) + + if show_progress: + print("正在下载视频...") + video_path = processor.download_video(video_info, show_progress=show_progress) + + if show_progress: + print("正在提取音频...") + audio_path = processor.extract_audio(video_path, show_progress=show_progress) + + if show_progress: + print("正在从音频中提取文本...") + text_content = processor.extract_text_from_audio(audio_path, show_progress=show_progress) + + result = { + "video_info": video_info, + "text": text_content, + "output_path": None + } + + # 保存到文件 + if output_dir: + output_base = Path(output_dir) + video_folder = output_base / video_info['video_id'] + video_folder.mkdir(parents=True, exist_ok=True) + + # 保存文案为 Markdown 格式 + transcript_path = video_folder / "transcript.md" + with open(transcript_path, 'w', encoding='utf-8') as f: + f.write(f"# {video_info['title']}\n\n") + f.write(f"| 属性 | 值 |\n") + f.write(f"|------|----|\n") + f.write(f"| 视频ID | `{video_info['video_id']}` |\n") + f.write(f"| 提取时间 | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')} |\n") + f.write(f"| 下载链接 | [点击下载]({video_info['url']}) |\n\n") + f.write(f"---\n\n") + f.write(f"## 文案内容\n\n") + f.write(text_content) + + result["output_path"] = str(video_folder) + + if show_progress: + print(f"文案已保存到: {transcript_path}") + + # 保存视频 (可选) + if save_video: + saved_video_path = video_folder / f"{video_info['video_id']}.mp4" + shutil.copy2(video_path, saved_video_path) + if show_progress: + print(f"视频已保存到: {saved_video_path}") + + # 清理临时文件 + if show_progress: + print("正在清理临时文件...") + processor.cleanup_files(video_path, audio_path) + + return result + + +def main(): + parser = argparse.ArgumentParser( + description="抖音无水印视频下载和文案提取工具", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + # 获取视频信息和下载链接 + python douyin_downloader.py --link "抖音分享链接" --action info + + # 下载视频 + python douyin_downloader.py --link "抖音分享链接" --action download --output ./videos + + # 提取文案并保存到文件 (需要设置 DOUYIN_API_KEY 环境变量) + python douyin_downloader.py --link "抖音分享链接" --action extract --output ./output + + # 提取文案并同时保存视频 + python douyin_downloader.py --link "抖音分享链接" --action extract --output ./output --save-video + """ + ) + + parser.add_argument("--link", "-l", required=True, help="抖音分享链接或包含链接的文本") + parser.add_argument("--action", "-a", choices=["info", "download", "extract"], + default="info", help="操作类型: info(获取信息), download(下载视频), extract(提取文案)") + parser.add_argument("--output", "-o", default="./output", help="输出目录 (默认 ./output)") + parser.add_argument("--api-key", "-k", help="硅基流动 API 密钥 (也可通过 DOUYIN_API_KEY 环境变量设置)") + parser.add_argument("--save-video", "-v", action="store_true", help="提取文案时同时保存视频") + parser.add_argument("--quiet", "-q", action="store_true", help="安静模式,减少输出") + + args = parser.parse_args() + + try: + if args.action == "info": + info = get_video_info(args.link) + print("\n" + "=" * 50) + print("视频信息:") + print("=" * 50) + print(f"视频ID: {info['video_id']}") + print(f"标题: {info['title']}") + print(f"下载链接: {info['url']}") + print("=" * 50) + + elif args.action == "download": + video_path = download_video(args.link, args.output) + print(f"\n视频已保存到: {video_path}") + + elif args.action == "extract": + result = extract_text( + args.link, + args.api_key, + output_dir=args.output, + save_video=args.save_video, + show_progress=not args.quiet + ) + + if not args.quiet: + print("\n" + "=" * 50) + print("提取完成!") + print("=" * 50) + print(f"视频ID: {result['video_info']['video_id']}") + print(f"标题: {result['video_info']['title']}") + if result['output_path']: + print(f"保存位置: {result['output_path']}") + print("=" * 50) + print("\n文案内容:\n") + print(result['text'][:500] + "..." if len(result['text']) > 500 else result['text']) + print("\n" + "=" * 50) + + except Exception as e: + print(f"\n错误: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/douyin_mcp_server/__init__.py b/douyin_mcp_server/__init__.py index 1e0983b..5b26db8 100644 --- a/douyin_mcp_server/__init__.py +++ b/douyin_mcp_server/__init__.py @@ -1,6 +1,6 @@ """抖音无水印链接提取 MCP 服务器""" -__version__ = "1.2.0" +__version__ = "1.3.0" __author__ = "yzfly" __email__ = "yz.liu.me@gmail.com" diff --git a/douyin_mcp_server/server.py b/douyin_mcp_server/server.py index a20cd0c..39e5839 100644 --- a/douyin_mcp_server/server.py +++ b/douyin_mcp_server/server.py @@ -242,13 +242,13 @@ async def extract_douyin_text( 返回: - 提取的文本内容 - 注意: 需要设置环境变量 DASHSCOPE_API_KEY + 注意: 需要设置环境变量 API_KEY """ try: # 从环境变量获取API密钥 - api_key = os.getenv('DASHSCOPE_API_KEY') + api_key = os.getenv('API_KEY') if not api_key: - raise ValueError("未设置环境变量 DASHSCOPE_API_KEY,请在配置中添加阿里云百炼API密钥") + raise ValueError("未设置环境变量 API_KEY,请在配置中添加阿里云百炼API密钥") processor = DouyinProcessor(api_key, model) @@ -328,11 +328,11 @@ def douyin_text_extraction_guide() -> str: ## 环境变量配置 请确保设置了以下环境变量: -- `DASHSCOPE_API_KEY`: 阿里云百炼API密钥 +- `API_KEY`: 阿里云百炼API密钥 ## 使用步骤 1. 复制抖音视频的分享链接 -2. 在Claude Desktop配置中设置环境变量 DASHSCOPE_API_KEY +2. 在Claude Desktop配置中设置环境变量 API_KEY 3. 使用相应的工具进行操作 ## 工具说明 @@ -349,7 +349,7 @@ def douyin_text_extraction_guide() -> str: "command": "uvx", "args": ["douyin-mcp-server"], "env": { - "DASHSCOPE_API_KEY": "your-dashscope-api-key-here" + "API_KEY": "your-api-key-here" } } } diff --git a/pyproject.toml b/pyproject.toml index 4daa514..36260be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "douyin-mcp-server" -version = "1.2.0" +version = "1.3.0" description = "MCP server for downloading Douyin videos and extracting text" readme = "README.md" license = {text = "MIT"}