Whole-Ad Product Swap: Deterministic Planning First, Model Only Where Forced

Whole-Ad Product Swap: Deterministic Planning First, Model Only Where Forced

全广告产品替换:优先确定性规划,仅在必要时使用模型

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.” Variant Multiplier 已经允许编辑替换成功广告中的某一个片段,并保留其余部分。来自实际生产任务的下一个需求——将整个成品广告中的 SL-603 产品替换为另一款助听器 SKU(SL-808)——则是一个不同性质的问题。这不再是“修改一个片段”,而是“在保持其他所有内容完全不变的前提下,修改广告中每一处提到的产品名称”。

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. 编辑的两句直接引语推动了整个包含五个 PR(Pull Request)的开发周期:一是转录编辑功能对于逐词修改来说过于僵化;二是“音乐、配音等应该保持不变,我们需要维持质量,不要进行过多的改动”。如果重新渲染会导致编辑明确要求保留的部分受损,那么无论模型有多强大,渲染路径本身就是错误的。

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. 先解决简单的:让编辑能够真正进行编辑。PR #67 在任何产品替换工作开始前就已发布,因为它对应了反馈中低成本且高价值的部分:“我在这里只能逐词选择,无法轻松修改整个句子”,以及“我希望能双击这些词然后直接输入”。这两者都是转录编辑器中的 UI 缺陷,而非流水线缺陷——即实现按句子或场景选择而非仅按单词选择,以及能够直接重写整行文字而非仅替换单个单词。将此功能作为独立部分先行发布,意味着后续更复杂的产品替换工作不必在代码变更中夹杂不相关的用户体验修复。

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. 该工具此前从未有过产品目录。PR #69 紧接在转录工作之后,属于纯粹的基础设施建设,本身没有用户可见的功能:即建立一个产品目录,因为在此之前 Variant Multiplier 对“产品”这一概念完全没有定义。编辑的表述明确了这一需求:“在这里提供一个产品选择器,用于选择 Pro Bluetooth、[其他 SKU] 以及未来可能涉及的成千上万种产品”。目录数据本身维护在主视频生成服务中,并同步到此工具,而不是进行复制导致数据不一致——确保产品身份有一个权威来源,供任何需要的工具读取。

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. 在触碰任何帧之前先规划替换。PR #70 是整个开发周期的核心,其标题直接阐明了工程决策:优先进行确定性规划,仅在确定性规则确实无法判断时才回退到模型。编辑的话语道出了简单粗暴方法的危险性:“如果我点击这里将 Pro 3.0 替换为 Pro Bluetooth,AI 就能改变一切”——从技术上讲确实如此,但这正是要避免的故障模式,因为“改变一切”也意味着你可能会意外改变音乐、节奏或没人要求改动的音质。

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. 该 PR 的输出是一个规划方案和逐行差异对比,编辑在任何渲染操作前都会进行审查,而不是直接输出视频。将“决定修改什么”与“渲染修改内容”清晰地分离开来,这使得后续的 PR 可以在不重做当前工作的前提下进行。

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.” 仅音频渲染:字节完全相同的画面。PR #71 执行了该规划,它是真正实现“保持质量不变,不要进行过多改动”这一目标的 PR——这是字面意义上的实现,而非模糊的愿景。现有的渲染路径是为片段替换构建的,它通过 libx264 重新编码每个片段,因为它必须将生成的 B-roll(辅助镜头)与主素材拼接。如果通过该路径进行纯音频修改,会导致 100% 的画面被重新编码并因此受损,而这些画面本无需任何改动。修复方案改为发布带有新音频的广告,同时保持画面与原始母带字节完全一致——无需重新编码,因为视频流根本没有被触碰。这就是“我们试图保持质量”与“因为比特位没有移动,所以不存在质量损失”之间的区别。

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. 字幕是没人问起但确实存在的缺口。PR #73 源于一个后续问题,而非错误报告:如果广告有字幕,我们需要更新它们吗?团队之前没有这样做——因为替换过程原封不动地复制了画面,嵌入在母带像素中的字幕被保留了下来,导致屏幕上显示的还是错误的产品名称,而新的音频却在说另一个产品。在实际的 SL-603 任务中,一行字幕在屏幕上显示的是旧产品的品牌名,而新的配音却在说新 SKU 的名称——这对实际交付给客户的产品来说是拒收风险,而不仅仅是外观上的小瑕疵。修复方案是从替换后的音频中重新生成字幕,从而确保屏幕文字与语音内容再次一致。

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. 对照内部参考标准验证修复,而不仅仅是进行测试。PR #74 是一个简短且严谨的收尾工作:在认为 #73 完成之前,将其字幕处理方案与主视频生成服务中的对应模块进行比对,后者被两个工具视为字幕处理的内部参考实现。审查确认该方案是正确的——两者都使用字幕轨道渲染而非硬编码的绘图指令,并且 Variant Multiplier 的字幕模块继承了兄弟模块在字幕时序上积累的宝贵修复经验,而不是重复造轮子。对照团队既定的参考标准来检查新实现,而不是直接发布。