static-ghost 0.4.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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,308 @@
1
+ Metadata-Version: 2.4
2
+ Name: static-ghost
3
+ Version: 0.4.0
4
+ Summary: Remove static watermarks from videos using LaMa inpainting
5
+ Author: redredchen01
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/redredchen01/static-ghost
8
+ Project-URL: Repository, https://github.com/redredchen01/static-ghost
9
+ Project-URL: Issues, https://github.com/redredchen01/static-ghost/issues
10
+ Keywords: video,watermark,removal,inpainting,lama,ffmpeg,iopaint
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Multimedia :: Video
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: opencv-python-headless>=4.8
20
+ Requires-Dist: numpy>=1.26
21
+ Requires-Dist: Pillow>=10.0
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest>=8.0; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # Static Ghost
27
+
28
+ Remove static watermarks from videos using LaMa inpainting.
29
+
30
+ Takes a video with a fixed-position watermark (TV logo, site branding, corner text), detects or lets you specify the watermark location, and removes it frame-by-frame using the [LaMa](https://github.com/advimman/lama) inpainting model via [IOPaint](https://github.com/Sanster/IOPaint).
31
+
32
+ ## How it works
33
+
34
+ ```
35
+ Input video → Extract frames → Crop watermark region → LaMa inpaint → Paste back → Reassemble video
36
+ (FFmpeg) (multiprocess) (IOPaint) (multiprocess) (FFmpeg)
37
+ ```
38
+
39
+ The **crop-and-paste optimization** only processes the small watermark region instead of the full frame — typically 10-15x fewer pixels, making a 10-minute 1080p video processable in ~2 hours on CPU instead of 20+.
40
+
41
+ ## Install
42
+
43
+ **Prerequisites:**
44
+ ```bash
45
+ brew install ffmpeg # or your package manager
46
+ pip install iopaint # LaMa inpainting engine
47
+ ```
48
+
49
+ **Install static-ghost:**
50
+ ```bash
51
+ git clone https://github.com/redredchen01/static-ghost.git
52
+ cd static-ghost
53
+ pip install -e ".[dev]"
54
+ ```
55
+
56
+ > **macOS note:** If `iopaint` is not in PATH after install:
57
+ > ```bash
58
+ > export PATH="$HOME/Library/Python/3.9/bin:$PATH"
59
+ > ```
60
+
61
+ ## Usage
62
+
63
+ ### Quick start — draw the watermark region
64
+
65
+ ```bash
66
+ static-ghost pick video.mp4 --dilation 15 --device mps -o video_clean.mp4
67
+ ```
68
+
69
+ Opens your browser with a frame from the video. Draw a rectangle around the watermark, click confirm, and it runs the full removal pipeline.
70
+
71
+ ### Specify coordinates directly
72
+
73
+ ```bash
74
+ static-ghost remove video.mp4 --region 1400,920,520,160 --dilation 15 --device mps
75
+ ```
76
+
77
+ Coordinates are `x,y,width,height` from the top-left corner. Multiple watermarks:
78
+
79
+ ```bash
80
+ static-ghost remove video.mp4 \
81
+ --region 1400,920,520,160 \
82
+ --region 20,15,200,60 \
83
+ --dilation 15
84
+ ```
85
+
86
+ ### Auto-detect watermark
87
+
88
+ ```bash
89
+ static-ghost detect video.mp4
90
+ ```
91
+
92
+ Uses multi-frame differencing to find regions that stay static across the video. Works best on opaque, high-contrast watermarks. For semi-transparent watermarks, raise the threshold:
93
+
94
+ ```bash
95
+ static-ghost detect video.mp4 --threshold 35
96
+ ```
97
+
98
+ ### Full auto pipeline
99
+
100
+ ```bash
101
+ static-ghost remove video.mp4 --device mps
102
+ ```
103
+
104
+ Auto-detects → shows preview → asks for confirmation → removes.
105
+
106
+ ## Options
107
+
108
+ | Flag | Default | Description |
109
+ |------|---------|-------------|
110
+ | `--region x,y,w,h` | — | Watermark bounding box (repeatable) |
111
+ | `--pick` | — | Open browser to draw region |
112
+ | `--dilation N` | 5 | Expand mask by N pixels (use 10-15 for logos) |
113
+ | `--device cpu\|mps` | cpu | `mps` = Apple Metal GPU, ~2x faster |
114
+ | `--threshold N` | 15 | Detection sensitivity (higher = more permissive) |
115
+ | `--keep-temp` | — | Keep extracted frames for debugging |
116
+ | `-o PATH` | `{name}_clean.mp4` | Output path |
117
+
118
+ ## Performance
119
+
120
+ Crop-and-paste mode (default). Times for 1080p 30fps video:
121
+
122
+ | Video length | Frames | CPU | MPS (Apple Metal) |
123
+ |-------------|--------|-----|-------------------|
124
+ | 30 sec | 900 | ~8 min | ~4 min |
125
+ | 10 min | 18,000 | ~2.5 hr | ~1.5 hr |
126
+ | 30 min | 54,000 | ~7.5 hr | ~4 hr |
127
+
128
+ Actual times depend on watermark size and system load. Use `--device mps` on macOS for best performance.
129
+
130
+ ## Tips
131
+
132
+ - **Always test on 30 seconds first.** Extract a clip with `ffmpeg -i input.mp4 -t 30 -c copy test.mp4` and verify the result before committing to a full video.
133
+ - **Be generous with region size.** It's better to over-cover the watermark area than to leave edges visible. Add 50-100px padding beyond the visible watermark.
134
+ - **Dilation matters.** Default is 5px. For large logos with solid color blocks, use 15-20.
135
+ - **Semi-transparent watermarks** are harder. Auto-detect may fail — use `--pick` or `--region` to specify manually.
136
+
137
+ ## Project structure
138
+
139
+ ```
140
+ static_ghost/
141
+ ├── cli.py # CLI entry point and flow orchestration
142
+ ├── video_engine.py # FFmpeg: probe, extract frames, merge
143
+ ├── detector.py # Multi-frame differencing watermark detection
144
+ ├── mask_generator.py # Coordinates → binary mask PNG
145
+ ├── fast_inpaint.py # Crop-inpaint-paste with multiprocessing
146
+ ├── inpainter.py # IOPaint CLI wrapper
147
+ └── picker.py # Browser-based interactive region selector
148
+ ```
149
+
150
+ ## Tests
151
+
152
+ ```bash
153
+ pip install -e ".[dev]"
154
+ pytest tests/ -v
155
+ ```
156
+
157
+ 20 tests covering all modules. Tests mock IOPaint so they run without it installed.
158
+
159
+ ---
160
+
161
+ # Agent Integration Guide
162
+
163
+ This section is for AI coding agents (Claude Code, Cursor, Copilot, etc.) that need to use static-ghost programmatically.
164
+
165
+ ## When to use this tool
166
+
167
+ - User wants to remove a watermark/logo from a **video** (not an image)
168
+ - The watermark is **static** — fixed position across all frames
169
+ - Examples: TV station logos, website branding, "SAMPLE" overlays
170
+
171
+ **Do NOT use for:** single images, moving/animated watermarks, subtitles (use subtitle extraction instead).
172
+
173
+ ## Python API
174
+
175
+ ```python
176
+ from static_ghost.video_engine import probe, extract_sample_frames, extract_all_frames, merge
177
+ from static_ghost.detector import Region, detect_static_regions, save_preview
178
+ from static_ghost.mask_generator import create_mask
179
+ from static_ghost.fast_inpaint import fast_remove
180
+ from static_ghost.inpainter import check_iopaint
181
+ ```
182
+
183
+ ### Step 1: Probe
184
+
185
+ ```python
186
+ meta = probe(video_path)
187
+ # Returns: {"width": 1920, "height": 1080, "fps": 30.0, "duration": 637.1, "codec": "h264", "audio_codec": "aac"}
188
+ total_frames = int(meta["fps"] * meta["duration"])
189
+ ```
190
+
191
+ ### Step 2: Get watermark coordinates
192
+
193
+ **Option A — User provides coordinates:**
194
+ ```python
195
+ regions = [Region(x=1400, y=920, w=520, h=160, confidence=1.0)]
196
+ ```
197
+
198
+ **Option B — Auto-detect:**
199
+ ```python
200
+ import tempfile
201
+ tmp = tempfile.mkdtemp()
202
+ sample_paths = extract_sample_frames(video_path, n=30, output_dir=tmp)
203
+ regions = detect_static_regions(sample_paths, threshold=15)
204
+ # If empty, try threshold=25, 35, 50
205
+ # If still empty, fall back to visual inspection or ask user
206
+ ```
207
+
208
+ **Option C — Visual inspection (when agent can see images):**
209
+ ```python
210
+ # Extract sample frames
211
+ paths = extract_sample_frames(video_path, n=5, output_dir=tmp)
212
+ # Read frames with vision tool, inspect corners for watermarks
213
+ # Crop suspected area to verify:
214
+ import cv2
215
+ img = cv2.imread(paths[0])
216
+ crop = img[h-150:h, w-500:w] # bottom-right corner
217
+ cv2.imwrite("/tmp/corner.png", crop)
218
+ # Estimate coordinates from visual inspection
219
+ ```
220
+
221
+ ### Step 3: Test on 30-second clip
222
+
223
+ ```python
224
+ import subprocess
225
+ subprocess.run(["ffmpeg", "-y", "-i", video_path, "-t", "30", "-c", "copy", "/tmp/test_30s.mp4"], capture_output=True, check=True)
226
+ ```
227
+
228
+ Run removal on clip, verify output visually, then proceed to full video.
229
+
230
+ ### Step 4: Run removal
231
+
232
+ ```python
233
+ from static_ghost.cli import parse_args, cmd_remove
234
+
235
+ args = parse_args([
236
+ "remove", video_path,
237
+ "--region", "1400,920,520,160",
238
+ "--dilation", "15",
239
+ "--device", "mps", # "cpu" if no Metal GPU
240
+ "-o", output_path,
241
+ ])
242
+ cmd_remove(args)
243
+ ```
244
+
245
+ **For long videos, run in background** (if your environment supports it) and check progress:
246
+ ```python
247
+ import os
248
+ # Count output frames in temp dir
249
+ tmp_dirs = [d for d in os.listdir("/var/folders/...") if d.startswith("static_ghost_")]
250
+ # Compare against total_frames for progress
251
+ ```
252
+
253
+ ### Step 5: Verify
254
+
255
+ ```python
256
+ orig_meta = probe(video_path)
257
+ clean_meta = probe(output_path)
258
+ assert orig_meta["width"] == clean_meta["width"]
259
+ assert orig_meta["height"] == clean_meta["height"]
260
+ assert abs(orig_meta["duration"] - clean_meta["duration"]) < 1.0
261
+ assert clean_meta["audio_codec"] is not None
262
+ # Visually verify sample frames from output
263
+ ```
264
+
265
+ ## Decision tree for agents
266
+
267
+ ```
268
+ User wants watermark removed from video
269
+
270
+ ├─ User provided coordinates? → Use them directly
271
+ ├─ Auto-detect finds regions? → Show to user for confirmation
272
+ ├─ Auto-detect fails?
273
+ │ ├─ Agent has vision? → Extract frames, inspect corners, estimate coords
274
+ │ └─ Agent has no vision? → Ask user for coordinates or use --pick
275
+
276
+ ├─ Test on 30s clip
277
+ │ ├─ Watermark gone? → Run full video
278
+ │ ├─ Partially visible? → Increase region size / dilation, re-test
279
+ │ └─ Artifacts? → Reduce dilation, re-test
280
+
281
+ └─ Run full video (background for >5 min videos)
282
+ ```
283
+
284
+ ## Time estimation
285
+
286
+ Before running the full video, benchmark 5 frames to estimate total time:
287
+
288
+ ```python
289
+ import time
290
+ test_paths = extract_sample_frames(video_path, n=5, output_dir="/tmp/bench_in")
291
+ os.makedirs("/tmp/bench_out", exist_ok=True)
292
+ start = time.time()
293
+ fast_remove("/tmp/bench_in", "/tmp/bench_out", regions, dilation=15, device="mps")
294
+ per_frame = (time.time() - start) / 5
295
+ est_minutes = per_frame * total_frames / 60
296
+ print(f"Estimated: {est_minutes:.0f} minutes")
297
+ ```
298
+
299
+ ## Common pitfalls
300
+
301
+ | Mistake | Fix |
302
+ |---------|-----|
303
+ | Region too small | Add 50-100px padding beyond visible watermark edges |
304
+ | Dilation too low for large logos | Use `--dilation 15` for logos with solid color blocks |
305
+ | Running full video without testing | Always test on 30s clip first |
306
+ | Forgetting `--device mps` on macOS | 2x speed improvement for free |
307
+ | Auto-detect on semi-transparent watermarks | Will likely fail — use manual coordinates |
308
+ | Not checking disk space | 1080p 10min ≈ 40-80GB temp space |
@@ -0,0 +1,283 @@
1
+ # Static Ghost
2
+
3
+ Remove static watermarks from videos using LaMa inpainting.
4
+
5
+ Takes a video with a fixed-position watermark (TV logo, site branding, corner text), detects or lets you specify the watermark location, and removes it frame-by-frame using the [LaMa](https://github.com/advimman/lama) inpainting model via [IOPaint](https://github.com/Sanster/IOPaint).
6
+
7
+ ## How it works
8
+
9
+ ```
10
+ Input video → Extract frames → Crop watermark region → LaMa inpaint → Paste back → Reassemble video
11
+ (FFmpeg) (multiprocess) (IOPaint) (multiprocess) (FFmpeg)
12
+ ```
13
+
14
+ The **crop-and-paste optimization** only processes the small watermark region instead of the full frame — typically 10-15x fewer pixels, making a 10-minute 1080p video processable in ~2 hours on CPU instead of 20+.
15
+
16
+ ## Install
17
+
18
+ **Prerequisites:**
19
+ ```bash
20
+ brew install ffmpeg # or your package manager
21
+ pip install iopaint # LaMa inpainting engine
22
+ ```
23
+
24
+ **Install static-ghost:**
25
+ ```bash
26
+ git clone https://github.com/redredchen01/static-ghost.git
27
+ cd static-ghost
28
+ pip install -e ".[dev]"
29
+ ```
30
+
31
+ > **macOS note:** If `iopaint` is not in PATH after install:
32
+ > ```bash
33
+ > export PATH="$HOME/Library/Python/3.9/bin:$PATH"
34
+ > ```
35
+
36
+ ## Usage
37
+
38
+ ### Quick start — draw the watermark region
39
+
40
+ ```bash
41
+ static-ghost pick video.mp4 --dilation 15 --device mps -o video_clean.mp4
42
+ ```
43
+
44
+ Opens your browser with a frame from the video. Draw a rectangle around the watermark, click confirm, and it runs the full removal pipeline.
45
+
46
+ ### Specify coordinates directly
47
+
48
+ ```bash
49
+ static-ghost remove video.mp4 --region 1400,920,520,160 --dilation 15 --device mps
50
+ ```
51
+
52
+ Coordinates are `x,y,width,height` from the top-left corner. Multiple watermarks:
53
+
54
+ ```bash
55
+ static-ghost remove video.mp4 \
56
+ --region 1400,920,520,160 \
57
+ --region 20,15,200,60 \
58
+ --dilation 15
59
+ ```
60
+
61
+ ### Auto-detect watermark
62
+
63
+ ```bash
64
+ static-ghost detect video.mp4
65
+ ```
66
+
67
+ Uses multi-frame differencing to find regions that stay static across the video. Works best on opaque, high-contrast watermarks. For semi-transparent watermarks, raise the threshold:
68
+
69
+ ```bash
70
+ static-ghost detect video.mp4 --threshold 35
71
+ ```
72
+
73
+ ### Full auto pipeline
74
+
75
+ ```bash
76
+ static-ghost remove video.mp4 --device mps
77
+ ```
78
+
79
+ Auto-detects → shows preview → asks for confirmation → removes.
80
+
81
+ ## Options
82
+
83
+ | Flag | Default | Description |
84
+ |------|---------|-------------|
85
+ | `--region x,y,w,h` | — | Watermark bounding box (repeatable) |
86
+ | `--pick` | — | Open browser to draw region |
87
+ | `--dilation N` | 5 | Expand mask by N pixels (use 10-15 for logos) |
88
+ | `--device cpu\|mps` | cpu | `mps` = Apple Metal GPU, ~2x faster |
89
+ | `--threshold N` | 15 | Detection sensitivity (higher = more permissive) |
90
+ | `--keep-temp` | — | Keep extracted frames for debugging |
91
+ | `-o PATH` | `{name}_clean.mp4` | Output path |
92
+
93
+ ## Performance
94
+
95
+ Crop-and-paste mode (default). Times for 1080p 30fps video:
96
+
97
+ | Video length | Frames | CPU | MPS (Apple Metal) |
98
+ |-------------|--------|-----|-------------------|
99
+ | 30 sec | 900 | ~8 min | ~4 min |
100
+ | 10 min | 18,000 | ~2.5 hr | ~1.5 hr |
101
+ | 30 min | 54,000 | ~7.5 hr | ~4 hr |
102
+
103
+ Actual times depend on watermark size and system load. Use `--device mps` on macOS for best performance.
104
+
105
+ ## Tips
106
+
107
+ - **Always test on 30 seconds first.** Extract a clip with `ffmpeg -i input.mp4 -t 30 -c copy test.mp4` and verify the result before committing to a full video.
108
+ - **Be generous with region size.** It's better to over-cover the watermark area than to leave edges visible. Add 50-100px padding beyond the visible watermark.
109
+ - **Dilation matters.** Default is 5px. For large logos with solid color blocks, use 15-20.
110
+ - **Semi-transparent watermarks** are harder. Auto-detect may fail — use `--pick` or `--region` to specify manually.
111
+
112
+ ## Project structure
113
+
114
+ ```
115
+ static_ghost/
116
+ ├── cli.py # CLI entry point and flow orchestration
117
+ ├── video_engine.py # FFmpeg: probe, extract frames, merge
118
+ ├── detector.py # Multi-frame differencing watermark detection
119
+ ├── mask_generator.py # Coordinates → binary mask PNG
120
+ ├── fast_inpaint.py # Crop-inpaint-paste with multiprocessing
121
+ ├── inpainter.py # IOPaint CLI wrapper
122
+ └── picker.py # Browser-based interactive region selector
123
+ ```
124
+
125
+ ## Tests
126
+
127
+ ```bash
128
+ pip install -e ".[dev]"
129
+ pytest tests/ -v
130
+ ```
131
+
132
+ 20 tests covering all modules. Tests mock IOPaint so they run without it installed.
133
+
134
+ ---
135
+
136
+ # Agent Integration Guide
137
+
138
+ This section is for AI coding agents (Claude Code, Cursor, Copilot, etc.) that need to use static-ghost programmatically.
139
+
140
+ ## When to use this tool
141
+
142
+ - User wants to remove a watermark/logo from a **video** (not an image)
143
+ - The watermark is **static** — fixed position across all frames
144
+ - Examples: TV station logos, website branding, "SAMPLE" overlays
145
+
146
+ **Do NOT use for:** single images, moving/animated watermarks, subtitles (use subtitle extraction instead).
147
+
148
+ ## Python API
149
+
150
+ ```python
151
+ from static_ghost.video_engine import probe, extract_sample_frames, extract_all_frames, merge
152
+ from static_ghost.detector import Region, detect_static_regions, save_preview
153
+ from static_ghost.mask_generator import create_mask
154
+ from static_ghost.fast_inpaint import fast_remove
155
+ from static_ghost.inpainter import check_iopaint
156
+ ```
157
+
158
+ ### Step 1: Probe
159
+
160
+ ```python
161
+ meta = probe(video_path)
162
+ # Returns: {"width": 1920, "height": 1080, "fps": 30.0, "duration": 637.1, "codec": "h264", "audio_codec": "aac"}
163
+ total_frames = int(meta["fps"] * meta["duration"])
164
+ ```
165
+
166
+ ### Step 2: Get watermark coordinates
167
+
168
+ **Option A — User provides coordinates:**
169
+ ```python
170
+ regions = [Region(x=1400, y=920, w=520, h=160, confidence=1.0)]
171
+ ```
172
+
173
+ **Option B — Auto-detect:**
174
+ ```python
175
+ import tempfile
176
+ tmp = tempfile.mkdtemp()
177
+ sample_paths = extract_sample_frames(video_path, n=30, output_dir=tmp)
178
+ regions = detect_static_regions(sample_paths, threshold=15)
179
+ # If empty, try threshold=25, 35, 50
180
+ # If still empty, fall back to visual inspection or ask user
181
+ ```
182
+
183
+ **Option C — Visual inspection (when agent can see images):**
184
+ ```python
185
+ # Extract sample frames
186
+ paths = extract_sample_frames(video_path, n=5, output_dir=tmp)
187
+ # Read frames with vision tool, inspect corners for watermarks
188
+ # Crop suspected area to verify:
189
+ import cv2
190
+ img = cv2.imread(paths[0])
191
+ crop = img[h-150:h, w-500:w] # bottom-right corner
192
+ cv2.imwrite("/tmp/corner.png", crop)
193
+ # Estimate coordinates from visual inspection
194
+ ```
195
+
196
+ ### Step 3: Test on 30-second clip
197
+
198
+ ```python
199
+ import subprocess
200
+ subprocess.run(["ffmpeg", "-y", "-i", video_path, "-t", "30", "-c", "copy", "/tmp/test_30s.mp4"], capture_output=True, check=True)
201
+ ```
202
+
203
+ Run removal on clip, verify output visually, then proceed to full video.
204
+
205
+ ### Step 4: Run removal
206
+
207
+ ```python
208
+ from static_ghost.cli import parse_args, cmd_remove
209
+
210
+ args = parse_args([
211
+ "remove", video_path,
212
+ "--region", "1400,920,520,160",
213
+ "--dilation", "15",
214
+ "--device", "mps", # "cpu" if no Metal GPU
215
+ "-o", output_path,
216
+ ])
217
+ cmd_remove(args)
218
+ ```
219
+
220
+ **For long videos, run in background** (if your environment supports it) and check progress:
221
+ ```python
222
+ import os
223
+ # Count output frames in temp dir
224
+ tmp_dirs = [d for d in os.listdir("/var/folders/...") if d.startswith("static_ghost_")]
225
+ # Compare against total_frames for progress
226
+ ```
227
+
228
+ ### Step 5: Verify
229
+
230
+ ```python
231
+ orig_meta = probe(video_path)
232
+ clean_meta = probe(output_path)
233
+ assert orig_meta["width"] == clean_meta["width"]
234
+ assert orig_meta["height"] == clean_meta["height"]
235
+ assert abs(orig_meta["duration"] - clean_meta["duration"]) < 1.0
236
+ assert clean_meta["audio_codec"] is not None
237
+ # Visually verify sample frames from output
238
+ ```
239
+
240
+ ## Decision tree for agents
241
+
242
+ ```
243
+ User wants watermark removed from video
244
+
245
+ ├─ User provided coordinates? → Use them directly
246
+ ├─ Auto-detect finds regions? → Show to user for confirmation
247
+ ├─ Auto-detect fails?
248
+ │ ├─ Agent has vision? → Extract frames, inspect corners, estimate coords
249
+ │ └─ Agent has no vision? → Ask user for coordinates or use --pick
250
+
251
+ ├─ Test on 30s clip
252
+ │ ├─ Watermark gone? → Run full video
253
+ │ ├─ Partially visible? → Increase region size / dilation, re-test
254
+ │ └─ Artifacts? → Reduce dilation, re-test
255
+
256
+ └─ Run full video (background for >5 min videos)
257
+ ```
258
+
259
+ ## Time estimation
260
+
261
+ Before running the full video, benchmark 5 frames to estimate total time:
262
+
263
+ ```python
264
+ import time
265
+ test_paths = extract_sample_frames(video_path, n=5, output_dir="/tmp/bench_in")
266
+ os.makedirs("/tmp/bench_out", exist_ok=True)
267
+ start = time.time()
268
+ fast_remove("/tmp/bench_in", "/tmp/bench_out", regions, dilation=15, device="mps")
269
+ per_frame = (time.time() - start) / 5
270
+ est_minutes = per_frame * total_frames / 60
271
+ print(f"Estimated: {est_minutes:.0f} minutes")
272
+ ```
273
+
274
+ ## Common pitfalls
275
+
276
+ | Mistake | Fix |
277
+ |---------|-----|
278
+ | Region too small | Add 50-100px padding beyond visible watermark edges |
279
+ | Dilation too low for large logos | Use `--dilation 15` for logos with solid color blocks |
280
+ | Running full video without testing | Always test on 30s clip first |
281
+ | Forgetting `--device mps` on macOS | 2x speed improvement for free |
282
+ | Auto-detect on semi-transparent watermarks | Will likely fail — use manual coordinates |
283
+ | Not checking disk space | 1080p 10min ≈ 40-80GB temp space |
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "static-ghost"
3
+ version = "0.4.0"
4
+ description = "Remove static watermarks from videos using LaMa inpainting"
5
+ readme = "README.md"
6
+ license = {text = "MIT"}
7
+ requires-python = ">=3.9"
8
+ authors = [
9
+ {name = "redredchen01"},
10
+ ]
11
+ keywords = ["video", "watermark", "removal", "inpainting", "lama", "ffmpeg", "iopaint"]
12
+ classifiers = [
13
+ "Development Status :: 4 - Beta",
14
+ "Intended Audience :: Developers",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Programming Language :: Python :: 3",
17
+ "Topic :: Multimedia :: Video",
18
+ ]
19
+ dependencies = [
20
+ "opencv-python-headless>=4.8",
21
+ "numpy>=1.26",
22
+ "Pillow>=10.0",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/redredchen01/static-ghost"
27
+ Repository = "https://github.com/redredchen01/static-ghost"
28
+ Issues = "https://github.com/redredchen01/static-ghost/issues"
29
+
30
+ [project.optional-dependencies]
31
+ dev = [
32
+ "pytest>=8.0",
33
+ ]
34
+
35
+ [project.scripts]
36
+ static-ghost = "static_ghost.cli:main"
37
+
38
+ [build-system]
39
+ requires = ["setuptools>=68.0"]
40
+ build-backend = "setuptools.build_meta"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"