The Review Room: A Human-in-the-Loop Backend for Previewing, Editing Lines, and Regenerating a Single Shot
A pipeline can't be fully automatic; today give it a web review console: view assets shot by shot, edit dialogue, regenerate a single shot, and turn every human edit into a traceable version.
Today's Goals
- Build a review page that displays assets shot by shot, supporting preview, line editing, and single-shot regeneration
- Feed human edits back into the workflow, so regeneration only affects the downstream nodes it touches
- Explain what version comparison and rollback need to store, and the trade-offs of a canvas-style workbench
Yesterday pushed output to several episodes in parallel; today we face its first consequence: once volume rises, human checking becomes the new bottleneck. When you finish, scroll back up and tick the three goals off.
Plain-Language Walkthrough
The screening room: where the human steps in
A proper production has a room called the screening room. It is not on set and not in the edit suite; it exists purely for looking. Director, producer and platform sit together, go through material take by take, and say keep this one, reshoot that one, change that line.
The room's existence is itself an engineering judgment: a film is not checked after shooting, it is checked while shooting. Wait until everything is shot and discover the actor wore the wrong jacket in shot three, and every shot that cuts against it was shot for nothing.
A generative pipeline is the same, only harsher — every step costs money, and rework is written straight onto the bill. So the question is not whether a human should intervene but where.
Three natural checkpoints, judged by how expensive that step's downstream is:
First, after the script is locked and before any asset is generated. Nothing has been spent yet, changing a line costs approximately zero, and it sets the direction of every asset that follows. This is the highest-value checkpoint and the easiest to skip, because there is no picture yet and it looks like there is "nothing to review."
Second, after first frames exist and before video generation. First frames are the cheapest tier on this line and video is the most expensive, one to two orders of magnitude apart for the same shot. Generating video from a bad first frame means spending the big money on a discarded image. This is the single highest-value human gate on the whole line.
Third, after the cut is assembled and before publishing. This one catches risk rather than quality: content safety, copyright, labeling compliance. Tomorrow covers it in full.
Of the three, today's console mainly serves the second: look at assets shot by shot and stop the line at any point.
Minimal information architecture: run, episode, shot
What the console looks like depends on how many layers you split the information into. This line's natural structure has three: a run, an episode, a shot.
A run is the outer layer, one runId and one directory. It answers "when was this batch produced and from what input."
An episode is the middle layer and the unit people actually care about. Operations does not ask whether shot seventeen is done; they ask whether episode three can ship.
A shot is the innermost layer and the only unit that can be redone on its own. This is the judgment that keeps recurring in this course: if the smallest redo unit is an episode, changing one line re-burns forty shots of video; if it is a shot, changing one line affects one shot's dubbing.
Lay the page out by shot, five things per card: the first frame, the shot video, the line, the voice audio, and that shot's version list. The page itself need not be complex; hand-written HTML and a few JSON endpoints suffice:
<section class="card">
<h3>s02 - close - 4s</h3>
<img src="/media/.../first-frame.png" alt="s02 first frame" />
<video src="/media/.../clip.mp4" controls></video>
<audio src="/media/.../voice-1.mp3" controls></audio>
<textarea rows="2">Five minutes from now? That is impossible</textarea>
<button>Compute blast radius</button>
<button class="primary">Regenerate affected nodes</button>
</section>The lab deliberately introduces no frontend framework: node:http serves it, the page is a single HTML file, and it still runs with pnpm install --ignore-workspace && pnpm start. The reason is that today teaches interfaces and data shapes, not build tooling. Ninety percent of a review console's value is in the backend data design; the frontend merely displays it.
The blast radius: who is affected by one changed line
When a human clicks regenerate, the system must answer one question: what did this edit invalidate?
The answer is computed from the dependency graph, not guessed. The relationships here are clear: the visual description determines the first frame, the first frame determines the shot video; the line determines the voice; every video and voice together determine the timeline.
So changing one line invalidates that shot's voice and the episode's timeline — because the voice duration changed and later shots' start and end times must be re-laid. The first frame and the video are entirely unaffected, not one frame re-burned. Changing the visual description is the opposite: first frame, video and timeline are invalidated, and the voice is untouched.
Mind the direction of the algorithm: propagate forward along "who depends on me" from the edited node, not upward to find dependencies. Searching upward re-runs unaffected upstream work, and it is the most common way to get this backwards.
// Dependency edges: frame -> clip; dialogue -> voice; all clips and voices -> timeline
const graph = [
{ id: 's02:frame', deps: [] },
{ id: 's02:clip', deps: ['s02:frame'] },
{ id: 's02:voice', deps: [] },
{ id: 'episode:timeline', deps: ['s02:clip', 's02:voice'] },
]
// Editing dialogue seeds at the voice; editing visual seeds at the frame
const SEED = { dialogue: 'voice', visual: 'frame' }
function affectedNodes(graph, shotId, field) {
const affected = new Set([`${shotId}:${SEED[field]}`])
// Propagate forward along "who depends on me" until a fixed point
for (let changed = true; changed; ) {
changed = false
for (const n of graph) {
if (affected.has(n.id)) continue
if (n.deps.some((d) => affected.has(d))) {
affected.add(n.id)
changed = true
}
}
}
return graph.map((n) => n.id).filter((id) => affected.has(id))
}
// Editing a line -> ['s02:voice', 'episode:timeline']; not one frame of video is re-burned# Dependency edges: frame -> clip; dialogue -> voice; all clips and voices -> timeline
GRAPH = [
{"id": "s02:frame", "deps": []},
{"id": "s02:clip", "deps": ["s02:frame"]},
{"id": "s02:voice", "deps": []},
{"id": "episode:timeline", "deps": ["s02:clip", "s02:voice"]},
]
# Editing dialogue seeds at the voice; editing visual seeds at the frame
SEED = {"dialogue": "voice", "visual": "frame"}
def affected_nodes(graph, shot_id: str, field: str) -> list[str]:
affected = {f"{shot_id}:{SEED[field]}"}
changed = True
while changed: # propagate forward along "who depends on me" until a fixed point
changed = False
for node in graph:
if node["id"] in affected:
continue
if any(dep in affected for dep in node["deps"]):
affected.add(node["id"])
changed = True
return [n["id"] for n in graph if n["id"] in affected]
# Editing a line -> ['s02:voice', 'episode:timeline']; no video is re-burnedOne step after computing the radius is often missed: unaffected nodes must have their artifacts copied from the previous version, not regenerated. Without that, however accurate your computation, you saved nothing.
And once computed, show the radius to the human before running anything. The console's orange banner reads "pending: dialogue changed; affects s02's voice and the episode timeline." That looks like a mere notice; in practice it is the console's most valuable piece of information, because it tells the human what this edit will cost before they commit. If one changed line means one re-synthesized voice clip, they click without hesitation; if the notice says three shots of video will be re-burned, they will stop and think about whether the change is worth it. Moving the cost in front of the decision beats handing over a bill afterwards.
How do you prove you actually saved? Do not compare file hashes — offline, the same prompt generates a byte-identical placeholder image, so matching hashes prove nothing about whether work was skipped. Count the API calls: the lab's self-check prints "this regeneration made 0 image calls, 0 video calls, 1 speech call," and that evidence holds equally against a real vendor.
A version is not a backup
Someone edits a version, decides it was worse, and wants to go back. That sounds like a backup, and it is not.
A backup means "something broke, bring it back": one recent good state is kept, and after restoring, the old one is gone. A version means "both exist": old and new coexist, can be compared side by side, can be switched between, and which to ship is a human judgment.
Review needs the latter. When the director says "wasn't the earlier one better," you must be able to put both side by side immediately, not offer to restore one so they can look again.
In data terms the difference shows up in three places:
First, artifacts are stored in per-version directories, v1/ and v2/, with nothing deleted. Disk is vastly cheaper than regeneration.
Second, the current version is a pointer, not a copy. Rolling back moves the pointer, touches no files, and is therefore instant and reversible.
Third, each version records what it re-ran and why. Storing artifacts alone is not enough — three days later, when someone asks why version two is better than version one, you need an answer.
// A shot's state: a list of versions plus a current pointer. Rollback moves the pointer only.
const shotState = {
current: 2,
versions: [
{
version: 1,
at: '2026-09-07T10:00:00Z',
dialogue: 'Five minutes from now? That is impossible',
artifacts: { 's02:frame': 'shots/s02/v1/first-frame.png', 's02:voice': 'shots/s02/v1/voice-1.mp3' },
regenerated: ['s02:frame', 's02:clip', 's02:voice'],
reason: 'initial generation',
},
{
version: 2,
at: '2026-09-07T10:12:31Z',
dialogue: 'Five minutes from now? Someone is playing a joke on me',
artifacts: { 's02:frame': 'shots/s02/v2/first-frame.png', 's02:voice': 'shots/s02/v2/voice-1.mp3' },
regenerated: ['s02:voice', 'episode:timeline'], // the frame was copied from v1
reason: 'human edit to dialogue',
},
],
}
const rollback = (state, version) => ({ ...state, current: version })# A shot's state: a list of versions plus a current pointer. Rollback moves the pointer only.
shot_state = {
"current": 2,
"versions": [
{
"version": 1,
"at": "2026-09-07T10:00:00Z",
"dialogue": "Five minutes from now? That is impossible",
"artifacts": {
"s02:frame": "shots/s02/v1/first-frame.png",
"s02:voice": "shots/s02/v1/voice-1.mp3",
},
"regenerated": ["s02:frame", "s02:clip", "s02:voice"],
"reason": "initial generation",
},
{
"version": 2,
"at": "2026-09-07T10:12:31Z",
"dialogue": "Five minutes from now? Someone is playing a joke on me",
"artifacts": {
"s02:frame": "shots/s02/v2/first-frame.png",
"s02:voice": "shots/s02/v2/voice-1.mp3",
},
"regenerated": ["s02:voice", "episode:timeline"], # the frame was copied from v1
"reason": "human edit to dialogue",
},
],
}
def rollback(state: dict, version: int) -> dict:
return {**state, "current": version}One knock-on effect is easily forgotten: rolling back one shot changes the episode's timeline. If version two's voice is a second longer than version one's, switching back re-lays every later shot's start and end times. So rollback is not only a pointer move; it also recomputes the timeline — which is cheap, being pure local computation.
Canvas or list
Anyone who has seen AI video tools will ask: why not a canvas? The node-graph workbench with dragging and zooming looks far more professional.
A canvas does have something irreplaceable: when the dependency relationships themselves are what the user edits. If users decide which image a shot's first frame comes from, or how two branches merge, then the dependencies are the object of editing and a canvas is their only intuitive expression.
But the price is real:
Interaction cost is high. Doing one "edit line and regenerate" on a canvas means locating the node, opening it, editing, then finding the run button. On a list it is one text field and one button. Review happens hundreds of times a day, and three extra seconds each is half an hour.
Mobile is basically unusable. And review is exactly the kind of task suited to a phone — a producer can go through an episode on the move.
Implementation is an order of magnitude larger. Dragging, zooming, edges, auto-layout, undo and redo all have to be written, and written badly they are painful.
There is a more fundamental difference: review is rhythmic work. People want the next one, the next one, the next one — a conveyor-belt momentum, stopping when something is wrong and continuing after. A list supports that rhythm natively; scrolling is progress. A canvas asks the person to keep relocating themselves in space, and with two hundred nodes spread out, just finding the next shot to look at takes seconds. A tool's shape should follow the rhythm of the work, not the shape of the data.
The criterion is simple: fixed dependencies mean a list; user-editable dependencies mean a canvas. This line's dependencies are fixed — the first frame determines the video, the line determines the voice — and users neither need nor should change that. So this course chooses a list.
Review notes must be structured
One last thing, and the one most often built as decoration: what the human says during review, whether it is stored, and how.
Stored as free text — "the lead's face is a bit off in this shot" — it is useless to the next round of automation, which cannot read it. Stored as structured fields, it is a different matter:
{
"shotId": "s03",
"verdict": "redo",
"tags": ["face-inconsistent", "subtitle-overflow"],
"comment": "the lead's face shape does not match the look test",
"at": "2026-09-07T10:20:00Z"
}With verdict and tags you can write rules like: a shot tagged face-inconsistent twice in a row stops being auto-retried and is flagged for a human; a character with a significantly higher face-inconsistent rate needs its look test redone.
There is a bonus: structured, these records double as an evaluation set. Accumulate one or two hundred records with a verdict and you have labeled data for "what humans consider acceptable" — and tomorrow, when a vision model scores automatically, the threshold that matches human judgment is found by back-testing against that data rather than guessing. That is another reason to build the console early: from the day it goes live, it accumulates data for you.
That is what "make human judgment reusable by the next round of automation" means — not having a model read human prose, but having the human's notes born in a shape a machine can use. The price is a few more buttons and preset tags on the console instead of one all-purpose text box. Tomorrow's automated QC picks these records up directly: machine-scored conclusions and human review conclusions use the same verdict and tags vocabulary.
Source Reading
Hands-On Lab
This is the only day whose lab starts an HTTP service, on port 3080. A long-running service cannot be fed input through a pipe, so the acceptance entry point is MOCK=1 SELFTEST=1 pnpm start: the process starts, runs the whole flow itself, prints a pass or fail per item, and exits accordingly. To click around by hand, use MOCK=1 pnpm start and open port 3080 in a browser.
- Run the solution's self-check first, understand what each of the seven items verifies, then look at the starter's four exercises.
- Implement the blast-radius computation and confirm via the self-check that editing a line touches only the voice and the timeline.
- Implement "copy unaffected artifacts from the previous version" and confirm this regeneration made zero image and video calls.
- Implement rollback; in the browser, edit a line, regenerate, then roll back, and see the image and audio really switch back.
- Implement structured review notes on disk, and open review.json in the artifact directory to confirm the fields are complete.
Interview Questions
Today's three questions are in the question bank below, focused on choosing human intervention points, computing the impact scope of a partial regeneration, and the data design of versions and rollback. Read the analysis before the key points — practicing the derivation beats memorizing them. The cn / global labels let you pick by target market.
Checklist and Tomorrow
- I can build a review page that displays assets shot by shot, supporting preview, line editing, and single-shot regeneration
- I can feed human edits back into the workflow, so regeneration only affects the downstream nodes it touches
- I can explain what version comparison and rollback need to store, and the trade-offs of a canvas-style workbench
- I can name the three human checkpoints and why the first-frame one has the highest value
- I can explain the semantic difference between a version and a backup, and why rollback recomputes the timeline
- All 5 lab acceptance criteria pass
- I can answer at least 2 of the 3 interview questions without looking at the key points
Tomorrow (D11) we hand today's human judgment to a machine: a vision model scores the cut item by item, low-scoring shots are automatically sent back, and content safety plus generated-content labeling land in the export step. The order is deliberate — with a human review flow and structured conclusions in place, machine review has a target to align to; do automated QC first and you cannot even settle on what score counts as a pass.
Interview questions
Where would you place human review checkpoints in an automated pipeline, and why there?一条自动化流水线要插入人工审核,你会把卡点放在哪几步?为什么?
Common in ChinaCommon overseasBasic#human-in-the-loop#pipeline-design#costHow to reason about it · think before answering
- This one tests cost awareness. Saying a human should look at every step marks someone who has not run this in production: humans are the expensive resource, and too many gates turn a pipeline back into handwork.
- Offer a reusable rule: put the gate immediately before the most expensive downstream step. To decide whether a position deserves a gate, ask how much money is wasted if something is wrong here.
- Applied to a generative pipeline that yields three positions: after the script is locked (free to change, yet it steers every asset that follows), after the first frame but before video generation (the frame is the cheapest step and the clip is the most expensive, one to two orders of magnitude apart), and after the final cut but before publishing (this one gates risk, not quality).
- Add the production view: a checkpoint is not necessarily blocking. The first two can auto-continue on timeout; only the compliance gate must hard-block, because you cannot let a legal check pass by timing out.
- State the counterintuitive part: the first gate is the one people skip, because there are no visuals yet and it looks like there is nothing to review, while it is the only gate where changes cost nothing.
- Expected follow-up: what if reviewers cannot keep up. Tier it. Machines score everything, humans only see the low scores, and human attention goes where the machine is unsure.
分析过程 · 先想清楚再作答
- 这题在考你有没有成本意识。答「每一步都让人看一眼」是没做过工程的回答——人是最贵的资源,卡点多了流水线就退化成手工作坊。
- 给一条可复用的判据:**卡点放在「下游最贵的那一步」之前**。判断某个位置该不该设卡,只问一句「如果这里错了,往后要白花多少钱」。
- 按这条判据落到生成式流水线上,会得到三个位置:剧本定稿之后(此时零成本,却决定了后面所有素材的方向)、首帧出来之后视频生成之前(首帧是最便宜的一档,视频是最贵的一档,同一个镜头差出一到两个数量级)、成片合成之后发布之前(这一道拦的不是质量而是合规风险)。
- 补一条生产视角:卡点不等于阻塞。第一和第二道可以做成「默认放行、超时自动继续」,只有第三道必须硬卡——合规问题不能靠超时放行。
- 结论里要点出一个反直觉的事实:最容易被跳过的恰恰是第一道,因为这时候还没有画面,看起来没什么可审的;但它是唯一一道改起来零成本的闸门。
- 可预期的追问是「人来不及审怎么办」。答案是分级:机器先打分,只把低分的推给人,人的时间花在机器拿不准的那部分上。
Key points
- Rule: place the gate right before the most expensive downstream step, judged by wasted spend if this step is wrong.
- Three positions: after script lock, after first frame and before video, after final cut and before publish.
- The first-frame gate pays best: the frame is the cheapest step and the clip the most expensive, one to two orders of magnitude apart.
- The first two gates can auto-continue on timeout; only the compliance gate hard-blocks.
- When reviewers are the bottleneck, tier it: machines score everything, humans only see low scores.
答题要点
- 判据是「卡点放在下游最贵的那一步之前」,问的是这里错了往后白花多少钱。
- 三个位置:剧本定稿后、首帧出来后视频生成前、成片合成后发布前。
- 首帧那一道性价比最高:首帧是最便宜的一档,视频是最贵的一档,同一个镜头差出一到两个数量级。
- 前两道可以默认放行加超时继续,只有合规那一道必须硬卡。
- 人力不够就分级:机器先打分,人只看低分的那些。
A user edits an intermediate input. How do you compute which downstream steps must rerun?用户改了中间一步的输入,怎么算出哪些下游需要重做?
Common in ChinaCommon overseasIntermediate#dag#incremental-recompute#costHow to reason about it · think before answering
- The signal lives at the two ends. Most candidates produce the middle part, propagation over a dependency graph, and drop both the direction and the finish.
- Direction: propagate forward along who-depends-on-me from the edited node, not backward to its dependencies. Getting it backward is insidious, because upstream nodes rerun, the output is still correct, the bill doubles, and no test catches it.
- Implementation: seed a set, sweep the graph repeatedly adding any node with a dependency already in the set until it stops growing, then return in topological order so the caller can just walk the array.
- The finish is what people forget: unaffected nodes must have their artifacts copied from the previous version, not regenerated. A perfect radius saves nothing without that copy.
- Then verification. Do not compare file hashes, because identical inputs often produce byte-identical output and a matching hash proves nothing. Count API calls instead; that evidence holds both offline and against a real vendor.
- Expected follow-up: what about forcing a rerun when nothing changed. Keep an explicit force flag and account for it separately, or you lose the ability to tell system-decided reruns from human-triggered ones.
分析过程 · 先想清楚再作答
- 这题的区分度在方向和收尾两处,很多人只答出中间那段「沿依赖图传播」,前后都丢了。
- 方向:从被改的节点**沿着「谁依赖我」正向传播**,不是往上游找依赖。写反的后果很隐蔽——上游会被一起重跑,结果是对的,钱多花了一倍,测试也发现不了。
- 落到实现:把种子节点放进集合,反复扫一遍图,只要某个节点的依赖里有一个已经在集合里就把它也加进来,跑到不动点为止;最后按拓扑序返回,调用方顺着数组跑就不会先跑下游后跑上游。
- 收尾这一步最容易漏:**没受影响的节点,产物要从上一版复制过来,不是重新生成**。半径算得再准,少了复制这一步就一分钱没省。
- 然后是怎么验证。不要比文件哈希——同样的输入很可能生成逐字节相同的结果,哈希相同证明不了没重跑。要数**接口调用次数**,这才是硬证据,而且在离线与真实两种模式下都成立。
- 可预期的追问是「输入没变但你想重跑怎么办」。留一个强制重跑的开关,并且把它和自动判定分开记账,否则你会分不清一次重跑是系统判的还是人手动点的。
Key points
- Propagate forward along who-depends-on-me from the edited node, never backward.
- Sweep to a fixed point and return in topological order so execution never runs downstream first.
- Copy artifacts for unaffected nodes from the previous version, or the computed radius saves nothing.
- Verify by counting API calls, not by comparing file hashes, since identical inputs can produce byte-identical output.
- Keep a separate force-rerun switch and account for it apart from automatic decisions.
答题要点
- 从被改的节点沿着「谁依赖我」正向传播,不是反向找依赖。
- 扫图到不动点,结果按拓扑序返回,保证执行顺序不会颠倒。
- 没受影响的节点要从上一版复制产物,否则半径算得再准也没省钱。
- 验证要数接口调用次数,不要比文件哈希——同样的输入可能产出逐字节相同的结果。
- 另留一个强制重跑开关,并与自动判定分开记账。
What must a version record hold for rollback? Are the final artifacts enough?版本回滚要存什么?只存最终产物够不够?
Common in ChinaCommon overseasIntermediate#versioning#rollback#data-modelingHow to reason about it · think before answering
- The hinge is are they enough, which signals the answer is no. Restate it as a claim: a version is not a backup. Saying that sentence gets you half the credit.
- The semantics differ. A backup means restore after an incident and only needs the latest good state. A version means both exist, side by side, switchable, with a human choosing. Review workflows need the latter.
- So each version stores three things: the artifacts themselves, kept in per-version directories with nothing deleted; the inputs that produced them, the line and the visual description, or nobody can explain the difference three days later; and which nodes reran plus why.
- The current version should be a pointer, not a copy. Rollback moves the pointer without touching files, which makes it instant and reversible, and makes switching forward again equally natural.
- Mention the knock-on effect, because it shows you have actually shipped this: rolling back one shot changes the whole episode timeline. If the new take of the voice is a second longer, every later shot shifts, so rollback must recompute the timeline. That part is cheap local computation.
- Expected follow-up: how long to keep versions. Scale it by artifact size and business value: keep small text forever, put a retention window on video, and after expiry keep only metadata and inputs so the artifact can be regenerated on demand.
分析过程 · 先想清楚再作答
- 题眼在「够不够」三个字,它在提示答案是否定的。先把问题重述成一句判断:**版本不是备份**,这句话说出来这题就答对了一半。
- 两者的语义不一样。备份是「出事了拿回来」,只需要保留最近一份好状态;版本是「两个都在」,要能并排对比、来回切换,最终选哪个由人定。审核场景要的是后者。
- 所以每个版本要存三类东西:产物本身(按版本分目录,一个文件都不删)、产生它的输入(那一版的台词与画面描述,否则三天后没人说得清两版差在哪)、以及这一版重跑了哪些节点与原因。
- 当前版本要设计成一个指针,不是一份拷贝。回滚就是把指针挪回去,不搬文件,因此是瞬时且可逆的;这也让「再切回新版本」变成理所当然的操作。
- 有一个连带影响必须提到,提了就说明你真做过:**回滚一镜会改变整集的时间轴**。新版配音比旧版长一秒,切回去之后后面所有镜头的起止时间都要重排。所以回滚之后要重算一次时间轴,好在这是纯本地计算,很便宜。
- 可预期的追问是「版本存多久」。按产物体积和业务价值定:小文本无限存,视频这种大件设一个保留期,过期只留元数据和输入,需要时可以按同样的输入重跑出来。
Key points
- A version is not a backup: backups keep the latest good state, versions keep old and new side by side.
- Store three things per version: artifacts in per-version directories, the inputs that produced them, and which nodes reran and why.
- Make the current version a pointer, not a copy, so rollback is instant and reversible.
- Rolling back one shot shifts the episode timeline, so recompute it after rollback; it is cheap local work.
- Set retention by size: keep text forever, expire large video and retain metadata plus inputs for regeneration.
答题要点
- 版本不是备份:备份只要最近一份好状态,版本要求新旧同时存在、能并排对比。
- 每版要存三类:产物(按版本分目录、不删)、产生它的输入、重跑的节点与原因。
- 当前版本是指针不是拷贝,回滚只挪指针,瞬时且可逆。
- 回滚一镜会改变整集时间轴,回滚后要重算一次——这是纯本地计算,很便宜。
- 保留策略按体积分级:文本长期留,大视频设保留期,过期只留元数据与输入以便按需重跑。