creator-editing-studio 1.3.5 → 1.3.6

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.
package/template/STYLE.md CHANGED
@@ -1,179 +1,179 @@
1
- <!-- This is YOUR style guide, and it starts deliberately half-empty.
2
-
3
- Parts 1 to 6 are craft standards: things that hold for any brand, most of them
4
- measured rather than argued. Leave them alone until you have a reason not to, and
5
- your edits will come out clean rather than random.
6
-
7
- Parts 7 and 8 are yours and begin blank. Every time you approve or reject something,
8
- write it down there. That is the half that makes edits look like YOURS instead of
9
- merely competent, and it is the half nobody can hand you. -->
10
-
11
- # House style
12
-
13
- What every edit must look, move and sound like. `CLAUDE.md` says *how* to build; this
14
- file says *what it must come out like*.
15
-
16
- When this file and a reference disagree, this file wins unless you say otherwise.
17
-
18
- ---
19
-
20
- ## 1. Your identity
21
-
22
- Everything visual comes from `src/design/tokens.ts`. That file is the single source of
23
- your typeface, your colours, your spacing and your type scale — change it there and every
24
- video changes with it. Never hard-code a colour or a font family into a composition.
25
-
26
- - **Type:** one display typeface. Heavy weights for headlines, numbers and hero words;
27
- medium for labels and pointers.
28
- - **Colour:** one accent, plus ink and a light ground. Resist a second accent — two
29
- accents read as a template, one reads as a brand.
30
- - On footage: accent text, with a soft glow so it survives a busy frame.
31
- - On a light ground: ink text, with the accent used only to mark the word that matters.
32
-
33
- ## 2. Type scale
34
-
35
- Sizes are a starting ladder, not a law. What matters is that you keep to *a* ladder.
36
-
37
- | Role | 16:9 (1920x1080) | 9:16 (1080x1920) | Notes |
38
- |---|---|---|---|
39
- | Hero word | 200–320px | 130–220px | One on screen at a time, never two |
40
- | Headline | ~116px | ~100px | Tight tracking, around -0.03em |
41
- | Big number | ~170px | ~150px | Tabular digits while a counter runs |
42
- | Label / eyebrow | ~40px | ~32px | Uppercase, open tracking, ~0.14em |
43
- | Pointer / list item | ~42px | ~36px | |
44
- | Captions | 46px | 42px | Crisp white, bold, soft dark shadow |
45
-
46
- **If a word needs to be bigger than the ladder to feel important, the layout is wrong,
47
- not the size.** Oversized type is the most common way an edit starts looking amateur.
48
-
49
- ## 3. Layout
50
-
51
- - **The speaker stays full screen by default.** Rounded cards and frames around a talking
52
- head make the edit look assembled rather than shot. A card is fine as a *transition*,
53
- not as a container.
54
- - Put text in the clear space the shot already gives you: sky, a wall, out-of-focus
55
- background. Check where the head lands on every cut before committing a position.
56
- - Captions live in one consistent zone per layout, over a dark area. They hide while the
57
- same words are on screen as a graphic, and never linger into the next scene.
58
- - Graphics are built from type, numbers, real screenshots and real footage. Decorative
59
- shapes that mean nothing age badly and read as filler.
60
-
61
- ## 4. Motion
62
-
63
- - The motion laws in `CLAUDE.md`, always. Eased, motion-blurred, nothing pops into place.
64
- - **Zoom through jump cuts.** A small push on the cut hides the join; a hard snap exposes it.
65
- - **Hard cuts only as a deliberate burst** — a few beats of solid colour synced to the
66
- words, then back to smooth. Everything else morphs from one state to the next.
67
- - Cover anything off-camera — reading, looking away, a fumble — with a graphic or a cut.
68
- - Counters start at zero and only go up. No grey placeholder digits.
69
- - Vary your shot lengths. Cutting everything to the same duration is the clearest sign
70
- an edit was made by a machine rather than by someone making decisions.
71
-
72
- ## 5. Sound (half the reel)
73
-
74
- - **The voice is never processed.** No reverb, no heavy compression, no "enhancement".
75
- - Sound effects are short — whooshes 0.4–0.8s, hits 0.3–0.6s — faded, and placed in the
76
- gaps between words rather than under them.
77
- - Every new visual gets its own fitting sound. Reusing a sound that belonged to a
78
- different look is the audio version of a template.
79
- - **Subtle beats loud.** Big cinematic impacts on text almost always read as too much.
80
- - Hand over voice and SFX stems plus a timecoded cue sheet with every delivery.
81
-
82
- ## 6. Colour, film looks and presets
83
-
84
- **Most of a look comes from the shoot, not the grade.** Golden hour or one good window,
85
- light behind or beside the subject, background well behind the subject, white balance
86
- fixed rather than auto. Automated grading is a starting point and only when the footage's
87
- light already resembles the reference — on mismatched light it will look wrong, and you
88
- should trust your eye over any automatic match.
89
-
90
- | Transition | When |
91
- |---|---|
92
- | whipPan | Between unrelated shots. Never twice in a row. |
93
- | push | Moving forward through a list or a sequence. |
94
- | maskWipe | A calm reveal. Pairs with a soft sound. |
95
- | colourFlip | A burst on a phrase. Two or three, then back to smooth. |
96
- | matchCut | Only when both shots share a shape or a direction. |
97
- | dip | Changing subject without drama. |
98
-
99
- | Caption look | When |
100
- |---|---|
101
- | clean | Default. Works over almost anything. |
102
- | block | Busy or bright footage, where white alone gets lost. |
103
- | highlight | Keeps the eye on the word being spoken. |
104
- | pop | Fast edits. Not over heavy motion. |
105
-
106
- | Film look | When |
107
- |---|---|
108
- | none | Graphics, screen recordings, anything where text is the subject. |
109
- | clean | Default for a talking head. Enough grain to stop flat areas banding. |
110
- | film | B-roll and travel. The one to reach for first. |
111
- | super8 | Memory and nostalgia moments only. Never a whole video. |
112
- | cinema | A held, wide, quiet shot. |
113
-
114
- **Three things here were measured, not guessed. Do not quietly undo them:**
115
-
116
- - Grain composites **normally**, not with `overlay`. On flat cream or flat ink there are
117
- no midtones, and overlay grain measures 0.000 stddev — the layer does nothing at all.
118
- - Grain is paid for in black level — blacks lift measurably at medium strength. Raise it
119
- per shot when a nostalgia moment is worth milkier blacks; never raise the default.
120
- - Halation blooms **highlights only**. The plate is crushed to black before it is blurred
121
- and screened back. Without that crush, a bright frame blooms end to end and the blacks
122
- go brown.
123
-
124
- Speed ramps are eased, never a hard speed switch. Below 0.4x needs 50fps or higher source
125
- footage — you cannot rescue slow motion that was not shot for it.
126
-
127
- ---
128
-
129
- ## 7. Where these defaults end and you begin
130
-
131
- The sections above are **defaults**: a starting point tuned so that a video made on day one,
132
- before you have told the system anything, comes out clean rather than embarrassing. Captions
133
- sit at a readable size with a measured shadow, cuts are eased, grain is restrained, the voice
134
- is untouched.
135
-
136
- Defaults are also the reason an edit can come out *anonymous*. They are what everybody gets.
137
- Three things move an edit from the default toward yours, in increasing order of power:
138
-
139
- **1. Your tokens.** `src/design/tokens.ts` carries your typeface, your colours, your scale.
140
- This makes an edit *yours to look at* immediately.
141
-
142
- **2. Your references.** Saved moments from edits you admire, each with a timecode and a note
143
- on what you like about that exact moment. These are read before the plan for every video, and
144
- they beat the defaults wherever the two disagree — a default is a guess that suits everyone,
145
- a reference is a fact about you.
146
-
147
- Roughly: **none** gives you clean and forgettable. **Three** is enough to spot a pattern.
148
- **Seven or eight** is enough to tell a preference from a coincidence, and it is where edits
149
- start looking authored.
150
-
151
- **3. This file, as it fills up.** Sections 8 and 9 are the part no default can supply and no
152
- competitor can copy, because they are a record of decisions only you have made. Every entry
153
- narrows the next edit toward what you actually want.
154
-
155
- Nothing here learns on its own. It gets better because the brief gets richer — so the ten
156
- minutes spent writing down why you rejected something is the highest-return ten minutes
157
- available to you.
158
-
159
- ---
160
-
161
- ## 8. Approved (your effects library)
162
-
163
- <!-- Empty on purpose. When something works, write it down here: what it is, and where
164
- you approved it. Next time you can ask for it by name instead of describing it
165
- again, and every future video inherits the decision. -->
166
-
167
- | Beat | What it is | Where you approved it |
168
- |---|---|---|
169
- | | | |
170
-
171
- ## 9. Rejected (don't do these again)
172
-
173
- <!-- This table is worth more than the one above. A list of things you have ruled out is
174
- the fastest way to stop an edit drifting back toward generic, because it removes
175
- options rather than adding them. Add a line every time you say "not that". -->
176
-
177
- | What | Why | Where |
178
- |---|---|---|
179
- | | | |
1
+ <!-- This is YOUR style guide, and it starts deliberately half-empty.
2
+
3
+ Parts 1 to 6 are craft standards: things that hold for any brand, most of them
4
+ measured rather than argued. Leave them alone until you have a reason not to, and
5
+ your edits will come out clean rather than random.
6
+
7
+ Parts 7 and 8 are yours and begin blank. Every time you approve or reject something,
8
+ write it down there. That is the half that makes edits look like YOURS instead of
9
+ merely competent, and it is the half nobody can hand you. -->
10
+
11
+ # House style
12
+
13
+ What every edit must look, move and sound like. `CLAUDE.md` says *how* to build; this
14
+ file says *what it must come out like*.
15
+
16
+ When this file and a reference disagree, this file wins unless you say otherwise.
17
+
18
+ ---
19
+
20
+ ## 1. Your identity
21
+
22
+ Everything visual comes from `src/design/tokens.ts`. That file is the single source of
23
+ your typeface, your colours, your spacing and your type scale — change it there and every
24
+ video changes with it. Never hard-code a colour or a font family into a composition.
25
+
26
+ - **Type:** one display typeface. Heavy weights for headlines, numbers and hero words;
27
+ medium for labels and pointers.
28
+ - **Colour:** one accent, plus ink and a light ground. Resist a second accent — two
29
+ accents read as a template, one reads as a brand.
30
+ - On footage: accent text, with a soft glow so it survives a busy frame.
31
+ - On a light ground: ink text, with the accent used only to mark the word that matters.
32
+
33
+ ## 2. Type scale
34
+
35
+ Sizes are a starting ladder, not a law. What matters is that you keep to *a* ladder.
36
+
37
+ | Role | 16:9 (1920x1080) | 9:16 (1080x1920) | Notes |
38
+ |---|---|---|---|
39
+ | Hero word | 200–320px | 130–220px | One on screen at a time, never two |
40
+ | Headline | ~116px | ~100px | Tight tracking, around -0.03em |
41
+ | Big number | ~170px | ~150px | Tabular digits while a counter runs |
42
+ | Label / eyebrow | ~40px | ~32px | Uppercase, open tracking, ~0.14em |
43
+ | Pointer / list item | ~42px | ~36px | |
44
+ | Captions | 46px | 42px | Crisp white, bold, soft dark shadow |
45
+
46
+ **If a word needs to be bigger than the ladder to feel important, the layout is wrong,
47
+ not the size.** Oversized type is the most common way an edit starts looking amateur.
48
+
49
+ ## 3. Layout
50
+
51
+ - **The speaker stays full screen by default.** Rounded cards and frames around a talking
52
+ head make the edit look assembled rather than shot. A card is fine as a *transition*,
53
+ not as a container.
54
+ - Put text in the clear space the shot already gives you: sky, a wall, out-of-focus
55
+ background. Check where the head lands on every cut before committing a position.
56
+ - Captions live in one consistent zone per layout, over a dark area. They hide while the
57
+ same words are on screen as a graphic, and never linger into the next scene.
58
+ - Graphics are built from type, numbers, real screenshots and real footage. Decorative
59
+ shapes that mean nothing age badly and read as filler.
60
+
61
+ ## 4. Motion
62
+
63
+ - The motion laws in `CLAUDE.md`, always. Eased, motion-blurred, nothing pops into place.
64
+ - **Zoom through jump cuts.** A small push on the cut hides the join; a hard snap exposes it.
65
+ - **Hard cuts only as a deliberate burst** — a few beats of solid colour synced to the
66
+ words, then back to smooth. Everything else morphs from one state to the next.
67
+ - Cover anything off-camera — reading, looking away, a fumble — with a graphic or a cut.
68
+ - Counters start at zero and only go up. No grey placeholder digits.
69
+ - Vary your shot lengths. Cutting everything to the same duration is the clearest sign
70
+ an edit was made by a machine rather than by someone making decisions.
71
+
72
+ ## 5. Sound (half the reel)
73
+
74
+ - **The voice is never processed.** No reverb, no heavy compression, no "enhancement".
75
+ - Sound effects are short — whooshes 0.4–0.8s, hits 0.3–0.6s — faded, and placed in the
76
+ gaps between words rather than under them.
77
+ - Every new visual gets its own fitting sound. Reusing a sound that belonged to a
78
+ different look is the audio version of a template.
79
+ - **Subtle beats loud.** Big cinematic impacts on text almost always read as too much.
80
+ - Hand over voice and SFX stems plus a timecoded cue sheet with every delivery.
81
+
82
+ ## 6. Colour, film looks and presets
83
+
84
+ **Most of a look comes from the shoot, not the grade.** Golden hour or one good window,
85
+ light behind or beside the subject, background well behind the subject, white balance
86
+ fixed rather than auto. Automated grading is a starting point and only when the footage's
87
+ light already resembles the reference — on mismatched light it will look wrong, and you
88
+ should trust your eye over any automatic match.
89
+
90
+ | Transition | When |
91
+ |---|---|
92
+ | whipPan | Between unrelated shots. Never twice in a row. |
93
+ | push | Moving forward through a list or a sequence. |
94
+ | maskWipe | A calm reveal. Pairs with a soft sound. |
95
+ | colourFlip | A burst on a phrase. Two or three, then back to smooth. |
96
+ | matchCut | Only when both shots share a shape or a direction. |
97
+ | dip | Changing subject without drama. |
98
+
99
+ | Caption look | When |
100
+ |---|---|
101
+ | clean | Default. Works over almost anything. |
102
+ | block | Busy or bright footage, where white alone gets lost. |
103
+ | highlight | Keeps the eye on the word being spoken. |
104
+ | pop | Fast edits. Not over heavy motion. |
105
+
106
+ | Film look | When |
107
+ |---|---|
108
+ | none | Graphics, screen recordings, anything where text is the subject. |
109
+ | clean | Default for a talking head. Enough grain to stop flat areas banding. |
110
+ | film | B-roll and travel. The one to reach for first. |
111
+ | super8 | Memory and nostalgia moments only. Never a whole video. |
112
+ | cinema | A held, wide, quiet shot. |
113
+
114
+ **Three things here were measured, not guessed. Do not quietly undo them:**
115
+
116
+ - Grain composites **normally**, not with `overlay`. On flat cream or flat ink there are
117
+ no midtones, and overlay grain measures 0.000 stddev — the layer does nothing at all.
118
+ - Grain is paid for in black level — blacks lift measurably at medium strength. Raise it
119
+ per shot when a nostalgia moment is worth milkier blacks; never raise the default.
120
+ - Halation blooms **highlights only**. The plate is crushed to black before it is blurred
121
+ and screened back. Without that crush, a bright frame blooms end to end and the blacks
122
+ go brown.
123
+
124
+ Speed ramps are eased, never a hard speed switch. Below 0.4x needs 50fps or higher source
125
+ footage — you cannot rescue slow motion that was not shot for it.
126
+
127
+ ---
128
+
129
+ ## 7. Where these defaults end and you begin
130
+
131
+ The sections above are **defaults**: a starting point tuned so that a video made on day one,
132
+ before you have told the system anything, comes out clean rather than embarrassing. Captions
133
+ sit at a readable size with a measured shadow, cuts are eased, grain is restrained, the voice
134
+ is untouched.
135
+
136
+ Defaults are also the reason an edit can come out *anonymous*. They are what everybody gets.
137
+ Three things move an edit from the default toward yours, in increasing order of power:
138
+
139
+ **1. Your tokens.** `src/design/tokens.ts` carries your typeface, your colours, your scale.
140
+ This makes an edit *yours to look at* immediately.
141
+
142
+ **2. Your references.** Saved moments from edits you admire, each with a timecode and a note
143
+ on what you like about that exact moment. These are read before the plan for every video, and
144
+ they beat the defaults wherever the two disagree — a default is a guess that suits everyone,
145
+ a reference is a fact about you.
146
+
147
+ Roughly: **none** gives you clean and forgettable. **Three** is enough to spot a pattern.
148
+ **Seven or eight** is enough to tell a preference from a coincidence, and it is where edits
149
+ start looking authored.
150
+
151
+ **3. This file, as it fills up.** Sections 8 and 9 are the part no default can supply and no
152
+ competitor can copy, because they are a record of decisions only you have made. Every entry
153
+ narrows the next edit toward what you actually want.
154
+
155
+ Nothing here learns on its own. It gets better because the brief gets richer — so the ten
156
+ minutes spent writing down why you rejected something is the highest-return ten minutes
157
+ available to you.
158
+
159
+ ---
160
+
161
+ ## 8. Approved (your effects library)
162
+
163
+ <!-- Empty on purpose. When something works, write it down here: what it is, and where
164
+ you approved it. Next time you can ask for it by name instead of describing it
165
+ again, and every future video inherits the decision. -->
166
+
167
+ | Beat | What it is | Where you approved it |
168
+ |---|---|---|
169
+ | | | |
170
+
171
+ ## 9. Rejected (don't do these again)
172
+
173
+ <!-- This table is worth more than the one above. A list of things you have ruled out is
174
+ the fastest way to stop an edit drifting back toward generic, because it removes
175
+ options rather than adding them. Add a line every time you say "not that". -->
176
+
177
+ | What | Why | Where |
178
+ |---|---|---|
179
+ | | | |
@@ -1,23 +1,23 @@
1
- # The Python half of the studio: transcription, word alignment, cut detection,
2
- # green-screen cutouts and colour grading.
3
- #
4
- # python -m venv .venv
5
- # .venv/bin/pip install -r requirements.txt (Windows: .venv\Scripts\pip)
6
- #
7
- # Python 3.11 or 3.12. Not 3.13+ — WhisperX's dependencies do not support it yet, and the
8
- # failure is a wall of build errors rather than a clear message.
9
- #
10
- # certifi is not optional on macOS: torch downloads the alignment model with urllib, which
11
- # on a python.org build has no root certificates at all.
12
-
13
- whisperx==3.8.6
14
- # BiRefNet, for cutting the subject out of ordinary footage so text can sit behind them.
15
- # MIT licensed and self-hosted: nothing uploads, and there is no per-video cost.
16
- transformers>=4.44
17
- timm>=1.0
18
- einops>=0.8
19
- kornia>=0.7
20
- torch>=2.2
21
- numpy>=1.26
22
- Pillow>=10.0
23
- certifi>=2024.2.2
1
+ # The Python half of the studio: transcription, word alignment, cut detection,
2
+ # green-screen cutouts and colour grading.
3
+ #
4
+ # python -m venv .venv
5
+ # .venv/bin/pip install -r requirements.txt (Windows: .venv\Scripts\pip)
6
+ #
7
+ # Python 3.11 or 3.12. Not 3.13+ — WhisperX's dependencies do not support it yet, and the
8
+ # failure is a wall of build errors rather than a clear message.
9
+ #
10
+ # certifi is not optional on macOS: torch downloads the alignment model with urllib, which
11
+ # on a python.org build has no root certificates at all.
12
+
13
+ whisperx==3.8.6
14
+ # BiRefNet, for cutting the subject out of ordinary footage so text can sit behind them.
15
+ # MIT licensed and self-hosted: nothing uploads, and there is no per-video cost.
16
+ transformers>=4.44
17
+ timm>=1.0
18
+ einops>=0.8
19
+ kornia>=0.7
20
+ torch>=2.2
21
+ numpy>=1.26
22
+ Pillow>=10.0
23
+ certifi>=2024.2.2
@@ -1,90 +1,90 @@
1
- """Check that a green-screen clip is frame-aligned with its original.
2
-
3
- Usage (from the project root):
4
- python scripts/check_pair.py <original> <greenscreen> [--samples 7]
5
-
6
- At several points through the clip, compares the subject pixels of green-screen frame K with
7
- original frames K-2..K+2. The offset with the smallest difference is the alignment; it must be the
8
- same at every sample (otherwise the clips drift).
9
- """
10
-
11
- import argparse
12
- import json
13
- import subprocess
14
-
15
- import numpy as np
16
- import sys as _sys, pathlib as _pl
17
- _sys.path.insert(0, str(_pl.Path(__file__).resolve().parent / 'lib'))
18
- from media import FFMPEG, FFPROBE # resolved per platform - never hardcode a tool path
19
-
20
-
21
-
22
-
23
- OFFSETS = range(-2, 3)
24
-
25
-
26
- def probe(path):
27
- out = subprocess.run(
28
- [FFPROBE, "-v", "error", "-select_streams", "v:0", "-count_frames",
29
- "-show_entries", "stream=width,height,r_frame_rate,nb_read_frames", "-of", "json", path],
30
- capture_output=True, text=True, check=True,
31
- )
32
- s = json.loads(out.stdout)["streams"][0]
33
- num, den = s["r_frame_rate"].split("/")
34
- return int(s["width"]), int(s["height"]), float(num) / float(den), int(s["nb_read_frames"])
35
-
36
-
37
- def frames(path, start, count, width, height, fps):
38
- out = subprocess.run(
39
- [FFMPEG, "-v", "error", "-ss", f"{start / fps:.6f}", "-i", path, "-frames:v", str(count),
40
- "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
41
- capture_output=True, check=True,
42
- )
43
- data = np.frombuffer(out.stdout, dtype=np.uint8)
44
- return data.reshape(-1, height, width, 3).astype(np.float32)
45
-
46
-
47
- def main():
48
- parser = argparse.ArgumentParser()
49
- parser.add_argument("original")
50
- parser.add_argument("greenscreen")
51
- parser.add_argument("--samples", type=int, default=7)
52
- args = parser.parse_args()
53
-
54
- w1, h1, fps1, n1 = probe(args.original)
55
- w2, h2, fps2, n2 = probe(args.greenscreen)
56
- print(f"original: {w1}x{h1} @ {fps1:.3f}fps, {n1} frames")
57
- print(f"greenscreen: {w2}x{h2} @ {fps2:.3f}fps, {n2} frames")
58
- if (w1, h1) != (w2, h2) or abs(fps1 - fps2) > 1e-3:
59
- print("MISMATCH: resolution or frame rate differ")
60
- return
61
-
62
- last = min(n1, n2) - 3
63
- points = np.linspace(3, last, args.samples).astype(int)
64
- results = []
65
- for k in points:
66
- green = frames(args.greenscreen, k, 1, w2, h2, fps2)[0]
67
- r, g, b = green[..., 0], green[..., 1], green[..., 2]
68
- background = (g > 120) & (g > r * 1.35) & (g > b * 1.35)
69
- subject = ~background
70
- # Erode the subject mask so blended edge pixels don't bias the comparison.
71
- for _ in range(4):
72
- subject = subject & np.roll(subject, 1, 0) & np.roll(subject, -1, 0) & np.roll(subject, 1, 1) & np.roll(subject, -1, 1)
73
- coverage = subject.mean()
74
- originals = frames(args.original, k + OFFSETS.start, len(OFFSETS), w1, h1, fps1)
75
- diffs = {off: float(np.abs(originals[i][subject] - green[subject]).mean()) for i, off in enumerate(OFFSETS) if i < len(originals)}
76
- best = min(diffs, key=diffs.get)
77
- results.append(best)
78
- pretty = " ".join(f"{off:+d}:{d:5.2f}" for off, d in diffs.items())
79
- print(f"frame {k:5d} ({k / fps1:6.2f}s) subject {coverage:5.1%} best offset {best:+d} [{pretty}]")
80
-
81
- if len(set(results)) == 1:
82
- off = results[0]
83
- print(f"\nRESULT: aligned with a constant offset of {off:+d} frame(s)"
84
- + (" — perfect." if off == 0 else " — shift the green-screen clip to match."))
85
- else:
86
- print(f"\nRESULT: offsets vary {sorted(set(results))} — clips drift; re-export both from the same timeline.")
87
-
88
-
89
- if __name__ == "__main__":
90
- main()
1
+ """Check that a green-screen clip is frame-aligned with its original.
2
+
3
+ Usage (from the project root):
4
+ python scripts/check_pair.py <original> <greenscreen> [--samples 7]
5
+
6
+ At several points through the clip, compares the subject pixels of green-screen frame K with
7
+ original frames K-2..K+2. The offset with the smallest difference is the alignment; it must be the
8
+ same at every sample (otherwise the clips drift).
9
+ """
10
+
11
+ import argparse
12
+ import json
13
+ import subprocess
14
+
15
+ import numpy as np
16
+ import sys as _sys, pathlib as _pl
17
+ _sys.path.insert(0, str(_pl.Path(__file__).resolve().parent / 'lib'))
18
+ from media import FFMPEG, FFPROBE # resolved per platform - never hardcode a tool path
19
+
20
+
21
+
22
+
23
+ OFFSETS = range(-2, 3)
24
+
25
+
26
+ def probe(path):
27
+ out = subprocess.run(
28
+ [FFPROBE, "-v", "error", "-select_streams", "v:0", "-count_frames",
29
+ "-show_entries", "stream=width,height,r_frame_rate,nb_read_frames", "-of", "json", path],
30
+ capture_output=True, text=True, check=True,
31
+ )
32
+ s = json.loads(out.stdout)["streams"][0]
33
+ num, den = s["r_frame_rate"].split("/")
34
+ return int(s["width"]), int(s["height"]), float(num) / float(den), int(s["nb_read_frames"])
35
+
36
+
37
+ def frames(path, start, count, width, height, fps):
38
+ out = subprocess.run(
39
+ [FFMPEG, "-v", "error", "-ss", f"{start / fps:.6f}", "-i", path, "-frames:v", str(count),
40
+ "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
41
+ capture_output=True, check=True,
42
+ )
43
+ data = np.frombuffer(out.stdout, dtype=np.uint8)
44
+ return data.reshape(-1, height, width, 3).astype(np.float32)
45
+
46
+
47
+ def main():
48
+ parser = argparse.ArgumentParser()
49
+ parser.add_argument("original")
50
+ parser.add_argument("greenscreen")
51
+ parser.add_argument("--samples", type=int, default=7)
52
+ args = parser.parse_args()
53
+
54
+ w1, h1, fps1, n1 = probe(args.original)
55
+ w2, h2, fps2, n2 = probe(args.greenscreen)
56
+ print(f"original: {w1}x{h1} @ {fps1:.3f}fps, {n1} frames")
57
+ print(f"greenscreen: {w2}x{h2} @ {fps2:.3f}fps, {n2} frames")
58
+ if (w1, h1) != (w2, h2) or abs(fps1 - fps2) > 1e-3:
59
+ print("MISMATCH: resolution or frame rate differ")
60
+ return
61
+
62
+ last = min(n1, n2) - 3
63
+ points = np.linspace(3, last, args.samples).astype(int)
64
+ results = []
65
+ for k in points:
66
+ green = frames(args.greenscreen, k, 1, w2, h2, fps2)[0]
67
+ r, g, b = green[..., 0], green[..., 1], green[..., 2]
68
+ background = (g > 120) & (g > r * 1.35) & (g > b * 1.35)
69
+ subject = ~background
70
+ # Erode the subject mask so blended edge pixels don't bias the comparison.
71
+ for _ in range(4):
72
+ subject = subject & np.roll(subject, 1, 0) & np.roll(subject, -1, 0) & np.roll(subject, 1, 1) & np.roll(subject, -1, 1)
73
+ coverage = subject.mean()
74
+ originals = frames(args.original, k + OFFSETS.start, len(OFFSETS), w1, h1, fps1)
75
+ diffs = {off: float(np.abs(originals[i][subject] - green[subject]).mean()) for i, off in enumerate(OFFSETS) if i < len(originals)}
76
+ best = min(diffs, key=diffs.get)
77
+ results.append(best)
78
+ pretty = " ".join(f"{off:+d}:{d:5.2f}" for off, d in diffs.items())
79
+ print(f"frame {k:5d} ({k / fps1:6.2f}s) subject {coverage:5.1%} best offset {best:+d} [{pretty}]")
80
+
81
+ if len(set(results)) == 1:
82
+ off = results[0]
83
+ print(f"\nRESULT: aligned with a constant offset of {off:+d} frame(s)"
84
+ + (" — perfect." if off == 0 else " — shift the green-screen clip to match."))
85
+ else:
86
+ print(f"\nRESULT: offsets vary {sorted(set(results))} — clips drift; re-export both from the same timeline.")
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()