Variant Multiplier already let an editor swap one section of a winning ad and keep the rest. The next request from a real production job — replacing product SL-603 with SL-808, a different hearing-aid SKU, across an entire finished ad — was a different shape of problem. It's not "change one section," it's "change every mention of the product, everywhere it appears, while keeping literally everything else the same." Two direct quotes from the editor drove the whole five-PR arc: the transcript editing was too rigid for word-by-word changes, and separately, "the music, voice, etc. should retain the same, we should keep the quality the same, and not make it do a lot of changes."
If a re-render can degrade something the editor explicitly asked to keep untouched, the render path is wrong for the job — no matter how good the model is.
The cheap fix first: let editors actually edit
PR #67 shipped before any product-swap work started, because it was the cheap, high-value half of the same feedback: "I am just able to select word by word here but I am not really able to change the whole sentence a lot easier," and separately, "I'm able to double click on these words and then just type it in." Both were UI gaps in the transcript editor, not pipeline gaps — selecting by sentence or scene instead of only by word, and retyping a line verbatim instead of only substituting individual words. Shipping this first, standalone, meant the harder product-swap work that followed didn't also have to carry an unrelated UX fix in its diff.
A product catalog the tool never had
PR #69, stacked directly on top of the transcript work, is pure groundwork with no user-visible feature of its own: a product catalog, because Variant Multiplier had no concept of "a product" at all before this. The editor's own framing made the requirement explicit: "have a product selection right here, for Pro Bluetooth, for [the other SKU], and maybe other tons of products" going forward. The catalog data itself is maintained in the main video-generation service and synced into this tool rather than duplicated and drifted — one canonical source for product identity, read by whichever tool needs it.
Planning the swap before touching a single frame
PR #70 is the center of the whole arc, and its title states the engineering decision directly: plan the swap deterministically first, and only fall back to a model where a deterministic rule genuinely can't decide. The editor's own words framed the danger of the naive approach: "if I click here Pro 3.0 → Pro Bluetooth, the AI will be able to change everything" — technically true, and exactly the failure mode to avoid, because "change everything" is also how you accidentally change the music, the pacing, or the voice quality nobody asked to touch.
type SwapPlanLine = {
lineIndex: number;
originalText: string;
matchKind: 'deterministic' | 'model-required';
newText: string;
};
function planLine(line: TranscriptLine, from: Product, to: Product): SwapPlanLine {
// exact product-name substitution: deterministic, no model call
if (line.text.includes(from.name)) {
return {
lineIndex: line.index,
originalText: line.text,
matchKind: 'deterministic',
newText: line.text.replaceAll(from.name, to.name),
};
}
// an indirect reference ("this device", a spec number tied to the old product)
// has no deterministic rule — fall to the model, scoped to this one line
return {
lineIndex: line.index,
originalText: line.text,
matchKind: 'model-required',
newText: null, // resolved by a scoped model call, not a blanket rewrite
};
}
The output of this PR is a plan and a per-line diff the editor reviews before anything renders — not a video. Deciding what changes is separated cleanly from rendering the change, which is what makes the next PR possible without redoing this one.
Audio-only render: a byte-identical picture
PR #71 renders the plan, and it's the PR that actually delivers on "keep the quality the same, don't make a lot of changes" — taken literally rather than as a vague aspiration. The existing render path, built for section swaps, re-encodes every segment through libx264 because it has to concatenate generated b-roll with master footage. Running a pure audio change through that path would re-encode, and therefore degrade, 100% of a picture that never needed to change at all. The fix instead reissues the ad with new audio and a picture that is byte-identical to the original master — no re-encode, because the video stream simply isn't touched. That's the difference between "we tried to preserve quality" and "there is no quality to lose because the bits didn't move."
Captions were the gap nobody had asked about yet
PR #73 came from a single follow-up question, not a bug report: if the ad has captions, do we update them? The team hadn't — because the swap copies the picture untouched, captions baked into the master's pixels survived exactly as they were, now describing the wrong product on screen while the new audio said something else. On the real SL-603 job, one caption line still read the old product's brand name on screen while the new voiceover said the new SKU's name — a rejection risk on an actual client deliverable, not a cosmetic gap. The fix regenerates captions from the finished swapped audio, so the on-screen text and the spoken audio agree again.
Verifying the fix against the in-house reference, not just testing it
PR #74 is a short, disciplined close-out: before considering #73 done, review its caption approach against the equivalent module in the main video-generation service, which both tools treat as the in-house reference implementation for captions. The review confirmed the approach was already right — both use subtitle-track rendering rather than burned-in draw commands, and Variant Multiplier's captions module carries forward the sibling's own hard-won caption-timing fixes rather than reinventing them. Checking a new implementation against the team's own established reference, rather than shipping the first version that passed its own tests, is what caught that this one didn't need a second round.
The pattern: separate the decision from the render
The core lesson across five PRs is the same one #70 states outright in its title: plan deterministically, and reach for a model only for the specific lines a deterministic rule genuinely can't resolve. A full-ad product swap sounds like it demands a single large generative pass. In practice, the vast majority of a script is exact product-name substitution — free, instant, and impossible to get subtly wrong the way a model rewrite can be. Reserving the model for the handful of lines that reference the product indirectly, and reserving a re-encode for the rare case that genuinely needs one, is what let this feature ship with a render path that provably can't degrade the 95% of the ad that was never supposed to change.