wafle-video-processor 0.1.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.
- wafle_video_processor-0.1.0/PKG-INFO +72 -0
- wafle_video_processor-0.1.0/README.md +60 -0
- wafle_video_processor-0.1.0/pyproject.toml +24 -0
- wafle_video_processor-0.1.0/setup.cfg +4 -0
- wafle_video_processor-0.1.0/src/wafle_video_processor.egg-info/PKG-INFO +72 -0
- wafle_video_processor-0.1.0/src/wafle_video_processor.egg-info/SOURCES.txt +11 -0
- wafle_video_processor-0.1.0/src/wafle_video_processor.egg-info/dependency_links.txt +1 -0
- wafle_video_processor-0.1.0/src/wafle_video_processor.egg-info/entry_points.txt +2 -0
- wafle_video_processor-0.1.0/src/wafle_video_processor.egg-info/top_level.txt +1 -0
- wafle_video_processor-0.1.0/src/waflevideoprocessor/__init__.py +12 -0
- wafle_video_processor-0.1.0/src/waflevideoprocessor/__main__.py +5 -0
- wafle_video_processor-0.1.0/src/waflevideoprocessor/cli.py +165 -0
- wafle_video_processor-0.1.0/src/waflevideoprocessor/processor.py +347 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wafle-video-processor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: WAFLE automatic watermark removal from videos via FFmpeg delogo. CPU-only, no GPU needed, faster than real-time.
|
|
5
|
+
Author: WAFLE
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/creandoaldia/web-ai-lab
|
|
8
|
+
Project-URL: Documentation, https://github.com/creandoaldia/web-ai-lab#readme
|
|
9
|
+
Keywords: wafle,watermark,delogo,ffmpeg,video-processing,meta-ai
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# wafle-video-processor
|
|
14
|
+
|
|
15
|
+
Automatic watermark removal from videos via FFmpeg `delogo` filter.
|
|
16
|
+
|
|
17
|
+
**Purpose**: Remove Meta AI / Facebook watermarks from downloaded videos.
|
|
18
|
+
**Target**: Semi-transparent "Meta AI" logo, static bottom-right position.
|
|
19
|
+
**Requirement**: No GPU — runs on CPU, faster than real-time.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install wafle-video-processor
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires FFmpeg in PATH:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
winget install FFmpeg
|
|
31
|
+
# or: https://ffmpeg.org/download.html
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## CLI Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Single file (in-place)
|
|
38
|
+
wafle-video-processor remove video.mp4
|
|
39
|
+
|
|
40
|
+
# Single file (new file)
|
|
41
|
+
wafle-video-processor remove video.mp4 -o clean.mp4
|
|
42
|
+
|
|
43
|
+
# Batch directory
|
|
44
|
+
wafle-video-processor batch ./videos/ -o ./cleaned/
|
|
45
|
+
|
|
46
|
+
# Custom watermark position/size
|
|
47
|
+
wafle-video-processor remove video.mp4 --position bottom-right --width-pct 8 --height-pct 4
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Python API
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from waflevideoprocessor import remove_watermark, has_ffmpeg
|
|
54
|
+
|
|
55
|
+
if not has_ffmpeg():
|
|
56
|
+
print("Install FFmpeg first")
|
|
57
|
+
|
|
58
|
+
result = remove_watermark("video.mp4", output_path="clean.mp4")
|
|
59
|
+
print(result["success"]) # True/False
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Integration
|
|
63
|
+
|
|
64
|
+
Automatically called by `wafle-automations-meta` after each video download
|
|
65
|
+
when `wafle-video-processor` is installed.
|
|
66
|
+
|
|
67
|
+
## Meta AI Watermark
|
|
68
|
+
|
|
69
|
+
- Semi-transparent "Meta AI" logo
|
|
70
|
+
- Bottom-right corner
|
|
71
|
+
- ~10% width × ~5% height
|
|
72
|
+
- Static position across all free-tier videos
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# wafle-video-processor
|
|
2
|
+
|
|
3
|
+
Automatic watermark removal from videos via FFmpeg `delogo` filter.
|
|
4
|
+
|
|
5
|
+
**Purpose**: Remove Meta AI / Facebook watermarks from downloaded videos.
|
|
6
|
+
**Target**: Semi-transparent "Meta AI" logo, static bottom-right position.
|
|
7
|
+
**Requirement**: No GPU — runs on CPU, faster than real-time.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install wafle-video-processor
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires FFmpeg in PATH:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
winget install FFmpeg
|
|
19
|
+
# or: https://ffmpeg.org/download.html
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## CLI Usage
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Single file (in-place)
|
|
26
|
+
wafle-video-processor remove video.mp4
|
|
27
|
+
|
|
28
|
+
# Single file (new file)
|
|
29
|
+
wafle-video-processor remove video.mp4 -o clean.mp4
|
|
30
|
+
|
|
31
|
+
# Batch directory
|
|
32
|
+
wafle-video-processor batch ./videos/ -o ./cleaned/
|
|
33
|
+
|
|
34
|
+
# Custom watermark position/size
|
|
35
|
+
wafle-video-processor remove video.mp4 --position bottom-right --width-pct 8 --height-pct 4
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Python API
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from waflevideoprocessor import remove_watermark, has_ffmpeg
|
|
42
|
+
|
|
43
|
+
if not has_ffmpeg():
|
|
44
|
+
print("Install FFmpeg first")
|
|
45
|
+
|
|
46
|
+
result = remove_watermark("video.mp4", output_path="clean.mp4")
|
|
47
|
+
print(result["success"]) # True/False
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Integration
|
|
51
|
+
|
|
52
|
+
Automatically called by `wafle-automations-meta` after each video download
|
|
53
|
+
when `wafle-video-processor` is installed.
|
|
54
|
+
|
|
55
|
+
## Meta AI Watermark
|
|
56
|
+
|
|
57
|
+
- Semi-transparent "Meta AI" logo
|
|
58
|
+
- Bottom-right corner
|
|
59
|
+
- ~10% width × ~5% height
|
|
60
|
+
- Static position across all free-tier videos
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "wafle-video-processor"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "WAFLE automatic watermark removal from videos via FFmpeg delogo. CPU-only, no GPU needed, faster than real-time."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
authors = [{name = "WAFLE"}]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["wafle", "watermark", "delogo", "ffmpeg", "video-processing", "meta-ai"]
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/creandoaldia/web-ai-lab"
|
|
17
|
+
Documentation = "https://github.com/creandoaldia/web-ai-lab#readme"
|
|
18
|
+
|
|
19
|
+
[project.scripts]
|
|
20
|
+
wafle-video-processor = "waflevideoprocessor.cli:main"
|
|
21
|
+
|
|
22
|
+
[tool.setuptools.packages.find]
|
|
23
|
+
where = ["src"]
|
|
24
|
+
include = ["waflevideoprocessor*"]
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: wafle-video-processor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: WAFLE automatic watermark removal from videos via FFmpeg delogo. CPU-only, no GPU needed, faster than real-time.
|
|
5
|
+
Author: WAFLE
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/creandoaldia/web-ai-lab
|
|
8
|
+
Project-URL: Documentation, https://github.com/creandoaldia/web-ai-lab#readme
|
|
9
|
+
Keywords: wafle,watermark,delogo,ffmpeg,video-processing,meta-ai
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# wafle-video-processor
|
|
14
|
+
|
|
15
|
+
Automatic watermark removal from videos via FFmpeg `delogo` filter.
|
|
16
|
+
|
|
17
|
+
**Purpose**: Remove Meta AI / Facebook watermarks from downloaded videos.
|
|
18
|
+
**Target**: Semi-transparent "Meta AI" logo, static bottom-right position.
|
|
19
|
+
**Requirement**: No GPU — runs on CPU, faster than real-time.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install wafle-video-processor
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Requires FFmpeg in PATH:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
winget install FFmpeg
|
|
31
|
+
# or: https://ffmpeg.org/download.html
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## CLI Usage
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# Single file (in-place)
|
|
38
|
+
wafle-video-processor remove video.mp4
|
|
39
|
+
|
|
40
|
+
# Single file (new file)
|
|
41
|
+
wafle-video-processor remove video.mp4 -o clean.mp4
|
|
42
|
+
|
|
43
|
+
# Batch directory
|
|
44
|
+
wafle-video-processor batch ./videos/ -o ./cleaned/
|
|
45
|
+
|
|
46
|
+
# Custom watermark position/size
|
|
47
|
+
wafle-video-processor remove video.mp4 --position bottom-right --width-pct 8 --height-pct 4
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Python API
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from waflevideoprocessor import remove_watermark, has_ffmpeg
|
|
54
|
+
|
|
55
|
+
if not has_ffmpeg():
|
|
56
|
+
print("Install FFmpeg first")
|
|
57
|
+
|
|
58
|
+
result = remove_watermark("video.mp4", output_path="clean.mp4")
|
|
59
|
+
print(result["success"]) # True/False
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Integration
|
|
63
|
+
|
|
64
|
+
Automatically called by `wafle-automations-meta` after each video download
|
|
65
|
+
when `wafle-video-processor` is installed.
|
|
66
|
+
|
|
67
|
+
## Meta AI Watermark
|
|
68
|
+
|
|
69
|
+
- Semi-transparent "Meta AI" logo
|
|
70
|
+
- Bottom-right corner
|
|
71
|
+
- ~10% width × ~5% height
|
|
72
|
+
- Static position across all free-tier videos
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
src/wafle_video_processor.egg-info/PKG-INFO
|
|
4
|
+
src/wafle_video_processor.egg-info/SOURCES.txt
|
|
5
|
+
src/wafle_video_processor.egg-info/dependency_links.txt
|
|
6
|
+
src/wafle_video_processor.egg-info/entry_points.txt
|
|
7
|
+
src/wafle_video_processor.egg-info/top_level.txt
|
|
8
|
+
src/waflevideoprocessor/__init__.py
|
|
9
|
+
src/waflevideoprocessor/__main__.py
|
|
10
|
+
src/waflevideoprocessor/cli.py
|
|
11
|
+
src/waflevideoprocessor/processor.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
waflevideoprocessor
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""WAFLE Video Processor — automatic watermark removal via FFmpeg delogo.
|
|
2
|
+
|
|
3
|
+
Removes Meta AI / Facebook watermarks from downloaded videos using
|
|
4
|
+
FFmpeg's delogo filter. CPU-only, no GPU required, faster than real-time.
|
|
5
|
+
|
|
6
|
+
Meta watermark: semi-transparent, static position bottom-right.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__description__ = "WAFLE automatic watermark removal via FFmpeg delogo"
|
|
11
|
+
|
|
12
|
+
from .processor import remove_watermark, stream_remove_watermark, batch_remove_watermarks, has_ffmpeg, WatermarkPosition
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""CLI interface for wafle-video-processor."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
import logging
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from . import __version__, __description__
|
|
8
|
+
from .processor import (
|
|
9
|
+
remove_watermark,
|
|
10
|
+
stream_remove_watermark,
|
|
11
|
+
batch_remove_watermarks,
|
|
12
|
+
has_ffmpeg,
|
|
13
|
+
WatermarkPosition,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
18
|
+
parser = argparse.ArgumentParser(
|
|
19
|
+
prog="wafle-video-processor",
|
|
20
|
+
description=__description__,
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
23
|
+
|
|
24
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
25
|
+
|
|
26
|
+
# Single file
|
|
27
|
+
single = sub.add_parser("remove", help="Remove watermark from a single video")
|
|
28
|
+
single.add_argument("input", help="Input video file")
|
|
29
|
+
single.add_argument("-o", "--output", help="Output video file (default: overwrite input)")
|
|
30
|
+
single.add_argument(
|
|
31
|
+
"--position", choices=[p.value for p in WatermarkPosition],
|
|
32
|
+
default="bottom-right", help="Watermark position (default: bottom-right)",
|
|
33
|
+
)
|
|
34
|
+
single.add_argument("--width-pct", type=float, default=10,
|
|
35
|
+
help="Logo width as %% of video width (default: 10)")
|
|
36
|
+
single.add_argument("--height-pct", type=float, default=5,
|
|
37
|
+
help="Logo height as %% of video height (default: 5)")
|
|
38
|
+
single.add_argument("--margin-pct", type=float, default=2,
|
|
39
|
+
help="Margin as %% of shortest side (default: 2)")
|
|
40
|
+
single.add_argument("--overwrite", action="store_true", help="Overwrite output if exists")
|
|
41
|
+
single.add_argument("--verbose", action="store_true", help="Verbose FFmpeg output")
|
|
42
|
+
|
|
43
|
+
# Stream from URL
|
|
44
|
+
stream = sub.add_parser("stream", help="Download from URL and remove watermark in a single streaming pass")
|
|
45
|
+
stream.add_argument("url", help="Video URL to download and process")
|
|
46
|
+
stream.add_argument("-o", "--output", required=True, help="Output video file path")
|
|
47
|
+
stream.add_argument("--width", type=int, default=1920, help="Video width in pixels (default: 1920)")
|
|
48
|
+
stream.add_argument("--height", type=int, default=1080, help="Video height in pixels (default: 1080)")
|
|
49
|
+
stream.add_argument("--position", choices=[p.value for p in WatermarkPosition],
|
|
50
|
+
default="bottom-right")
|
|
51
|
+
stream.add_argument("--width-pct", type=float, default=10)
|
|
52
|
+
stream.add_argument("--height-pct", type=float, default=5)
|
|
53
|
+
stream.add_argument("--margin-pct", type=float, default=2)
|
|
54
|
+
stream.add_argument("--overwrite", action="store_true")
|
|
55
|
+
stream.add_argument("--verbose", action="store_true")
|
|
56
|
+
|
|
57
|
+
# Batch directory
|
|
58
|
+
batch = sub.add_parser("batch", help="Remove watermarks from all videos in a directory")
|
|
59
|
+
batch.add_argument("input_dir", help="Directory with video files")
|
|
60
|
+
batch.add_argument("-o", "--output-dir", help="Output directory (default: in-place)")
|
|
61
|
+
batch.add_argument("--pattern", default="*.mp4",
|
|
62
|
+
help="Glob pattern for video files (default: *.mp4)")
|
|
63
|
+
batch.add_argument("--position", choices=[p.value for p in WatermarkPosition],
|
|
64
|
+
default="bottom-right")
|
|
65
|
+
batch.add_argument("--width-pct", type=float, default=10)
|
|
66
|
+
batch.add_argument("--height-pct", type=float, default=5)
|
|
67
|
+
batch.add_argument("--margin-pct", type=float, default=2)
|
|
68
|
+
batch.add_argument("--overwrite", action="store_true")
|
|
69
|
+
batch.add_argument("--verbose", action="store_true")
|
|
70
|
+
|
|
71
|
+
return parser
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main(argv: list = None) -> int:
|
|
75
|
+
if argv is None:
|
|
76
|
+
argv = sys.argv[1:]
|
|
77
|
+
|
|
78
|
+
# Always allow --help and --version without ffmpeg
|
|
79
|
+
if argv and argv[0] in ("--help", "-h", "--version"):
|
|
80
|
+
parser = build_parser()
|
|
81
|
+
args = parser.parse_args(argv)
|
|
82
|
+
return 0
|
|
83
|
+
|
|
84
|
+
if not has_ffmpeg():
|
|
85
|
+
print("ERROR: FFmpeg not found in PATH.", file=sys.stderr)
|
|
86
|
+
print("Install it with: winget install FFmpeg", file=sys.stderr)
|
|
87
|
+
print("Or download from: https://ffmpeg.org/download.html", file=sys.stderr)
|
|
88
|
+
return 1
|
|
89
|
+
|
|
90
|
+
parser = build_parser()
|
|
91
|
+
args = parser.parse_args(argv)
|
|
92
|
+
|
|
93
|
+
log_level = logging.INFO if args.verbose else logging.WARNING
|
|
94
|
+
logging.basicConfig(level=log_level, format="%(levelname)s: %(message)s")
|
|
95
|
+
|
|
96
|
+
position = WatermarkPosition(args.position)
|
|
97
|
+
verbosity = "info" if args.verbose else "error"
|
|
98
|
+
|
|
99
|
+
if args.command == "stream":
|
|
100
|
+
result = stream_remove_watermark(
|
|
101
|
+
args.url, args.output,
|
|
102
|
+
position=position,
|
|
103
|
+
width_pct=args.width_pct,
|
|
104
|
+
height_pct=args.height_pct,
|
|
105
|
+
margin_pct=args.margin_pct,
|
|
106
|
+
width=args.width,
|
|
107
|
+
height=args.height,
|
|
108
|
+
overwrite=args.overwrite,
|
|
109
|
+
verbosity=verbosity,
|
|
110
|
+
)
|
|
111
|
+
if result["success"]:
|
|
112
|
+
print(f"OK: {result['output_path']}")
|
|
113
|
+
print(f" delogo: x={result['delogo_params']['x']} "
|
|
114
|
+
f"y={result['delogo_params']['y']} "
|
|
115
|
+
f"w={result['delogo_params']['w']} "
|
|
116
|
+
f"h={result['delogo_params']['h']}")
|
|
117
|
+
print(f" bytes: {result.get('bytes_processed', '?')}")
|
|
118
|
+
return 0
|
|
119
|
+
else:
|
|
120
|
+
print(f"ERROR: {result.get('error', 'Unknown error')}", file=sys.stderr)
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
elif args.command == "remove":
|
|
124
|
+
result = remove_watermark(
|
|
125
|
+
args.input, args.output,
|
|
126
|
+
position=position,
|
|
127
|
+
width_pct=args.width_pct,
|
|
128
|
+
height_pct=args.height_pct,
|
|
129
|
+
margin_pct=args.margin_pct,
|
|
130
|
+
overwrite=args.overwrite,
|
|
131
|
+
verbosity=verbosity,
|
|
132
|
+
)
|
|
133
|
+
if result["success"]:
|
|
134
|
+
print(f"OK: {result['output_path']}")
|
|
135
|
+
print(f" delogo: x={result['delogo_params']['x']} "
|
|
136
|
+
f"y={result['delogo_params']['y']} "
|
|
137
|
+
f"w={result['delogo_params']['w']} "
|
|
138
|
+
f"h={result['delogo_params']['h']}")
|
|
139
|
+
return 0
|
|
140
|
+
else:
|
|
141
|
+
print(f"ERROR: {result.get('error', 'Unknown error')}", file=sys.stderr)
|
|
142
|
+
return 1
|
|
143
|
+
|
|
144
|
+
elif args.command == "batch":
|
|
145
|
+
results = batch_remove_watermarks(
|
|
146
|
+
args.input_dir, args.output_dir, args.pattern,
|
|
147
|
+
position=position,
|
|
148
|
+
width_pct=args.width_pct,
|
|
149
|
+
height_pct=args.height_pct,
|
|
150
|
+
margin_pct=args.margin_pct,
|
|
151
|
+
overwrite=args.overwrite,
|
|
152
|
+
verbosity=verbosity,
|
|
153
|
+
)
|
|
154
|
+
success = sum(1 for r in results if r.get("success"))
|
|
155
|
+
failed = [r for r in results if not r.get("success")]
|
|
156
|
+
print(f"Processed: {success} OK, {len(failed)} failed")
|
|
157
|
+
for f in failed:
|
|
158
|
+
print(f" FAIL: {f.get('input_path', '?')} — {f.get('error', '')}")
|
|
159
|
+
return 1 if failed else 0
|
|
160
|
+
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
if __name__ == "__main__":
|
|
165
|
+
sys.exit(main())
|
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
"""Core FFmpeg delogo logic for automatic watermark removal.
|
|
2
|
+
|
|
3
|
+
Meta AI watermark characteristics:
|
|
4
|
+
- Semi-transparent "Meta AI" logo
|
|
5
|
+
- Static position: bottom-right corner
|
|
6
|
+
- Size: approximately 10% width x 5% height
|
|
7
|
+
- Constant across all generated videos (free tier)
|
|
8
|
+
|
|
9
|
+
No GPU needed — FFmpeg delogo is CPU-only, faster than real-time.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import io
|
|
13
|
+
import json
|
|
14
|
+
import logging
|
|
15
|
+
import subprocess
|
|
16
|
+
import shutil
|
|
17
|
+
from enum import Enum
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Optional, List, Union, Callable
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger("wafle-video-processor")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class WatermarkPosition(Enum):
|
|
25
|
+
BOTTOM_RIGHT = "bottom-right"
|
|
26
|
+
BOTTOM_LEFT = "bottom-left"
|
|
27
|
+
TOP_RIGHT = "top-right"
|
|
28
|
+
TOP_LEFT = "top-left"
|
|
29
|
+
CUSTOM = "custom"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Default Meta AI watermark parameters (bottom-right, 10% width × 5% height)
|
|
33
|
+
DEFAULT_META_WATERMARK = {
|
|
34
|
+
"position": WatermarkPosition.BOTTOM_RIGHT,
|
|
35
|
+
"width_pct": 10,
|
|
36
|
+
"height_pct": 5,
|
|
37
|
+
"margin_pct": 2,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def has_ffmpeg() -> bool:
|
|
42
|
+
"""Check if ffmpeg is available in PATH."""
|
|
43
|
+
return shutil.which("ffmpeg") is not None
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _compute_delogo_params(
|
|
47
|
+
video_path: Path,
|
|
48
|
+
position: WatermarkPosition,
|
|
49
|
+
width_pct: float,
|
|
50
|
+
height_pct: float,
|
|
51
|
+
margin_pct: float,
|
|
52
|
+
) -> dict:
|
|
53
|
+
"""Use ffprobe to get video dimensions and compute delogo coordinates.
|
|
54
|
+
|
|
55
|
+
Meta AI videos are typically 1080p or 720p.
|
|
56
|
+
delogo filter syntax: delogo=x:y:w:h
|
|
57
|
+
"""
|
|
58
|
+
ffprobe = shutil.which("ffprobe") or str(Path(shutil.which("ffmpeg")).parent / "ffprobe")
|
|
59
|
+
try:
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
[ffprobe, "-v", "quiet", "-print_format", "json", "-show_streams",
|
|
62
|
+
str(video_path)],
|
|
63
|
+
capture_output=True, text=True, timeout=30,
|
|
64
|
+
)
|
|
65
|
+
data = json.loads(result.stdout)
|
|
66
|
+
width, height = 0, 0
|
|
67
|
+
for stream in data.get("streams", []):
|
|
68
|
+
if stream.get("codec_type") == "video":
|
|
69
|
+
width = stream.get("width", 0)
|
|
70
|
+
height = stream.get("height", 0)
|
|
71
|
+
break
|
|
72
|
+
if not width or not height:
|
|
73
|
+
raise ValueError(f"Could not detect video dimensions for {video_path}")
|
|
74
|
+
except Exception as e:
|
|
75
|
+
logger.warning(f"ffprobe failed, falling back to 1920x1080: {e}")
|
|
76
|
+
width, height = 1920, 1080
|
|
77
|
+
|
|
78
|
+
logo_w = max(int(width * width_pct / 100), 20)
|
|
79
|
+
logo_h = max(int(height * height_pct / 100), 10)
|
|
80
|
+
margin = max(int(min(width, height) * margin_pct / 100), 5)
|
|
81
|
+
|
|
82
|
+
positions = {
|
|
83
|
+
WatermarkPosition.BOTTOM_RIGHT: (width - logo_w - margin, height - logo_h - margin),
|
|
84
|
+
WatermarkPosition.BOTTOM_LEFT: (margin, height - logo_h - margin),
|
|
85
|
+
WatermarkPosition.TOP_RIGHT: (width - logo_w - margin, margin),
|
|
86
|
+
WatermarkPosition.TOP_LEFT: (margin, margin),
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if position == WatermarkPosition.CUSTOM:
|
|
90
|
+
raise ValueError("Use custom_x and custom_y for WatermarkPosition.CUSTOM")
|
|
91
|
+
|
|
92
|
+
x, y = positions[position]
|
|
93
|
+
return {"x": x, "y": y, "w": logo_w, "h": logo_h, "width": width, "height": height}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def remove_watermark(
|
|
97
|
+
input_path: Union[str, Path],
|
|
98
|
+
output_path: Optional[Union[str, Path]] = None,
|
|
99
|
+
position: WatermarkPosition = WatermarkPosition.BOTTOM_RIGHT,
|
|
100
|
+
width_pct: float = 10,
|
|
101
|
+
height_pct: float = 5,
|
|
102
|
+
margin_pct: float = 2,
|
|
103
|
+
overwrite: bool = False,
|
|
104
|
+
verbosity: str = "error",
|
|
105
|
+
) -> dict:
|
|
106
|
+
"""Remove watermark from video using FFmpeg delogo filter.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
input_path: Path to input video file.
|
|
110
|
+
output_path: Output path. If None, overwrites input (in-place).
|
|
111
|
+
position: Watermark position (default: BOTTOM_RIGHT).
|
|
112
|
+
width_pct: Logo width as percentage of video width (default: 10).
|
|
113
|
+
height_pct: Logo height as percentage of video height (default: 5).
|
|
114
|
+
margin_pct: Margin as percentage of shortest side (default: 2).
|
|
115
|
+
overwrite: Overwrite output file if exists (default: False).
|
|
116
|
+
verbosity: FFmpeg log level: "error", "warning", "info", "quiet" (default: "error").
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
dict with keys: success, input_path, output_path, delogo_params, ffmpeg_cmd
|
|
120
|
+
"""
|
|
121
|
+
if not has_ffmpeg():
|
|
122
|
+
return {"success": False, "error": "FFmpeg not found in PATH. Install it first: winget install FFmpeg"}
|
|
123
|
+
ffmpeg = shutil.which("ffmpeg")
|
|
124
|
+
input_path = Path(input_path)
|
|
125
|
+
|
|
126
|
+
if not input_path.exists():
|
|
127
|
+
return {"success": False, "error": f"Input file not found: {input_path}"}
|
|
128
|
+
|
|
129
|
+
# If no output path, create a temp file then replace input
|
|
130
|
+
in_place = output_path is None
|
|
131
|
+
if in_place:
|
|
132
|
+
temp_output = input_path.with_suffix(f".clean{input_path.suffix}")
|
|
133
|
+
out_path = temp_output
|
|
134
|
+
else:
|
|
135
|
+
out_path = Path(output_path)
|
|
136
|
+
|
|
137
|
+
if out_path.exists() and not overwrite:
|
|
138
|
+
return {"success": False, "error": f"Output exists and overwrite=False: {out_path}"}
|
|
139
|
+
|
|
140
|
+
params = _compute_delogo_params(input_path, position, width_pct, height_pct, margin_pct)
|
|
141
|
+
delogo_expr = f"delogo=x={params['x']}:y={params['y']}:w={params['w']}:h={params['h']}"
|
|
142
|
+
|
|
143
|
+
cmd = [
|
|
144
|
+
ffmpeg, "-y" if overwrite else "-n",
|
|
145
|
+
"-i", str(input_path),
|
|
146
|
+
"-vf", delogo_expr,
|
|
147
|
+
"-c:a", "copy",
|
|
148
|
+
"-progress", "pipe:1",
|
|
149
|
+
"-v", verbosity,
|
|
150
|
+
str(out_path),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
logger.info(f"Running: {' '.join(cmd)}")
|
|
154
|
+
logger.info(f"delogo region: x={params['x']}, y={params['y']}, w={params['w']}, h={params['h']}")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
process = subprocess.run(
|
|
158
|
+
cmd, capture_output=True, text=True, timeout=600,
|
|
159
|
+
)
|
|
160
|
+
if process.returncode != 0:
|
|
161
|
+
return {
|
|
162
|
+
"success": False,
|
|
163
|
+
"error": f"FFmpeg exited with code {process.returncode}",
|
|
164
|
+
"stderr": process.stderr[-500:],
|
|
165
|
+
"input_path": str(input_path),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# If in-place, replace original with clean version
|
|
169
|
+
if in_place:
|
|
170
|
+
input_path.unlink()
|
|
171
|
+
temp_output.rename(input_path)
|
|
172
|
+
out_path = input_path
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
"success": True,
|
|
176
|
+
"input_path": str(input_path),
|
|
177
|
+
"output_path": str(out_path),
|
|
178
|
+
"delogo_params": params,
|
|
179
|
+
"ffmpeg_cmd": " ".join(cmd),
|
|
180
|
+
}
|
|
181
|
+
except subprocess.TimeoutExpired:
|
|
182
|
+
return {"success": False, "error": "FFmpeg timed out (>10min)"}
|
|
183
|
+
except Exception as e:
|
|
184
|
+
return {"success": False, "error": str(e), "input_path": str(input_path)}
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def stream_remove_watermark(
|
|
188
|
+
video_url: str,
|
|
189
|
+
output_path: Union[str, Path],
|
|
190
|
+
downloader: Optional[Callable] = None,
|
|
191
|
+
position: WatermarkPosition = WatermarkPosition.BOTTOM_RIGHT,
|
|
192
|
+
width_pct: float = 10,
|
|
193
|
+
height_pct: float = 5,
|
|
194
|
+
margin_pct: float = 2,
|
|
195
|
+
width: int = 1920,
|
|
196
|
+
height: int = 1080,
|
|
197
|
+
overwrite: bool = False,
|
|
198
|
+
verbosity: str = "error",
|
|
199
|
+
) -> dict:
|
|
200
|
+
"""Download video from URL and remove watermark in a single streaming pass.
|
|
201
|
+
|
|
202
|
+
Watermarked data is piped through FFmpeg delogo in-memory — the
|
|
203
|
+
watermarked version NEVER touches disk. Only the clean file is saved.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
video_url: URL of the video to download and process.
|
|
207
|
+
output_path: Where to save the clean video.
|
|
208
|
+
downloader: Optional callable that returns a file-like iterator of bytes.
|
|
209
|
+
If None, uses requests.get(video_url, stream=True).
|
|
210
|
+
position: Watermark position (default: BOTTOM_RIGHT).
|
|
211
|
+
width_pct: Logo width as percentage of video width (default: 10).
|
|
212
|
+
height_pct: Logo height as percentage of video height (default: 5).
|
|
213
|
+
margin_pct: Margin as percentage of shortest side (default: 2).
|
|
214
|
+
width: Video width in pixels (default: 1920). Used when ffprobe can't probe the stream.
|
|
215
|
+
height: Video height in pixels (default: 1080).
|
|
216
|
+
overwrite: Overwrite output if exists (default: False).
|
|
217
|
+
verbosity: FFmpeg log level (default: "error").
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
dict with keys: success, output_path, delogo_params, ffmpeg_cmd
|
|
221
|
+
"""
|
|
222
|
+
if not has_ffmpeg():
|
|
223
|
+
return {"success": False, "error": "FFmpeg not found in PATH. Install it first: winget install FFmpeg"}
|
|
224
|
+
ffmpeg = shutil.which("ffmpeg")
|
|
225
|
+
output_path = Path(output_path)
|
|
226
|
+
|
|
227
|
+
if output_path.exists() and not overwrite:
|
|
228
|
+
return {"success": False, "error": f"Output exists and overwrite=False: {output_path}"}
|
|
229
|
+
|
|
230
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
231
|
+
|
|
232
|
+
# Compute delogo params using default or provided dimensions
|
|
233
|
+
logo_w = max(int(width * width_pct / 100), 20)
|
|
234
|
+
logo_h = max(int(height * height_pct / 100), 10)
|
|
235
|
+
margin = max(int(min(width, height) * margin_pct / 100), 5)
|
|
236
|
+
|
|
237
|
+
positions = {
|
|
238
|
+
WatermarkPosition.BOTTOM_RIGHT: (width - logo_w - margin, height - logo_h - margin),
|
|
239
|
+
WatermarkPosition.BOTTOM_LEFT: (margin, height - logo_h - margin),
|
|
240
|
+
WatermarkPosition.TOP_RIGHT: (width - logo_w - margin, margin),
|
|
241
|
+
WatermarkPosition.TOP_LEFT: (margin, margin),
|
|
242
|
+
}
|
|
243
|
+
if position == WatermarkPosition.CUSTOM:
|
|
244
|
+
raise ValueError("Use custom_x and custom_y for WatermarkPosition.CUSTOM")
|
|
245
|
+
|
|
246
|
+
x, y = positions[position]
|
|
247
|
+
params = {"x": x, "y": y, "w": logo_w, "h": logo_h, "width": width, "height": height}
|
|
248
|
+
|
|
249
|
+
delogo_expr = f"delogo=x={x}:y={y}:w={logo_w}:h={logo_h}"
|
|
250
|
+
cmd = [
|
|
251
|
+
ffmpeg, "-y",
|
|
252
|
+
"-i", "pipe:0",
|
|
253
|
+
"-vf", delogo_expr,
|
|
254
|
+
"-c:a", "copy",
|
|
255
|
+
"-progress", "pipe:1",
|
|
256
|
+
"-v", verbosity,
|
|
257
|
+
str(output_path),
|
|
258
|
+
]
|
|
259
|
+
|
|
260
|
+
logger.info(f"Streaming delogo: {delogo_expr}")
|
|
261
|
+
logger.info(f"FFmpeg cmd: {' '.join(cmd)}")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
# Default downloader uses requests
|
|
265
|
+
if downloader is None:
|
|
266
|
+
import requests
|
|
267
|
+
resp = requests.get(video_url, timeout=120, stream=True,
|
|
268
|
+
headers={"User-Agent": "Mozilla/5.0"})
|
|
269
|
+
resp.raise_for_status()
|
|
270
|
+
|
|
271
|
+
def _default_downloader():
|
|
272
|
+
for chunk in resp.iter_content(chunk_size=8192):
|
|
273
|
+
if chunk:
|
|
274
|
+
yield chunk
|
|
275
|
+
|
|
276
|
+
source_iter = _default_downloader()
|
|
277
|
+
else:
|
|
278
|
+
source_iter = downloader()
|
|
279
|
+
|
|
280
|
+
proc = subprocess.Popen(
|
|
281
|
+
cmd, stdin=subprocess.PIPE,
|
|
282
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
total = 0
|
|
286
|
+
for chunk in source_iter:
|
|
287
|
+
proc.stdin.write(chunk)
|
|
288
|
+
total += len(chunk)
|
|
289
|
+
proc.stdin.close()
|
|
290
|
+
|
|
291
|
+
stdout, stderr = proc.communicate(timeout=600)
|
|
292
|
+
|
|
293
|
+
if proc.returncode != 0:
|
|
294
|
+
return {
|
|
295
|
+
"success": False,
|
|
296
|
+
"error": f"FFmpeg exited with code {proc.returncode}",
|
|
297
|
+
"stderr": stderr.decode("utf-8", errors="replace")[-500:],
|
|
298
|
+
"input_url": video_url,
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
"success": True,
|
|
303
|
+
"output_path": str(output_path),
|
|
304
|
+
"delogo_params": params,
|
|
305
|
+
"ffmpeg_cmd": " ".join(cmd),
|
|
306
|
+
"bytes_processed": total,
|
|
307
|
+
}
|
|
308
|
+
except subprocess.TimeoutExpired:
|
|
309
|
+
return {"success": False, "error": "FFmpeg timed out (>10min)"}
|
|
310
|
+
except Exception as e:
|
|
311
|
+
return {"success": False, "error": str(e), "input_url": video_url}
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def batch_remove_watermarks(
|
|
315
|
+
input_dir: Union[str, Path],
|
|
316
|
+
output_dir: Optional[Union[str, Path]] = None,
|
|
317
|
+
pattern: str = "*.mp4",
|
|
318
|
+
**kwargs,
|
|
319
|
+
) -> List[dict]:
|
|
320
|
+
"""Remove watermarks from all matching videos in a directory.
|
|
321
|
+
|
|
322
|
+
Args:
|
|
323
|
+
input_dir: Directory containing videos.
|
|
324
|
+
output_dir: Output directory (default: same as input, in-place).
|
|
325
|
+
pattern: Glob pattern for video files (default: "*.mp4").
|
|
326
|
+
**kwargs: Passed to remove_watermark().
|
|
327
|
+
|
|
328
|
+
Returns:
|
|
329
|
+
List of result dicts, one per file.
|
|
330
|
+
"""
|
|
331
|
+
input_dir = Path(input_dir)
|
|
332
|
+
if not input_dir.exists():
|
|
333
|
+
return [{"success": False, "error": f"Directory not found: {input_dir}"}]
|
|
334
|
+
|
|
335
|
+
results = []
|
|
336
|
+
videos = sorted(input_dir.glob(pattern))
|
|
337
|
+
if not videos:
|
|
338
|
+
return [{"success": False, "error": f"No videos matching '{pattern}' in {input_dir}"}]
|
|
339
|
+
|
|
340
|
+
for video in videos:
|
|
341
|
+
out = None
|
|
342
|
+
if output_dir:
|
|
343
|
+
out = Path(output_dir) / video.name
|
|
344
|
+
result = remove_watermark(video, output_path=out, **kwargs)
|
|
345
|
+
results.append(result)
|
|
346
|
+
|
|
347
|
+
return results
|