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 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 合并
@@ -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,3 @@
1
+ from revideo.cli import main
2
+
3
+ main()
@@ -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,2 @@
1
+ [console_scripts]
2
+ revideo = revideo.cli:main
@@ -0,0 +1 @@
1
+ fastmcp>=0.4.0
@@ -0,0 +1 @@
1
+ revideo
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+