Dayward AI
Week 1 · D1About 4 hours

What an AI Short-Drama Production Pipeline Looks Like: Breaking Down the Stages, a Task-Graph Architecture, and Choosing Among Four Categories of Generation Models

First get clear on which stages an episode of vertical short drama passes through and where the money goes, translate those stages into a directed acyclic graph, then write the four generation-provider interfaces that run through the whole course, so the whole line keeps working even with no balance left.

Today's goals 0/3

Sign in to tick these off and save your progress.

Today's Goals

  1. Name the stages an episode of vertical short drama passes through from concept to finished cut, and which of them a model can genuinely handle today
  2. Draw the production flow as a directed acyclic graph, and point out which nodes can run in parallel and which must run in sequence
  3. Write one unified generation-provider interface layer that lets the same business code switch between an offline placeholder and a real provider

This course assumes you write TypeScript and requires no film knowledge and no other agent course. Fourteen days do one thing: turn one sentence into an episode of vertical short drama fit to publish, then grow that script into a production line that runs in parallel, can be reviewed, and controls its cost. Today writes no script and generates no images and builds the skeleton — come back and tick off the three goals.

Plain-Language Walkthrough

The crew's ledger: which cell on this line the money goes into

Establish an analogy first, and every day of this course returns to it: what you are building is not a script but a film crew.

Before a real crew starts, the producer issues two things: a call sheet (what time, where, which scenes, who is needed) and a process chart (the script locks before storyboards, storyboards lock before sets are dressed, sets are built before cameras roll, and shooting finishes before editing). The former manages resources and the latter order — the same things as a scheduler and a task graph in backend engineering, arrived at a century earlier.

An episode of vertical short drama, translated into our language, is six stages: script → character and scene assets → a first frame per shot → shot video → voiceover → editing and assembly. Those six are this course's first six days and the six nodes in today's lab's graph.

How many people and days each stage takes a traditional crew varies widely between productions and we will not guess. The AI side's ledger can be worked out on the spot. At MiniMax's official pay-as-you-go prices in yuan: images at ¥0.025 each; video by the second at ¥0.50 for 768P, ¥0.80 for 2K, and ¥0.33 for 480P; and speech published only as a resource pack (an HD package at ¥630 for two million characters), converting to a little over three yuan per ten thousand characters — a converted figure, not an official list price.

For one episode of forty shots at six seconds each:

TextText
video   40 shots × 6 s × ¥0.50/s          = ¥120.00
images  40 first frames + 10 look tests × ¥0.025 = ¥1.25
speech  about 800 characters at a converted ¥3 per 10k = ¥0.24
text    tens of thousands of tokens for script and review, under one yuan
                                            total about ¥122

One number in that ledger dominates: video is 98%, and it is simultaneously the slowest step (an asynchronous task, queued and polled) and the likeliest to fail (content review, rate limiting, timeouts).

So this course has one judgment running through it, to be remembered now and returned to every day:

The most expensive, slowest, and most failure-prone thing on this line is video generation, so the whole engineering design revolves around calling the video endpoint one fewer time.

Idempotence, caching, reference image reuse, draft and final tiers, the budget circuit breaker — everything in week two that sounds like engineering is a corollary of that sentence. Somebody who only wants to string AI together cannot write them, because they do not know what they are saving.

Drawing the stages as a graph: why it is not a chain of sequential waits

A first version of a pipeline like this is nearly always a chain of awaits: write the script, wait; generate images, wait; generate video, wait; record voice, wait. It runs, and three problems appear together on the first real run.

First, it queues things that could happen at once: voiceover depends only on the lines and not on the visuals, and yet it waits half an hour behind forty videos. Second, it has no checkpoint — the thirty-seventh video times out and thirty-six results sit in memory, gone the moment the process exits. Third, it cannot say which step it is stuck at.

A crew solved that long ago: a process chart is not a straight line but a graph with dependencies. Set dressing and costume tests run at the same time because both depend only on the script; and rolling camera waits for both. That structure is a task graph (a directed acyclic graph), and its three words each govern one thing: directed is who comes before whom, acyclic is no looping back, and graph is more than one path.

Today's graph looks like this:

Script and character cards Character reference sheets First frame per shot Shot video Line voiceover Timeline
Mermaid source
mermaidmermaid
graph LR
  script[Script and character cards] --> assets[Character reference sheets]
  assets --> frames[First frame per shot]
  frames --> clips[Shot video]
  script --> voice[Line voiceover]
  clips --> timeline[Timeline]
  voice --> timeline

Note the voice branch: it comes straight off script, bypassing the whole visual chain. That is the first thing a graph gives over a queue — parallelism is expressed by the structure directly and needs no manual judgment in code.

Executing that graph needs one algorithm: topological sort, ordering nodes so each comes after all of its dependencies, raising an error on a cycle along the way. Acyclic is not an adjective but a guarantee this code provides:

dag.ts
export type TaskNode = { id: string; deps: string[]; run: () => Promise<string[]> }
 
// Depth-first with three states: unvisited / visiting / done.
// Reaching a "visiting" node again means the dependencies looped back to it, which is a cycle.
export function topoSort(nodes: TaskNode[]): TaskNode[] {
  const byId = new Map(nodes.map((n) => [n.id, n]))
  const state = new Map<string, 'visiting' | 'done'>()
  const order: TaskNode[] = []
 
  const visit = (id: string, trail: string[]) => {
    if (state.get(id) === 'done') return
    if (state.get(id) === 'visiting') throw new Error(`the task graph has a cycle: ${[...trail, id].join(' -> ')}`)
    const node = byId.get(id)
    if (!node) throw new Error(`node ${id} depends on a node that does not exist`) // a silently skipped step is far harder to diagnose than an error
    state.set(id, 'visiting')
    for (const dep of node.deps) visit(dep, [...trail, id])
    state.set(id, 'done')
    order.push(node)
  }
 
  for (const n of nodes) visit(n.id, [])
  return order
}

The engineering cost must be stated too: a task graph is not free. Each node needs a clear definition of which artifacts are its input and which its output, or it is only a pretty dependency declaration. Today does run-the-graph only, leaving idempotence, resumption, and concurrency to days 8 and 9 — all of which rest on today's premise that artifacts are written to fixed locations on disk.

A map of the four generation model categories: which cell of the line each occupies

Short drama production uses four categories of generation model, each matching one stage:

StageWhat it doesInterface shape
TextWrites the script, scores reviews, drafts titlesPOST /v1/chat/completions, OpenAI-compatible, one request and back
ImageCharacter reference sheets, scene images, first framesPOST /v1/image_generation, one request and back, returning image links
VideoGenerates a shot clip from a first framePOST /v1/video_generation returns only a task id; it must be polled and collected
SpeechLine voiceoverPOST /v1/t2a_v2, one request and back

This course's first-priority vendor is MiniMax, for a very practical reason: one vendor covers all four categories, so one key runs the whole line for real without opening four accounts for a course. Authentication is one header: Authorization: Bearer plus your key, along with Content-Type: application/json — that GroupId query parameter in older tutorials is historical and absent from today's OpenAPI, so do not copy it.

Three of the four return in one request, and only video is an asynchronous task. That path takes four steps: submit and receive a task_id, poll GET /v1/query/video_generation with it (the enum is capitalized: Preparing, Queueing, Processing, Success, Fail), obtain a file_id on success, and exchange it via GET /v1/files/retrieve for a download_url before downloading. Break any step and all you hold is a task id. Incidentally, the same vendor also runs another version of the video interface with a different path, different status enum, and different collection method — this course's code uses only the one above, which previews the next section.

Three more potholes are guaranteed if not written down:

  • Image links expire after 24 hours, and video download links have no documented expiry and are likewise temporary. So downloading to disk is not optional but part of the flow.
  • The speech endpoint's data.audio is a hex string by default, not base64. In Node that is Buffer.from(json.data.audio, "hex"), and decoding it as base64 gives you noise.
  • The video endpoint's prompt_optimizer defaults to true: your prompt is rewritten by the vendor before reaching the model. That is an invisible variable for character consistency, covered on day 3.

Other vendors — Alibaba Cloud, Kuaishou, Volcano Engine, Google, Runway — get form comparison only, with no implementation and no specific model version numbers (generation models iterate very fast, and hard-coding a version plants a point that must expire; a batch of older models was measurably retired in September 2026).

What is worth remembering are four shape differences that do not change with versions, all measured:

DimensionWhat the difference looks like
Auth headerDifferent shapes. Some take one Bearer, others add an async switch header or an API version header
Task status enumDifferent casing and wording. Some capitalize, some are lowercase, and some return a boolean
Result link expiryAnywhere from a day to a month, each vendor's own choice
Character consistencySome take a reference image per request, others require creating a reusable subject id and referencing it by a marker in the prompt

Note the last column especially: none of these four differences is about capability — all four categories exist everywhere — and all of them are about shape. That fact alone decides the next section's necessity.

A thin provider interface: keeping vendor differences behind a door

The previous section already said half of it: capabilities are alike everywhere and the shapes differ entirely. Business code written against any one vendor gets marinated in that vendor's shape — the number of auth headers, the casing of the status enum, how long links live, how reference images are passed — and once those seep into business logic, switching vendors is not writing one more implementation class but rereading all the business code.

Something that shapes the code, in passing: MiniMax has no official Node SDK for application code, so today's provider layer is written with bare fetch. That is not laziness — having no SDK makes the boundary clearer: the only network exit is those few lines of fetch of your own.

Scatter vendor calls across a dozen business files and you lose three things at once, and those three are the three reasons for this abstraction.

Reason one: it runs offline. With no balance, no network, and inside CI, the pipeline should still run to completion and produce files. That means collapsing the network exit to one place so the offline and real implementations share one interface.

Reason two: switching vendors and running several. Video through MiniMax today and another vendor tomorrow for a particular shot's camera movement. Because the difference is shape rather than capability, as long as business code says one generation action rather than one vendor's four steps, switching is writing one more implementation class — the enum's casing, the extra header, reference image or subject id, all shut inside that class.

Reason three: metering converges. What each call cost must be recorded in one place, and calls scattered across business code never produce a complete ledger. Day 12's whole chapter rests on that convergence.

What should the interface look like? The key is defining it by business action rather than by a vendor's HTTP request. Those four video steps are one action to business code: give me a clip. So the interface has one generate:

providers/types.ts
export interface VideoProvider {
  readonly name: string
  readonly model: string
  /** Asynchronous: submits, polls, collects, and downloads internally, appearing to the caller as one await */
  generate(input: {
    prompt: string
    outPath: string
    durationSec: number
    firstFrame?: string
    resolution?: string
    onProgress?: (stage: string) => void // a status callback, so the caller can print progress without knowing the protocol
  }): Promise<{ path: string; costCny: number }>
}
 
// The only selection point. Business code imports only this function and never a vendor implementation.
export function createProviders(): Providers {
  return process.env.MOCK === '1' ? createMockProviders() : createMiniMaxProviders()
}

Note outPath: the interface requires the implementation to land the file at a given path rather than returning a URL or a buffer. That design was forced by the previous section's expiring links — writing landing into the contract means no implementation can forget it.

This abstraction's cost must be stated too, or it becomes the platitude that abstraction is always good: you sand off each vendor's unique capabilities. One supports first and last frames together, another supports structured camera parameters, and a common interface cannot hold them. The remedy is not widening the interface but leaving an optional passthrough field used explicitly where needed — explicitly admitting that this one place is bound to one vendor.

It must run offline too: placeholder assets are not a shortcut

"Wire up the real endpoint first and add the mock once it works" is the habit this course most wants you to drop.

The reason concerns debugging rhythm. One complete run of this line calls the video endpoint over forty times, taking tens of minutes and over a hundred yuan for real. Get one index wrong in the timeline and seeing it takes half an hour and a hundred yuan — after three of those you stop wanting to touch that code.

Offline mode compresses that loop to seconds. Today's lab's offline implementation returns no fake JSON and genuinely generates files with ffmpeg: a placeholder image is a solid vertical frame whose color is hashed from the shot description, a placeholder video is a test pattern with an audio track, and a placeholder voiceover is a sine wave whose duration is estimated from the line's character count (0.22 seconds per Chinese character across the course).

providers/mock.ts
// The stub is only at the network exit: no fetch here, and what comes out is a real file.
class MockImageProvider implements ImageProvider {
  async generate(input: { prompt: string; outPath: string; seed?: number }) {
    // The color comes from an input hash — change the sentence and the image changes color, proving the logic ran
    const key = `${input.prompt}|${input.seed ?? ''}`
    await ffmpeg(['-f', 'lavfi', '-i', `color=c=${colorOf(key)}:s=1080x1920`, '-frames:v', '1', input.outPath])
    return { path: input.outPath, costCny: 0 }
  }
}

Judging an offline implementation has one standard: change an input and the output must change. Change a sentence and the placeholder color changes, the placeholder audio's duration changes, the timeline's total changes — showing the business logic in between (state machines, timeline arithmetic, ffmpeg argument assembly) genuinely ran. If the artifacts are identical whatever the input, what you validated is that the program did not crash.

So the stub's position has a hard rule: only at the four providers' network exits, with not one if (process.env.MOCK) in business code — once business logic branches, what you validate offline is a different program.

A glance at the end state: what this line becomes in week two

Finally, two minutes on these fourteen days' shape, which makes today's foundation clearer.

Week one produces one episode: D2 structures the script, D3 the character and scene assets, D4 the shot videos, D5 voiceover and subtitles, D6 editing and assembly, D7 stringing them into a line that runs end to end. Week two turns that script into a production line: D8 a workflow engine, D9 concurrency and quotas, D10 a review console, D11 QC and compliance, D12 cost and model routing, D13 multi-platform distribution, D14 five episodes plus a portfolio.

Look back at the first section's judgment — the most expensive, slowest, most failure-prone thing is video — and every day of week two is its corollary: D8's idempotence exists so a failure does not regenerate video, D9's quota gate exists so hitting a rate limit does not burn allowance, D12's draft tier exists so tuning runs on the cheap tier, and D3's asset reuse exists so one reference sheet serves forty shots.

The course order follows the same reasoning. Writing the provider interface before the script is because once the interface is settled, the other thirteen days fill content into it; make it work, then make it good, then make it many, stable, and cheap — reverse that and you optimize something that does not yet exist.

Source Reading

Hands-On Lab

🧪 D1 lab: a production line task graph and a four-provider skeleton that runs the whole flow offline

Code location: labs/ai-drama-pipeline/day-01-pipeline-skeleton

Acceptance criteria:

  1. MOCK=1 pnpm start finishes, printing elapsed time and artifact counts for the six nodes script, assets, frames, clips, voice, and timeline in order, with a closing ledger line showing 14 artifacts.
  2. The artifact directory really holds files: two character reference sheets, three first frames, three clips, three voiceovers, two JSON files, and one run record.
  3. ffprobe on the first shot's clip shows 1080x1920, two streams of h264 and aac, and a duration of 6 seconds.
  4. The three first frames differ in color (the placeholder color is hashed from the shot description rather than one shade of gray).
  5. The timeline JSON has the three shots end to end: 0 to 6000, 6000 to 12000, 12000 to 18000, with no overlap and no gap.
  6. Adding a dependency from the script node to the timeline node creates a cycle, and rerunning reports the task graph has a cycle and exits.

Confirm ffmpeg is installed before starting (ffmpeg -version produces output) and use MOCK=1 throughout — no key is needed today. starter/ runs to completion as-is with acceptance criteria 3 through 6 unmet, one per numbered exercise; when stuck, compare against solution/'s file of the same name.

  1. Run the solution's MOCK=1 pnpm start once, read the terminal output alongside the artifact directory, and get a mental picture of what done looks like.
  2. Back in the starter, open src/index.ts and see how the six nodes' deps connect, drawing the graph on paper and marking each node's input and output artifacts.
  3. Do exercises 1 and 2: complete the offline image and video ffmpeg arguments, then use ffprobe to confirm a clip is 1080x1920, 6 seconds, with an audio track, and that the three first frames differ in color.
  4. Do exercise 3: implement topological sort and cycle detection, and the "topoSort is not implemented" warning should disappear; then deliberately create a cycle and confirm it errors and exits.
  5. Do exercise 4: compute the timeline's start and end milliseconds, open the timeline JSON and confirm the three shots are end to end; finally swap in a sentence of your own and rerun, confirming the placeholder assets genuinely changed with it.

Interview Questions

Today's 3 questions are in the question bank below, weighted toward the pipeline's layering and decoupling, the provider abstraction's boundary, and why running offline is an engineering requirement rather than a toy. Expand each one and read the analysis before the key points — practicing the derivation beats memorizing bullets. The high-frequency labels for the domestic and overseas markets are there so you can pick by target market.

Checklist and Tomorrow

  • Name the stages an episode of vertical short drama passes through from concept to finished cut, and which of them a model can genuinely handle today
  • Draw the production flow as a directed acyclic graph, and point out which nodes can run in parallel and which must run in sequence
  • Write one unified generation-provider interface layer that lets the same business code switch between an offline placeholder and a real provider
  • Say without notes which cell of this line the money mainly goes into, and which engineering designs that fact implies
  • Say which four extra steps the video interface has over the other three, and why landing files belongs in the interface contract
  • All 6 acceptance criteria of the lab pass
  • Answer at least 2 of the 3 interview questions without looking at the key points

Tomorrow (D2) we enter the writers' room: having the model turn one sentence into structured data of character cards, scenes, and shots rather than a stretch of prose. Why that order? Because on today's graph, all five nodes other than the script take their input from fields of the shot data — visual into the image prompt, camera into the video prompt, dialogue into the voiceover, durationSec into the timeline. Without a structured script, the next five steps have nothing to eat. D2 also adds a reviewer role to the script agent so drafts improve in a loop, and distils cross-episode settings into a world bible — a document used right through to day 14's five-episode season.

Interview questions

  • Why wrap a vendor SDK in your own provider interface, and when does that layer become a liability?为什么要在厂商 SDK 之上再套一层自己的 provider 接口?什么时候这层反而是负担?
    Common in ChinaCommon overseasBasic#provider-abstraction#architecture

    How to reason about it · think before answering

    1. The screen is whether you have ever actually swapped a vendor. Answering only decoupling and easy replacement is what everyone says; the signal is naming what the layer buys and what it costs.
    2. How to break it down: ask what you lose without the layer. Three concrete things — offline runnability (you can only stub when network egress is funneled into one place), multi-vendor coexistence (business code expresses an action, not one vendor's four-step flow), and metering (every call's cost must be recorded in exactly one place).
    3. Then place the abstraction: define it by business action, not by the vendor's HTTP request. Submit, poll, retrieve, download for an async video job is one generate to the caller; leaking those four steps upward defeats the purpose.
    4. Conclusion and cost: the layer sands off vendor-specific capabilities, such as first-and-last-frame conditioning or structured camera parameters. The fix is not a wider interface but one optional passthrough field, so the single call site explicitly admits it is vendor-bound.
    5. When it is a liability: single vendor forever and no offline path. Two warning signs — adding a vendor forced a signature change across the other implementations, or a vendor-only parameter name appeared in the interface. Both mean you abstracted the least common multiple of vendor features.
    6. Likely follow-up: why not just use an aggregation gateway or SDK? You still need your own interface, because aggregators normalize protocols but not your on-disk artifact contract or your cost ledger.

    分析过程 · 先想清楚再作答

    1. 这题在筛「有没有真的换过一次厂商」。只答「解耦、方便替换」的人,说的是一句所有人都会说的话,区分度在于你能不能给出「这层带来了什么、又赔上了什么」的具体清单。
    2. 怎么拆:先问自己「如果不套这层,哪些能力会散掉」。答案有三样,而且都能落到具体文件上——离线可跑(网络出口收敛到一处才可能打桩)、多厂商并存(业务代码写的是动作而不是某家的四步流程)、计量收口(每次调用的花费必须有唯一一处记账)。
    3. 接着说抽象的位置:接口要按业务动作定义,不按厂商的 HTTP 请求定义。异步视频任务的提交、轮询、取件、下载四步,对业务代码来说是一个 generate;把这四步漏到业务层,抽象就白做了。
    4. 结论与代价:这层会磨掉各家的独有能力(某家支持首尾帧、某家支持结构化运镜参数)。正确处理不是把接口撑大,而是留一个可选透传字段,让需要它的那一处显式承认自己绑定了某一家。
    5. 什么时候是负担:你只会用一家、也永远不会离线跑的时候;以及出现两个信号时——为加一个厂商改了接口签名让另外三个实现跟着改,或者接口里出现了只有一家有的参数名。这两个信号说明抽象抽在了厂商能力的最小公倍数上,位置错了。
    6. 可预期的追问:那要不要直接用某个统一网关或聚合 SDK?可以,但你仍然需要自己的接口,因为聚合层解决的是协议差异,解决不了你自己的落盘契约与记账口径。

    Key points

    • Name three concrete reasons: offline runnability, multi-vendor coexistence, and a single metering point
    • Define the interface by business action; submit-poll-retrieve-download stays inside the implementation
    • Put the output file path in the contract, because vendor image and video URLs are short-lived temporary links
    • The cost is losing vendor-specific features; handle it with one optional passthrough field, not a fatter interface
    • Two signs you abstracted wrong: adding a vendor changes the signature, or a vendor-only parameter leaks into the interface

    答题要点

    • 三个理由要说具体:离线可跑、多厂商并存、计量收口,每一个都对应一处真实代码
    • 接口按业务动作定义,异步任务的提交轮询取件下载四步必须关在实现里
    • 把落盘路径写进接口契约,因为厂商返回的图片与视频链接都是会失效的临时链接
    • 代价是磨掉独有能力,用可选透传字段处理,而不是撑大公共接口
    • 两个「抽错了」的信号:加厂商要改签名、接口里出现厂商专有参数名
  • What does modeling a multi-step generation pipeline as a task graph buy you over a chain of sequential awaits, and what does it cost?把一条多步生成流程建成任务图,比一串顺序 await 多拿到了什么?代价是什么?
    Common in ChinaCommon overseasIntermediate#task-graph#pipeline-design

    How to reason about it · think before answering

    1. The question is what you gain, not what a DAG is. Reciting the definition scores nothing; name three capabilities the sequential version cannot have, each with a concrete scenario.
    2. Break it down by inverting the three pains of sequential code. First, parallelism is expressed by the graph itself — voice-over depends only on the lines, yet a sequential run queues it behind forty video jobs. Second, resumability — each node writes artifacts to a fixed path, so shot 37 failing does not destroy the first 36. Third, observability — you can say which node is stuck, not merely that some await is pending.
    3. Add the higher-signal point: cycle detection. Topological sort throws when dependencies form a cycle, and that is the only thing enforcing the acyclic part. Without it, a wrong dependency silently skips a step or reorders execution, which is painful to debug.
    4. Conclusion and cost: a task graph is not free. Every node needs a declared input and output artifact set, otherwise the graph is decorative. That artifact contract is also the precondition for idempotency and resume later on.
    5. Likely follow-up: should you adopt a workflow engine instead? Judge by node count and failure rate — worth it at a dozen-plus nodes with high failure and human review; for three to five nodes a hand-written graph plus topological sort is cheaper than another system to operate.

    分析过程 · 先想清楚再作答

    1. 这题的题眼在「多拿到了什么」,不在「什么是 DAG」。背出有向无环图定义的人拿不到分,答对的人会给出三样顺序版拿不到的能力,并各配一个具体场景。
    2. 怎么拆:把顺序版的三个痛点倒过来说。第一,并行的可能性被图结构直接表达——配音只依赖台词、和画面无关,顺序版里它却要排在四十次视频生成后面。第二,有断点——每个节点的产物落在磁盘固定位置,第三十七个镜头失败时前三十六个还在。第三,可观测——你能回答「现在卡在哪个节点」,顺序版只能回答「卡在某个 await」。
    3. 补一条区分度更高的:环检测。拓扑排序在发现依赖成环时抛错,这是「无环」两个字唯一的执行者;没有它,依赖写错只会表现成漏跑一步或者顺序错乱,非常难查。
    4. 结论与代价:任务图不是免费的,你必须为每个节点定义清楚输入产物与输出产物,否则它只是一张漂亮的依赖声明。这份产物契约同时也是后面做幂等与断点续跑的前提。
    5. 可预期的追问:那是不是应该直接上工作流引擎?判据是节点数与失败率——十几个节点、失败率高、需要人工介入时才值得;三五个节点的流程用一张手写的图加拓扑排序就够,引入引擎反而多一套要运维的东西。

    Key points

    • Three things sequential code cannot give: parallelism expressed by structure, resumability after failure, and knowing which node is stuck
    • Topological sort also detects cycles, the only mechanism enforcing the acyclic property
    • The cost is declaring input and output artifacts per node; without that the graph is decorative
    • That artifact contract is the precondition for idempotency and resume
    • Adopt a workflow engine based on node count and failure rate; a hand-written graph wins for three to five nodes

    答题要点

    • 三样顺序版拿不到的:并行由图结构表达、失败后有断点、能说清卡在哪个节点
    • 拓扑排序顺带做环检测,这是「有向无环」里「无环」的唯一执行者
    • 代价是必须为每个节点声明输入产物与输出产物,否则图只是装饰
    • 这份产物契约同时是后续做幂等与断点续跑的前提
    • 上不上工作流引擎按节点数与失败率判断,三五个节点手写图更划算
  • For a system that depends heavily on paid third-party generation APIs, how do you make it developable and testable without keys — and how do you prove the offline mode is not fooling you?一个重度依赖付费第三方生成接口的系统,怎么做到没有密钥也能开发和测试?怎么证明这套离线模式没有骗自己?
    Common in ChinaCommon overseasDeep dive#offline-testing#test-strategy

    How to reason about it · think before answering

    1. All the signal is in the second half. Everyone says mock it; only people who have done it can say how they prove the mock is honest, because most mocks only guarantee the program does not crash.
    2. Break it down by first fixing where the stub goes: only at the network egress, inside each provider's one method. No environment check belongs in business code — the moment business logic branches, offline runs a different program and your testing says nothing about production.
    3. Then fix the quality of the stub: the offline implementation should emit artifacts of the real shape rather than a constant. For a media pipeline, actually generate placeholder files locally (solid-color frames, a test pattern with an audio track, a sine-wave clip); for retrieval, return well-formed fake documents; for streaming, emit chunks with realistic pacing. The point is to force downstream parsing, state machines, and timeline math to execute.
    4. The one test that proves it is honest: change an input and the output must change. Placeholder color tracks the shot description, placeholder audio length tracks the line length, total runtime tracks shot count. If every input yields identical artifacts, you only verified that nothing crashed.
    5. State the payoff too: a real run costs tens of minutes and real money, so an off-by-one takes half an hour to surface. Offline collapses that loop to seconds, which is what makes continued refactoring affordable. This is an engineering requirement, not a toy.
    6. Likely follow-up: who then covers the real path? Layer it — offline covers business logic and regression, while a small set of smoke tests exercises the real vendors on a schedule. They verify different things and do not substitute for each other.

    分析过程 · 先想清楚再作答

    1. 这题的区分度全在后半句。前半句人人都会答「打 mock」,能答出「怎么证明它没骗自己」的才是真做过——因为绝大多数 mock 的实际效果是「保证程序不崩」,而不是「保证逻辑正确」。
    2. 怎么拆:先定桩的位置。桩只打在网络出口上,也就是每个 provider 的那一个方法里;业务代码里一个环境变量判断都不该有。一旦业务逻辑分叉,离线跑的就是另一个程序,你验的东西和线上没关系。
    3. 再定桩的质量:离线实现要产出真实形态的产物,而不是返回一个常量。做媒体流水线就用本地工具真的生成占位文件(纯色图、测试画面加音轨、正弦波音频),做检索就返回结构完整的假文档,做流式就按节奏一段段吐。目的是让下游的解析、状态机、时间轴计算真的被执行一遍。
    4. 证明它没骗自己的判据只有一条:**改一个输入,输出必须跟着变**。占位图的颜色随镜头描述变、占位音频时长随台词字数变、总时长随分镜数变——这说明中间的业务逻辑跑过了。如果换什么输入产物都一样,你验的只是没崩。
    5. 还要说收益:一次真跑几十分钟、上百块钱,一个下标写错就要等半小时才看得到;离线把这个反馈循环压到几秒,团队才会愿意持续重构这段代码。这是工程要求,不是玩具。
    6. 可预期的追问:那真实路径谁来保证?答案是分层——离线模式覆盖业务逻辑与回归测试,真实路径靠少量的冒烟用例定期跑,两者验的是不同的东西,不能互相替代。

    Key points

    • Stub only at the network egress; business code contains no offline branch
    • The offline implementation must emit real-shaped artifacts so downstream parsing, state machines, and timeline math actually run
    • The single acceptance test is that changing an input changes the output; otherwise you only verified it did not crash
    • The payoff is collapsing a tens-of-minutes, real-money feedback loop into seconds, which is what makes refactoring affordable
    • Cover the real path with a small scheduled smoke suite; it verifies something different from the offline mode

    答题要点

    • 桩只打在网络出口,业务代码里不出现任何离线判断分支
    • 离线实现要产出真实形态的产物,让下游解析、状态机、时间轴计算真的执行
    • 唯一的验收判据是「改一个输入,输出跟着变」,做不到就只验了没崩
    • 收益是把几十分钟上百块的反馈循环压到几秒,团队才敢持续重构
    • 真实路径靠少量定期冒烟用例覆盖,与离线模式验的是不同的东西

Comments