wizz-method 2.11.2 → 2.13.0
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/README.md +2 -1
- package/package.json +1 -1
- package/skills-registry.yaml +38 -29
- package/src/bmm-skills/module.yaml +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/04-criativos.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/11-checklist-roi.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/12-mineracao-ofertas.md +1 -1
- package/src/modules/lowticket/skills/wizz-lowticket-metodologia/knowledge/pontos-em-aberto.md +2 -2
- package/src/modules/wizz/_shared/video-pipeline.md +12 -4
- package/src/modules/wizz/agents/wizz-designer/SKILL.md +1 -1
- package/src/modules/wizz/agents/wizz-maestro/SKILL.md +1 -1
- package/src/modules/wizz/agents/wizz-social/SKILL.md +1 -0
- package/src/skills-lib/wizz-remotion-best-practices/rules/sfx.md +3 -1
- package/src/skills-lib/wizz-router/references/routing-table-flat.md +3 -2
- package/src/skills-lib/wizz-video-effects/SKILL.md +85 -0
- package/src/skills-lib/wizz-video-effects/assets/effects.config.template.json +178 -0
- package/src/skills-lib/wizz-video-effects/assets/remotion/EffectsLayer.tsx +87 -0
- package/src/skills-lib/wizz-video-effects/assets/remotion/README.md +37 -0
- package/src/skills-lib/wizz-video-effects/assets/remotion/useEffects.ts +120 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/CREDITS.md +56 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/cash.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/chime.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/click-soft.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/click.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/coin.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/error.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/impact-bass-1.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/impact-bass-2.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/key-press.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/notif.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/notification.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/paste.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/ping.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/pop.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/riser-metal.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/riser.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/send.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/sparkle.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/stamp.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/success.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/type.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/typing.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/whoosh-cinematic.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/whoosh-short.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/assets/sfx/whoosh.mp3 +0 -0
- package/src/skills-lib/wizz-video-effects/references/audio.md +87 -0
- package/src/skills-lib/wizz-video-effects/references/data-contract.md +145 -0
- package/src/skills-lib/wizz-video-effects/references/do-not-use.md +54 -0
- package/src/skills-lib/wizz-video-effects/references/editing-patterns.md +58 -0
- package/src/skills-lib/wizz-video-effects/references/gates.md +37 -0
- package/src/skills-lib/wizz-video-effects/references/sfx-licensing.md +40 -0
- package/src/skills-lib/wizz-video-effects/references/when-to-use.md +129 -0
- package/src/skills-lib/wizz-video-effects/scripts/budget.py +211 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_excess.py +218 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_loudness.py +175 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_rhythm.py +137 -0
- package/src/skills-lib/wizz-video-effects/scripts/check_safe_zone.py +221 -0
- package/src/skills-lib/wizz-video-effects/scripts/config.py +583 -0
- package/src/skills-lib/wizz-video-effects/scripts/fill_breaths.py +355 -0
- package/src/skills-lib/wizz-video-effects/scripts/measure_density.py +122 -0
- package/src/skills-lib/wizz-video-effects/scripts/normalize_audio.py +189 -0
- package/src/skills-lib/wizz-video-effects/scripts/sound.py +342 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_budget.py +131 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_check_loudness.py +156 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_checks.py +190 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_config.py +135 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_fill_breaths.py +179 -0
- package/src/skills-lib/wizz-video-effects/scripts/test_sound.py +173 -0
- package/src/skills-lib/wizz-video-effects/scripts/timeline.py +85 -0
- package/tools/installer/banner.js +80 -29
- package/tools/installer/commands/catalog.js +87 -0
- package/tools/installer/commands/install.js +9 -0
- package/tools/installer/guided/detect-tools.js +53 -0
- package/tools/installer/guided/flow.js +203 -0
- package/tools/installer/guided/labels.js +185 -0
- package/tools/installer/guided/messages.js +132 -0
- package/tools/installer/guided/outro.js +32 -0
- package/tools/installer/guided/profiles.js +95 -0
- package/tools/installer/guided/steps.js +154 -0
- package/tools/installer/install-messages.yaml +3 -3
- package/tools/installer/modules/external-manager.js +4 -0
- package/tools/installer/ui.js +57 -22
- package/wizz-modules.yaml +1 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# Timeline data contract
|
|
2
|
+
|
|
3
|
+
The scripts in `scripts/` read and write one JSON object per piece: the **timeline**. It is the hand-off between whoever plans the edit (HyperFrames screens, `ctc-align` timing) and whoever renders the audio and effects (Remotion). Times are always **seconds from the start of the piece**, as floats. Every list is optional: an absent list is an empty list, never an error.
|
|
4
|
+
|
|
5
|
+
Numbers that decide anything (gaps, ceilings, depths) are never in the timeline: they live in `effects.config.json` and are cited by key.
|
|
6
|
+
|
|
7
|
+
## Top-level keys
|
|
8
|
+
|
|
9
|
+
| Key | Type | Read by | Written by | Meaning |
|
|
10
|
+
| --- | --- | --- | --- | --- |
|
|
11
|
+
| `fps` | int | `sound.py duck` | planner | render fps; default `frame.fps` of the config |
|
|
12
|
+
| `duration` | float | every check | planner | piece length; if absent, derived from the largest scene end (reports say so) |
|
|
13
|
+
| `safe_zone_profile` | string | `check_safe_zone.py`, `measure_density.py`, `fill_breaths.py` | planner | `organic` or `ad` (any key of `safe_zones`); overrides the config default for this piece |
|
|
14
|
+
| `words[]` | list | `sound.py duck`, `fill_breaths.py` | `ctc-align` | spoken words |
|
|
15
|
+
| `scenes[]` | list | `check_excess.py`, `check_rhythm.py`, `fill_breaths.py` | planner, `fill_breaths.py` | screens |
|
|
16
|
+
| `sfx[]` | list | `sound.py duck`, `fill_breaths.py` | planner, `fill_breaths.py` | point sounds |
|
|
17
|
+
| `punches[]` | list | `check_excess.py`, `check_rhythm.py` | planner, `fill_breaths.py` | zoom punches |
|
|
18
|
+
| `shakes[]` | list | same | planner | short shakes (`budget.per_effect.shake.max_dur_s`) |
|
|
19
|
+
| `flashes[]` | list | same | planner, `fill_breaths.py` | opacity flashes |
|
|
20
|
+
| `interrupts[]` | list | same | planner | pattern interrupts |
|
|
21
|
+
| `elements[]` | list | `check_rhythm.py` | planner | element entries (cards, labels) that count as visual change |
|
|
22
|
+
| `ducking[]` | list | renderer | `sound.py duck --write` | narration duck windows |
|
|
23
|
+
|
|
24
|
+
### `words[]` (ctc-align output, unchanged)
|
|
25
|
+
|
|
26
|
+
```json
|
|
27
|
+
{ "text": "price", "start": 1.84, "end": 2.21, "score": -0.12 }
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Only `start` and `end` are required. Breaths are the gaps between words at least `breaths.min_silence_s` long.
|
|
31
|
+
|
|
32
|
+
### `scenes[]`
|
|
33
|
+
|
|
34
|
+
```json
|
|
35
|
+
{
|
|
36
|
+
"start": 3.2,
|
|
37
|
+
"dur": 4.1,
|
|
38
|
+
"transition": "whip",
|
|
39
|
+
"sfx": { "event": "block_cut", "src": "sfx/whoosh.mp3", "vol": 0.55 }
|
|
40
|
+
}
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
- `transition`: `cut`, `whip`, `zoom` or any name the renderer knows. Anything other than `""` or `cut` counts as a `transition` event in the budget.
|
|
44
|
+
- `sfx` (optional): the cut sound, played at `start`. Same shape as an `sfx[]` entry without `at`.
|
|
45
|
+
|
|
46
|
+
### `sfx[]`
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{ "at": 5.4, "event": "verdict", "src": "sfx/stamp.mp3", "vol": 0.65, "dur": 0.45 }
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- `event`: the semantic key of `sound.events`. Always write it: without it, scripts must rebuild the event from the `(file, vol)` pair, which only works while that pair is unique in the config.
|
|
53
|
+
- `src`: relative to `paths.media_root` (Remotion `staticFile(src)`).
|
|
54
|
+
- `vol`: copied from the config, never chosen by hand.
|
|
55
|
+
- `dur` (optional): seconds. Without it, `sound.py duck` probes the file, then falls back to `ducking.assumed_sfx_duration_s`.
|
|
56
|
+
|
|
57
|
+
### Effects: `punches[]`, `shakes[]`, `flashes[]`, `interrupts[]`, `elements[]`
|
|
58
|
+
|
|
59
|
+
Each item is a number (the instant) or an object with `at`:
|
|
60
|
+
|
|
61
|
+
```json
|
|
62
|
+
"punches": [2.4, { "at": 9.1, "vol": 0.5 }],
|
|
63
|
+
"interrupts": [{ "at": 14.0, "kind": "question-card" }],
|
|
64
|
+
"elements": [{ "at": 0.6, "kind": "text" }]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### `ducking[]`
|
|
68
|
+
|
|
69
|
+
Written by `python3 scripts/sound.py duck TIMELINE --write`. One window per group of SFX that sound over the **start of a spoken word**; nothing else ducks.
|
|
70
|
+
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"start": 5.37,
|
|
74
|
+
"end": 6.05,
|
|
75
|
+
"depth_db": 4.0,
|
|
76
|
+
"attack_s": 0.03,
|
|
77
|
+
"release_s": 0.2,
|
|
78
|
+
"from_frame": 161,
|
|
79
|
+
"to_frame": 182,
|
|
80
|
+
"cause": "verdict@5.400s over word 'price'@5.500s"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Shape of each window: the gain ramps linearly from 0 dB to `-depth_db` over `attack_s` starting at `start` (so full depth is in place when the SFX hits), holds, then ramps back to 0 dB over the last `release_s` before `end`. Overlapping windows are merged, deepest depth wins. Numbers come from the `ducking` block of the config, which marks them as an extrapolation without a primary source.
|
|
85
|
+
|
|
86
|
+
Remotion volume callback for the narration (same math as `gain_db_at` in `sound.py`). `frame` is relative to the `<Audio>`, so add the narration's start frame when it does not start at 0:
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
type Duck = { start: number; end: number; depth_db: number; attack_s: number; release_s: number };
|
|
90
|
+
|
|
91
|
+
export const narrationVolume = (ducking: Duck[], fps: number, startFrame = 0) => (frame: number) => {
|
|
92
|
+
const t = (frame + startFrame) / fps;
|
|
93
|
+
let db = 0;
|
|
94
|
+
for (const w of ducking) {
|
|
95
|
+
if (t < w.start || t > w.end) continue;
|
|
96
|
+
let shape = 1;
|
|
97
|
+
if (t < w.start + w.attack_s) shape = (t - w.start) / w.attack_s;
|
|
98
|
+
else if (t > w.end - w.release_s) shape = (w.end - t) / w.release_s;
|
|
99
|
+
db = Math.min(db, -w.depth_db * Math.max(0, Math.min(1, shape)));
|
|
100
|
+
}
|
|
101
|
+
return Math.pow(10, db / 20);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
// <Audio src={staticFile(narration)} volume={narrationVolume(data.ducking, fps)} />
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Minimal example
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"fps": 30,
|
|
112
|
+
"duration": 12.0,
|
|
113
|
+
"safe_zone_profile": "ad",
|
|
114
|
+
"words": [
|
|
115
|
+
{ "text": "Stop", "start": 0.2, "end": 0.5 },
|
|
116
|
+
{ "text": "scrolling", "start": 0.55, "end": 1.1 },
|
|
117
|
+
{ "text": "this", "start": 1.6, "end": 1.8 },
|
|
118
|
+
{ "text": "costs", "start": 1.85, "end": 2.2 },
|
|
119
|
+
{ "text": "nine", "start": 5.5, "end": 5.8 }
|
|
120
|
+
],
|
|
121
|
+
"scenes": [
|
|
122
|
+
{ "start": 0.0, "dur": 5.0, "transition": "cut" },
|
|
123
|
+
{ "start": 5.0, "dur": 7.0, "transition": "whip", "sfx": { "event": "block_cut", "src": "sfx/whoosh.mp3", "vol": 0.55 } }
|
|
124
|
+
],
|
|
125
|
+
"sfx": [{ "at": 5.4, "event": "price", "src": "sfx/cash.mp3", "vol": 0.55 }],
|
|
126
|
+
"punches": [1.35],
|
|
127
|
+
"flashes": [],
|
|
128
|
+
"shakes": [],
|
|
129
|
+
"interrupts": [],
|
|
130
|
+
"elements": [{ "at": 0.6, "kind": "text" }],
|
|
131
|
+
"ducking": []
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Which script touches what
|
|
136
|
+
|
|
137
|
+
| Script | Reads | Writes | Exit codes |
|
|
138
|
+
| --- | --- | --- | --- |
|
|
139
|
+
| `fill_breaths.py TIMELINE [--narration F@OFF] [--visual MP4] [--write\|--out P]` | `words`, `scenes`, effects, `sfx`, `duration` | `scenes[].transition`, `scenes[].sfx`, `punches`, `flashes`, `sfx` | 0, 2 |
|
|
140
|
+
| `sound.py duck TIMELINE [--write]` | `sfx`, `scenes[].sfx`, `words`, `fps` | `ducking` | 0, 2 |
|
|
141
|
+
| `check_excess.py TIMELINE... [--warn]` | effects, `scenes`, `duration` | nothing | 0 pass, 1 fail, 2 error |
|
|
142
|
+
| `check_rhythm.py TIMELINE...` | `scenes`, effects, `elements`, `duration` | nothing | always 0 (warn only) |
|
|
143
|
+
| `check_safe_zone.py MP4 [--timeline T]` | `safe_zone_profile` | nothing | 0, 1, 2 |
|
|
144
|
+
| `measure_density.py MP4 [--timeline T]` | `safe_zone_profile` | nothing | 0, 1, 2 |
|
|
145
|
+
| `check_loudness.py MP4` | the final file only | nothing | 0, 1, 2 |
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# What this system does not do
|
|
2
|
+
|
|
3
|
+
A catalogue says what is possible; this page says what was already decided, so the discussion does not repeat. Every line has a reason. A refusal without a reason is fashion, and fashion changes; a refusal with a reason is a rule.
|
|
4
|
+
|
|
5
|
+
Before proposing a new effect, look for it here. If it is listed, it is not a gap, it is a decision, and reopening it needs a new argument. If it is neither here nor in [when-to-use.md](when-to-use.md), it is undecided territory: propose it **with its "do not use when" already written**. An effect without a refusal rule does not enter this system.
|
|
6
|
+
|
|
7
|
+
## Cut for legibility
|
|
8
|
+
|
|
9
|
+
These pieces sell by being read in three seconds on a phone. Anything that dirties the image works against that.
|
|
10
|
+
|
|
11
|
+
| Effect | Reason |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Film grain | adds noise to an image whose job is to be sharp |
|
|
14
|
+
| VHS, scanlines | degradation aesthetic over a product sold as modern |
|
|
15
|
+
| Dust, scratches | same problem, and unrelated to the subject |
|
|
16
|
+
| Vignette | darkens corners where the layout already places content, and eats the safe zone |
|
|
17
|
+
| Light leak | removes contrast from text exactly where it is unpredictable |
|
|
18
|
+
| Chromatic aberration | illegibility pretending to be style, worst on small text |
|
|
19
|
+
| Letterbox | in a vertical format it is lost area where space is already short |
|
|
20
|
+
|
|
21
|
+
The 2026 research found VHS and grain described as a trend only for brand and aesthetic content, never for performance ads with product and price on screen ([editing-patterns.md](editing-patterns.md)).
|
|
22
|
+
|
|
23
|
+
## Cut for cost without return
|
|
24
|
+
|
|
25
|
+
| Effect | Reason |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Liquid, morph | complex, fragile in deterministic render, no evidence of retention |
|
|
28
|
+
| Lens distortion | same, and it fights the safe zone in ways that are hard to measure |
|
|
29
|
+
| Particles | heavy to render, and in a UI creative they illustrate nothing |
|
|
30
|
+
|
|
31
|
+
## Cut for contradicting a rule of the system
|
|
32
|
+
|
|
33
|
+
- **Crossdissolve and fade between takes.** The default is the hard cut: in vertical short video the cut is what holds attention. Fade only at the very start or end, if ever.
|
|
34
|
+
- **Automatic beat detection.** The beat of a narrated piece is the voice, and forced alignment (ctc-align) gives speech boundaries to the millisecond. A beat detector would give a worse signal over a clock that is already better.
|
|
35
|
+
- **Speed ramp.** Over narration it desynchronises speech from screen, and speech anchors all timing.
|
|
36
|
+
- **Typewriter out of context.** Only where the narration talks about writing. It competes with the first sentence, which is the most expensive sentence of the piece.
|
|
37
|
+
- **More shake to raise dynamism.** The refusal that repeats most in production: dynamism grows through **cut, punch, element entrance and new scene**, never through more vibration. Shake and drift are seasoning.
|
|
38
|
+
- **Meme sounds** ("vine boom", "record scratch", "glitch"). No performance data and a comedy tone; see [sfx-licensing.md](sfx-licensing.md).
|
|
39
|
+
- **Trending picker audio in ads.** Licensing, not taste; see [sfx-licensing.md](sfx-licensing.md).
|
|
40
|
+
|
|
41
|
+
## Avatar rule, which is also a refusal
|
|
42
|
+
|
|
43
|
+
**No reused avatar clip. No lip sync against audio that is not this piece's audio.** Without a take generated with the exact words of that block, the piece goes out **without the avatar**, in screens only. A whole piece was once rejected because the actor's mouth said last week's script while a gate showed green.
|
|
44
|
+
|
|
45
|
+
## Waiting list (gaps, not refusals)
|
|
46
|
+
|
|
47
|
+
A refusal closes a discussion; a gap invites it.
|
|
48
|
+
|
|
49
|
+
| Missing | Status |
|
|
50
|
+
|---|---|
|
|
51
|
+
| Background music | the most expensive open decision, because of licence and mix. The config accepts an optional music bed; the file must come from a source cleared for the piece's platform |
|
|
52
|
+
| Callouts | often requested, never built. For offers that demonstrate screens it is what is most missed |
|
|
53
|
+
| Re-enabling a giant-word overlay (`textpunch`) | it must go through the layout bands first; drawn on top of the frame it ignored the stage |
|
|
54
|
+
| Silence before the CTA | no reliable source; test as A/B, not as a rule |
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Editing patterns for ads and Reels: what has a source, and what does not
|
|
2
|
+
|
|
3
|
+
Research done on 2026-09-24 with real web sources (every link below was accessed that day). Each claim is tagged:
|
|
4
|
+
|
|
5
|
+
- **DATA**: official platform documentation, or a named vendor with real measurement infrastructure.
|
|
6
|
+
- **OPINION**: agency, creator or tool blog. Most "2026 benchmark" results on the web come from a cluster of look-alike blogs with no sample, method or primary source; treat their numbers as planning hints, never as a gate.
|
|
7
|
+
|
|
8
|
+
The rule for this skill follows from the tags: **a number backed only by OPINION becomes a config default that warns, never a gate that blocks.**
|
|
9
|
+
|
|
10
|
+
## Hook (0 to 3 s)
|
|
11
|
+
|
|
12
|
+
- **DATA.** Hook rate (thumb-stop ratio) is 3-second plays divided by impressions in Meta Ads Manager. The hook window of this skill is therefore 3 s (`rhythm.hook_s`). Sources: [Coinis glossary](https://coinis.com/glossary/hook-rate), [Adrio](https://adrio.ai/blog/thumbstop-rate-vs-hook-rate-vs-hold-rate).
|
|
13
|
+
- **DATA.** TikTok recommends stating the content's proposition in the first 3 seconds and says top ads use pattern interrupts (unexpected visual, bold claim, curiosity gap) that stop the scroll in 2 to 3 s. [TikTok Ads Help, creative best practices](https://ads.tiktok.com/help/article/creative-best-practices).
|
|
14
|
+
- **OPINION.** "Good" hook rate: floor 25%, good 30 to 40%, elite 40%+. [Motion, Creative Benchmarks 2026](https://motionapp.com/thumbstop-pulse/creative-benchmarks-2026) (full report not readable).
|
|
15
|
+
- **Consequence here:** the first frame already moves and already says something. The budget curve gives the hook a small bonus, never a spike: dynamism is spread over the whole video, not packed into the opening.
|
|
16
|
+
|
|
17
|
+
## Rhythm and retention in the body
|
|
18
|
+
|
|
19
|
+
- **DATA.** ThruPlay = watched to 15 s or to the end if shorter. Hold rate = viewers reaching 15 s (or 75%) divided by viewers past 3 s; it isolates body retention from the hook. [AdSights, ThruPlay](https://www.adsights.ai/resources/glossary/metrics/thruplay), [AdSights, hold rate vs ThruPlay](https://www.adsights.ai/resources/comparisons/hold-rate-vs-thruplay-rate).
|
|
20
|
+
- **OPINION.** Cut cadence: TikTok every 1.5 to 3 s, Reels every 2.5 to 4 s, "change something on screen every 3 to 5 s". [edicionvideopro](https://edicionvideopro.com/en/editing-for-platforms-video-marketing/pattern-interrupts-tiktok-retention-guide/). No primary source.
|
|
21
|
+
- **In this skill:** `rhythm.max_static_s` (longest stretch with no visual change), `rhythm.hook_cut_every_s` and `rhythm.body_cut_every_s` are defaults read by `check_rhythm.py`, which **warns only**.
|
|
22
|
+
|
|
23
|
+
## Call to action
|
|
24
|
+
|
|
25
|
+
- **OPINION.** Repeat the CTA visually mid-offer and again on the final frame (end card): name, plain-language offer, CTA, high contrast. [Sovran](https://sovran.ai/blog/direct-response-video-ad-endings).
|
|
26
|
+
- **In this skill:** a documentation rule, not a gate. The CTA itself is a script decision (who says what, and when) and belongs to the script, not to the edit. In the CTA block the edit lowers effect density: no punch or zoom on the final card, price and button readable.
|
|
27
|
+
|
|
28
|
+
## Captions and on-screen text
|
|
29
|
+
|
|
30
|
+
- **DATA (historical).** 80 to 85% of feed video is watched without sound ([Digiday, 2016-05-17](https://digiday.com/media/silent-world-facebook-video/)). The number is from 2016 and is repeated, not re-measured; it still justifies designing sound-off first.
|
|
31
|
+
- **OPINION.** Word-by-word (karaoke) captions dominate in 2026; high contrast (white or yellow with black outline), lower-middle third. [OpusClip](https://www.opus.pro/blog/tiktok-caption-subtitle-best-practices).
|
|
32
|
+
- **In this skill:** captions come from the ctc-align word timestamps; the caption band is exclusive (nothing readable inside it); every claim that matters is also on screen as text, because the SFX never carries critical information.
|
|
33
|
+
|
|
34
|
+
## Two safe zones, not one
|
|
35
|
+
|
|
36
|
+
| Profile | Source | Numbers |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| `ad` | **DATA**, [Meta Business Help Center: text overlays and the Safe Zone for ads in Stories and Reels](https://www.facebook.com/business/help/980593475366490/) | keep about 14% top, 35% bottom and 6% each side free of text, logo and key elements |
|
|
39
|
+
| `organic` | **OPINION** ([campaignswift](https://campaignswift.com/blog/instagram-safe-zone-sizes), [pod2reels](https://www.pod2reels.com/blog/instagram-reels-safe-zone-guide)) plus measurement from production | the organic Reels UI (profile, caption, like/comment/share column, audio line) covers roughly 320 to 400 px at the bottom of a 1080x1920 frame and the right column |
|
|
40
|
+
|
|
41
|
+
The piece declares its profile; `check_safe_zone.py` measures against that profile. A piece that runs both as an ad and as an organic post is checked against both.
|
|
42
|
+
|
|
43
|
+
## Organic reach on Instagram
|
|
44
|
+
|
|
45
|
+
- **DATA.** Adam Mosseri, reported 2025-01-22: the three signals that matter most for ranking are watch time, likes per reach and sends per reach; sends weigh more for reaching non-followers. [Social Media Today](https://www.socialmediatoday.com/news/instagram-shares-algorithm-insights-2025/738034/). The "a send is worth 3 to 15 likes" multipliers found on blogs do not appear in any direct quote: do not use them.
|
|
46
|
+
- **DATA.** Accounts that repost third-party content 10+ times in 30 days are excluded from recommendations; the original is shown instead of near-identical reposts. [MediaPost](https://www.mediapost.com/publications/article/414773/instagram-algorithm-update-discourages-reposted-co.html), [PetaPixel, 2026-04-30](https://petapixel.com/2026/04/30/new-instagram-policies-target-reposted-content/).
|
|
47
|
+
- **Consequence for editing:** originality is a distribution rule, not taste. Each piece gets its own screens and its own dynamics; reuse the technical structure (bands, gates, data contract), never the composition.
|
|
48
|
+
|
|
49
|
+
## What reads as dated
|
|
50
|
+
|
|
51
|
+
- **OPINION, contradictory.** Some sources call VHS, scanlines and grain a 2026 trend; the same searches say heavy production lost ground to "barely edited" authenticity. Where VHS appears as a trend, it is brand or aesthetic content, never performance ads with product and price on screen. The refusals in [do-not-use.md](do-not-use.md) stand.
|
|
52
|
+
- **OPINION.** Classic talking head, ring-light testimonial and USP list are recognised as ads "in half a second" ([Pixis](https://pixis.ai/blog/ugc-in-2026-whats-working-whats-stale-and-how-to-refresh-your-hook-library/)).
|
|
53
|
+
|
|
54
|
+
## Gaps (recorded, not hidden)
|
|
55
|
+
|
|
56
|
+
- "Silence before the CTA" as a practice: no reliable dated source found. It is an A/B hypothesis, not a rule.
|
|
57
|
+
- The pattern-interrupt retention figure (58% vs 32%) and the "zoom +68% engagement" figure have no primary source.
|
|
58
|
+
- Meta's official video spec page returned 403; length guidance (6 to 15 s feed vs 15 to 60 s) comes from blogs and describes awareness use, not direct-response selling.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Gates: what each one measures, and what none of them sees
|
|
2
|
+
|
|
3
|
+
Two sentences this system paid to learn, more than once: **a gate that checks a different file from the one that will be rendered is not a gate**, and **green without proof is worth less than red**, because red at least says something needs eyes.
|
|
4
|
+
|
|
5
|
+
Thresholds are cited by config key. Every script exits `0` pass, `1` fail, `2` input missing or unreadable, so a pipeline can chain them with `set -e`.
|
|
6
|
+
|
|
7
|
+
## The table
|
|
8
|
+
|
|
9
|
+
| Gate | Runs on | Measures | Blocks? | What it does NOT see |
|
|
10
|
+
|---|---|---|---|---|
|
|
11
|
+
| `config.py` | the config | coherence of every block (ranges, event volumes under `sound.relative_ceiling`, budget values); refuses the whole config on the first error | yes | whether the numbers are right for this offer |
|
|
12
|
+
| `sound.py coverage` | config + SFX folder | sounds no event uses, events pointing at a missing file | reports | whether the sound fits the event (that is taste, reviewed by ear) |
|
|
13
|
+
| `normalize_audio.py --check` | SFX folder | every file at `sound.reference_level_db`, peak under `sound.max_peak_db` | yes | silent tails: it measures the whole file, so trim first |
|
|
14
|
+
| `check_excess.py` | timeline JSON | every sound together (`sfx[]` and scene cut sounds) against the three refusals: unknown event, collision, same sound in a row; per effect kind: ceiling per minute, smallest gap, longest run in a row (`budget.per_effect.*`); dominant effects of different kinds closer than `budget.gap_min_global`; always reports the densest 10 s cluster | yes (`--warn` only while calibrating) | effects drawn inside the screens themselves (HyperFrames timelines); only what the timeline declares |
|
|
15
|
+
| `check_rhythm.py` | timeline JSON | longest stretch with no visual change (`rhythm.max_static_s`), cut cadence in the hook (`rhythm.hook_s`, `rhythm.hook_cut_every_s`) and body (`rhythm.body_cut_every_s`), CTA tail excluded (`rhythm.cta_tail_s`). SFX never count as change | **no, warns only** | motion inside a screen; its numbers come from weak sources ([editing-patterns.md](editing-patterns.md)) |
|
|
16
|
+
| `check_safe_zone.py` | **final MP4** | per row, local contrast (std-dev, `safe_zone_gate.dp_min`) on every frame; the highest and lowest content rows must sit inside the piece's profile (`safe_zones.<profile>` top and bottom) | yes | whether what invades is text, a card or background glow; left and right edges. Full-screen footage fails by construction: frame it inside the zone. On a fail, extract the frame and look |
|
|
17
|
+
| `measure_density.py` | **final MP4** | how many of `density.n_bands` bands across the active safe zone have content (`density.dp_min`), sampled every `density.step_s`; fails under `density.min_filled` | yes | whether the content makes sense there: a band full of noise counts like a band full of information |
|
|
18
|
+
| `check_loudness.py` | **final MP4 or audio** | integrated loudness and true peak (ffmpeg `loudnorm`) against `loudness.*`; prints the two-pass fix and warns when the fix will switch loudnorm to dynamic compression | yes | the balance inside the mix (voice vs SFX); that is the level contract and the ear |
|
|
19
|
+
|
|
20
|
+
Order that matches the pipeline: config and coverage once per project; excess and rhythm on the timeline before the expensive render; safe zone, density and loudness on the final file.
|
|
21
|
+
|
|
22
|
+
## Two profiles, measured separately
|
|
23
|
+
|
|
24
|
+
The safe zone depends on where the piece runs: `safe_zones.ad` (Meta's ad guidance for Stories and Reels) and `safe_zones.organic` (the organic Reels UI). The timeline's `safe_zone_profile` picks the one to measure. A piece that runs as ad and as organic post passes both: run `check_safe_zone.py` again with `--profile` for the other one.
|
|
25
|
+
|
|
26
|
+
## Failures already paid for
|
|
27
|
+
|
|
28
|
+
- **A gate that could never fail.** The density measurement once ran with `|| true` in the build script and never rejected anything. A gate that cannot fail is decoration.
|
|
29
|
+
- **Verified the wrong file.** A voice check matched files by basename while the render played a different file with the same name; five pieces went green with old audio. Match by path, measure the file that renders.
|
|
30
|
+
- **Measured before the render finished.** A file read while still being written returned a plausible number over a third of the film. Unreadable samples are an error, never skipped.
|
|
31
|
+
- **Sampling missed the peak.** One frame per second missed entry animations lasting tenths of a second; the safe-zone gate decodes every frame.
|
|
32
|
+
- **Micro-zoom ate the margin.** An element 10 px inside the safe zone in the screen render crossed it in the final file, because the assembly scales the whole frame. Measure the final MP4, never the isolated screen.
|
|
33
|
+
- **A threshold that always fails gets bypassed by habit.** The safe-zone gate inherited the density threshold and flagged background glow on almost every frame; it now has its own (`safe_zone_gate.dp_min`).
|
|
34
|
+
|
|
35
|
+
## What no gate replaces
|
|
36
|
+
|
|
37
|
+
Every gate here can be green with a broken frame: a chip rendered blank, the actor covering the price, a mouth saying last week's script. **Extract frames from the final file and look**, including one frame of the CTA block, every time.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SFX and music licensing: organic post vs paid ad
|
|
2
|
+
|
|
3
|
+
Research done on 2026-09-24; every source accessed that day. The question is never "is this sound trending" but "can this file run in a paid ad on the platform where the piece will run".
|
|
4
|
+
|
|
5
|
+
## The central trap
|
|
6
|
+
|
|
7
|
+
- **Trending audio from the Instagram or TikTok picker is for personal use.** Meta mutes it at boost approval (reported since about Q1 2026 by [killthedj](https://killthedj.com/meta-sound-collection/); the official Meta announcement was not found, treat as third-party report). TikTok forbids the general library for commercial use; businesses must use the Commercial Music Library ([TikTok Ads Help](https://ads.tiktok.com/help/article/commercial-music-library?lang=en)). This is structural, not taste: **no trending picker audio in any ad.**
|
|
8
|
+
- **Meta has two things called "Sound Collection".** The in-app picker (availability varies by account type and region; "available in the app is not cleared for paid" per [usethirdchair, 2026-09-11](https://usethirdchair.com/blog/meta-sound-collection-explained-for-brands-and-rights-holders)) and the **Meta Business Sound Collection** (Business Suite / sound.facebook.com, 14,000+ royalty-free tracks and effects, cleared for videos, ads, Reels and Stories **inside Facebook and Instagram only**: [Meta Business Help 880459498798521](https://www.facebook.com/business/help/880459498798521), [1821972904537292](https://www.facebook.com/business/help/1821972904537292)).
|
|
9
|
+
- **TikTok CML is valid only inside TikTok.** Reposting the same video elsewhere loses the licence ([Velveteen](https://www.velveteen.fm/guides/tiktok-for-musicians/tiktok-commercial-music-library-explained)).
|
|
10
|
+
|
|
11
|
+
## Source table
|
|
12
|
+
|
|
13
|
+
| Source | Organic Reels/TikTok | Paid ad | Attribution | Traps |
|
|
14
|
+
|---|---|---|---|---|
|
|
15
|
+
| Instagram picker trending audio | yes | **no** | n/a | muted at boost approval, no warning beforehand |
|
|
16
|
+
| TikTok general library | yes | **no** | n/a | commercial use requires the CML |
|
|
17
|
+
| Meta Sound Collection (in-app picker) | varies | not guaranteed | no | in the app does not mean cleared for paid |
|
|
18
|
+
| Meta Business Sound Collection | yes | yes, Facebook/Instagram only | no | not portable to TikTok or YouTube |
|
|
19
|
+
| TikTok CML | yes | yes, TikTok only | no | loses the licence outside TikTok; SFX coverage not confirmed in the official docs |
|
|
20
|
+
| **Pixabay Content License** ([summary](https://pixabay.com/service/license-summary/)) | yes | **yes**, mixed into a work | no | no standalone use (reselling the raw file); no printing on merch; content showing third-party brands; **zero indemnification** ([PicDefense](https://picdefense.io/resources/source-intel/pixabay/)); keep the licence certificate per file |
|
|
21
|
+
| Mixkit ([terms](https://mixkit.co/terms/), [license](https://mixkit.co/license/)) | yes | yes per the general terms | no | the specific "Sound Effects Free License" text could not be read on 2026-09-24: **open it by hand before adding Mixkit files** |
|
|
22
|
+
| Freesound CC0 | yes | yes | no | mixed library: confirm the licence of each file |
|
|
23
|
+
| Freesound CC-BY | yes | yes | **always** | visible credit is hard in a 15 s ad; keep a per-file record |
|
|
24
|
+
| Freesound CC-BY-NC | only truly non-commercial | **never** | yes | "commercial gain" includes ads and brand content |
|
|
25
|
+
| Epidemic Sound ([Pro plan](https://help.epidemicsound.com/hc/en-us/articles/26247236323858-Pro-plan)) | depends on plan | Pro and up (paid social); Business broader | no | confirm what happens to published work after cancelling |
|
|
26
|
+
| Artlist ([licence](https://help.artlist.io/hc/en-us/articles/29490991524253-Understanding-Artlist-s-license)) | yes | Pro and up only | no | the Social plan explicitly excludes paid media |
|
|
27
|
+
|
|
28
|
+
## What this skill ships
|
|
29
|
+
|
|
30
|
+
- `assets/sfx/` holds only files with proven Pixabay provenance (see `assets/sfx/CREDITS.md` for the file list, derived cuts and what was left out).
|
|
31
|
+
- Portability decides the default: Pixabay files work on Meta ads, Reels and TikTok without switching library per channel. If a pipeline ever renders per platform, the Meta Business Sound Collection becomes a zero-risk option for Meta-only pieces.
|
|
32
|
+
- **Left out on purpose:** "vine boom", "record scratch" and "glitch". None has performance data, every source is a meme blog or a post count, and the comedy tone does not fit product and UI demos. They are out as fashion, not as a licence ban; reopening needs an argument, not enthusiasm.
|
|
33
|
+
- `cash` and `coin` stay: the concept is alive (TikTok discovery pages), but the file must come from a licensed library, never a viral clip captured from a feed.
|
|
34
|
+
|
|
35
|
+
## Adding a sound
|
|
36
|
+
|
|
37
|
+
1. Download from a source in the table whose "Paid ad" column says yes for the piece's platform.
|
|
38
|
+
2. Save the licence certificate or the page URL and date next to the entry in `CREDITS.md`.
|
|
39
|
+
3. Run `normalize_audio.py` so the file enters the level contract.
|
|
40
|
+
4. Map it to a semantic event in the config (the event is the key, the file is a column). Run `sound.py` for coverage.
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# When to use each effect, and when not
|
|
2
|
+
|
|
3
|
+
A catalogue of effects is easy; the refusal rule is what decides whether a video looks professional or zooms every two seconds.
|
|
4
|
+
|
|
5
|
+
**No number on this page is canonical.** Every value lives in the project's `effects.config.json` and is cited here by key. If this page and the config disagree, the config wins and this page is wrong.
|
|
6
|
+
|
|
7
|
+
## How to read
|
|
8
|
+
|
|
9
|
+
Each effect has seven fields. The first three are the decision; the last four are execution.
|
|
10
|
+
|
|
11
|
+
| Field | Question |
|
|
12
|
+
|---|---|
|
|
13
|
+
| **Purpose** | what narrative work the effect does |
|
|
14
|
+
| **Enters when** | the trigger: always an event in the speech or on screen, never the clock |
|
|
15
|
+
| **Does not enter when** | the refusal; if empty, the effect is not ready for automatic use |
|
|
16
|
+
| **Intensity / duration** | config key or component constant |
|
|
17
|
+
| **Frequency** | ceiling and minimum gap from the previous one |
|
|
18
|
+
| **Conflicts** | what it cannot coexist with |
|
|
19
|
+
| **Priority** | the 0-1 `strength` it carries into the budget when competing for an instant |
|
|
20
|
+
|
|
21
|
+
Strength is not the effect's importance: it is the strength of the **narrative claim of that concrete instant**. The same punch is worth 0.9 on the sentence that turns the offer and 0.3 on an ordinary breath.
|
|
22
|
+
|
|
23
|
+
## The rule above all others
|
|
24
|
+
|
|
25
|
+
**An effect that always happens stops marking what matters.**
|
|
26
|
+
|
|
27
|
+
A production system once had the rule "zoom punch on every SFX with volume >= 0.5". Almost every SFX sat at 0.50 or 0.55, so it fired on all of them: one zoom every 2.5 seconds, measured. The effect that existed to underline became the background. Hence the three laws:
|
|
28
|
+
|
|
29
|
+
1. **Every effect goes through the budget.** No path writes an effect straight into the timeline data.
|
|
30
|
+
2. **When it does not fit, switch effect; do not drop the instant.** Where the screen has headroom at the top, `punch`; where content sits against the safe zone, `flash`, which only touches opacity and moves no pixel.
|
|
31
|
+
3. **The instant comes from a speech breath, never from the clock.** An effect on top of a word competes with it (`fill_breaths.py`).
|
|
32
|
+
|
|
33
|
+
## Camera movement
|
|
34
|
+
|
|
35
|
+
### `punch`: zoom pulse
|
|
36
|
+
- **Purpose:** underline the sentence just said. The system's exclamation mark.
|
|
37
|
+
- **Enters when:** the breath right after a sentence that carries the argument (the offer turn, the number that hurts, the promise), and at a narrative block cut.
|
|
38
|
+
- **Does not enter when:** screen content sits inside the punch margin of the piece's safe-zone profile (computed from `breaths.punch_peak_scale`); the punch scales around the centre and multiplies with micro-zoom, so at its peak it pushes content through the safe zone. `fill_breaths.py --visual` measures this per breath and gives the breath to a `flash` instead. Nor over a block of text the viewer must read whole, nor twice in the same sentence.
|
|
39
|
+
- **Intensity / duration:** peak about 1.035 (1.05 when the triggering sound is loud), window about 0.22 s.
|
|
40
|
+
- **Frequency:** `budget.per_effect.punch` (`max_per_min`, `gap_min`, `max_in_row`).
|
|
41
|
+
- **Conflicts:** `shake` in the same window, never both.
|
|
42
|
+
- **Priority:** 0.9 offer turn, 0.7 block cut, 0.4 an unloaded breath; below that it is not worth spending.
|
|
43
|
+
|
|
44
|
+
### `microZoom`: slow continuous zoom
|
|
45
|
+
- **Purpose:** take the image out of photograph state. Not noticed; it is what keeps a still shot from looking still.
|
|
46
|
+
- **Enters when:** by default, on any shot longer than two seconds.
|
|
47
|
+
- **Does not enter when:** the screen already has strong motion of its own, or content is within about 21 px of the safe-zone edge: micro-zoom eats that margin and has failed pieces by one pixel.
|
|
48
|
+
- **Frequency:** continuous; does not count against the budget. It multiplies the punch, which is why the punch ceiling must account for it.
|
|
49
|
+
|
|
50
|
+
### `drift`: handheld camera
|
|
51
|
+
- **Purpose:** physical presence for a shot that is otherwise a generated image.
|
|
52
|
+
- **Enters when:** actor shot or scene with depth.
|
|
53
|
+
- **Does not enter when:** the screen is a list, a table or anything read line by line. **Never raise its amplitude.**
|
|
54
|
+
- **Conflicts:** `shake`.
|
|
55
|
+
|
|
56
|
+
## Impact
|
|
57
|
+
|
|
58
|
+
### `shake`: short tremor
|
|
59
|
+
- **Purpose:** physical weight for something that lands: a stamp, a seal, a verdict.
|
|
60
|
+
- **Enters when:** the `verdict` sound event fires. That is the only trigger, declared in the config (`triggers` on the event).
|
|
61
|
+
- **Does not enter when:** there is no visual impact; never sustained, never as background rhythm, **never over text the viewer must read**.
|
|
62
|
+
- **Frequency:** `budget.per_effect.shake`, deliberately the rarest effect in the system, with `max_dur_s`.
|
|
63
|
+
- **Conflicts:** `drift` and `punch` in the same window.
|
|
64
|
+
- **Priority:** 0.95. When a verdict lands it almost always wins: it is the only effect with an exclusive semantic trigger.
|
|
65
|
+
|
|
66
|
+
### `flash`: cut flash
|
|
67
|
+
- **Purpose:** mark a change without displacing anything. The punch's substitute exactly where the punch does not fit.
|
|
68
|
+
- **Enters when:** narrative block cut; as punch substitute on a screen pressed against the safe zone.
|
|
69
|
+
- **Does not enter when:** inside the same block without a screen change: a flash without a cut reads as a render glitch.
|
|
70
|
+
- **Frequency:** `budget.per_effect.flash`; more tolerant than punch because it moves no pixel.
|
|
71
|
+
- **Priority:** 0.6 block cut, 0.35 as substitute.
|
|
72
|
+
|
|
73
|
+
## Transitions
|
|
74
|
+
|
|
75
|
+
The default transition is the **hard cut**. A transition is justified only when the screen change is also a change of subject.
|
|
76
|
+
|
|
77
|
+
- **`whip`** (blurred sweep): block cut during fast speech (`breaths.fast_speech_wps` and up; `breaths.pace_profiles` picks transition and cut sound per pace). Not on dragging speech or within the same subject. Duration in **frames**, while everything else is in seconds.
|
|
78
|
+
- **`zoom`** (scale transition): block cut in a dragging stretch, where whip would be violent. Not when the next shot opens with a punch.
|
|
79
|
+
- **`cut`**: every other cut. Never refused; it is the default.
|
|
80
|
+
- Fade and crossdissolve: cut (see [do-not-use.md](do-not-use.md)).
|
|
81
|
+
|
|
82
|
+
All transitions share `budget.per_effect.transition`.
|
|
83
|
+
|
|
84
|
+
## Pattern interrupts
|
|
85
|
+
|
|
86
|
+
Share a tight budget (`budget.per_effect.interrupt`): a break that repeats stops breaking.
|
|
87
|
+
|
|
88
|
+
- **`suddenzoom`**: the sentence reframes the argument ("but you won't like the answer"). Not in the same block as another interrupt.
|
|
89
|
+
- **`freeze`**: a reaction to hold (the price, the result, the face). Not while speech continues on top: freezing the image with the voice running reads as a render failure.
|
|
90
|
+
|
|
91
|
+
## Text and captions
|
|
92
|
+
|
|
93
|
+
- **Karaoke caption:** always when there is narration, from ctc-align word timestamps. The caption band is **exclusive**: no readable content inside it, ever. Silenced while a headline is on screen, but keep that window short: silencing captions removes a whole density band.
|
|
94
|
+
- **Headline:** at the start of a block, anchoring the subject. It does not repeat the caption; the price leads the headline only if the hook is the price. An unknown style name must fail, never fall back silently to a default.
|
|
95
|
+
|
|
96
|
+
## Element entrance and exit
|
|
97
|
+
|
|
98
|
+
- **Entrance:** on the word that names the element, not before: nothing appears before it is mentioned. Two levels avoid the empty screen while waiting: the element exists as **base** (opacity about 0.4) from the start of the beat and rises to **highlight** (opacity 1) on the word.
|
|
99
|
+
- **Exit:** an element **recedes to context** (higher, smaller, low opacity); it never vanishes into emptiness. It leaves for good only when something else enters at the same instant.
|
|
100
|
+
|
|
101
|
+
## Rhythm by stretch
|
|
102
|
+
|
|
103
|
+
Read by `check_rhythm.py`, which warns and never blocks (the sources are weak, see [editing-patterns.md](editing-patterns.md)):
|
|
104
|
+
|
|
105
|
+
- **Hook** closes at `rhythm.hook_s` (3 s, the window Meta's hook rate measures). Something moves from the first frame.
|
|
106
|
+
- **Body:** no stretch longer than `rhythm.max_static_s` without any visual change; cut cadence around `rhythm.body_cut_every_s`.
|
|
107
|
+
- **CTA:** lower effect density; no punch or zoom on the final card; price and button readable. Repeat the CTA visually mid-offer and on the final frame.
|
|
108
|
+
|
|
109
|
+
## Avatar
|
|
110
|
+
|
|
111
|
+
Only for pieces with a generated avatar (`avatar.*`, optional block):
|
|
112
|
+
|
|
113
|
+
- **Full-shot ceiling:** after it, the actor leaves the shot or recedes to a card. The cut prefers the end of a sentence, then the longest pause near the ceiling, and only as a last resort the raw clock.
|
|
114
|
+
- **Occlusion ceiling:** a full-shot actor with nothing on top for longer is a layout error.
|
|
115
|
+
- **Take ceiling:** a generated avatar's gesture cycle repeats visibly after about 7.5 s.
|
|
116
|
+
- After each render, look at a frame of the CTA block: the actor has covered the price and the button with every gate green.
|
|
117
|
+
|
|
118
|
+
## Checklist before approving a piece
|
|
119
|
+
|
|
120
|
+
No gate replaces this; several items are here because they failed in production.
|
|
121
|
+
|
|
122
|
+
1. The excess gate passes, or what fails is justified in writing.
|
|
123
|
+
2. No dominant effect closer than `budget.gap_min_global` to another.
|
|
124
|
+
3. The caption band is clean **in the rendered frame**, not in a lint.
|
|
125
|
+
4. A frame of the CTA block, looked at: price and button visible.
|
|
126
|
+
5. If there is an avatar, the mouth matches this piece's audio. Look; do not trust the gate.
|
|
127
|
+
6. No shake over text meant to be read.
|
|
128
|
+
7. Loudness gate passes on the final file.
|
|
129
|
+
8. The largest cluster of effects is not an accident: it marks something, or it goes.
|