revideo 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- revideo-1.0.0/PKG-INFO +144 -0
- revideo-1.0.0/README.md +136 -0
- revideo-1.0.0/pyproject.toml +15 -0
- revideo-1.0.0/revideo/__init__.py +0 -0
- revideo-1.0.0/revideo/__main__.py +3 -0
- revideo-1.0.0/revideo/cli.py +97 -0
- revideo-1.0.0/revideo/config.py +76 -0
- revideo-1.0.0/revideo/mcp_server.py +56 -0
- revideo-1.0.0/revideo/pipeline.py +150 -0
- revideo-1.0.0/revideo/processors/__init__.py +0 -0
- revideo-1.0.0/revideo/processors/audio_bgm.py +18 -0
- revideo-1.0.0/revideo/processors/base.py +12 -0
- revideo-1.0.0/revideo/processors/color.py +17 -0
- revideo-1.0.0/revideo/processors/crop.py +15 -0
- revideo-1.0.0/revideo/processors/file_hash.py +15 -0
- revideo-1.0.0/revideo/processors/metadata.py +16 -0
- revideo-1.0.0/revideo/processors/mirror.py +14 -0
- revideo-1.0.0/revideo/processors/rotate.py +17 -0
- revideo-1.0.0/revideo/processors/scale.py +15 -0
- revideo-1.0.0/revideo/processors/speed.py +17 -0
- revideo-1.0.0/revideo/utils/__init__.py +0 -0
- revideo-1.0.0/revideo/utils/ffmpeg.py +27 -0
- revideo-1.0.0/revideo/utils/scene_detect.py +38 -0
- revideo-1.0.0/revideo/utils/tempfile.py +17 -0
- revideo-1.0.0/revideo.egg-info/PKG-INFO +144 -0
- revideo-1.0.0/revideo.egg-info/SOURCES.txt +29 -0
- revideo-1.0.0/revideo.egg-info/dependency_links.txt +1 -0
- revideo-1.0.0/revideo.egg-info/entry_points.txt +2 -0
- revideo-1.0.0/revideo.egg-info/requires.txt +1 -0
- revideo-1.0.0/revideo.egg-info/top_level.txt +1 -0
- revideo-1.0.0/setup.cfg +4 -0
revideo-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: revideo
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: 视频伪原创工具 - 基于 ffmpeg 的视频去重处理
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastmcp>=0.4.0
|
|
8
|
+
|
|
9
|
+
# revideo — 视频伪原创工具
|
|
10
|
+
|
|
11
|
+
基于 ffmpeg 的视频去重处理工具,通过多种手段修改视频以应对平台原创检测。
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
### 前置依赖
|
|
16
|
+
|
|
17
|
+
- **Python ≥ 3.12**
|
|
18
|
+
- **ffmpeg**(需在 PATH 中可用)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 克隆项目
|
|
22
|
+
git clone https://git.des8.com/heibai2006/revideo.git
|
|
23
|
+
cd revideo
|
|
24
|
+
|
|
25
|
+
# 安装依赖(uv 自动处理)
|
|
26
|
+
uv sync
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
验证安装:
|
|
30
|
+
```bash
|
|
31
|
+
uv run revideo --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 处理单个视频(输出到 output/ 目录)
|
|
38
|
+
uv run revideo video.mp4 -o output/
|
|
39
|
+
|
|
40
|
+
# 处理目录下所有 mp4
|
|
41
|
+
uv run revideo ./videos/ -o output/
|
|
42
|
+
|
|
43
|
+
# 多文件处理后拼合为一个视频
|
|
44
|
+
uv run revideo a.mp4 b.mp4 c.mp4 -o output/ --concat
|
|
45
|
+
|
|
46
|
+
# 添加片头片尾 + 背景音乐
|
|
47
|
+
uv run revideo video.mp4 --intro 片头.mp4 --outro 片尾.mp4 --bgm music.mp3
|
|
48
|
+
|
|
49
|
+
# MCP 模式(接入大模型)
|
|
50
|
+
uv run revideo --mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 输出格式
|
|
54
|
+
|
|
55
|
+
处理完成后,单文件输出 `源文件_已处理.mp4`,合并输出 `源文件_合并.mp4`:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
视频A.mp4→视频A_已处理.mp4
|
|
59
|
+
视频B.mp4→视频B_已处理.mp4
|
|
60
|
+
已合并到 视频A_合并.mp4
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 参数说明
|
|
64
|
+
|
|
65
|
+
| 参数 | 说明 |
|
|
66
|
+
|------|------|
|
|
67
|
+
| `input` | 输入视频文件或目录(可多个) |
|
|
68
|
+
| `-o, --output` | 输出目录,默认 `output` |
|
|
69
|
+
| `--preset {light\|standard\|heavy}` | 预设力度,默认 `standard` |
|
|
70
|
+
| `--no-video` | 禁用视频层面所有处理 |
|
|
71
|
+
| `--no-audio` | 禁用音频层面所有处理 |
|
|
72
|
+
| `--no-metadata` | 禁用元数据层面所有处理 |
|
|
73
|
+
| `--only-video` | 仅启用视频层面(可叠加 `--only-audio`) |
|
|
74
|
+
| `--only-audio` | 仅启用音频层面 |
|
|
75
|
+
| `--only-metadata` | 仅启用元数据层面 |
|
|
76
|
+
| `--concat` | 多文件处理完成后拼合为单个视频 |
|
|
77
|
+
| `--intro` | 片头文件 |
|
|
78
|
+
| `--outro` | 片尾文件 |
|
|
79
|
+
| `--bgm` | 背景音乐文件 |
|
|
80
|
+
| `--mcp` | 启动 MCP stdio 服务器 |
|
|
81
|
+
| `--keep-original` | 保留中间文件(调试用) |
|
|
82
|
+
|
|
83
|
+
## 预设说明
|
|
84
|
+
|
|
85
|
+
| 预设 | 启用组 | 说明 |
|
|
86
|
+
|------|--------|------|
|
|
87
|
+
| `light` | video | 仅画面处理,快速去重 |
|
|
88
|
+
| `standard` | video + audio + metadata | 全组启用(默认) |
|
|
89
|
+
| `heavy` | 全部 | 全组启用 + 更激进参数 |
|
|
90
|
+
|
|
91
|
+
## 伪原创手段详解
|
|
92
|
+
|
|
93
|
+
### 1. 视频层面 (video) — 影响画面像素
|
|
94
|
+
|
|
95
|
+
| 方法 | 说明 | 随机范围 |
|
|
96
|
+
|------|------|----------|
|
|
97
|
+
| 裁切 (crop) | 切除画面边缘像素 | 1%~3% 边缘 |
|
|
98
|
+
| 缩放 (scale) | 轻微放大缩小 | 1.01x~1.05x |
|
|
99
|
+
| 镜像翻转 (mirror) | 水平或垂直翻转 | 随机二选一 |
|
|
100
|
+
| 旋转 (rotate) | 小角度倾斜 | ±0.5°~2° |
|
|
101
|
+
| 色彩调整 (color) | 微调色相/饱和度/亮度 | 色相±10, 饱和度0.95~1.05 |
|
|
102
|
+
| 锐化/模糊 (sharpen) | 轻微锐化或高斯模糊 | — |
|
|
103
|
+
| 变速 (speed) | 微变速并补偿音频音调 | 0.95x~1.05x |
|
|
104
|
+
| 帧率修改 (framerate) | 修改输出帧率后平滑 | — |
|
|
105
|
+
| 场景分割重组 (scene-split) | 场景/静音检测→乱序→拼接 | 阈值可调 |
|
|
106
|
+
| 片段倒放 (reverse) | 随机短片段倒放 | — |
|
|
107
|
+
| 画中画 (pip) | 角落叠加缩小版画面 | 右下角 |
|
|
108
|
+
| 水印/蒙版 (watermark) | 半透明图层 | — |
|
|
109
|
+
| 边框特效 (border) | 装饰边框/模糊边框 | — |
|
|
110
|
+
| 字幕/贴图 | 硬编码文字或贴图 | — |
|
|
111
|
+
| 片头/片尾拼接 | 拼接指定素材到首尾 | — |
|
|
112
|
+
| 片段插入 | 插入无关素材片段 | — |
|
|
113
|
+
| 总时长修改 | 综合变速+抽帧 | — |
|
|
114
|
+
|
|
115
|
+
### 2. 音频层面 (audio) — 音频流处理
|
|
116
|
+
|
|
117
|
+
| 方法 | 说明 |
|
|
118
|
+
|------|------|
|
|
119
|
+
| 背景音乐 (bgm) | 叠加 BGM,自动降低原声音量 (ducking) |
|
|
120
|
+
| 音调调整 (pitch) | 轻微升降调而不改变速度 |
|
|
121
|
+
| 音量曲线 (volume) | 随机化音量变化包络 |
|
|
122
|
+
| 静音检测分割 | 在静音处分割视频片段 |
|
|
123
|
+
| 静音移除 | 去除静音段落,压缩节奏 |
|
|
124
|
+
|
|
125
|
+
### 3. 元数据层面 (metadata) — 容器/编码/文件
|
|
126
|
+
|
|
127
|
+
| 方法 | 说明 |
|
|
128
|
+
|------|------|
|
|
129
|
+
| 格式转换 | 更换容器格式 (mp4↔mkv↔avi↔mov) |
|
|
130
|
+
| 编码器变更 | 更换视频编码 (H.264↔H.265↔VP9) |
|
|
131
|
+
| 元数据擦除 | 清除旧元数据,写入随机新数据 |
|
|
132
|
+
| 文件哈希修改 | 末尾添加填充字节改变 MD5 |
|
|
133
|
+
|
|
134
|
+
## MCP 模式
|
|
135
|
+
|
|
136
|
+
启动 MCP 服务器后,大模型可通过 STDIO 调用:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
uv run revideo --mcp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
可用工具:
|
|
143
|
+
- `process_video` — 处理单个视频,支持所有参数
|
|
144
|
+
- `process_directory` — 批量处理目录,支持 concat 合并
|
revideo-1.0.0/README.md
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# revideo — 视频伪原创工具
|
|
2
|
+
|
|
3
|
+
基于 ffmpeg 的视频去重处理工具,通过多种手段修改视频以应对平台原创检测。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
### 前置依赖
|
|
8
|
+
|
|
9
|
+
- **Python ≥ 3.12**
|
|
10
|
+
- **ffmpeg**(需在 PATH 中可用)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
# 克隆项目
|
|
14
|
+
git clone https://git.des8.com/heibai2006/revideo.git
|
|
15
|
+
cd revideo
|
|
16
|
+
|
|
17
|
+
# 安装依赖(uv 自动处理)
|
|
18
|
+
uv sync
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
验证安装:
|
|
22
|
+
```bash
|
|
23
|
+
uv run revideo --help
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 快速开始
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 处理单个视频(输出到 output/ 目录)
|
|
30
|
+
uv run revideo video.mp4 -o output/
|
|
31
|
+
|
|
32
|
+
# 处理目录下所有 mp4
|
|
33
|
+
uv run revideo ./videos/ -o output/
|
|
34
|
+
|
|
35
|
+
# 多文件处理后拼合为一个视频
|
|
36
|
+
uv run revideo a.mp4 b.mp4 c.mp4 -o output/ --concat
|
|
37
|
+
|
|
38
|
+
# 添加片头片尾 + 背景音乐
|
|
39
|
+
uv run revideo video.mp4 --intro 片头.mp4 --outro 片尾.mp4 --bgm music.mp3
|
|
40
|
+
|
|
41
|
+
# MCP 模式(接入大模型)
|
|
42
|
+
uv run revideo --mcp
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 输出格式
|
|
46
|
+
|
|
47
|
+
处理完成后,单文件输出 `源文件_已处理.mp4`,合并输出 `源文件_合并.mp4`:
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
视频A.mp4→视频A_已处理.mp4
|
|
51
|
+
视频B.mp4→视频B_已处理.mp4
|
|
52
|
+
已合并到 视频A_合并.mp4
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## 参数说明
|
|
56
|
+
|
|
57
|
+
| 参数 | 说明 |
|
|
58
|
+
|------|------|
|
|
59
|
+
| `input` | 输入视频文件或目录(可多个) |
|
|
60
|
+
| `-o, --output` | 输出目录,默认 `output` |
|
|
61
|
+
| `--preset {light\|standard\|heavy}` | 预设力度,默认 `standard` |
|
|
62
|
+
| `--no-video` | 禁用视频层面所有处理 |
|
|
63
|
+
| `--no-audio` | 禁用音频层面所有处理 |
|
|
64
|
+
| `--no-metadata` | 禁用元数据层面所有处理 |
|
|
65
|
+
| `--only-video` | 仅启用视频层面(可叠加 `--only-audio`) |
|
|
66
|
+
| `--only-audio` | 仅启用音频层面 |
|
|
67
|
+
| `--only-metadata` | 仅启用元数据层面 |
|
|
68
|
+
| `--concat` | 多文件处理完成后拼合为单个视频 |
|
|
69
|
+
| `--intro` | 片头文件 |
|
|
70
|
+
| `--outro` | 片尾文件 |
|
|
71
|
+
| `--bgm` | 背景音乐文件 |
|
|
72
|
+
| `--mcp` | 启动 MCP stdio 服务器 |
|
|
73
|
+
| `--keep-original` | 保留中间文件(调试用) |
|
|
74
|
+
|
|
75
|
+
## 预设说明
|
|
76
|
+
|
|
77
|
+
| 预设 | 启用组 | 说明 |
|
|
78
|
+
|------|--------|------|
|
|
79
|
+
| `light` | video | 仅画面处理,快速去重 |
|
|
80
|
+
| `standard` | video + audio + metadata | 全组启用(默认) |
|
|
81
|
+
| `heavy` | 全部 | 全组启用 + 更激进参数 |
|
|
82
|
+
|
|
83
|
+
## 伪原创手段详解
|
|
84
|
+
|
|
85
|
+
### 1. 视频层面 (video) — 影响画面像素
|
|
86
|
+
|
|
87
|
+
| 方法 | 说明 | 随机范围 |
|
|
88
|
+
|------|------|----------|
|
|
89
|
+
| 裁切 (crop) | 切除画面边缘像素 | 1%~3% 边缘 |
|
|
90
|
+
| 缩放 (scale) | 轻微放大缩小 | 1.01x~1.05x |
|
|
91
|
+
| 镜像翻转 (mirror) | 水平或垂直翻转 | 随机二选一 |
|
|
92
|
+
| 旋转 (rotate) | 小角度倾斜 | ±0.5°~2° |
|
|
93
|
+
| 色彩调整 (color) | 微调色相/饱和度/亮度 | 色相±10, 饱和度0.95~1.05 |
|
|
94
|
+
| 锐化/模糊 (sharpen) | 轻微锐化或高斯模糊 | — |
|
|
95
|
+
| 变速 (speed) | 微变速并补偿音频音调 | 0.95x~1.05x |
|
|
96
|
+
| 帧率修改 (framerate) | 修改输出帧率后平滑 | — |
|
|
97
|
+
| 场景分割重组 (scene-split) | 场景/静音检测→乱序→拼接 | 阈值可调 |
|
|
98
|
+
| 片段倒放 (reverse) | 随机短片段倒放 | — |
|
|
99
|
+
| 画中画 (pip) | 角落叠加缩小版画面 | 右下角 |
|
|
100
|
+
| 水印/蒙版 (watermark) | 半透明图层 | — |
|
|
101
|
+
| 边框特效 (border) | 装饰边框/模糊边框 | — |
|
|
102
|
+
| 字幕/贴图 | 硬编码文字或贴图 | — |
|
|
103
|
+
| 片头/片尾拼接 | 拼接指定素材到首尾 | — |
|
|
104
|
+
| 片段插入 | 插入无关素材片段 | — |
|
|
105
|
+
| 总时长修改 | 综合变速+抽帧 | — |
|
|
106
|
+
|
|
107
|
+
### 2. 音频层面 (audio) — 音频流处理
|
|
108
|
+
|
|
109
|
+
| 方法 | 说明 |
|
|
110
|
+
|------|------|
|
|
111
|
+
| 背景音乐 (bgm) | 叠加 BGM,自动降低原声音量 (ducking) |
|
|
112
|
+
| 音调调整 (pitch) | 轻微升降调而不改变速度 |
|
|
113
|
+
| 音量曲线 (volume) | 随机化音量变化包络 |
|
|
114
|
+
| 静音检测分割 | 在静音处分割视频片段 |
|
|
115
|
+
| 静音移除 | 去除静音段落,压缩节奏 |
|
|
116
|
+
|
|
117
|
+
### 3. 元数据层面 (metadata) — 容器/编码/文件
|
|
118
|
+
|
|
119
|
+
| 方法 | 说明 |
|
|
120
|
+
|------|------|
|
|
121
|
+
| 格式转换 | 更换容器格式 (mp4↔mkv↔avi↔mov) |
|
|
122
|
+
| 编码器变更 | 更换视频编码 (H.264↔H.265↔VP9) |
|
|
123
|
+
| 元数据擦除 | 清除旧元数据,写入随机新数据 |
|
|
124
|
+
| 文件哈希修改 | 末尾添加填充字节改变 MD5 |
|
|
125
|
+
|
|
126
|
+
## MCP 模式
|
|
127
|
+
|
|
128
|
+
启动 MCP 服务器后,大模型可通过 STDIO 调用:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
uv run revideo --mcp
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
可用工具:
|
|
135
|
+
- `process_video` — 处理单个视频,支持所有参数
|
|
136
|
+
- `process_directory` — 批量处理目录,支持 concat 合并
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "revideo"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "视频伪原创工具 - 基于 ffmpeg 的视频去重处理"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
dependencies = [
|
|
8
|
+
"fastmcp>=0.4.0",
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
[project.scripts]
|
|
12
|
+
revideo = "revideo.cli:main"
|
|
13
|
+
|
|
14
|
+
[tool.uv]
|
|
15
|
+
package = true
|
|
File without changes
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from revideo.config import PRESET_MAP, ProcessingConfig, VideoGroupConfig, AudioGroupConfig, MetadataGroupConfig
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def parse_args(argv: list[str] | None = None) -> tuple[list[str], ProcessingConfig, bool, argparse.ArgumentParser]:
|
|
9
|
+
parser = argparse.ArgumentParser(
|
|
10
|
+
prog="revideo",
|
|
11
|
+
description="视频伪原创工具 — 基于 ffmpeg 的视频去重处理",
|
|
12
|
+
epilog="""模式说明:
|
|
13
|
+
CLI 模式 直接传入视频文件或目录进行伪原创处理,3组开关灵活控制
|
|
14
|
+
MCP 模式 通过 --mcp 启动 STDIO 服务器,供大模型调用
|
|
15
|
+
|
|
16
|
+
示例:
|
|
17
|
+
uv run revideo video.mp4 -o output/
|
|
18
|
+
uv run revideo ./videos/ -o output/ --no-audio
|
|
19
|
+
uv run revideo a.mp4 b.mp4 -o output/ --concat
|
|
20
|
+
uv run revideo video.mp4 --intro 片头.mp4 --outro 片尾.mp4 --bgm music.mp3
|
|
21
|
+
uv run revideo --mcp""",
|
|
22
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
parser.add_argument("input", nargs="*", help="输入视频文件或目录")
|
|
26
|
+
parser.add_argument("-o", "--output", default="output", help="输出目录")
|
|
27
|
+
parser.add_argument("--preset", choices=["light", "standard", "heavy"], default="standard", help="预设力度")
|
|
28
|
+
|
|
29
|
+
parser.add_argument("--no-video", action="store_true", help="禁用视频层面所有处理")
|
|
30
|
+
parser.add_argument("--no-audio", action="store_true", help="禁用音频层面所有处理")
|
|
31
|
+
parser.add_argument("--no-metadata", action="store_true", help="禁用元数据层面所有处理")
|
|
32
|
+
|
|
33
|
+
parser.add_argument("--only-video", action="store_true", help="仅启用视频层面处理")
|
|
34
|
+
parser.add_argument("--only-audio", action="store_true", help="仅启用音频层面处理")
|
|
35
|
+
parser.add_argument("--only-metadata", action="store_true", help="仅启用元数据层面处理")
|
|
36
|
+
|
|
37
|
+
parser.add_argument("--intro", type=str, default=None, help="片头文件")
|
|
38
|
+
parser.add_argument("--outro", type=str, default=None, help="片尾文件")
|
|
39
|
+
parser.add_argument("--bgm", type=str, default=None, help="背景音乐文件")
|
|
40
|
+
parser.add_argument("--keep-original", action="store_true", help="保留中间文件")
|
|
41
|
+
parser.add_argument("--concat", action="store_true", help="多文件处理完成后拼合成一个视频")
|
|
42
|
+
parser.add_argument("--mcp", action="store_true", help="启动 MCP stdio 模式服务器")
|
|
43
|
+
|
|
44
|
+
args = parser.parse_args(argv)
|
|
45
|
+
|
|
46
|
+
config = ProcessingConfig(
|
|
47
|
+
output_dir=args.output,
|
|
48
|
+
intro=args.intro,
|
|
49
|
+
outro=args.outro,
|
|
50
|
+
bgm=args.bgm,
|
|
51
|
+
keep_original=args.keep_original,
|
|
52
|
+
concat=args.concat,
|
|
53
|
+
preset=args.preset,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
# Apply preset
|
|
57
|
+
if args.preset in PRESET_MAP:
|
|
58
|
+
p = PRESET_MAP[args.preset]
|
|
59
|
+
if "video" in p:
|
|
60
|
+
config.video = VideoGroupConfig(**{**config.video.__dict__, **p["video"]})
|
|
61
|
+
if "audio" in p:
|
|
62
|
+
config.audio = AudioGroupConfig(**{**config.audio.__dict__, **p["audio"]})
|
|
63
|
+
if "metadata" in p:
|
|
64
|
+
config.metadata = MetadataGroupConfig(**{**config.metadata.__dict__, **p["metadata"]})
|
|
65
|
+
|
|
66
|
+
# --no-{group} overrides
|
|
67
|
+
if args.no_video:
|
|
68
|
+
config.video.enabled = False
|
|
69
|
+
if args.no_audio:
|
|
70
|
+
config.audio.enabled = False
|
|
71
|
+
if args.no_metadata:
|
|
72
|
+
config.metadata.enabled = False
|
|
73
|
+
|
|
74
|
+
# --only-{group} overrides: if any --only is set, disable groups not mentioned
|
|
75
|
+
only_set = [k for k in ("video", "audio", "metadata") if getattr(args, f"only_{k}")]
|
|
76
|
+
if only_set:
|
|
77
|
+
config.video.enabled = "video" in only_set
|
|
78
|
+
config.audio.enabled = "audio" in only_set
|
|
79
|
+
config.metadata.enabled = "metadata" in only_set
|
|
80
|
+
|
|
81
|
+
return args.input, config, args.mcp, parser
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def main():
|
|
85
|
+
inputs, config, mcp_mode, parser = parse_args()
|
|
86
|
+
|
|
87
|
+
if mcp_mode:
|
|
88
|
+
from revideo.mcp_server import run_mcp
|
|
89
|
+
run_mcp()
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
if not inputs:
|
|
93
|
+
parser.print_help()
|
|
94
|
+
sys.exit(0)
|
|
95
|
+
|
|
96
|
+
from revideo.pipeline import run_pipeline
|
|
97
|
+
run_pipeline(inputs, config)
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class VideoGroupConfig:
|
|
7
|
+
enabled: bool = True
|
|
8
|
+
crop: bool = True
|
|
9
|
+
scale: bool = True
|
|
10
|
+
mirror: bool = True
|
|
11
|
+
rotate: bool = True
|
|
12
|
+
color: bool = True
|
|
13
|
+
sharpen: bool = True
|
|
14
|
+
speed: bool = True
|
|
15
|
+
framerate: bool = True
|
|
16
|
+
scene_split: bool = True
|
|
17
|
+
reverse: bool = True
|
|
18
|
+
pip: bool = True
|
|
19
|
+
watermark: bool = True
|
|
20
|
+
border: bool = True
|
|
21
|
+
subtitle: bool = True
|
|
22
|
+
intro_outro: bool = True
|
|
23
|
+
insert: bool = True
|
|
24
|
+
duration: bool = True
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class AudioGroupConfig:
|
|
29
|
+
enabled: bool = True
|
|
30
|
+
bgm: bool = True
|
|
31
|
+
pitch: bool = True
|
|
32
|
+
volume: bool = True
|
|
33
|
+
silence_split: bool = True
|
|
34
|
+
silence_remove: bool = True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class MetadataGroupConfig:
|
|
39
|
+
enabled: bool = True
|
|
40
|
+
format: bool = True
|
|
41
|
+
codec: bool = True
|
|
42
|
+
metadata: bool = True
|
|
43
|
+
file_hash: bool = True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class ProcessingConfig:
|
|
48
|
+
video: VideoGroupConfig = field(default_factory=VideoGroupConfig)
|
|
49
|
+
audio: AudioGroupConfig = field(default_factory=AudioGroupConfig)
|
|
50
|
+
metadata: MetadataGroupConfig = field(default_factory=MetadataGroupConfig)
|
|
51
|
+
output_dir: str = "output"
|
|
52
|
+
intro: Optional[str] = None
|
|
53
|
+
outro: Optional[str] = None
|
|
54
|
+
bgm: Optional[str] = None
|
|
55
|
+
keep_original: bool = False
|
|
56
|
+
concat: bool = False
|
|
57
|
+
preset: str = "standard"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
PRESET_MAP = {
|
|
61
|
+
"light": {
|
|
62
|
+
"video": {"enabled": True},
|
|
63
|
+
"audio": {"enabled": False},
|
|
64
|
+
"metadata": {"enabled": False},
|
|
65
|
+
},
|
|
66
|
+
"standard": {
|
|
67
|
+
"video": {"enabled": True},
|
|
68
|
+
"audio": {"enabled": True},
|
|
69
|
+
"metadata": {"enabled": True},
|
|
70
|
+
},
|
|
71
|
+
"heavy": {
|
|
72
|
+
"video": {"enabled": True},
|
|
73
|
+
"audio": {"enabled": True},
|
|
74
|
+
"metadata": {"enabled": True},
|
|
75
|
+
},
|
|
76
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from fastmcp import FastMCP
|
|
2
|
+
|
|
3
|
+
from revideo.config import ProcessingConfig
|
|
4
|
+
from revideo.pipeline import run_pipeline
|
|
5
|
+
|
|
6
|
+
mcp = FastMCP("revideo")
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@mcp.tool()
|
|
10
|
+
def process_video(input_path: str, output_dir: str = "output",
|
|
11
|
+
preset: str = "standard",
|
|
12
|
+
no_video: bool = False, no_audio: bool = False,
|
|
13
|
+
no_metadata: bool = False,
|
|
14
|
+
only_video: bool = False, only_audio: bool = False,
|
|
15
|
+
only_metadata: bool = False,
|
|
16
|
+
intro: str | None = None, outro: str | None = None,
|
|
17
|
+
bgm: str | None = None,
|
|
18
|
+
concat: bool = False) -> str:
|
|
19
|
+
config = ProcessingConfig(
|
|
20
|
+
output_dir=output_dir,
|
|
21
|
+
preset=preset,
|
|
22
|
+
intro=intro,
|
|
23
|
+
outro=outro,
|
|
24
|
+
bgm=bgm,
|
|
25
|
+
concat=concat,
|
|
26
|
+
)
|
|
27
|
+
if no_video:
|
|
28
|
+
config.video.enabled = False
|
|
29
|
+
if no_audio:
|
|
30
|
+
config.audio.enabled = False
|
|
31
|
+
if no_metadata:
|
|
32
|
+
config.metadata.enabled = False
|
|
33
|
+
only_set = [k for k, v in [("video", only_video), ("audio", only_audio), ("metadata", only_metadata)] if v]
|
|
34
|
+
if only_set:
|
|
35
|
+
config.video.enabled = "video" in only_set
|
|
36
|
+
config.audio.enabled = "audio" in only_set
|
|
37
|
+
config.metadata.enabled = "metadata" in only_set
|
|
38
|
+
|
|
39
|
+
run_pipeline([input_path], config)
|
|
40
|
+
return f"处理完成: {input_path}"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@mcp.tool()
|
|
44
|
+
def process_directory(dir_path: str, output_dir: str = "output",
|
|
45
|
+
preset: str = "standard",
|
|
46
|
+
concat: bool = False) -> str:
|
|
47
|
+
import glob
|
|
48
|
+
from pathlib import Path
|
|
49
|
+
videos = sorted(glob.glob(str(Path(dir_path) / "*.mp4")))
|
|
50
|
+
config = ProcessingConfig(output_dir=output_dir, preset=preset, concat=concat)
|
|
51
|
+
run_pipeline(videos, config)
|
|
52
|
+
return f"批量处理完成: {len(videos)} 个文件"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def run_mcp():
|
|
56
|
+
mcp.run(transport="stdio")
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from revideo.config import ProcessingConfig
|
|
6
|
+
from revideo.utils.ffmpeg import run
|
|
7
|
+
from revideo.utils.tempfile import TempDir
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def resolve_inputs(inputs: list[str]) -> tuple[list[Path], Path | None, Path | None]:
|
|
11
|
+
files: list[Path] = []
|
|
12
|
+
intro: Path | None = None
|
|
13
|
+
outro: Path | None = None
|
|
14
|
+
|
|
15
|
+
for name in inputs:
|
|
16
|
+
p = Path(name)
|
|
17
|
+
stem = p.stem
|
|
18
|
+
if stem == "片头":
|
|
19
|
+
intro = p
|
|
20
|
+
elif stem == "片尾":
|
|
21
|
+
outro = p
|
|
22
|
+
else:
|
|
23
|
+
files.append(p)
|
|
24
|
+
|
|
25
|
+
return files, intro, outro
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def concat_videos(video_paths: list[Path], output_path: Path):
|
|
29
|
+
n = len(video_paths)
|
|
30
|
+
|
|
31
|
+
# Probe first video for target dimensions
|
|
32
|
+
from revideo.utils.ffmpeg import probe
|
|
33
|
+
info = probe(str(video_paths[0]))
|
|
34
|
+
streams = info.get("streams", [])
|
|
35
|
+
vinfo = next((s for s in streams if s["codec_type"] == "video"), {})
|
|
36
|
+
tw = vinfo.get("width", 640)
|
|
37
|
+
th = vinfo.get("height", 360)
|
|
38
|
+
# Ensure even dimensions
|
|
39
|
+
tw += tw % 2
|
|
40
|
+
th += th % 2
|
|
41
|
+
|
|
42
|
+
inputs = []
|
|
43
|
+
for p in video_paths:
|
|
44
|
+
inputs += ["-i", str(p)]
|
|
45
|
+
|
|
46
|
+
filter_parts = []
|
|
47
|
+
for i in range(n):
|
|
48
|
+
filter_parts.append(
|
|
49
|
+
f"[{i}:v]scale={tw}:{th}:force_original_aspect_ratio=1,"
|
|
50
|
+
f"pad={tw}:{th}:(ow-iw)/2:(oh-ih)/2,setsar=1[v{i}];"
|
|
51
|
+
f"[{i}:a]aresample=44100[a{i}];"
|
|
52
|
+
)
|
|
53
|
+
filter_parts.append("".join(f"[v{i}][a{i}]" for i in range(n)) + f"concat=n={n}:v=1:a=1[outv][outa]")
|
|
54
|
+
|
|
55
|
+
run(inputs + [
|
|
56
|
+
"-filter_complex", "".join(filter_parts),
|
|
57
|
+
"-map", "[outv]", "-map", "[outa]",
|
|
58
|
+
"-crf", "18", str(output_path)
|
|
59
|
+
])
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def process_single(input_path: Path, output_path: Path, config: ProcessingConfig):
|
|
63
|
+
print(f"{input_path.name}→{output_path.name}")
|
|
64
|
+
|
|
65
|
+
current = input_path
|
|
66
|
+
temp_files: list[Path] = []
|
|
67
|
+
|
|
68
|
+
if config.video.enabled:
|
|
69
|
+
from revideo.processors.crop import CropProcessor
|
|
70
|
+
from revideo.processors.scale import ScaleProcessor
|
|
71
|
+
from revideo.processors.mirror import MirrorProcessor
|
|
72
|
+
from revideo.processors.rotate import RotateProcessor
|
|
73
|
+
from revideo.processors.color import ColorProcessor
|
|
74
|
+
from revideo.processors.speed import SpeedProcessor
|
|
75
|
+
|
|
76
|
+
processors = []
|
|
77
|
+
if config.video.crop:
|
|
78
|
+
processors.append(CropProcessor(config))
|
|
79
|
+
if config.video.scale:
|
|
80
|
+
processors.append(ScaleProcessor(config))
|
|
81
|
+
if config.video.mirror:
|
|
82
|
+
processors.append(MirrorProcessor(config))
|
|
83
|
+
if config.video.rotate:
|
|
84
|
+
processors.append(RotateProcessor(config))
|
|
85
|
+
if config.video.color:
|
|
86
|
+
processors.append(ColorProcessor(config))
|
|
87
|
+
if config.video.speed:
|
|
88
|
+
processors.append(SpeedProcessor(config))
|
|
89
|
+
|
|
90
|
+
for processor in processors:
|
|
91
|
+
tmp = output_path.with_suffix(f".{processor.name}{output_path.suffix}")
|
|
92
|
+
current = processor.process(current, tmp)
|
|
93
|
+
temp_files.append(tmp)
|
|
94
|
+
|
|
95
|
+
if config.audio.enabled and config.bgm:
|
|
96
|
+
from revideo.processors.audio_bgm import BgmProcessor
|
|
97
|
+
if config.audio.bgm:
|
|
98
|
+
proc = BgmProcessor(config)
|
|
99
|
+
tmp = output_path.with_suffix(f".bgm{output_path.suffix}")
|
|
100
|
+
current = proc.process(current, tmp)
|
|
101
|
+
temp_files.append(tmp)
|
|
102
|
+
|
|
103
|
+
if config.metadata.enabled:
|
|
104
|
+
from revideo.processors.metadata import MetadataProcessor
|
|
105
|
+
from revideo.processors.file_hash import FileHashProcessor
|
|
106
|
+
if config.metadata.metadata:
|
|
107
|
+
proc = MetadataProcessor(config)
|
|
108
|
+
tmp = output_path.with_suffix(f".meta{output_path.suffix}")
|
|
109
|
+
current = proc.process(current, tmp)
|
|
110
|
+
temp_files.append(tmp)
|
|
111
|
+
if config.metadata.file_hash:
|
|
112
|
+
proc = FileHashProcessor(config)
|
|
113
|
+
current = proc.process(current, output_path)
|
|
114
|
+
|
|
115
|
+
if current != output_path:
|
|
116
|
+
shutil.move(str(current), str(output_path))
|
|
117
|
+
|
|
118
|
+
if not config.keep_original:
|
|
119
|
+
for f in temp_files:
|
|
120
|
+
if f.exists() and f != current:
|
|
121
|
+
f.unlink(missing_ok=True)
|
|
122
|
+
|
|
123
|
+
print(f"完成: {output_path}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def run_pipeline(inputs: list[str], config: ProcessingConfig):
|
|
127
|
+
files, intro, outro = resolve_inputs(inputs)
|
|
128
|
+
|
|
129
|
+
if not files:
|
|
130
|
+
print("错误: 未找到可处理的视频文件")
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
|
|
133
|
+
out_dir = Path(config.output_dir)
|
|
134
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
135
|
+
|
|
136
|
+
if config.concat and len(files) > 1:
|
|
137
|
+
with TempDir(keep=config.keep_original) as tmp_dir:
|
|
138
|
+
processed = []
|
|
139
|
+
for f in files:
|
|
140
|
+
tmp_path = tmp_dir / f.name
|
|
141
|
+
process_single(f, tmp_path, config)
|
|
142
|
+
processed.append(tmp_path)
|
|
143
|
+
first_stem = files[0].stem
|
|
144
|
+
out_path = out_dir / f"{first_stem}_合并.mp4"
|
|
145
|
+
concat_videos(processed, out_path)
|
|
146
|
+
print(f"已合并到 {out_path.name}")
|
|
147
|
+
else:
|
|
148
|
+
for f in files:
|
|
149
|
+
out_path = out_dir / f"{f.stem}_已处理{f.suffix}"
|
|
150
|
+
process_single(f, out_path, config)
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from revideo.processors.base import Processor
|
|
3
|
+
from revideo.utils.ffmpeg import run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BgmProcessor(Processor):
|
|
7
|
+
name = "bgm"
|
|
8
|
+
|
|
9
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
10
|
+
bgm_path = self.config.bgm
|
|
11
|
+
if not bgm_path:
|
|
12
|
+
return input_path
|
|
13
|
+
run(["-i", str(input_path), "-i", str(bgm_path),
|
|
14
|
+
"-filter_complex",
|
|
15
|
+
"[1:a]volume=0.15[bgm];[0:a][bgm]amix=inputs=2:duration=first:dropout_transition=2[out]",
|
|
16
|
+
"-map", "0:v", "-map", "[out]", "-c:v", "copy",
|
|
17
|
+
"-crf", "18", str(output_path)])
|
|
18
|
+
return output_path
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from revideo.utils.ffmpeg import run
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Processor:
|
|
6
|
+
name: str = "base"
|
|
7
|
+
|
|
8
|
+
def __init__(self, config: object):
|
|
9
|
+
self.config = config
|
|
10
|
+
|
|
11
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
12
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from revideo.processors.base import Processor
|
|
4
|
+
from revideo.utils.ffmpeg import run
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ColorProcessor(Processor):
|
|
8
|
+
name = "color"
|
|
9
|
+
|
|
10
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
11
|
+
h = random.uniform(-10, 10)
|
|
12
|
+
s = random.uniform(0.95, 1.05)
|
|
13
|
+
b = random.uniform(0.95, 1.05)
|
|
14
|
+
run(["-i", str(input_path),
|
|
15
|
+
"-vf", f"hue=h={h}:s={s},eq=brightness={b-1:.3f}",
|
|
16
|
+
"-c:a", "copy", "-crf", "18", str(output_path)])
|
|
17
|
+
return output_path
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from revideo.processors.base import Processor
|
|
4
|
+
from revideo.utils.ffmpeg import run
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class CropProcessor(Processor):
|
|
8
|
+
name = "crop"
|
|
9
|
+
|
|
10
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
11
|
+
crop_pct = random.uniform(0.01, 0.03)
|
|
12
|
+
run(["-i", str(input_path),
|
|
13
|
+
"-vf", f"crop=iw*(1-{crop_pct}):ih*(1-{crop_pct})",
|
|
14
|
+
"-c:a", "copy", "-crf", "18", str(output_path)])
|
|
15
|
+
return output_path
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from revideo.processors.base import Processor
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class FileHashProcessor(Processor):
|
|
7
|
+
name = "filehash"
|
|
8
|
+
|
|
9
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
10
|
+
if input_path != output_path:
|
|
11
|
+
import shutil
|
|
12
|
+
shutil.copy2(input_path, output_path)
|
|
13
|
+
with open(output_path, "ab") as f:
|
|
14
|
+
f.write(b"\x00" * 1024)
|
|
15
|
+
return output_path
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from revideo.processors.base import Processor
|
|
3
|
+
from revideo.utils.ffmpeg import run
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MetadataProcessor(Processor):
|
|
7
|
+
name = "metadata"
|
|
8
|
+
|
|
9
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
10
|
+
run(["-i", str(input_path),
|
|
11
|
+
"-map_metadata", "-1",
|
|
12
|
+
"-metadata", "title=Video",
|
|
13
|
+
"-metadata", "artist=revideo",
|
|
14
|
+
"-metadata", "comment=processed by revideo",
|
|
15
|
+
"-c", "copy", str(output_path)])
|
|
16
|
+
return output_path
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from revideo.processors.base import Processor
|
|
4
|
+
from revideo.utils.ffmpeg import run
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class MirrorProcessor(Processor):
|
|
8
|
+
name = "mirror"
|
|
9
|
+
|
|
10
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
11
|
+
flip = random.choice(["hflip", "vflip"])
|
|
12
|
+
run(["-i", str(input_path),
|
|
13
|
+
"-vf", flip, "-c:a", "copy", "-crf", "18", str(output_path)])
|
|
14
|
+
return output_path
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import math
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from revideo.processors.base import Processor
|
|
5
|
+
from revideo.utils.ffmpeg import run
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RotateProcessor(Processor):
|
|
9
|
+
name = "rotate"
|
|
10
|
+
|
|
11
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
12
|
+
angle = random.uniform(0.5, 2.0) * random.choice([-1, 1])
|
|
13
|
+
rad = angle * math.pi / 180
|
|
14
|
+
run(["-i", str(input_path),
|
|
15
|
+
"-vf", f"rotate={rad}:c=black@0",
|
|
16
|
+
"-c:a", "copy", "-crf", "18", str(output_path)])
|
|
17
|
+
return output_path
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from revideo.processors.base import Processor
|
|
4
|
+
from revideo.utils.ffmpeg import run
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class ScaleProcessor(Processor):
|
|
8
|
+
name = "scale"
|
|
9
|
+
|
|
10
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
11
|
+
factor = random.uniform(1.01, 1.05)
|
|
12
|
+
run(["-i", str(input_path),
|
|
13
|
+
"-vf", f"scale=iw*{factor}:ih*{factor}:flags=bilinear",
|
|
14
|
+
"-c:a", "copy", "-crf", "18", str(output_path)])
|
|
15
|
+
return output_path
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import random
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from revideo.processors.base import Processor
|
|
4
|
+
from revideo.utils.ffmpeg import run
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SpeedProcessor(Processor):
|
|
8
|
+
name = "speed"
|
|
9
|
+
|
|
10
|
+
def process(self, input_path: Path, output_path: Path) -> Path:
|
|
11
|
+
factor = random.uniform(0.95, 1.05)
|
|
12
|
+
audio_factor = 1.0 / factor
|
|
13
|
+
run(["-i", str(input_path),
|
|
14
|
+
"-vf", f"setpts={factor}*PTS",
|
|
15
|
+
"-af", f"atempo={audio_factor}",
|
|
16
|
+
"-crf", "18", str(output_path)])
|
|
17
|
+
return output_path
|
|
File without changes
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
import json
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def probe(path: str | Path) -> dict:
|
|
7
|
+
result = subprocess.run(
|
|
8
|
+
["ffprobe", "-v", "quiet", "-print_format", "json", "-show_format", "-show_streams", str(path)],
|
|
9
|
+
capture_output=True, text=True, check=True,
|
|
10
|
+
)
|
|
11
|
+
return json.loads(result.stdout)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def run(args: list[str], capture_output: bool = False) -> subprocess.CompletedProcess:
|
|
15
|
+
return subprocess.run(
|
|
16
|
+
["ffmpeg", "-y"] + args,
|
|
17
|
+
capture_output=capture_output, text=True, check=True,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_filter(args: list[str], filter_str: str, filter_complex: str | None = None) -> list[str]:
|
|
22
|
+
result = args[:]
|
|
23
|
+
if filter_complex:
|
|
24
|
+
result += ["-filter_complex", filter_complex]
|
|
25
|
+
else:
|
|
26
|
+
result += ["-vf", filter_str]
|
|
27
|
+
return result
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import subprocess
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def detect_scenes(path: str | Path, threshold: float = 0.4) -> list[float]:
|
|
8
|
+
result = subprocess.run(
|
|
9
|
+
["ffmpeg", "-i", str(path), "-vf", f"select='gt(scene,{threshold})',showinfo",
|
|
10
|
+
"-vsync", "vfr", "-f", "null", "-"],
|
|
11
|
+
capture_output=True, text=True, check=True,
|
|
12
|
+
)
|
|
13
|
+
timestamps = []
|
|
14
|
+
for line in result.stderr.splitlines():
|
|
15
|
+
m = re.search(r"pts_time:([\d.]+)", line)
|
|
16
|
+
if m:
|
|
17
|
+
timestamps.append(float(m.group(1)))
|
|
18
|
+
return timestamps
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def detect_silence(path: str | Path, noise_db: str = "-30dB", duration: str = "0.5") -> list[dict]:
|
|
22
|
+
result = subprocess.run(
|
|
23
|
+
["ffmpeg", "-i", str(path), "-af", f"silencedetect=noise={noise_db}:d={duration}", "-f", "null", "-"],
|
|
24
|
+
capture_output=True, text=True, check=True,
|
|
25
|
+
)
|
|
26
|
+
silences = []
|
|
27
|
+
start, end = None, None
|
|
28
|
+
for line in result.stderr.splitlines():
|
|
29
|
+
m = re.search(r"silence_start: ([\d.]+)", line)
|
|
30
|
+
if m:
|
|
31
|
+
start = float(m.group(1))
|
|
32
|
+
m = re.search(r"silence_end: ([\d.]+)", line)
|
|
33
|
+
if m:
|
|
34
|
+
end = float(m.group(1))
|
|
35
|
+
if start is not None:
|
|
36
|
+
silences.append({"start": start, "end": end})
|
|
37
|
+
start = None
|
|
38
|
+
return silences
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class TempDir:
|
|
7
|
+
def __init__(self, keep: bool = False):
|
|
8
|
+
self._keep = keep
|
|
9
|
+
self._path: Path | None = None
|
|
10
|
+
|
|
11
|
+
def __enter__(self) -> Path:
|
|
12
|
+
self._path = Path(tempfile.mkdtemp(prefix="revideo_"))
|
|
13
|
+
return self._path
|
|
14
|
+
|
|
15
|
+
def __exit__(self, *args):
|
|
16
|
+
if not self._keep and self._path and self._path.exists():
|
|
17
|
+
shutil.rmtree(self._path)
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: revideo
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: 视频伪原创工具 - 基于 ffmpeg 的视频去重处理
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: fastmcp>=0.4.0
|
|
8
|
+
|
|
9
|
+
# revideo — 视频伪原创工具
|
|
10
|
+
|
|
11
|
+
基于 ffmpeg 的视频去重处理工具,通过多种手段修改视频以应对平台原创检测。
|
|
12
|
+
|
|
13
|
+
## 安装
|
|
14
|
+
|
|
15
|
+
### 前置依赖
|
|
16
|
+
|
|
17
|
+
- **Python ≥ 3.12**
|
|
18
|
+
- **ffmpeg**(需在 PATH 中可用)
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# 克隆项目
|
|
22
|
+
git clone https://git.des8.com/heibai2006/revideo.git
|
|
23
|
+
cd revideo
|
|
24
|
+
|
|
25
|
+
# 安装依赖(uv 自动处理)
|
|
26
|
+
uv sync
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
验证安装:
|
|
30
|
+
```bash
|
|
31
|
+
uv run revideo --help
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## 快速开始
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# 处理单个视频(输出到 output/ 目录)
|
|
38
|
+
uv run revideo video.mp4 -o output/
|
|
39
|
+
|
|
40
|
+
# 处理目录下所有 mp4
|
|
41
|
+
uv run revideo ./videos/ -o output/
|
|
42
|
+
|
|
43
|
+
# 多文件处理后拼合为一个视频
|
|
44
|
+
uv run revideo a.mp4 b.mp4 c.mp4 -o output/ --concat
|
|
45
|
+
|
|
46
|
+
# 添加片头片尾 + 背景音乐
|
|
47
|
+
uv run revideo video.mp4 --intro 片头.mp4 --outro 片尾.mp4 --bgm music.mp3
|
|
48
|
+
|
|
49
|
+
# MCP 模式(接入大模型)
|
|
50
|
+
uv run revideo --mcp
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 输出格式
|
|
54
|
+
|
|
55
|
+
处理完成后,单文件输出 `源文件_已处理.mp4`,合并输出 `源文件_合并.mp4`:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
视频A.mp4→视频A_已处理.mp4
|
|
59
|
+
视频B.mp4→视频B_已处理.mp4
|
|
60
|
+
已合并到 视频A_合并.mp4
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## 参数说明
|
|
64
|
+
|
|
65
|
+
| 参数 | 说明 |
|
|
66
|
+
|------|------|
|
|
67
|
+
| `input` | 输入视频文件或目录(可多个) |
|
|
68
|
+
| `-o, --output` | 输出目录,默认 `output` |
|
|
69
|
+
| `--preset {light\|standard\|heavy}` | 预设力度,默认 `standard` |
|
|
70
|
+
| `--no-video` | 禁用视频层面所有处理 |
|
|
71
|
+
| `--no-audio` | 禁用音频层面所有处理 |
|
|
72
|
+
| `--no-metadata` | 禁用元数据层面所有处理 |
|
|
73
|
+
| `--only-video` | 仅启用视频层面(可叠加 `--only-audio`) |
|
|
74
|
+
| `--only-audio` | 仅启用音频层面 |
|
|
75
|
+
| `--only-metadata` | 仅启用元数据层面 |
|
|
76
|
+
| `--concat` | 多文件处理完成后拼合为单个视频 |
|
|
77
|
+
| `--intro` | 片头文件 |
|
|
78
|
+
| `--outro` | 片尾文件 |
|
|
79
|
+
| `--bgm` | 背景音乐文件 |
|
|
80
|
+
| `--mcp` | 启动 MCP stdio 服务器 |
|
|
81
|
+
| `--keep-original` | 保留中间文件(调试用) |
|
|
82
|
+
|
|
83
|
+
## 预设说明
|
|
84
|
+
|
|
85
|
+
| 预设 | 启用组 | 说明 |
|
|
86
|
+
|------|--------|------|
|
|
87
|
+
| `light` | video | 仅画面处理,快速去重 |
|
|
88
|
+
| `standard` | video + audio + metadata | 全组启用(默认) |
|
|
89
|
+
| `heavy` | 全部 | 全组启用 + 更激进参数 |
|
|
90
|
+
|
|
91
|
+
## 伪原创手段详解
|
|
92
|
+
|
|
93
|
+
### 1. 视频层面 (video) — 影响画面像素
|
|
94
|
+
|
|
95
|
+
| 方法 | 说明 | 随机范围 |
|
|
96
|
+
|------|------|----------|
|
|
97
|
+
| 裁切 (crop) | 切除画面边缘像素 | 1%~3% 边缘 |
|
|
98
|
+
| 缩放 (scale) | 轻微放大缩小 | 1.01x~1.05x |
|
|
99
|
+
| 镜像翻转 (mirror) | 水平或垂直翻转 | 随机二选一 |
|
|
100
|
+
| 旋转 (rotate) | 小角度倾斜 | ±0.5°~2° |
|
|
101
|
+
| 色彩调整 (color) | 微调色相/饱和度/亮度 | 色相±10, 饱和度0.95~1.05 |
|
|
102
|
+
| 锐化/模糊 (sharpen) | 轻微锐化或高斯模糊 | — |
|
|
103
|
+
| 变速 (speed) | 微变速并补偿音频音调 | 0.95x~1.05x |
|
|
104
|
+
| 帧率修改 (framerate) | 修改输出帧率后平滑 | — |
|
|
105
|
+
| 场景分割重组 (scene-split) | 场景/静音检测→乱序→拼接 | 阈值可调 |
|
|
106
|
+
| 片段倒放 (reverse) | 随机短片段倒放 | — |
|
|
107
|
+
| 画中画 (pip) | 角落叠加缩小版画面 | 右下角 |
|
|
108
|
+
| 水印/蒙版 (watermark) | 半透明图层 | — |
|
|
109
|
+
| 边框特效 (border) | 装饰边框/模糊边框 | — |
|
|
110
|
+
| 字幕/贴图 | 硬编码文字或贴图 | — |
|
|
111
|
+
| 片头/片尾拼接 | 拼接指定素材到首尾 | — |
|
|
112
|
+
| 片段插入 | 插入无关素材片段 | — |
|
|
113
|
+
| 总时长修改 | 综合变速+抽帧 | — |
|
|
114
|
+
|
|
115
|
+
### 2. 音频层面 (audio) — 音频流处理
|
|
116
|
+
|
|
117
|
+
| 方法 | 说明 |
|
|
118
|
+
|------|------|
|
|
119
|
+
| 背景音乐 (bgm) | 叠加 BGM,自动降低原声音量 (ducking) |
|
|
120
|
+
| 音调调整 (pitch) | 轻微升降调而不改变速度 |
|
|
121
|
+
| 音量曲线 (volume) | 随机化音量变化包络 |
|
|
122
|
+
| 静音检测分割 | 在静音处分割视频片段 |
|
|
123
|
+
| 静音移除 | 去除静音段落,压缩节奏 |
|
|
124
|
+
|
|
125
|
+
### 3. 元数据层面 (metadata) — 容器/编码/文件
|
|
126
|
+
|
|
127
|
+
| 方法 | 说明 |
|
|
128
|
+
|------|------|
|
|
129
|
+
| 格式转换 | 更换容器格式 (mp4↔mkv↔avi↔mov) |
|
|
130
|
+
| 编码器变更 | 更换视频编码 (H.264↔H.265↔VP9) |
|
|
131
|
+
| 元数据擦除 | 清除旧元数据,写入随机新数据 |
|
|
132
|
+
| 文件哈希修改 | 末尾添加填充字节改变 MD5 |
|
|
133
|
+
|
|
134
|
+
## MCP 模式
|
|
135
|
+
|
|
136
|
+
启动 MCP 服务器后,大模型可通过 STDIO 调用:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
uv run revideo --mcp
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
可用工具:
|
|
143
|
+
- `process_video` — 处理单个视频,支持所有参数
|
|
144
|
+
- `process_directory` — 批量处理目录,支持 concat 合并
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
revideo/__init__.py
|
|
4
|
+
revideo/__main__.py
|
|
5
|
+
revideo/cli.py
|
|
6
|
+
revideo/config.py
|
|
7
|
+
revideo/mcp_server.py
|
|
8
|
+
revideo/pipeline.py
|
|
9
|
+
revideo.egg-info/PKG-INFO
|
|
10
|
+
revideo.egg-info/SOURCES.txt
|
|
11
|
+
revideo.egg-info/dependency_links.txt
|
|
12
|
+
revideo.egg-info/entry_points.txt
|
|
13
|
+
revideo.egg-info/requires.txt
|
|
14
|
+
revideo.egg-info/top_level.txt
|
|
15
|
+
revideo/processors/__init__.py
|
|
16
|
+
revideo/processors/audio_bgm.py
|
|
17
|
+
revideo/processors/base.py
|
|
18
|
+
revideo/processors/color.py
|
|
19
|
+
revideo/processors/crop.py
|
|
20
|
+
revideo/processors/file_hash.py
|
|
21
|
+
revideo/processors/metadata.py
|
|
22
|
+
revideo/processors/mirror.py
|
|
23
|
+
revideo/processors/rotate.py
|
|
24
|
+
revideo/processors/scale.py
|
|
25
|
+
revideo/processors/speed.py
|
|
26
|
+
revideo/utils/__init__.py
|
|
27
|
+
revideo/utils/ffmpeg.py
|
|
28
|
+
revideo/utils/scene_detect.py
|
|
29
|
+
revideo/utils/tempfile.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastmcp>=0.4.0
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
revideo
|
revideo-1.0.0/setup.cfg
ADDED