AI 影片生成任務完成後,為何仍要進入審核狀態?
Why should a completed AI video job enter a review state?
這個教學示例以 reducer 區分 AI 影片生成任務的伺服器完成與人工審核,只有另行執行審核動作才會進入 ready。示例以 request ID 和 job ID 忽略過期或錯誤任務事件,並通過 10 項本地斷言;它不會呼叫網絡或生成服務,也不代表產品內部實作。
If a generation job reports success, should your UI immediately present its video as ready to publish?
A useful UI separates server completion from a person approving the output. This post shows a small reducer for that distinction, including stale-event checks and ten executable assertions. It makes no network request.
Disclosure: I work on marketing and communications for AI Pop Video. This post and example were prepared with AI assistance. The assertions below were run locally; they are not an integration test of the product or its generation service.
Start with the user-visible contract
Consider this two-photo video interface. Its default template currently asks for two portraits and lists a 15-second output and 150-credit cost. A user has several distinct decisions: confirm the selected template and cost, submit a request, wait for processing, inspect the result, then decide whether it is suitable to share. Recheck the live interface before using any current template values.
The reducer below is an independent teaching example. It does not describe that product’s internal implementation. Its phases are idle, submitting, processing, review, ready and error. A successful server event moves to review. Only a separate review action moves to ready.
Keep request and job identities
A late response from an older request should not change the active request. Compare the request ID before handling asynchronous events. Once the server has accepted a job, also compare the job ID on completion. These are local correlation checks; they do not authenticate the caller or replace server authorization.
import assert from 'node:assert/strict';
export const initialState = { phase: 'idle', requestId: null, jobId: null, error: null };
export function transition(state, event = {}) {
if (event.type === 'START') {
if (!['idle', 'error'].includes(state.phase) || !event.requestId) return state;
return { phase: 'submitting', requestId: event.requestId, jobId: null, error: null };
}
if (event.requestId !== state.requestId) return state;
if (event.type === 'ACCEPTED' && state.phase === 'submitting' && event.jobId) {
return { ...state, phase: 'processing', jobId: event.jobId };
}
if (event.type === 'COMPLETE' && state.phase === 'processing' && event.jobId === state.jobId) {
return { ...state, phase: 'review' };
}
if (event.type === 'APPROVE_REVIEW' && state.phase === 'review') {
return { ...state, phase: 'ready' };
}
if (event.type === 'FAIL' && ['submitting', 'processing'].includes(state.phase)) {
return { ...state, phase: 'error', error: event.message || 'Request failed' };
}
return state;
}
const start = transition(initialState, { type: 'START', requestId: 'r1' });
assert.equal(start.phase, 'submitting');
assert.equal(transition(start, { type: 'START', requestId: 'r2' }), start);
assert.equal(transition(start, { type: 'COMPLETE', requestId: 'r1', jobId: 'j1' }), start);
const processing = transition(start, { type: 'ACCEPTED', requestId: 'r1', jobId: 'j1' });
assert.equal(processing.phase, 'processing');
assert.equal(transition(processing, { type: 'COMPLETE', requestId: 'old', jobId: 'j1' }), processing);
assert.equal(transition(processing, { type: 'COMPLETE', requestId: 'r1', jobId: 'wrong' }), processing);
const review = transition(processing, { type: 'COMPLETE', requestId: 'r1', jobId: 'j1' });
assert.equal(review.phase, 'review');
assert.equal(transition(review, { type: 'APPROVE_REVIEW', requestId: 'r1' }).phase, 'ready');
assert.equal(transition(initialState, { type: 'APPROVE_REVIEW' }), initialState);
assert.equal(transition(processing, { type: 'FAIL', requestId: 'r1' }).phase, 'error');
console.log('10 assertions passed. Pure state transitions; no network or generation request.');
Run the example
Save the complete code as review-state.mjs and run node review-state.mjs in Node.js. The local run produced 10 assertions passed. The tests cover the happy path, a second START while submitting, completion before acceptance, an old request, a wrong job, review approval in the wrong phase and failure during processing.
The test fixtures are synthetic IDs. No user photos, credentials, credit balance or generation API is involved. ready means the UI recorded a review action in this example. It is not proof of a successful public upload.
Connect controls to phases
While submitting or processing, disable the generate control and show the current task. In review, show the result alongside a clear checklist for faces, hands, framing and audio. Keep the review action distinct from download or public publishing. In error, display the actual known failure and decide whether a retry is appropriate.
A disabled button alone cannot prevent duplicate requests: another tab or a modified client can still send them. The server needs its own request validation, ownership checks and idempotency strategy. This reducer deliberately leaves those backend concerns unimplemented.
Preserve uncertainty
If a connection drops, do not automatically assume the server rejected the job. In a real integration, retrieve the authoritative task status before deciding how the UI should recover or whether a retry could create another charged task. The example has no status-recovery or charging logic.
Finally, review the source-photo and audio permissions and the destination platform’s AI disclosure controls separately. A completed generation task, a reviewed video and a published post are different outcomes. Representing them as separate states makes that distinction visible in the interface.
來源:DEV Community · dev.to