framesig 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.
framesig-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Fernando Aporta Franco (ferinazumaDEV)
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,261 @@
1
+ Metadata-Version: 2.4
2
+ Name: framesig
3
+ Version: 0.1.0
4
+ Summary: Detect on-screen events in any video by pixel signature — declarative YAML, cached scores, game-agnostic.
5
+ Author-email: Fernando Aporta Franco <ferinazumaDEV@users.noreply.github.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/ferinazumaDEV/framesig
8
+ Project-URL: Source, https://github.com/ferinazumaDEV/framesig
9
+ Keywords: computer-vision,video,event-detection,opencv,ffmpeg,signal-processing
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Topic :: Multimedia :: Video
15
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
16
+ Requires-Python: >=3.9
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Requires-Dist: numpy<3,>=1.24
20
+ Requires-Dist: opencv-python-headless<5,>=4.8
21
+ Requires-Dist: PyYAML<7,>=6
22
+ Provides-Extra: dev
23
+ Requires-Dist: pytest<9,>=7; extra == "dev"
24
+ Dynamic: license-file
25
+
26
+ # framesig
27
+
28
+ **Find *when* something happens on screen — in any video, from any game or source — by its pixel signature.**
29
+
30
+ ![Python](https://img.shields.io/badge/python-3.9%2B-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Tests](https://img.shields.io/badge/tests-88%20passing-brightgreen)
31
+
32
+ framesig doesn't know what a "kill" or a "death screen" looks like — and it doesn't need to. You describe an event as **a region of the frame + a colour/brightness signature** in a few lines of YAML, and framesig scans the video and hands you back the **timestamps** where that signature appears. Red flash in the HUD, a coloured kill-feed row, a fade to black, a hard cut — same tiny config, no model, no training, no per-game code.
33
+
34
+ <p align="center">
35
+ <img src="assets/demo.gif" alt="framesig detecting HUD events in a synthetic clip" width="560">
36
+ </p>
37
+
38
+ <sub>Above: framesig watching four independent regions of a synthetic clip. Boxes light up the instant their signature fires; the number is the live score.</sub>
39
+
40
+ ---
41
+
42
+ ## Why it's useful
43
+
44
+ - **Source-agnostic.** It only ever reasons about pixels in a rectangle, never about the game. The same tool works on League, CS, a slideshow, or security footage.
45
+ - **Declarative.** Events live in YAML, not code. Tweak a region or a threshold, re-run — no recompiling, no re-scanning (see caching below).
46
+ - **Cheap to re-tune.** Scanning a video is the expensive part; framesig caches the raw per-frame scores, so changing a threshold is *instant*.
47
+ - **Self-contained demo.** `framesig demo` renders its own test clip with ffmpeg, scans it, and charts the result — no external video or model to download.
48
+
49
+ ## Features
50
+
51
+ - Four built-in detectors: `channel_dominance`, `color_fraction`, `brightness`, `scene_change`.
52
+ - Regions in resolution-independent fractions (or absolute pixels).
53
+ - Frame sub-sampling at a configurable rate for speed.
54
+ - JSON event output, with peak time, peak/mean score, duration and sample count per event.
55
+ - On-disk **score cache** keyed so that changing a threshold reuses the scan, while changing a detector invalidates it.
56
+ - A pure-OpenCV score-timeline chart renderer (no matplotlib dependency).
57
+ - Clean Python API and a `framesig` CLI.
58
+
59
+ ## Install
60
+
61
+ framesig needs only `numpy`, `opencv-python-headless` and `PyYAML`. (`ffmpeg` on your `PATH` is optional — it's used only to *generate* the sample clip in `gen-sample` / `demo`; scanning real videos does not need it.)
62
+
63
+ ```bash
64
+ git clone https://github.com/ferinazumaDEV/framesig
65
+ cd framesig
66
+ python -m venv .venv && . .venv/bin/activate
67
+ pip install -e ".[dev]"
68
+ ```
69
+
70
+ ## Quickstart
71
+
72
+ Render a synthetic test clip, scan it, and chart the detections in one command:
73
+
74
+ ```console
75
+ $ framesig demo
76
+ [1/3] rendering synthetic clip with ffmpeg...
77
+ [2/3] scanning for pixel signatures...
78
+ sample.mp4 640x360 15.0s 150 samples @ 10.0 fps (scan)
79
+ 7 event(s) across 4 signature(s)
80
+ death_screen: 2
81
+ [ 2.00s -> 2.40s] peak 1.00 @ 2.00s (5 samples)
82
+ [ 7.50s -> 7.90s] peak 1.00 @ 7.50s (5 samples)
83
+ kill_feed: 3
84
+ [ 4.00s -> 4.30s] peak 0.98 @ 4.00s (4 samples)
85
+ [ 4.80s -> 5.00s] peak 0.98 @ 4.80s (3 samples)
86
+ [ 9.40s -> 9.80s] peak 0.98 @ 9.40s (5 samples)
87
+ white_flash: 1
88
+ [ 11.00s -> 11.30s] peak 0.98 @ 11.00s (4 samples)
89
+ scene_cut: 1
90
+ [ 13.00s -> 13.00s] peak 0.35 @ 13.00s (1 samples)
91
+ [3/3] rendering score-timeline chart...
92
+ done. outputs in framesig_demo/
93
+ ```
94
+
95
+ Every event lands exactly where the clip was painted — two death flashes, three kill-feed rows, one white flash, one scene cut. The chart it writes:
96
+
97
+ <p align="center">
98
+ <img src="assets/timeline.png" alt="framesig score timelines with thresholds and detected events" width="820">
99
+ </p>
100
+
101
+ Each lane is one signature: the blue curve is the raw score, the red dashed line is the threshold, the green bands are the detected events.
102
+
103
+ ## Usage
104
+
105
+ ### CLI
106
+
107
+ ```console
108
+ $ framesig gen-sample sample.mp4
109
+ wrote sample.mp4 (640x360, 15s)
110
+
111
+ $ framesig scan sample.mp4 -c examples/flash.yaml -o events.json
112
+ sample.mp4 640x360 15.0s 150 samples @ 10.0 fps (scan)
113
+ 7 event(s) across 4 signature(s)
114
+ death_screen: 2
115
+ [ 2.00s -> 2.40s] peak 1.00 @ 2.00s (5 samples)
116
+ ...
117
+ wrote events.json
118
+ ```
119
+
120
+ Run it again and the scan is served from cache — note the `(cache)` tag:
121
+
122
+ ```console
123
+ $ framesig scan sample.mp4 -c examples/flash.yaml -o events.json
124
+ sample.mp4 640x360 15.0s 150 samples @ 10.0 fps (cache)
125
+ ```
126
+
127
+ `events.json` (one signature shown):
128
+
129
+ ```json
130
+ {
131
+ "video": "sample.mp4",
132
+ "meta": { "native_fps": 30.0, "frame_count": 450, "width": 640, "height": 360,
133
+ "sample_fps": 10.0, "step": 3, "sample_period": 0.1,
134
+ "samples": 150, "duration": 15.0 },
135
+ "from_cache": false,
136
+ "events": {
137
+ "death_screen": [
138
+ { "signature": "death_screen", "start": 2.0, "end": 2.4, "duration": 0.5,
139
+ "peak_t": 2.0, "peak_score": 1.0, "mean_score": 1.0, "samples": 5 }
140
+ ]
141
+ }
142
+ }
143
+ ```
144
+
145
+ Handy flags: `--sample-fps N` (override sampling rate), `--no-cache`, `--chart timeline.png`, `-q`.
146
+
147
+ ### Config
148
+
149
+ A signature is a **detector** applied to a **region**, plus rules for turning the score timeline into discrete events:
150
+
151
+ ```yaml
152
+ sample_fps: 10 # analyse ~10 frames per second of video
153
+ cache_dir: .framesig_cache # optional; default is a .framesig_cache
154
+ # folder next to the video
155
+
156
+ regions: # bounds are fractions of the frame by default
157
+ hud_top: { x: 0.00, y: 0.00, w: 1.00, h: 0.55 }
158
+ kill_feed: { x: 0.08, y: 0.74, w: 0.84, h: 0.18 }
159
+ minimap: { x: 1500, y: 800, w: 400, h: 250, unit: pixels } # absolute pixels
160
+
161
+ signatures:
162
+ - name: death_screen
163
+ region: hud_top
164
+ detector: channel_dominance # "relative red", robust to compression
165
+ params: { channel: red, gain: 2.0 }
166
+ threshold: 0.30 # score >= 0.30 counts as active
167
+ min_duration: 0.15 # drop blips shorter than 0.15 s
168
+ merge_gap: 0.25 # bridge flickers up to 0.25 s apart
169
+
170
+ - name: kill_feed
171
+ region: kill_feed
172
+ detector: color_fraction # % of red pixels in the band (two HSV ranges: red wraps hue)
173
+ params:
174
+ hsv_low: [0, 120, 70]
175
+ hsv_high: [10, 255, 255]
176
+ hsv_low2: [170, 120, 70]
177
+ hsv_high2: [179, 255, 255]
178
+ threshold: 0.20
179
+ ```
180
+
181
+ See [`examples/flash.yaml`](examples/flash.yaml) for the full four-signature config.
182
+
183
+ ### Python API
184
+
185
+ ```python
186
+ from framesig import load_config, scan_video, detect_all
187
+
188
+ config = load_config("examples/flash.yaml")
189
+ result = scan_video("sample.mp4", config) # scores get cached on disk
190
+ events = detect_all(config, result) # applying thresholds is free
191
+
192
+ for name, evs in events.items():
193
+ for e in evs:
194
+ print(f"{name}: {e.peak_t:.2f}s (score {e.peak_score:.2f})")
195
+ ```
196
+
197
+ ```
198
+ death_screen: 2.00s (score 1.00)
199
+ death_screen: 7.50s (score 1.00)
200
+ kill_feed: 4.00s (score 0.98)
201
+ ...
202
+ ```
203
+
204
+ ## Detectors
205
+
206
+ | detector | measures | good for | key params |
207
+ |---|---|---|---|
208
+ | `channel_dominance` | how much one BGR channel beats the other two | red death/kill flashes; survives compression, ignores brightness | `channel`, `gain` |
209
+ | `color_fraction` | fraction of pixels inside one or more HSV ranges | coloured HUD elements (kill feed, objective banners) | `hsv_low/high`, `hsv_low2/high2` |
210
+ | `brightness` | mean luminance | white flashes (`invert: false`), fades to black (`invert: true`) | `invert` |
211
+ | `scene_change` | mean absolute difference from the previous sampled frame | hard cuts, big transitions | — |
212
+
213
+ `framesig detectors` lists them at runtime.
214
+
215
+ ## How it works
216
+
217
+ ```
218
+ video ──▶ sub-sample frames ──▶ crop each region ──▶ detector score in [0,1]
219
+
220
+ per-frame score timelines
221
+
222
+ ┌─────────────────────┴─── cached on disk ───┐
223
+ ▼ │
224
+ threshold · merge_gap · min_duration │
225
+ ▼ │
226
+ events ◀── re-run with new thresholds, free ◀────┘
227
+ ```
228
+
229
+ 1. **Sample.** The scanner walks the video once and keeps roughly `sample_fps` frames per second.
230
+ 2. **Score.** Each signature crops its region and asks its detector for one number in `[0, 1]`.
231
+ 3. **Cache.** Those score timelines are written to `.framesig_cache/`, keyed by a fingerprint of the video **and** of the *score-relevant* config (sampling rate, regions, detector params) — but **not** thresholds. So re-tuning a threshold is a cache hit; changing a detector transparently invalidates it.
232
+ 4. **Detect.** Thresholding turns each timeline into events: consecutive active samples become a run, nearby runs merge (`merge_gap`), too-short runs are dropped (`min_duration`).
233
+
234
+ ## Testing
235
+
236
+ ```console
237
+ $ pytest
238
+ 88 passed
239
+ ```
240
+
241
+ The suite includes an end-to-end test that renders the synthetic clip with ffmpeg and asserts framesig recovers exactly the events baked into it, at the right timestamps — plus unit tests for every detector, the event logic, config validation, the scanner's error paths and the cache.
242
+
243
+ ## Sibling tools
244
+
245
+ framesig is one of a set of small, dependency-light tools I build and maintain in the open — focused utilities that each do one job well and turn messy input into clean, structured output. If framesig fits into your pipeline, these siblings share the same engineering-first philosophy:
246
+
247
+ - [The GEO Handbook](https://github.com/ferinazumaDEV/generative-engine-optimization-handbook) — the open reference on getting content cited by AI answer engines (ChatGPT, Perplexity, Google AI Overviews, Gemini, Copilot).
248
+ - [typedout](https://github.com/ferinazumaDEV/typedout) — reliable structured output from any LLM: schema-validated JSON with tolerant repair and retries.
249
+ - [politeclient](https://github.com/ferinazumaDEV/politeclient) — a polite, bulletproof HTTP client for Python: retries with backoff, per-host rate-limiting, caching, pagination.
250
+ - [scaffld](https://github.com/ferinazumaDEV/scaffld) — scaffold fully-wired Python projects (tests, CI, pre-commit, license) from templates, with a TUI.
251
+ - Hub & writing: [zentimes.es](https://zentimes.es).
252
+
253
+ By [ferinazumaDEV](https://github.com/ferinazumaDEV).
254
+
255
+ ## License
256
+
257
+ MIT — see [LICENSE](LICENSE).
258
+
259
+ ---
260
+
261
+ <sub>Built by Fernando ([@ferinazumaDEV](https://github.com/ferinazumaDEV)).</sub>
@@ -0,0 +1,236 @@
1
+ # framesig
2
+
3
+ **Find *when* something happens on screen — in any video, from any game or source — by its pixel signature.**
4
+
5
+ ![Python](https://img.shields.io/badge/python-3.9%2B-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Tests](https://img.shields.io/badge/tests-88%20passing-brightgreen)
6
+
7
+ framesig doesn't know what a "kill" or a "death screen" looks like — and it doesn't need to. You describe an event as **a region of the frame + a colour/brightness signature** in a few lines of YAML, and framesig scans the video and hands you back the **timestamps** where that signature appears. Red flash in the HUD, a coloured kill-feed row, a fade to black, a hard cut — same tiny config, no model, no training, no per-game code.
8
+
9
+ <p align="center">
10
+ <img src="assets/demo.gif" alt="framesig detecting HUD events in a synthetic clip" width="560">
11
+ </p>
12
+
13
+ <sub>Above: framesig watching four independent regions of a synthetic clip. Boxes light up the instant their signature fires; the number is the live score.</sub>
14
+
15
+ ---
16
+
17
+ ## Why it's useful
18
+
19
+ - **Source-agnostic.** It only ever reasons about pixels in a rectangle, never about the game. The same tool works on League, CS, a slideshow, or security footage.
20
+ - **Declarative.** Events live in YAML, not code. Tweak a region or a threshold, re-run — no recompiling, no re-scanning (see caching below).
21
+ - **Cheap to re-tune.** Scanning a video is the expensive part; framesig caches the raw per-frame scores, so changing a threshold is *instant*.
22
+ - **Self-contained demo.** `framesig demo` renders its own test clip with ffmpeg, scans it, and charts the result — no external video or model to download.
23
+
24
+ ## Features
25
+
26
+ - Four built-in detectors: `channel_dominance`, `color_fraction`, `brightness`, `scene_change`.
27
+ - Regions in resolution-independent fractions (or absolute pixels).
28
+ - Frame sub-sampling at a configurable rate for speed.
29
+ - JSON event output, with peak time, peak/mean score, duration and sample count per event.
30
+ - On-disk **score cache** keyed so that changing a threshold reuses the scan, while changing a detector invalidates it.
31
+ - A pure-OpenCV score-timeline chart renderer (no matplotlib dependency).
32
+ - Clean Python API and a `framesig` CLI.
33
+
34
+ ## Install
35
+
36
+ framesig needs only `numpy`, `opencv-python-headless` and `PyYAML`. (`ffmpeg` on your `PATH` is optional — it's used only to *generate* the sample clip in `gen-sample` / `demo`; scanning real videos does not need it.)
37
+
38
+ ```bash
39
+ git clone https://github.com/ferinazumaDEV/framesig
40
+ cd framesig
41
+ python -m venv .venv && . .venv/bin/activate
42
+ pip install -e ".[dev]"
43
+ ```
44
+
45
+ ## Quickstart
46
+
47
+ Render a synthetic test clip, scan it, and chart the detections in one command:
48
+
49
+ ```console
50
+ $ framesig demo
51
+ [1/3] rendering synthetic clip with ffmpeg...
52
+ [2/3] scanning for pixel signatures...
53
+ sample.mp4 640x360 15.0s 150 samples @ 10.0 fps (scan)
54
+ 7 event(s) across 4 signature(s)
55
+ death_screen: 2
56
+ [ 2.00s -> 2.40s] peak 1.00 @ 2.00s (5 samples)
57
+ [ 7.50s -> 7.90s] peak 1.00 @ 7.50s (5 samples)
58
+ kill_feed: 3
59
+ [ 4.00s -> 4.30s] peak 0.98 @ 4.00s (4 samples)
60
+ [ 4.80s -> 5.00s] peak 0.98 @ 4.80s (3 samples)
61
+ [ 9.40s -> 9.80s] peak 0.98 @ 9.40s (5 samples)
62
+ white_flash: 1
63
+ [ 11.00s -> 11.30s] peak 0.98 @ 11.00s (4 samples)
64
+ scene_cut: 1
65
+ [ 13.00s -> 13.00s] peak 0.35 @ 13.00s (1 samples)
66
+ [3/3] rendering score-timeline chart...
67
+ done. outputs in framesig_demo/
68
+ ```
69
+
70
+ Every event lands exactly where the clip was painted — two death flashes, three kill-feed rows, one white flash, one scene cut. The chart it writes:
71
+
72
+ <p align="center">
73
+ <img src="assets/timeline.png" alt="framesig score timelines with thresholds and detected events" width="820">
74
+ </p>
75
+
76
+ Each lane is one signature: the blue curve is the raw score, the red dashed line is the threshold, the green bands are the detected events.
77
+
78
+ ## Usage
79
+
80
+ ### CLI
81
+
82
+ ```console
83
+ $ framesig gen-sample sample.mp4
84
+ wrote sample.mp4 (640x360, 15s)
85
+
86
+ $ framesig scan sample.mp4 -c examples/flash.yaml -o events.json
87
+ sample.mp4 640x360 15.0s 150 samples @ 10.0 fps (scan)
88
+ 7 event(s) across 4 signature(s)
89
+ death_screen: 2
90
+ [ 2.00s -> 2.40s] peak 1.00 @ 2.00s (5 samples)
91
+ ...
92
+ wrote events.json
93
+ ```
94
+
95
+ Run it again and the scan is served from cache — note the `(cache)` tag:
96
+
97
+ ```console
98
+ $ framesig scan sample.mp4 -c examples/flash.yaml -o events.json
99
+ sample.mp4 640x360 15.0s 150 samples @ 10.0 fps (cache)
100
+ ```
101
+
102
+ `events.json` (one signature shown):
103
+
104
+ ```json
105
+ {
106
+ "video": "sample.mp4",
107
+ "meta": { "native_fps": 30.0, "frame_count": 450, "width": 640, "height": 360,
108
+ "sample_fps": 10.0, "step": 3, "sample_period": 0.1,
109
+ "samples": 150, "duration": 15.0 },
110
+ "from_cache": false,
111
+ "events": {
112
+ "death_screen": [
113
+ { "signature": "death_screen", "start": 2.0, "end": 2.4, "duration": 0.5,
114
+ "peak_t": 2.0, "peak_score": 1.0, "mean_score": 1.0, "samples": 5 }
115
+ ]
116
+ }
117
+ }
118
+ ```
119
+
120
+ Handy flags: `--sample-fps N` (override sampling rate), `--no-cache`, `--chart timeline.png`, `-q`.
121
+
122
+ ### Config
123
+
124
+ A signature is a **detector** applied to a **region**, plus rules for turning the score timeline into discrete events:
125
+
126
+ ```yaml
127
+ sample_fps: 10 # analyse ~10 frames per second of video
128
+ cache_dir: .framesig_cache # optional; default is a .framesig_cache
129
+ # folder next to the video
130
+
131
+ regions: # bounds are fractions of the frame by default
132
+ hud_top: { x: 0.00, y: 0.00, w: 1.00, h: 0.55 }
133
+ kill_feed: { x: 0.08, y: 0.74, w: 0.84, h: 0.18 }
134
+ minimap: { x: 1500, y: 800, w: 400, h: 250, unit: pixels } # absolute pixels
135
+
136
+ signatures:
137
+ - name: death_screen
138
+ region: hud_top
139
+ detector: channel_dominance # "relative red", robust to compression
140
+ params: { channel: red, gain: 2.0 }
141
+ threshold: 0.30 # score >= 0.30 counts as active
142
+ min_duration: 0.15 # drop blips shorter than 0.15 s
143
+ merge_gap: 0.25 # bridge flickers up to 0.25 s apart
144
+
145
+ - name: kill_feed
146
+ region: kill_feed
147
+ detector: color_fraction # % of red pixels in the band (two HSV ranges: red wraps hue)
148
+ params:
149
+ hsv_low: [0, 120, 70]
150
+ hsv_high: [10, 255, 255]
151
+ hsv_low2: [170, 120, 70]
152
+ hsv_high2: [179, 255, 255]
153
+ threshold: 0.20
154
+ ```
155
+
156
+ See [`examples/flash.yaml`](examples/flash.yaml) for the full four-signature config.
157
+
158
+ ### Python API
159
+
160
+ ```python
161
+ from framesig import load_config, scan_video, detect_all
162
+
163
+ config = load_config("examples/flash.yaml")
164
+ result = scan_video("sample.mp4", config) # scores get cached on disk
165
+ events = detect_all(config, result) # applying thresholds is free
166
+
167
+ for name, evs in events.items():
168
+ for e in evs:
169
+ print(f"{name}: {e.peak_t:.2f}s (score {e.peak_score:.2f})")
170
+ ```
171
+
172
+ ```
173
+ death_screen: 2.00s (score 1.00)
174
+ death_screen: 7.50s (score 1.00)
175
+ kill_feed: 4.00s (score 0.98)
176
+ ...
177
+ ```
178
+
179
+ ## Detectors
180
+
181
+ | detector | measures | good for | key params |
182
+ |---|---|---|---|
183
+ | `channel_dominance` | how much one BGR channel beats the other two | red death/kill flashes; survives compression, ignores brightness | `channel`, `gain` |
184
+ | `color_fraction` | fraction of pixels inside one or more HSV ranges | coloured HUD elements (kill feed, objective banners) | `hsv_low/high`, `hsv_low2/high2` |
185
+ | `brightness` | mean luminance | white flashes (`invert: false`), fades to black (`invert: true`) | `invert` |
186
+ | `scene_change` | mean absolute difference from the previous sampled frame | hard cuts, big transitions | — |
187
+
188
+ `framesig detectors` lists them at runtime.
189
+
190
+ ## How it works
191
+
192
+ ```
193
+ video ──▶ sub-sample frames ──▶ crop each region ──▶ detector score in [0,1]
194
+
195
+ per-frame score timelines
196
+
197
+ ┌─────────────────────┴─── cached on disk ───┐
198
+ ▼ │
199
+ threshold · merge_gap · min_duration │
200
+ ▼ │
201
+ events ◀── re-run with new thresholds, free ◀────┘
202
+ ```
203
+
204
+ 1. **Sample.** The scanner walks the video once and keeps roughly `sample_fps` frames per second.
205
+ 2. **Score.** Each signature crops its region and asks its detector for one number in `[0, 1]`.
206
+ 3. **Cache.** Those score timelines are written to `.framesig_cache/`, keyed by a fingerprint of the video **and** of the *score-relevant* config (sampling rate, regions, detector params) — but **not** thresholds. So re-tuning a threshold is a cache hit; changing a detector transparently invalidates it.
207
+ 4. **Detect.** Thresholding turns each timeline into events: consecutive active samples become a run, nearby runs merge (`merge_gap`), too-short runs are dropped (`min_duration`).
208
+
209
+ ## Testing
210
+
211
+ ```console
212
+ $ pytest
213
+ 88 passed
214
+ ```
215
+
216
+ The suite includes an end-to-end test that renders the synthetic clip with ffmpeg and asserts framesig recovers exactly the events baked into it, at the right timestamps — plus unit tests for every detector, the event logic, config validation, the scanner's error paths and the cache.
217
+
218
+ ## Sibling tools
219
+
220
+ framesig is one of a set of small, dependency-light tools I build and maintain in the open — focused utilities that each do one job well and turn messy input into clean, structured output. If framesig fits into your pipeline, these siblings share the same engineering-first philosophy:
221
+
222
+ - [The GEO Handbook](https://github.com/ferinazumaDEV/generative-engine-optimization-handbook) — the open reference on getting content cited by AI answer engines (ChatGPT, Perplexity, Google AI Overviews, Gemini, Copilot).
223
+ - [typedout](https://github.com/ferinazumaDEV/typedout) — reliable structured output from any LLM: schema-validated JSON with tolerant repair and retries.
224
+ - [politeclient](https://github.com/ferinazumaDEV/politeclient) — a polite, bulletproof HTTP client for Python: retries with backoff, per-host rate-limiting, caching, pagination.
225
+ - [scaffld](https://github.com/ferinazumaDEV/scaffld) — scaffold fully-wired Python projects (tests, CI, pre-commit, license) from templates, with a TUI.
226
+ - Hub & writing: [zentimes.es](https://zentimes.es).
227
+
228
+ By [ferinazumaDEV](https://github.com/ferinazumaDEV).
229
+
230
+ ## License
231
+
232
+ MIT — see [LICENSE](LICENSE).
233
+
234
+ ---
235
+
236
+ <sub>Built by Fernando ([@ferinazumaDEV](https://github.com/ferinazumaDEV)).</sub>
@@ -0,0 +1,50 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "framesig"
7
+ version = "0.1.0"
8
+ description = "Detect on-screen events in any video by pixel signature — declarative YAML, cached scores, game-agnostic."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "Fernando Aporta Franco", email = "ferinazumaDEV@users.noreply.github.com" }]
13
+ keywords = [
14
+ "computer-vision",
15
+ "video",
16
+ "event-detection",
17
+ "opencv",
18
+ "ffmpeg",
19
+ "signal-processing",
20
+ ]
21
+ classifiers = [
22
+ "Development Status :: 4 - Beta",
23
+ "Intended Audience :: Developers",
24
+ "License :: OSI Approved :: MIT License",
25
+ "Programming Language :: Python :: 3",
26
+ "Topic :: Multimedia :: Video",
27
+ "Topic :: Scientific/Engineering :: Image Processing",
28
+ ]
29
+ dependencies = [
30
+ "numpy>=1.24,<3",
31
+ "opencv-python-headless>=4.8,<5",
32
+ "PyYAML>=6,<7",
33
+ ]
34
+
35
+ [project.optional-dependencies]
36
+ dev = ["pytest>=7,<9"]
37
+
38
+ [project.scripts]
39
+ framesig = "framesig.cli:main"
40
+
41
+ [project.urls]
42
+ Homepage = "https://github.com/ferinazumaDEV/framesig"
43
+ Source = "https://github.com/ferinazumaDEV/framesig"
44
+
45
+ [tool.setuptools.packages.find]
46
+ where = ["src"]
47
+
48
+ [tool.pytest.ini_options]
49
+ testpaths = ["tests"]
50
+ addopts = "-q"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,78 @@
1
+ """framesig — detect on-screen events in any video by pixel signature.
2
+
3
+ Define a region of interest plus a colour/brightness signature in YAML, and
4
+ framesig scans the video (sub-sampling frames, caching scores) to report the
5
+ timestamps where the event appears. It is game- and source-agnostic: it only
6
+ ever reasons about pixels, never about what produced them.
7
+
8
+ Typical use::
9
+
10
+ from framesig import load_config, scan_video, detect_all
11
+
12
+ config = load_config("signatures.yaml")
13
+ result = scan_video("clip.mp4", config)
14
+ events = detect_all(config, result)
15
+ for name, evs in events.items():
16
+ for e in evs:
17
+ print(name, round(e.peak_t, 2))
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from .cache import ScoreCache
23
+ from .config import Config, Signature, load_config, parse_config
24
+ from .detectors import (
25
+ Brightness,
26
+ ChannelDominance,
27
+ ColorFraction,
28
+ Detector,
29
+ SceneChange,
30
+ available_detectors,
31
+ build_detector,
32
+ )
33
+ from .errors import (
34
+ ConfigError,
35
+ DependencyError,
36
+ FramesigError,
37
+ VideoError,
38
+ )
39
+ from .events import Event, detect_events
40
+ from .regions import Region
41
+ from .scanner import ScanResult, detect_all, scan_video
42
+ from .videogen import GroundTruth, SampleVideo, generate_sample_video
43
+
44
+ __version__ = "0.1.0"
45
+
46
+ __all__ = [
47
+ "__version__",
48
+ # config
49
+ "Config",
50
+ "Signature",
51
+ "load_config",
52
+ "parse_config",
53
+ # regions & detectors
54
+ "Region",
55
+ "Detector",
56
+ "Brightness",
57
+ "ChannelDominance",
58
+ "ColorFraction",
59
+ "SceneChange",
60
+ "available_detectors",
61
+ "build_detector",
62
+ # scanning & events
63
+ "ScanResult",
64
+ "scan_video",
65
+ "detect_all",
66
+ "Event",
67
+ "detect_events",
68
+ "ScoreCache",
69
+ # sample generation
70
+ "generate_sample_video",
71
+ "SampleVideo",
72
+ "GroundTruth",
73
+ # errors
74
+ "FramesigError",
75
+ "ConfigError",
76
+ "VideoError",
77
+ "DependencyError",
78
+ ]