Make the pre-roll move, and give it a voice - #291
Merged
Merged
Conversation
The rendered ads were a still image with a five-second duration. Frames at 0.2s, 2s and 4s were indistinguishable: the copy sat in the lower-left of a 1920x1080 frame with the top half empty, and the motion was a 40px rise and a 4% scale spread across the whole ad, which at that size is invisible. The composition is now centred and fills the frame, the headline is sized to its own length rather than fixed, and the animation has beats a viewer can actually see: the brand and an accent rule arrive, the headline builds a word at a time, the CTA lands with a small overshoot, and the artwork pushes 12% across the ad instead of 4%. The old test required the whole headline to be legible at frame 0. Honouring that is what produced an ad where nothing happened, so the rule is now narrower and still protects the opening instant: the brand is visible immediately — the poster is cut from frame 0 — and the copy finishes assembling inside the first quarter of the ad rather than half of it, with the CTA on screen for roughly the back half. Narration is new. The script is derived from the approved copy rather than written afresh, because an ad whose voiceover claims something its banner does not is a compliance problem: headline, then the call to action with the destination, which is the whole of what five seconds can say. It is recorded in the snapshot, so it is part of the render hash — two campaigns with identical copy share one encode, and changing the copy changes the voiceover along with the picture. Synthesis is best effort in the strict sense: no key, a non-200, an empty body or a network error all return null and the silent cut ships. A pre-roll with no audio is a working ad; a render that fails because a speech API was rate limited is not. An explicit null key now means "no key" rather than falling through to the environment. `??` had collapsed those two cases, so a caller asking for silence quietly performed a paid API call. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan48 finding(s) HIGH/CRITICAL: 2 | MEDIUM: 31 | LOW: 15
Snippets are redacted; ThreatCrush never prints matched credential material. |
| if (buf.byteLength === 0) return null; | ||
|
|
||
| const filePath = path.join(input.workDir, "narration.mp3"); | ||
| await writeFile(filePath, buf); |
| ): string { | ||
| const headline = escapeHtml(snapshot.headline); | ||
| const headlineText = snapshot.headline; | ||
| const headline = escapeHtml(headlineText); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You called the output garbage with no audio. Both fair — here's what was wrong and what changed.
It was a still image with a duration
Frames at 0.2s, 2s and 4s were indistinguishable. The copy sat in the lower-left of a 1920×1080 frame with the top half empty, and the entire motion budget was a 40px rise and a 4% scale spread across five seconds — invisible at that size.
Now:
A rule I deliberately changed
The old test required the whole headline to be legible at frame 0 — "an ad that spends its first beat assembling itself says nothing for a tenth of its life." Honouring that literally is what produced an ad where nothing happened.
The rule is now narrower and still protects the opening instant: the brand is visible immediately (the poster is cut from frame 0), the copy finishes assembling inside the first quarter of the ad rather than half of it, and the CTA is on screen for roughly the back half. The test says so, with the reasoning.
Narration
The script is derived from the approved copy, never written afresh — an ad whose voiceover claims something its banner does not is a compliance problem, not a stylistic one. Headline, then the call to action with the destination:
It's recorded in the snapshot, so it is part of the render hash: two campaigns with identical copy share one encode, and changing the copy changes the voiceover along with the picture. Captions already follow from the same field.
Synthesis is best-effort in the strict sense. No key, a non-200, an empty body, or a network error all return null and the silent cut ships. A pre-roll with no audio is a working ad; a render that fails because a speech API was rate-limited is not.
One bug found by its own test: an explicit
nullkey fell through to the environment variable, because??collapsed "no key" and "use the configured key" into one case — so a caller asking for silence quietly performed a paid API call.Cost
ElevenLabs currently shows 72,673 characters remaining this period. At ~50 characters a script, 174 campaigns is ~8,700 — comfortably inside it.
ELEVENLABS_API_KEYis provisioned on the Railway service (value never printed).Verification
10 new narration tests, plus the updated composition and snapshot rules. 2580 passed / 1 failed repo-wide — the pre-existing
tracker-geofailure. Both typechecks clean.The visual and audio result can only be judged on a real render, so I'll queue one after this deploys, download it, and look at it before saying it's fixed.