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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ wafle-video-processor = waflevideoprocessor.cli:main
@@ -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,5 @@
1
+ """Entry point for `python -m waflevideoprocessor`."""
2
+ import sys
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -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