adanvil 0.2.0__py3-none-any.whl

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.
Files changed (127) hide show
  1. adanvil/__init__.py +1 -0
  2. adanvil/_bin.py +39 -0
  3. adanvil/_proc.py +20 -0
  4. adanvil/assets/EDITING_GUIDE.md +292 -0
  5. adanvil/assets/fonts/Montserrat-VF.ttf +0 -0
  6. adanvil/assets/fonts/OFL-Montserrat.txt +93 -0
  7. adanvil/assets/fonts/OFL-PlusJakartaSans.txt +93 -0
  8. adanvil/assets/fonts/OFL-Poppins.txt +93 -0
  9. adanvil/assets/fonts/OFL-Roboto.txt +93 -0
  10. adanvil/assets/fonts/OFL-TikTokSans.txt +93 -0
  11. adanvil/assets/fonts/PlusJakartaSans-VF.ttf +0 -0
  12. adanvil/assets/fonts/Poppins-Bold.ttf +0 -0
  13. adanvil/assets/fonts/Poppins-SemiBold.ttf +0 -0
  14. adanvil/assets/fonts/Roboto-VF.ttf +0 -0
  15. adanvil/assets/fonts/TikTokSans-VF.ttf +0 -0
  16. adanvil/assets/sfx_pack.json +166 -0
  17. adanvil/audio.py +1096 -0
  18. adanvil/batch.py +512 -0
  19. adanvil/brand.py +292 -0
  20. adanvil/busy.py +175 -0
  21. adanvil/captionfaces.py +334 -0
  22. adanvil/cli.py +540 -0
  23. adanvil/config.py +97 -0
  24. adanvil/endcard.py +779 -0
  25. adanvil/ffmpeg.py +481 -0
  26. adanvil/formats.py +1104 -0
  27. adanvil/jobs.py +687 -0
  28. adanvil/library/__init__.py +53 -0
  29. adanvil/library/annotate.py +189 -0
  30. adanvil/library/catalog.py +395 -0
  31. adanvil/library/index.py +879 -0
  32. adanvil/library/onepass.py +113 -0
  33. adanvil/library/overview.py +184 -0
  34. adanvil/library/separate_audio.py +128 -0
  35. adanvil/library/vision_index.py +336 -0
  36. adanvil/library/vision_search.py +225 -0
  37. adanvil/media/__init__.py +67 -0
  38. adanvil/media/cache.py +80 -0
  39. adanvil/media/facefocus.py +155 -0
  40. adanvil/media/filters.py +189 -0
  41. adanvil/media/reveal.py +709 -0
  42. adanvil/media/segment.py +744 -0
  43. adanvil/media/sheet.py +423 -0
  44. adanvil/media/split.py +388 -0
  45. adanvil/music.py +834 -0
  46. adanvil/musicgen.py +300 -0
  47. adanvil/musicgen_worker.py +89 -0
  48. adanvil/onboard/__init__.py +18 -0
  49. adanvil/onboard/build.py +461 -0
  50. adanvil/onboard/questionnaire.py +356 -0
  51. adanvil/onboard/site.py +959 -0
  52. adanvil/project.py +192 -0
  53. adanvil/qc/__init__.py +16 -0
  54. adanvil/qc/pair.py +435 -0
  55. adanvil/qc/report.py +888 -0
  56. adanvil/qc/score.py +1341 -0
  57. adanvil/reference/__init__.py +13 -0
  58. adanvil/reference/breakdown.py +263 -0
  59. adanvil/reference/decode.py +517 -0
  60. adanvil/render/__init__.py +5 -0
  61. adanvil/render/animated.py +158 -0
  62. adanvil/render/cards.py +312 -0
  63. adanvil/render/compose.py +1128 -0
  64. adanvil/render/inset.py +250 -0
  65. adanvil/render/stickers.py +535 -0
  66. adanvil/safezones.py +225 -0
  67. adanvil/server.py +2888 -0
  68. adanvil/sfx.py +651 -0
  69. adanvil/speech/__init__.py +9 -0
  70. adanvil/speech/analyze.py +465 -0
  71. adanvil/speech/captions.py +253 -0
  72. adanvil/speech/corrections.py +136 -0
  73. adanvil/speech/library.py +67 -0
  74. adanvil/speech/model.py +44 -0
  75. adanvil/speech/store.py +107 -0
  76. adanvil/speech/takes.py +257 -0
  77. adanvil/speech/transcribe.py +433 -0
  78. adanvil/storyboard/__init__.py +98 -0
  79. adanvil/storyboard/craft.py +776 -0
  80. adanvil/storyboard/devices.py +560 -0
  81. adanvil/storyboard/patch.py +151 -0
  82. adanvil/storyboard/recipes/before_after.yaml +101 -0
  83. adanvil/storyboard/recipes/comment_reply.yaml +138 -0
  84. adanvil/storyboard/recipes/customer_diary.yaml +109 -0
  85. adanvil/storyboard/recipes/hook_plus_product.yaml +81 -0
  86. adanvil/storyboard/recipes/meme_text.yaml +100 -0
  87. adanvil/storyboard/recipes/remake_long.yaml +134 -0
  88. adanvil/storyboard/recipes/text_hook.yaml +98 -0
  89. adanvil/storyboard/recipes/ugc_talking.yaml +129 -0
  90. adanvil/storyboard/recipes/voiceover_ad.yaml +150 -0
  91. adanvil/storyboard/recipes.py +168 -0
  92. adanvil/storyboard/schema.py +1293 -0
  93. adanvil/storyboard/sfx.py +96 -0
  94. adanvil/storyboard/snap.py +247 -0
  95. adanvil/storyboard/validate.py +2059 -0
  96. adanvil/text/__init__.py +29 -0
  97. adanvil/text/animate.py +203 -0
  98. adanvil/text/chunking.py +228 -0
  99. adanvil/text/emoji.py +194 -0
  100. adanvil/text/fonts.py +92 -0
  101. adanvil/text/gallery.py +142 -0
  102. adanvil/text/layout.py +105 -0
  103. adanvil/text/styles.py +1207 -0
  104. adanvil/textremoval/__init__.py +19 -0
  105. adanvil/textremoval/core.py +456 -0
  106. adanvil/variants.py +662 -0
  107. adanvil/vision/__init__.py +30 -0
  108. adanvil/vision/analyse.py +97 -0
  109. adanvil/vision/client.py +349 -0
  110. adanvil/vision/engine.py +208 -0
  111. adanvil/vision/framing.py +250 -0
  112. adanvil/vision/labels.py +112 -0
  113. adanvil/vision/setup.py +111 -0
  114. adanvil/vision/store.py +145 -0
  115. adanvil/vision/versions.py +146 -0
  116. adanvil/vision/worker.py +342 -0
  117. adanvil/vocab.py +108 -0
  118. adanvil/voice/__init__.py +34 -0
  119. adanvil/voice/engines.py +830 -0
  120. adanvil/voice/generate.py +779 -0
  121. adanvil/voice/setup.py +106 -0
  122. adanvil/voice/worker.py +304 -0
  123. adanvil-0.2.0.dist-info/METADATA +275 -0
  124. adanvil-0.2.0.dist-info/RECORD +127 -0
  125. adanvil-0.2.0.dist-info/WHEEL +4 -0
  126. adanvil-0.2.0.dist-info/entry_points.txt +3 -0
  127. adanvil-0.2.0.dist-info/licenses/LICENSE +21 -0
adanvil/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
adanvil/_bin.py ADDED
@@ -0,0 +1,39 @@
1
+ import os
2
+ import shutil
3
+ from pathlib import Path
4
+
5
+
6
+ def ffmpeg_path() -> str:
7
+ configured = os.environ.get("ADANVIL_FFMPEG")
8
+ if configured:
9
+ if Path(configured).is_file():
10
+ return configured
11
+ raise FileNotFoundError(
12
+ f"ADANVIL_FFMPEG points to a missing file: {configured!r}. "
13
+ "Install ffmpeg or set ADANVIL_FFMPEG to its executable path."
14
+ )
15
+ resolved = shutil.which("ffmpeg")
16
+ if resolved:
17
+ return resolved
18
+ raise FileNotFoundError(
19
+ "ffmpeg was not found on PATH. Install ffmpeg >= 6.1 with libzimg, "
20
+ "or set ADANVIL_FFMPEG to its executable path."
21
+ )
22
+
23
+
24
+ def ffprobe_path() -> str:
25
+ configured = os.environ.get("ADANVIL_FFPROBE")
26
+ if configured:
27
+ if Path(configured).is_file():
28
+ return configured
29
+ raise FileNotFoundError(
30
+ f"ADANVIL_FFPROBE points to a missing file: {configured!r}. "
31
+ "Install ffprobe or set ADANVIL_FFPROBE to its executable path."
32
+ )
33
+ resolved = shutil.which("ffprobe")
34
+ if resolved:
35
+ return resolved
36
+ raise FileNotFoundError(
37
+ "ffprobe was not found on PATH. Install ffmpeg >= 6.1, "
38
+ "or set ADANVIL_FFPROBE to its executable path."
39
+ )
adanvil/_proc.py ADDED
@@ -0,0 +1,20 @@
1
+ """Subprocess flags shared by every launcher.
2
+
3
+ On Windows a console program started from a process without a console opens its
4
+ own window: every ffmpeg call would flash a terminal. CREATE_NO_WINDOW gives the
5
+ child a hidden console that its own children inherit, so nothing pops up.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ import subprocess
12
+
13
+ NO_WINDOW: int = getattr(subprocess, "CREATE_NO_WINDOW", 0) if os.name == "nt" else 0
14
+
15
+
16
+ def detached_flags() -> int:
17
+ """Flags for a background runner that must outlive its parent, without a window."""
18
+ if os.name != "nt":
19
+ return 0
20
+ return NO_WINDOW | getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0)
@@ -0,0 +1,292 @@
1
+ # AdAnvil editing guide
2
+
3
+ The doctrine an AI agent follows when it edits vertical 9:16 video ads with AdAnvil. It comes from editing real paid ads and being corrected on them. Read it once before your first storyboard, then use the recipe files (`recipes/*.yaml`) for the shape of each ad type.
4
+
5
+ ## 1. The job
6
+
7
+ You judge, AdAnvil executes.
8
+
9
+ - **You decide**: what the reference ad is doing, which footage fits, what the text says, where it sits, whether the result looks right, and what to ask the human.
10
+ - **AdAnvil does**: cuts, colour handling, text rendering, text removal, audio, encoding, automatic checks and control images. It is deterministic, so the same storyboard gives the same video.
11
+ - **Your loop**: decode the reference, ask the compliance questions, pick a recipe, fill its slots with footage, write the storyboard, run `storyboard_validate`, render a preview, look at the contact sheet and full-resolution crops, fix, render final, deliver.
12
+ - Do not fight the tool. If something looks wrong, change the storyboard (footage, timing, text, position), not the pipeline.
13
+ - A validation warning is a question, not an order: fix it or be able to say why the ad is better with it. A failure is a rule the recipe treats as a blocker.
14
+
15
+ ## 2. Before editing
16
+
17
+ ### 2.1 Decode the reference ad completely
18
+
19
+ Do this even for a 10 second ad. What you skip here shows up as a rejected edit later.
20
+
21
+ 1. Probe the file (duration, size, frame rate, colour tags, audio streams).
22
+ 2. Find the scene cuts and build a timestamped contact sheet, then read it in vision.
23
+ 3. Transcribe the voice if there is one, and note the music: the decode returns its `music_profile` (tempo, pulse, feel `calm`, `medium` or `energetic`; it mixes voice and music when the reference speaks).
24
+ 4. Write a table: time, shot, burnt-in text, role of the shot (hook, myth, mechanism, proof, product, offer, call to action).
25
+ 5. Write the angle in one sentence (what belief or feeling the ad plays on) and the structure of its text (urgency, secret, reply to a comment, reminder, gratitude, negative bait turned around).
26
+ Also read who speaks in the text. An emoji or a name followed by `:` and quotes (`😩 : « ... »`) means a character is speaking. In the breakdown set `text_speaker` (`none`, `character`, `narrator`, `viewer`, `brand`) and `text_device` (`none`, `quote`, `punchline`, `question`, `label`, `subtitle_of_speech`, `price_or_offer`, `cta`). The remake must keep the same device (who says what), not just the words: an emoji kept as decoration without the colon and the quotes loses the dialogue. `ref_text_device` warns when it does.
27
+ 6. Mark what must be replaced (competitor footage, other brand's product, burnt-in text, logos) and what can be kept.
28
+ 7. In the breakdown, give every shot an `emotion` (`none`, `neutral`, `worried`, `frustrated`, `sad`, `tired`, `closed`, `skeptical`, `shocked`, `curious`, `relaxed`, `happy`, `satisfied`) and an `action` (free short text, for example "wipes the counter and sighs"). For every reference shot, note what the person feels and does; pick a library shot with the same emotion and the same kind of action. Note which faces answer a punch or a question: a reference whose "Are you sure?" is met by a skeptical or shocked face needs reaction faces in the remake, not smiles. Annotate your segments with `role` and `emotion`: `storyboard_validate` warns `emotion_mismatch` (a smiling face under a complaint, a flat face on a punch the reference answers with a reaction) and `ref_unannotated_emotion`.
29
+
30
+ ### 2.2 Ask the compliance questions BEFORE editing
31
+
32
+ Put the questions to the human in one message and record the answers (a short decision log next to the storyboard).
33
+
34
+ - **Claims**: which claims does the reference make (voice and text)? Which of them may this brand make? Anything about age, body, health, money or income, "cure" or "guaranteed result" wording, or before/after promises needs an explicit answer.
35
+ - **Before/after authenticity**: same person, same framing and light, unretouched, not AI generated? Was the result obtained with this product?
36
+ - **Third-party marks**: logos, competitor names, other brands' products in the frame, watermarks.
37
+ - **Foreign currency and prices**: any price or currency in the reference that does not fit the brand's market or offer.
38
+ - **Press and endorsement mentions**: "As seen in" strips, awards, celebrity references. Are they confirmed and sourced?
39
+ - **Consent of the people shown**: written consent for paid advertising for every customer, creator or public figure visible.
40
+ - **Provenance**: an ad found in an ad library belongs to an advertiser, not to a customer. Do not assume a video is customer content from its look.
41
+ - **Music and voice licences**: is the reference track cleared for reuse?
42
+ - **Numbers**: durations and quantities in the text ("Day 30", "in 5 weeks") come from the human or the source of truth, never from timestamps in the video.
43
+
44
+ If the human overrules a reservation, propose compliant alternatives once, then execute their decision and record it. Do not reopen it in later versions.
45
+
46
+ ## 3. Picking footage
47
+
48
+ - **One person per video when that person speaks on camera or the text says "I".** Changing the person, or the voice, mid-video does not feel natural.
49
+ - **A narrator voice-over (the brand speaking, "tu"/"you" to the viewer) is a general text:** vary people like the reference does (`ref_people`), with different ages and looks in different settings.
50
+ - **The product must appear exactly as it is sold today** (current packaging, current version) in every product-only shot. Older versions, prototypes and other brands' products do not go in. When several versions exist, filter on the `product_version` annotation of the catalog and look at each product shot of the final contact sheet with the current version's visible traits in mind. If unsure, show the human a comparison sheet instead of guessing.
51
+ - **Avoid**: competitor footage, watermarks, AI generated before/afters, surgical or medical imagery, footage filmed by someone who did not consent, and another brand's product shown as yours.
52
+ - **Cut in the middle of the useful gesture** (the lid coming off, the shoe hitting the pavement, the first sip, the smile starting). The contact sheet can be a moment off, so extract the exact frame at your chosen timecode and look at it before you commit.
53
+ - **Prefer bright, well-lit shots.** Limit dark, washed-out or colour-cast shots to one or two in an ad.
54
+ - **Talking-head UGC**: when the ad is a creator speaking to camera, the speech is the footage. Index with speech (`library_index` transcribes the clips with sound, fast, so a key word per line can be misheard: run `speech_refine` on the takes you keep), choose each line from its best take with `speech_takes` (no filler, no restart, a complete ending, a natural speech rate, a good level) instead of watching every take, and read `speech_view` before cutting: its `clean_segments` are the cuts that keep the speech and drop the pauses and hesitations. One creator for the whole ad; use the other clips as B-roll over the voice.
55
+ - **Customer diaries**: the customer's own clips carry the ad. Add product shots as hands and product only, never another person's face, and do not add shots of unrelated accessories.
56
+ - Use the footage catalog: search before you scan folders again, and only catalogue what is new.
57
+ - **Long jobs:** indexing a big folder runs as a job; wait with `job_status(job_id, wait_seconds=90)`; if the connection drops, call `job_status` again (with `project_dir` if needed).
58
+ - **B-roll and stock must be relevant:** a B-roll or stock shot shows the brand's own category and target (no lipstick shot for a coffee ad, no cat in a dog-food ad, nobody outside the target customer) and says the same thing as its text.
59
+ - **A diagram or 3D animation must be readable in one second:** one idea, a recognisable subject, ideally named by the text or voice at the same moment, and the ad explains it. When in doubt, use a real usage shot instead.
60
+ - **Problem first, then the solution:** the opening beats show the target living the problem (a worried or frustrated face, a runner out of breath, a messy kitchen, the old way of doing things) with no product on screen; the product appears only when the story turns to the solution. Translate every reference shot to the brand's product: a mascara ad's eye macro becomes a close-up of the brand's own key detail (the pour for a coffee brand, the sole for a running shoe, the dashboard for an app).
61
+ - **Then let the solution breathe:** after the turn, several shots of different people using the product, happy and relaxed, with a visible sign of the result (a calm face, a tidy kitchen, an easy run), before the offer; one product-only shot is not a payoff.
62
+ - **The brand's own CTA image wins when the human confirmed it:** end on it and write any doubt in the delivery notes rather than replacing it.
63
+ - **Choose by what the shot expresses, not only by its subject:** annotate library clips with `emotion` and search with `library_search(emotion=[...])`. A face that smiles under a complaint ("my mornings are a mess") contradicts the text; a reaction (skeptical, shocked, worried) belongs on the lines that provoke it.
64
+ - **Stock and diagram B-roll:** footage the brand put in its library is the brand's to use unless it is excluded by the brand rules; a diagram or 3D animation is the usual replacement for a reference's explanatory shots (anatomy, engine, app architecture).
65
+ - Rushes with burnt-in text, bars or unusual stabilisation crops: plan the text removal method and the crop before you pick timecodes.
66
+
67
+ ## 4. Rhythm
68
+
69
+ | Element | Rule |
70
+ |---|---|
71
+ | Usage or customer shot | 2 to 2.5 seconds |
72
+ | Product-only shot | 1.6 to 2 seconds, never below 1 second (a 0.6 to 1.2 second product cadence read as "too busy") |
73
+ | Hands-only insert in a diary | 0.6 to 0.8 second, about 3 seconds in total |
74
+ | Fast text-hook demo shots | 1.2 to 3.5 seconds |
75
+ | Before and after shots | 2 to 3 seconds each |
76
+ | Caption on screen | about 3 seconds |
77
+ | End card | 3 seconds as an image, or about 4.4 seconds as a product shot with a text call to action |
78
+ | Product zoom | slow, 1.00 to 1.04 over the shot, not 1.07 |
79
+
80
+ - Cut hard. Do not add transitions or effects between shots, the cuts carry the rhythm.
81
+ - The recipe's total duration is a target range, not a goal in itself. A 20 second ad that lands its message beats a 24 second one.
82
+ - If a segment drags, shorten the shot before adding another one. If it feels frantic, lengthen it.
83
+ - Never show the same shot twice in one ad.
84
+
85
+ ## 5. Text
86
+
87
+ **Placement**
88
+ - Text never covers a face, eyes or mouth, nor the product being used.
89
+ - The free area of the shot decides the zone. Main text goes at the top (y about 70 to 330) when the top is free, and at the bottom (y about 1250 to 1500) when the face is high or the head is tilted up. Bubbles and call to action text sit at the bottom.
90
+ - On shots with a raised face, put the text at the bottom centre (y about 1300 to 1340).
91
+ - If the text takes too much of the image, reduce it (cartouche 42 px, bubble 32 px, fewer lines) rather than moving it onto a face.
92
+ - Text never renders wider than 960 px. Keep lines short so the renderer does not shrink them below about 50 px on a main text.
93
+
94
+ **Content**
95
+ - **One strong hook.** State the belief or the result, do not ask a soft question. Do not offer variants of the hook inside one video, one strong one is enough.
96
+ - **Keep the structure of the reference and adapt it.** Rewriting away from it produces texts viewers do not understand. Every element needs an obvious link to the text (a comment bubble must answer the hook).
97
+ - The age, status or situation in the text must fit the person on screen.
98
+ - **Long text is split into two beats** that follow each other (half of the video each), never one block.
99
+ - **Sentence case by default**: a capital at the start of each sentence, not all caps, except deliberate key words, stamps, or a house style the brand asks for.
100
+ - **Claims wording**: describe what the product is, does and feels like, in terms the brand can prove ("small-batch roasted", "ready in five minutes", "feels lighter"), never a medical outcome, a cure, a guaranteed result or a comparison with a named competitor. The brand's banned words list is checked automatically, and it is a floor, not a full review.
101
+ - Strong or shocking words are allowed only when the human explicitly asks for them.
102
+ - Numbers of days, weeks or amounts come from the human, not from the video.
103
+
104
+ **Styles (pick by context, not by taste)**
105
+ - `pill`: calm shots, brand-voice captions.
106
+ - `outline`: native social hook over moving shots, no box because a box ruins the shot.
107
+ - `outline_soft`: diaries and customer footage, transparent text with a strong outline so it stands out on phone video.
108
+ - `meme`: one long humorous text, five short lines, about 52 px, block at y about 1290.
109
+ - `box`: offers, prices and call to action text on a product end card. The text is centred on its visual middle, with about half an em of side padding.
110
+ - `label`: a small clean tag, a white rounded card with dark text and a soft shadow, no accent bar. A small tag or callout only, never the main text.
111
+ - The validated main styles are `outline` (native social text over moving shots) and `pill` (calm shots).
112
+ - `comment`: one real comment only, facebook look, blurred avatar and name, real reaction count, placed where it hides no face (often mid-frame, y about 880 on full-body shots).
113
+ - Match the reference's signature text look when it has one (font, shape, position), measured on a frame in pixels.
114
+ - Emoji: pick one provider and stay consistent inside the ad; `brand_get` returns `emoji_available` (the characters of the brand's emoji folder, or `"all"` for twemoji). Do not paste emoji from a font that lacks them.
115
+ - Check that each caption stays on screen until its end time and remains present on the last frame it should cover.
116
+
117
+ **Animated captions (`animation`)**
118
+ - Captions are static by default (`animation: none`). Static is right for calm shots, brand-voice text and offers. Animate the captions of a spoken or punchy social ad: it is the look viewers expect.
119
+ - `karaoke`: the whole chunk stays on screen and the word being spoken is lit. Use it for captions that follow speech (`voice_captions`, `speech_captions`, 2 to 4 words a chunk): the eye follows the voice.
120
+ - `word`: one or two big words at a time (30% larger than `size`; a word of up to three letters is shown with the next one), the spoken one lit. Use it for punchy hooks and short key lines, not for whole sentences.
121
+ - `pop`: each chunk scales in from 85% to 100% in 120 ms with an ease-out (`bounce: true`: an overshoot to 108% over 180 ms). Use it for written captions (`caption`), where nothing is spoken word by word. Emoji are part of the chunk and pop in with it.
122
+ - The lit word uses `highlight` (default the brand's `visual.highlight_color`, else yellow `#FFD400`), may be drawn `highlight_scale` times larger (1.1 to 1.2 reads well, more looks like a bug) and sit on a rounded `highlight_box`. Choose a highlight that contrasts with the text and with the shot: yellow or green on white outline text. On a `pill` or `box` style, a `highlight_box` in the brand colour does the job (with no `highlight`, the word turns black or white to stay readable).
123
+ - One animation per ad, one lit word at a time, 2 to 4 words a chunk. The text never moves from one state to the next, so a lit word cannot make the line jump.
124
+ - A plain `caption` with `karaoke` or `word` spreads its words evenly between `start` and `end`; give `word_times` when you know when each word is said. Generated captions use the real word times.
125
+ - Look at it: `caption_preview` draws any state of an animated caption (`chunk_index` and `word_index` in the overlay, or the time `t`).
126
+
127
+ **Keyword emphasis**
128
+ - `emphasis: ["word", ...]` draws those words in the highlight colour, with or without an animation (case and accents ignored). Emphasise the one word that sells (the result, the number, the offer), two at most per chunk.
129
+ - In a typed caption or in the `show` text of a voice line, `*word*` does the same: `{"say": "forty nine dollars", "show": "only *$49* today"}`. The voice never says the stars.
130
+ - Numbers and prices work: `"65,90 €"`, `"65,90"`, `"65.90€"` and `"-20%"` match the price however it is spaced, a multi-word entry (`"livraison offerte"`) matches those consecutive words, and `*65,90 €*` or `*-20%*` mark any text, spaces included. The stars are never drawn. `storyboard_validate` and `caption_preview` warn `emphasis_unmatched` when an entry matches nothing in its caption.
131
+
132
+ **Chunks**
133
+ - Captions generated from a voice or from speech never run across the end of a sentence, stop at a comma once they hold two words (so a clause is cut where a reader would cut it), and a mark such as `?`, `!` or `»` never starts a chunk.
134
+
135
+ **Visual devices**
136
+ - Beyond plain cuts an ad can use these devices: a **reveal** (a divider sweeps a before picture away to show the after picture), a **split screen** (honest before/after, problem and solution, two products), a **picture card** (a photo or a review screenshot that comes in over the video), a **video inset** (a creator reacting over a product page or a review, a product close-up in a corner), **stickers and arrows** (point at one detail: a price, a feature, a score), a **progress bar** (longer ads) and a **punch-in** (a sharp zoom on the strongest word of a talking head). The tool section of the agent guide has the fields of each.
137
+ - **Use one or two devices per ad.** Each must answer a need of the story. A split screen, an arrow, a badge, a progress bar and a punch-in in the same 20 seconds look cheap and bury the product.
138
+ - Reveal and split screen: use them only when both pictures are true and different (same person, framing and light, unretouched, confirmed): see Before and after below. A reveal gets one `focus` for both sides and 3 to 3.5 s; a picture card is an `image` overlay with `animation`, `frame` and `rotate`.
139
+ - Split screen: use it only when both pictures are true (same person, framing and light, unretouched, confirmed). Pick `side_by_side` for portrait footage and `top_bottom` for landscape footage, set each panel's `focus` on the subject, keep labels to two or three words (`FROM THIS`, `TO THIS`, in a `label_style` of `label`, `pill` or `outline`), and a `gap` of 6 to 12 px in a `gap_color` that matches the ad.
140
+ - Overlay times are snapped to the frame grid at render time: the nearest frame, and the frame of the cut when a start or an end is within half a frame of a segment cut (a caption timed 7.9003 s over a cut on frame 237 starts on that frame, not the next one).
141
+ - Video inset: place it where it hides no face and none of the captions (`caption_covered_by_inset`, `inset_out_of_frame`); a circle suits a face, a rounded rectangle a screen recording. Without `audio` it is silent; with `audio: true` it is the only voice at that moment.
142
+ - Stickers: they point at something real on the shot below, so measure the position on a frame; one sticker at a time, 1 to 2 s each; an arrow or a circle with `animation: draw`, a badge with `pop`.
143
+ - Progress bar: only above about 25 s, at the bottom, thin (6 to 10 px), in the brand accent.
144
+ - Punch-in: 1.1 to 1.25 on the strongest word, hold about half a second, once or twice per shot; above 1.25 it reads as a digital zoom (`digital_zoom`).
145
+ - Stills in the middle of a video stay `static`, unless a slow zoom adds something (a product shot that needs life, a before/after pair). Then `zoom_to` goes from 1.00 to 1.06: the zoom is drawn at sub-pixel precision (the same on ffmpeg 6.1 and 9), so the picture never trembles, and a larger amount only reads as a digital zoom. The same holds for a `reveal` `motion: zoom_in` and a clip `zoom` on a low-resolution source.
146
+
147
+ **Before and after**
148
+ - A before/after is the strongest proof in an ad when the two pictures are real and the difference can be seen at a glance, and the weakest when they are not. It is carried by one device, never two: a **reveal** (a divider sweeps the before picture away to show the after picture), a **split screen**, or a **picture card** (a photo that comes in over the video).
149
+ - **Two real pictures**: the same person, the same framing and distance, the same light, unretouched, not generated or enhanced by AI, obtained with this product, and the person's written consent for paid advertising. The human confirms each point; you never decide it from the pictures, and you never present an AI or retouched picture as a real result. The interval ("after 30 days") comes from the human.
150
+ - **A visible difference**: two crops of the same take show nothing (`split_same_source` warns when both sides are one file at overlapping moments). Look at the frame at mid-sweep: if the two sides are hard to tell apart at a glance, change the pair, not the effect. Keep both sides in one orientation family (`reveal_orientation`) and give the segment one `focus` on the face or the product so they line up, or `focus: "face"`: with the vision environment (`vision_setup`) the main face of each side is kept centred at `face_y` (0.38 of the frame) and, when both sides show one face, `align_faces` (on by default) also gives both faces the same size.
151
+ - **Reveal first**: the viewer watches the change happen. About 3 to 3.5 s with the defaults: 0.8 s on the before (`hold_before`), a 0.9 s `sweep`, then at least 0.6 s on the after (`reveal_after_short`). `left_to_right` by default, `top_to_bottom` for a standing person or a surface, `back_and_forth` for a slider look (a longer `sweep`, 1.6 s or more), `motion: zoom_in` for life. Two short labels in the ad's language, never a claim.
152
+ - **Choose the pair by looking, with `compare_pair`**: before writing the segment, compare EVERY available pair, the photos, the brand's side-by-side composites split with `crop` (`before_crop` and `after_crop`, `{x, y, w, h}` in source pixels; a reveal side takes the same `crop` on an image, so the composite needs no preparing) and frames of before/after clips (`before_t`, `after_t`). The tool returns `visible_difference` from 0 to 100 (under 12 not visible, under 30 subtle, under 55 visible, 55 and over obvious), measured on the face when a skin-region heuristic finds one in both pictures, else on the whole frame after aligning sizes, and a side-by-side preview image. LOOK at every preview and use the pair whose difference is obvious at a glance: a low score looks the same to the viewer. Still images of the same pose beat moving clips for a reveal. The score ranks, your eyes decide, and the human confirms the pair.
153
+ - **Split screen** for a side by side comparison to study for longer (two products, two methods). **Picture card** (`image` overlay with `animation`, `frame`, `rotate`) when the proof is a quick one inside another shot: a customer photo pair or a review screenshot that comes in over a talking-head ad for 2 to 3 s (`pop`, `slide_up`, `slide_left`, `slide_right` or `fade`, a `frame` with a `radius`, a `border_width` and a `shadow`, a small `rotate` for a photo card look) and leaves with `exit: true`, while the creator keeps talking. Keep it out of faces and captions.
154
+ - **Use the brand's real creator photos**: for a before/after, prefer the photos of the person the viewer sees elsewhere in the ad (the creator, the customer who speaks), shown as a card or a reveal over that person's own shot, rather than an anonymous composite taken from the website. The same face in the ad and in the proof is what makes the change believable.
155
+ - **Compliance**: the caption describes the picture, not a promise. No medical outcome, no "guaranteed result", no result for everyone (use the brand's own "results vary" wording if it has one). Record the consent and the confirmed pair in the decision log.
156
+
157
+ ## 6. Endings
158
+
159
+ - **Always end on a call to action.** Even when nobody asked for it, the human will want it.
160
+ - **No caption on the end card image, and the call to action written once.** `caption_on_end_card` warns when a written, voice or speech caption stays more than 0.2 s on the end card image (the last segment labelled `end_card` when it is an image, or the brand's CTA image): end the caption before it. `cta_text_missing` warns when `brand.yaml` has a CTA wording (`product.cta_text`) and no caption contains it (case, accents and quotes ignored): add it as a caption on the end, unless your end card image already shows it. Ending on the brand's own CTA image counts as showing it.
161
+ - **The brand's own CTA image comes first.** When the brand has a ready-made one (`cta_image` in `brand.yaml`, a `cta/` folder or `brand/`), use it as the last segment (`image`, `zoom_in`, about 3 seconds). Compose an `end_card` only when there is none. `storyboard_validate` warns `cta_image_unused` when `brand.yaml` has a `cta_image` and the last segment is neither that image nor labelled `end_card` with a reason in `gaps` (type `other`).
162
+ - Options: the brand's end card image (3 seconds, slow zoom), or a clean product shot of about 4.4 seconds with the call to action in a `box` text (2.4 seconds was too short to read), or a product shot with the guarantee (or another reassurance such as free returns or a free trial) written large above the box (about 3 seconds).
163
+ - If the brand offers a guarantee, free returns or a free trial, put that reassurance line before the end card, or on it, in the brand's real words and with the terms checked against the landing page. Never invent terms, and leave the line out when the brand has none.
164
+ - Check that press mentions or endorsements on the end card image are confirmed.
165
+ - **No ready-made or up-to-date CTA image?** Compose one with `end_card` from a real product photo checked for the current version, the real offer, guarantee and call to action wording (a pill, a title with one accent word, the photo in a white card, badges, a button). Use it as the last segment (`type: image`, `motion: zoom_in`, about 3 seconds, label `end_card`), after the last caption, with no subtitles on it. With a voice-over it lasts the last line plus 1.5 seconds. Look at the result before using it. `crop` is in the source's display pixels, before scaling (on a 1920x1080 frame, `{"x": 600, "y": 100, "w": 700, "h": 700}` keeps a 700 px square); the tool returns `source_size` so you can measure it.
166
+ - An offer or discount shown on screen must match the landing page at the time of launch.
167
+
168
+ ## 7. Sound
169
+
170
+ - With no voice, reuse the reference's music when the human has cleared it, or pick a licensed track. Shorter than the video, it loops with a crossfade. Longer, it is trimmed with a fade out. The audio always ends with the video.
171
+ - **Never leave silence at the end.** If there is a voice, keep music running for a short time after the last word, especially over the end card.
172
+ - **Generated voice-over**, when the recipe needs one:
173
+ - The voice speaks on behalf of the brand, never as an invented customer telling a made-up story. Only real customer sentences, quoted as such ("A customer wrote to us..."), belong in the script.
174
+ - Use one voice per video, generated sentence by sentence, trimmed just before the first word and just after the last, and read back with a transcription tool to catch repeated or swallowed words.
175
+ - Use a reference clip in the target language, or the accent will drift.
176
+ - The voice language is the spec's `language`; without it, the `language` of `brand.yaml` (`fr-FR` counts as `fr`), else `en`. Set it explicitly when the script is not in the brand's language.
177
+ - Mix the music low enough to be heard but not to compete. The tool brings the bed to a reference level first (-11 LUFS, plain gain, bounded to -12 / +15 dB), then applies `music_volume`: 0.32 to 0.38 sits about 5 dB under the voice whatever the track (a level of 0.16 was inaudible; `ad_score` asks for the voice 4.5 dB above the music, which 0.38 or less gives). Pick a track whose energy matches the reference: a soft piano under a fast edit feels flat. Use `music_profile` to compare: it returns each track's tempo (BPM), `pulse` (how clearly the attacks stand out), onset rate, loudness range and a feel. Choose the library track whose profile is closest to the reference's. Feel thresholds: `calm` when the pulse is under 2.0 (no clear beat, the tempo is then null); `energetic` when the pulse is 2.2 or more and the tempo 110 BPM or more; `medium` otherwise.
178
+ - **Music: where a track comes from and what it needs** (`music_search`, `music_fetch`):
179
+ - Tracks come from `music/`: your own licensed files, tracks fetched with `music_fetch` (Openverse, and Freesound when `FREESOUND_API_KEY` is set), or tracks made locally with `music_generate` (optional, `adanvil music-setup`; it needs about 20 GB of free memory and is refused below that, `ADANVIL_MUSIC_MIN_FREE_GB`; it follows the prompt loosely, so measure the result with `music_profile`).
180
+ - **Licence rules.** A paid ad is a commercial use that edits the track (cut, loop, fade, mix), so the licence must allow both. Accepted: CC0, Public Domain Mark, CC BY, CC BY-SA. Refused, never offered by the search: any NC (non-commercial) and any ND (no derivatives) licence, and Freesound's "Attribution NonCommercial" and "Sampling+". When in doubt, the track is not used.
181
+ - **Attribution.** CC BY and CC BY-SA need the author credited. `music_fetch` writes the exact line in `music/<file>.licence.json` (`attribution_text`); the render report and `ad_score` repeat it in the warning `music_attribution_required`. Put that text in the ad's description (the post caption), not on the video. CC0 and public domain need nothing. Keep the source link: the record has it.
182
+ - **Where the licence is read.** First `music/<file>.licence.json`; when there is none, the line of that file name in `music/LICENCES.txt` (written by `music_fetch`, or by hand as `<file>: <title>, <author>, ..., licence <name>`); the report says which one it used (`read_from`: `music/LICENCES.txt line 3 (no sidecar)`). A ledger line gives the licence name; it counts as needing an attribution when it has an `Attribution required: "..."` part or a CC BY / CC BY-SA name.
183
+ - **A track with no record** (no sidecar and no line in `LICENCES.txt`) raises `music_licence_unknown` in the render report and `ad_score`, and the manifest says `unknown`. Check that its licence allows an ad, write the record (`<file>.licence.json` with at least `licence`, `title`, `author`, `attribution_required`, `attribution_text`), or fetch the track again with `music_fetch`.
184
+ - **Cover art.** Fetched mp3 files carry an embedded picture. An audio file whose only video stream is that picture (`attached_pic`, or one still mjpeg/png frame in a file with audio and an audio extension) is audio everywhere: validation, `music_profile`, `music_beats`, the audio bed. `music_fetch` also removes the picture (`-map 0:a -c copy`, same container) so the stored file is clean.
185
+ - **The feel guess of `music_search` is only a hint.** `fit.feel_guess` is read from the track's OWN title, tags and genres (never from the feel you asked for): `calm` (ambient, piano, chill, lofi, sad, ...), `medium` (pop, indie, jazz, folk, corporate, ...), `energetic` (upbeat, rock, metal, edm, epic, ...), and `unknown` when nothing matches or two feels tie. `fit_to` ranks by it, after the length. The real answer is the measured profile that `music_fetch` returns (`feel`, `tempo_bpm`, pulse): trust that one.
186
+ - **Picking a track for an ad.** 1. Decide the feel from the reference (`music_profile` of its soundtrack) or from the offer: calm for a soft skincare story, energetic for a fast text ad; write it in `audio.music_feel`. 2. `music_search(query, mood, fit_to="storyboard", storyboard="storyboards/<name>.json")`: the tracks at least as long as the ad come first. 3. Listen to the `preview_url` of the top three, then `music_fetch` the one or two you like. 4. Compare the measured `feel` and `tempo_bpm` with the target (`music_profile` thresholds above) and choose; for a music-led ad check `music_beats`. 5. Set `audio.music`, render, and read the warnings: attribution text into the description.
187
+ - A Freesound track is the high-quality MP3 preview (the original file needs a login); an Openverse track is the file the source serves. Search words are English: "upbeat acoustic", "calm piano", "lofi", "corporate".
188
+ - **A creator's own sound (`audio.clips`)**, for talking-head UGC:
189
+ - The bed is the original audio of the clip segments, joined exactly like the picture (every piece lasts its segment, a 10 ms crossfade at each cut) and normalised on its own to `voice_loudness` (-15 LUFS), like a voice-over. Music goes under it at `music_volume` (0.2 to 0.3 for a speaking creator, so it shows only in the pauses and under B-roll). It cannot be combined with a generated voice or a reference soundtrack.
190
+ - **Cut on the pauses.** Take cut points from the `clean_segments` of `speech_view`: they keep 0.06 s before a word and 0.10 s after, drop the pauses over 0.35 s and the fillers (euh, um, uh, er, bah, ben; genre, du coup and like only when said alone), and never fall inside a word. A cut inside a spoken word is a click and a swallowed syllable: `cut_mid_word` warns about it (not when the next segment resumes the same source where this one stopped, within one frame: the voice goes on across that cut).
191
+ - **B-roll over the voice** (an L-cut): a segment with `audio_from` keeps its own picture and plays another clip's sound from the `in` point where the sentence goes on, so the creator never stops talking when the picture changes. Over a long sentence, change the picture every 1.5 to 3.5 s.
192
+ - **Right words before captions.** Indexing uses a small, fast model so the speech is searchable at once. Once the takes are chosen, call `speech_refine` on those clips (a stronger model and the brand vocabulary: brand name, product name and the optional `speech.vocabulary` of `brand.yaml`), then cut and caption. A word that is still wrong can be shown right with `speech_captions.corrections` (`{"heard": "shown"}`), only as a close spelling of the heard word (`rites` to `rides`): every correction is listed in a `speech_corrections` warning for a human to check, and a correction that changes the word too much is refused, because it would put words in the speaker's mouth.
193
+ - **Voice recorded apart.** A video whose own sound is silent has the speech status `silent_audio` and, when audio files nearby share words with its name, `separate_audio_candidates` (for example `HOOK1.mov` and `VOIX OFF/HOOK1.1.m4a`). Audio files in `footage/` are indexed as `audio` entries and are valid `audio_from` sources. AdAnvil does not sync them: lay the file on the segment with `audio_from`, then check on a preview that the lips and the voice match.
194
+ - **What the creator says is a claim.** A kept spoken word that is a banned word of the brand raises `banned_word_spoken`; cut it out or take another take. Apply the same compliance questions to the speech as to the text.
195
+ - An image (end card) and a clip without sound are silence: keep the music under the end card so the audio does not stop before the video (QC `audio_tail`).
196
+ - Captions come from the speech (`speech_captions`: chunks of 3 words, upper case, outline, hesitations left out), not from typed text, so they follow every cut; check one chunk with `caption_preview`.
197
+ - **Sound design** (optional): a whoosh on a cut, a pop when a caption appears, a ding on the price, a riser before the reveal, cuts on the beat of the music.
198
+ - Build the pack once: `sfx_list(fetch=true)` or `adanvil sfx-fetch <project_dir>` puts about 18 sounds in `sfx/`, each from a pinned CC0 source or synthesised in code, trimmed and set to one level (loudest 400 ms at -16 LUFS, peak under -1.5 dBFS); sources and licences are in `sfx/LICENCES.txt`. Your own files in `sfx/` work too.
199
+ - Place a sound with `{"type": "sfx", "source": "sfx/pop.wav", "at": 3.2, "volume": 0.8}`, or let `audio.sfx_auto` do it (`cuts`, `captions`, `every`, `min_gap` 0.6 s, `volume` 0.5). Sounds are mixed over the finished bed, never clip (true peak under -1 dB), never pass the end of the ad, and work with a voice, clips or music alone. A riser stops at its peak: it ends where the reveal begins.
200
+ - Use sounds on fast text ads, reveals and offers. A sound on every cut gets tiring: every second cut, or only the key moments (the reveal, the price, the end card). One kind of sound per role, under a voice (volume 0.3 to 0.6), never three at once. None on a calm testimonial or a creator talking.
201
+ - Music-led ads: cut on the beat. `music_beats` lists the beats of the track as it will play; `storyboard_snap` moves each cut within `max_shift` of a beat onto it (it keeps `in` points, segment order and the total duration, and never makes a segment shorter than 0.5 s; with `save_as` the new storyboard is named `save_as` and renders to `renders/<save_as>.mp4`, without it the name gets `_snapped`, so a render never overwrites the original); `storyboard_validate` reports an info `beat_alignment` with the share of cuts on a beat. Errors: `sfx_missing`, `sfx_not_audio`, `sfx_after_end`; warning `sfx_auto_empty`.
202
+ - **Never put `loudnorm` in a filter graph that also trims the output length.** It eats the last one or two seconds. Normalise the voice on its own first, then mix.
203
+ - Check that audio duration equals video duration and that the level is not zero in the last seconds.
204
+
205
+ ## 8. Colour and technical traps the tool already handles
206
+
207
+ You do not need to work around these. Know they exist so you check the outcome, not the mechanism.
208
+
209
+ - **HDR sources**: phone rushes may be HLG or PQ. AdAnvil tone-maps them to SDR and tags the output bt709, because otherwise SDR shots turn dark red on some players and the contact sheet will not show it. Do not pre-convert your sources.
210
+ - **Dark shots**: an automatic gamma lifts shots whose mean brightness is low. If a shot still looks muddy, pick another shot rather than stacking corrections.
211
+ - **Still images**: a still image inside a montage is rendered without per-frame zoom, since that makes it jitter by a pixel. Use a static image over a blurred copy of itself. Do not ask for an animated zoom on a still in the middle of an ad.
212
+ - **End card zoom**: the end image is upscaled first, so the slow zoom stays smooth.
213
+ - **Zoom on video**: done with a time-driven scale and crop, never a `zoompan` filter on video (it produced tens of thousands of frames for a two second shot).
214
+ - **Rotation and framing**: the tool honours the rotation flag of phone footage. Give crops in the coordinates you see on screen.
215
+ - **No fake close-ups.** `crop` is for removing edges or burnt-in text, up to about 1.25x. Cropping a medium shot to fake a close-up looks zoomed and soft. Close-ups come from footage filmed close. `storyboard_validate` warns `digital_zoom` when a segment enlarges its source more than 1.25x beyond a plain cover-fit (`crop`, `zoom.to` and `freeze.zoom` all count).
216
+ - **Burnt-in text removal**: three methods, chosen per segment. `delogo` (a measured box, then a caption at least as large as the box), `mask` (a pixel mask for fixed text, invisible under a transparent caption), `crop_out` (a band above or below the text, cleanest when the text sits near an edge). If a caption is placed over erased text, make it cover the erased area.
217
+ - **Freeze and zoom** on an inset (before/after inserts): end the segment on the frame you want, then hold and zoom towards the inset.
218
+ - **Output contract**: H.264, yuv420p 8-bit, bt709 tags, 30 fps, 1080x1920, AAC. If a player shows wrong colours, probe the file before touching the edit.
219
+
220
+ ## 9. QC checklist
221
+
222
+ Look at the contact sheet first, then at full-resolution crops of the risky zones, then run the technical checks.
223
+
224
+ **On the contact sheet (16 frames)**
225
+ - Text is readable, not cut, not overflowing the width, and never on a face.
226
+ - The product is visible where it should be, and every product-only shot shows the current version.
227
+ - No black bars, no letterbox leftovers, no shot that is very dark or very red compared with its neighbours.
228
+ - People are varied (as in the reference) under a narrator voice-over or a general text, and the same person throughout when that person speaks on camera or the text says "I".
229
+ - The story reads without sound.
230
+ - No product on screen before the story turns to the solution; the opening faces live the problem.
231
+ - Every B-roll or stock shot shows the brand's own category and target and says the same thing as its text; every diagram or 3D animation is readable in one second, and when in doubt a real usage shot replaced it.
232
+ - Every face fits its text: no smile under a complaint, a visible reaction where the reference has one.
233
+
234
+ **On full-resolution crops**
235
+ - Areas where burnt-in text was removed, at three different instants.
236
+ - The end card during its zoom (a tile of the same detail every six frames should not jump).
237
+ - Emoji, small text and caption edges.
238
+ - Freeze and zoom shots on the inset.
239
+
240
+ **Technical**
241
+ - The file decodes without error and its duration and 1080x1920 size are as expected.
242
+ - Pixel format and colour tags are `yuv420p`, bt709, bt709, bt709.
243
+ - Audio and video durations match, the audio is present until the end, the voice is not cut mid-word (with `audio.clips`: no `cut_mid_word` warning, no stretch of silence longer than the cut margins inside the speech, and no `banned_word_spoken` left).
244
+ - Every caption is present until its end time, and the last caption is not missing.
245
+ - The validator reports no failure, and every warning is fixed or justified.
246
+
247
+ **Score the ad before delivery** (`ad_score`)
248
+ - `qc_run` says the file is correct; `ad_score(project_dir, video, storyboard)` says whether the ad is any good. Run it on the final-quality render with its storyboard, LOOK at `safe_zones_preview.jpg` and at the frame of each top finding, fix the findings by impact (each has a time, a severity and a fix), render again and aim for 80 or more. `storyboard_validate(..., score_check=true)` runs its cheap half earlier, as warnings prefixed `score_`.
249
+ - Six sub-scores, each from 100 down by its findings (weights): hook 20, pacing 15, reading 20, story 20, safe zones 10, audio 15. Every threshold is a guideline, not a rule: a finding is a question.
250
+ - **Hook**, first 1.5 s: a cut or motion in the first second, a face or the product, a written text within 0.5 s, audio from the first half second.
251
+ - **Pacing**: cuts per 10 s by quarter, the longest shot, a shot over 4 s in the middle of a fast ad (shots of 2.5 s or less on average), a shot over 6 s, a slow ad.
252
+ - **Reading**: a written caption stays on screen 0.25 s per word + 0.6 s (at least 1.2 s), at most 7 words per line, and a contrast of 3:1 or more against the pixels around its letters on the real frames (a WCAG-like luminance ratio; high under 2:1).
253
+ - **Story**: no product before the story turns to the solution, the product shown at least once, the call to action written or spoken (the brand's own wording when it has one), an end card or the call to action in the last 4 s.
254
+ - **Audio**: loudness from -20 to -10 LUFS, the voice at least 4.5 dB above the music (the default `music_volume` 0.35 leaves about 5 dB and passes), no silence of 0.7 s or more.
255
+ - **Platform safe zones**: the apps draw a top bar, a column of buttons on the right and a bottom band (account, description, call to action) over the ad, and a caption or key sticker under them is hidden. Pixel boxes (left, top, right, bottom) on a 1080x1920 frame, scaled to other sizes; a box that overlaps one by 24 px or more in both directions is a finding. Stories have no button column.
256
+ - `tiktok` `top_bar` (0, 0, 1080, 150), `tiktok` `right_buttons` (900, 760, 1080, 1480), `tiktok` `bottom_band` (0, 1480, 1080, 1920)
257
+ - `reels` `top_bar` (0, 0, 1080, 250), `reels` `right_buttons` (940, 1000, 1080, 1580), `reels` `bottom_band` (0, 1580, 1080, 1920)
258
+ - `stories` `top_bar` (0, 0, 1080, 250), `stories` `bottom_band` (0, 1580, 1080, 1920)
259
+ - `feed` `top_bar` (0, 0, 1080, 100), `feed` `bottom_band` (0, 1780, 1080, 1920): the placement of 4:5 and 1:1 renders, scaled to the frame height; `ad_score` picks it by itself for such a video
260
+ - Without the storyboard the score runs on the pixels and the sound and lists in `skipped` the checks that need words, timings or roles.
261
+
262
+ **Compliance, once more on the final text**
263
+ - No banned word, no medical claim, no unconfirmed mention, and the guarantee, price and offer are consistent with the landing page.
264
+ - Report to the human whatever remains open.
265
+
266
+ ## 10. Iterating with the human
267
+
268
+ - **Deliver a complete video**, not a storyboard in words. The human judges on the real video and iterates quickly.
269
+ - **Change only what was asked.** Re-render the same video with the requested change, keep everything else identical, and keep the same file name.
270
+ - **Apply cross-cutting fixes to the whole batch.** If a font, an emoji provider, a call to action or a colour setting is corrected on one ad, correct it on all ads that share the same process before the human has to ask.
271
+ - **Keep versions.** Put earlier renders in a clearly named folder (for example `previous/v1`) so the human never confuses them, and never overwrite a version they have not seen.
272
+ - **Wait for approval before naming and filing.** Name and store the final files only after the human says the video is right, so you do not update a filed file on every round.
273
+ - **Read the feedback as a rule.** When a correction generalises (a placement, a size, a rhythm, a style refusal), write it into the project's learnings file with the date, so the next ad starts from it. Consolidate the learnings into the recipes on a regular schedule.
274
+ - **Do not add what they did not ask for.** An ad that already tells a story is improved by cutting, not by adding.
275
+ - When a decision is theirs (a risky concept, a claim, a person), give your reservation once, execute what they decide and record it.
276
+
277
+ ## 11. Testing variants and formats
278
+
279
+ Paid social is won by testing. One ad is a guess; a handful of versions that differ in ONE thing tell you which guess was right.
280
+
281
+ **What to vary first: the hook, the first 2 to 3 seconds.** It decides whether anyone watches the rest. Then the call to action, then the body. Change one axis at a time when the budget is small, and keep EVERYTHING else identical (same footage after the hook, same voice, same music, same end card), or the result tells you nothing.
282
+
283
+ **How many.** A common start is 3 hooks x 1 body x 2 calls to action = 6 versions. More than that splits the budget so thin that none of them gets a verdict. The matrix is capped at 24.
284
+
285
+ **The tools.**
286
+ - `variants_build(project_dir, base, variants, name_pattern="{base}_{hook}_{cta}")`: `variants` is `{axis: [{id, changes}]}` for up to 3 axes (`hook`, `body`, `cta`, or any names). A change is a `storyboard_patch` change (`{path, value}` or `{path, remove: true}`) or a slot replacement `{replace_label: "hook", segments: [...], overlays: [...]}`: it swaps all the segments of that label (they must be next to each other) and moves everything after the slot by the difference of duration. Overlays that were on the old segments go away; the new `overlays` are timed from the start of the slot; the later overlays, their word times and the end of the overlays that span the slot follow. It is refused, with the reason, when a recorded voice-over (`audio.voice`) would no longer match the picture, when an overlay only partly overlaps the slot (the test has a tolerance of 0.001 s, so a range built from summed durations still counts as inside), or when the label is not one run of segments. An option with `changes: []` (or no `changes`) is the control: the base unchanged. Timed sounds follow the slot like overlays: an `sfx` overlay after it moves by the difference of duration; one inside it goes away when the new slot brings its own `sfx` (timed from the slot start), else it stays at the same offset; the result lists what moved (`moved`). Every error of every variant comes back at once (variant, path, message), not one per call. A base that is itself a variant keeps its ids in the new ones (`hook=h2;cta=ca;fix=sq`). Each variant is saved as `storyboards/<name>.json` and validated (an existing storyboard file is NEVER overwritten, whoever made it, an earlier `variants_build` of the same base included: the error lists every file that would be replaced, `overwrite=true` replaces them; renders may be replaced, storyboards may not); the result lists duration and warnings, and a contact sheet shows two strips per variant, the first 3 s on top and the last 3 s below (so a call to action that differs between variants is visible; when the ad ends on an image end card, the bottom strip is the 3 s BEFORE the card, the CTA part, because a card shared by all variants would make every bottom row identical): LOOK at it, a hook that does not read at that size will not read in the feed.
287
+ - `render_batch(project_dir, storyboards, quality, batch, formats)` (`quality` is `final` or `preview`; `draft` is an alias of `preview` everywhere a quality is taken: the files are `*.preview.mp4` at half the size, and the report says `preview`): ONE detached job renders them in order with a shared segment cache, so the body and the end card are built once for all the versions (the first render is the slow one, the others only encode). `job_status` shows the progress per render, each output as it finishes, the total time and `cache_builds`.
288
+ - `render_formats(project_dir, storyboard, formats)` or the storyboard field `formats: ["9:16", "4:5", "1:1"]`: the same ad in each ratio.
289
+
290
+ **Formats per placement.** `9:16` for Stories, Reels and TikTok, `4:5` for the Feed (the largest vertical picture the Feed shows), `1:1` for the placements that still want a square. All three are 1080 px wide. For each format the footage is re-cropped (face-aware with `focus: "face"`, else by `focus`), a still or an end card that does not fit is shown whole over its blurred copy, and captions, stickers and images keep their place by bands: the part of the frame the platform leaves free is mapped onto the free part of the new frame, then anything that would still leave it is pushed back inside (the notes of the result list each move). Text is scaled to the format: `min(1, out_height / design_height * k)`, which keeps the share of the frame height in 4:5 and about 0.8 of it in 1:1, never under 38 px for main text. A caption that lands on a still or an end card shown whole is moved to the largest free band of the picture (above, below, or an empty area of the picture), so it never covers the card's text or button; if no band fits, it shrinks to the minimum and the result carries `caption_over_card_content`. The same warning comes from validation and `ad_score` in 9:16 when a caption sits on a card's content, and `qc_run` saves the end card frame (`endcard.jpg`). A caption never sits on a face in another format: `render_formats` carries the face boxes of the footage under each caption (the stored vision faces, else the vision worker when it is installed, else the check is skipped and a note says so) to the new frame, and a caption keeps the place the re-layout gave it when that place is free; otherwise it moves the least it can, to the nearest free spot on the same side of the face (a hook at the top stays above the head), clear of the content of a still shown whole and, when possible, of the other overlays on screen; it shrinks to the readable minimum if needed, else the note `caption_on_face` says it cannot. A spot is only taken after it is CHECKED: the caption is moved there, measured, and `caption_on_face` is run on that final position against the faces of every sampled moment of its time range (one look every 0.2 s, not a handful); a spot that fails is dropped for the next one (other side of the face, then a smaller size). When the head crosses the whole free band at some moment, the caption goes where it covers a face at the fewest looks and the note says how many. Each move is in the notes with its distance (`moved +33px off a face (same side of it)`), and only a move that passed the check says "off a face". After the layout AND the per-format overrides, one final check on the final position gives the last note per caption (`caption_on_face: overlay 3 (caption) cannot avoid a face in 1:1 (it covers one at 7.9s ...)`, or `still covers a face ... after its formats[...].y`); `qc_run` repeats the same test on the rendered frames (four looks per caption) and, when the source face data and the rendered-frame detection disagree, reports both (`source face data says it covers a face; rendered-frame detection finds none`). "On a face" means on the face core: the detector box from the eye line to the chin with a small margin, so a caption above the hair or on the forehead does not count. Detections that are not people (a drawing or diagram on a tablet or poster: confidence under 0.88) and tiny faces (under 6% of the picture width) are ignored. The layout is predicted from faces detected on the source footage at each sampled moment (with the same detector and frame size as `qc_run`, not the stored index, which is sampled every second or so and moves too little for a head that turns), so on an extreme close-up with a zoom or a punch check the frame all the same. `ad_score` and `qc_run` run the same test (one function, same core box and same filters) on the rendered frames, in every format including 9:16 (finding or check `caption_on_face`, needs the vision environment). To place one overlay by hand in one format, give it `formats: {"1:1": {"y": 900}, "4:5": {"hidden": true}}` (keys `x`, `y`, `size`, `anchor`, `hidden`; `x` is `x_center` for a caption): applied last, after the automatic layout, for that format only. When the storyboard is already 9:16, the 9:16 output is the storyboard as it is (the band remap applies to the other ratios only), so it scores like the plain `render_batch` render. `qc_run` crops its overlays where the effective storyboard of that render has them (`renders/<batch>/_storyboards/<name>.json`, found by itself or passed as `storyboard`), so a moved caption is not cut in half. `ad_score` and `qc_run` use the safe zones of the matching placement (`feed`: a 100 px band at the top and 140 px at the bottom on a 1080x1920 frame, scaled to the frame height). Look at each format: a face that is cropped, a caption that sits on the product, or a card that lost its button needs a hand-made version, not a layout rule.
291
+
292
+ **Naming and manifest.** `<base>_<hook>_<cta>` for a variant, `_9x16`, `_4x5` or `_1x1` added for a format: `summer_h2_c1_4x5.mp4`. Every batch writes `renders/<batch>/manifest.csv` with one row per file (`file`, `variant ids`, `format`, `duration`, `score`, `hook text`, `CTA id`, `CTA text`, `music licence`, `quality`), ready to upload to an ads manager, next to the videos, their `.report.json` and `_sheet.jpg`. `CTA text` is the text of the last call to action part (the segments labelled `cta` or `end_card`, else the segments after the last labelled one), each text with its role and the most prominent first, joined by ` | `: `caption: 65,90 € au lieu de 89,90 € | badge: Satisfaite ou remboursée 60 jours`. A phrase that two CTAs share in different roles (the guarantee as the big caption in B, as the badge in A) therefore still tells them apart; an emoji alone never does. `CTA id` is the id of the `cta` axis; when the batch holds several CTA variants, `CTA text` lists only the role-and-text pieces that are not shared by all of them (the whole text when nothing differs or there is no `cta` axis; computed over the variants rendered in that call). `music licence` is the licence of `audio.music` (`CC0 1.0`, `CC BY 4.0; attribution required`, `unknown` for a track with no licence record, empty without music): read it before uploading. `quality` is the quality that was rendered AND scored (`preview` or `final`): a preview scores lower than its final, so compare only rows of the same quality. The `width` and `height` of each render in the job report are those of the file written (a preview is half the storyboard's size). Older manifests are read with an empty `quality`. Rendering into an existing batch folder merges the rows by file name (the same file is replaced, the others are kept). The score is `ad_score` of the render (preview renders too, on the preview file) with the safe zones of its placement.
Binary file
@@ -0,0 +1,93 @@
1
+ Copyright 2024 The Montserrat.Git Project Authors (https://github.com/JulietaUla/Montserrat.git)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ https://openfontlicense.org
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,93 @@
1
+ Copyright 2020 The Plus Jakarta Sans Project Authors (https://github.com/tokotype/PlusJakartaSans)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.