video-pipeline
End-to-end video production workflow for the AI Edit Video system - from the user request to the MP4 in outputs/, coordinating HyperFrames (scenes) and Remotion (assembly). Read this when starting any
SKILL.md.claude/skills/video-pipeline/SKILL.md
GitHubVideo Pipeline - from request to MP4
Engine roles (never swap them)
| Task | Engine | Why |
|---|---|---|
| Scene motion graphics: kinetic typography, karaoke captions, count-ups, data tables, callouts, shaders | HyperFrames | HTML + GSAP is its strength, and the Vietnamese skills already ship the fixes |
| Stitching scenes + source footage, transitions between scenes, mixing audio + sound effects, full-video caption overlay, final export | Remotion | Programmatic assembly, <Sequence>/<Audio>/<OffthreadVideo> |
How the two engines talk to each other: intermediate MP4 files in video-projects/<name>/renders/. HyperFrames does not know Remotion exists and vice versa - the only shared thing is the manifest.
Project lifecycle
1. Initialize
- Create
video-projects/<kebab-case-name>/withindex.html,compositions/,assets/,renders/,hyperframes.json,meta.json. meta.jsonis the central manifest:
{
"id": "tiktok-paper-gpt5",
"name": "Mổ xẻ paper GPT-5",
"width": 1080, "height": 1920, "fps": 30,
"status": "draft",
"scenes": [
{ "id": "s01-hook", "src": "compositions/s01-hook.html", "durationInFrames": 90, "render": "renders/s01-hook.mp4" },
{ "id": "s02-talking","srcVideo": "assets/talking-head.mp4", "from": 2.5, "to": 14.0 }
],
"audio": {
"voice": "assets/voice.mp3",
"sfx": [ { "file": "assets/sound-effects/whoosh-01.mp3", "atFrame": 88 } ]
},
"output": null
}
scenes[]is the contract between the two engines: a scene withsrcis rendered by HyperFrames; a scene withsrcVideois footage used as-is. Remotion reads this file to assemble - never hardcode the scene list in the Remotion code.- The user edits
meta.jsontoo. The dashboard has a video editor (timeline + live Remotion preview) that writesscenes,audio,captions,subtitles,subtitleStyleandoverlaysstraight into this file - between your turns, or before your session even started. So, every single time:- Read
meta.jsonagain right before you change it - never trust the copy you read earlier in the session. - Edit in place (the
Edittool on the exact keys you are changing). NeverWritethe whole file back from memory: that silently throws away whatever the user just dragged, trimmed or retyped. - Keep every field you do not recognise, at any depth (extra keys on a scene, a cue,
audio...). They belong to the user, the editor or the system. The server snapshots the timeline when your session starts and ends (video-projects/<id>/.history/), so the user can always roll back - but a clobbered edit is still lost work for them.
- Read
2. Build the scenes (HyperFrames)
- Copy
assets/brand/brand-tokens.cssinto the project, write compositions following thewindow.__timelinesconvention. - Vietnamese videos: apply the verified fixes (gradient text losing diacritics -> render text as real elements + use
background-clipcorrectly; check that every diacritic is present on the first/last frame of each reveal). - Lint clean before rendering:
npx hyperframes lint. - Render a draft of each scene:
npx hyperframes render --quality draft --output renders/<scene>.draft.mp4.
3. Frame verification (MANDATORY, before assembly)
ffmpeg -ss <hero-moment-seconds> -i renders/<scene>.draft.mp4 -frames:v 1 verify/<scene>.png
Inspect every image: does the Vietnamese text keep all its diacritics? Is any text overflowing the edges? Any unexpected white/black frames? Is a face cropped? If anything is wrong, fix the scene - do not move on.
4. Assembly (Remotion) - see the remotion-assemble skill for details
- The Remotion composition reads
meta.json, builds<Sequence>entries fromscenes[], and inserts sfx atatFrame. - Render a full draft -> review it in the web UI -> approve.
5. Automated QC (MANDATORY, blocks final)
curl -s -X POST http://localhost:6869/api/projects/<id>/qc -H "content-type: application/json" -d "{}"
The server measures the latest draft with ffmpeg and returns { report: { status, checks[] } }:
| check | meaning | what to do on fail |
|---|---|---|
resolution |
dimensions/fps do not match meta.json |
re-render with the correct settings |
loudness |
far off -14 LUFS | adjust the mix volume, do not fix it by pulling the peak |
truepeak |
clipping (> -0.5 dBTP) | lower the source gain, see "SFX loudness and lead silence" in the noti-tiktok-vn skill (voice headroom before layering SFX) |
blackframes |
black frames in the MIDDLE of the video (leading/trailing fades are ignored) | a gap at a scene change - fix transitionOverlap/durationInFrames |
freeze |
frozen picture >= 2s | the scene is missing animation, or the footage is broken |
tail-silence |
silent tail > 1.2s | trim the tail |
av-duration |
picture and sound differ by > 0.5s | wrong total durationInFrames |
safe-area |
ALWAYS passes, returns frames = images with the obstructed bands outlined in red |
you MUST Read every image and judge for yourself; if any text sits in the red zone, pull it inward, see the key-layout skill |
status: "fail"-> the server rejects theassemble-finaljob with 409 QC_REQUIRED / QC_FAILED. Fix the root cause, re-render the draft, run QC again. Do not useforce: trueto dodge it unless the user explicitly asks.status: "warn"-> consider fixing, not blocking.- The final report must state the QC results and how each fail/warn check was handled.
6. Final
- Re-render the HyperFrames scenes at
--quality standard. - Remotion final render ->
outputs/<project>-v<N>.mp4. - Update
meta.json:status: "done",output: "outputs/...". The web UI reads status from here.
7. Thumbnail + publish package
curl -s -X POST http://localhost:6869/api/projects/<id>/thumbnail -H "content-type: application/json" -d "{\"title\":\"...\",\"frameAt\":12}"
curl -s -X POST http://localhost:6869/api/projects/<id>/publish -H "content-type: application/json" -d "{}"
publish generates .srt/.vtt from the transcript and has the AI write the title/description/hashtags for TikTok, YouTube and Facebook following the Style Design. Precondition: the transcript must be the FINAL one. If it was cut/remapped, write the final version to assets/transcript.final.json (the transcript loader prefers this file) - otherwise the subtitles will drift out of sync with the cut video.
Source kind: footage, article or text request
brief.sourceKind says what the video is built from (the "Nguồn video" card on the project page). The edit prompt always prints a "Nguồn của video" section for it:
video(default) - edit the uploaded files inassets/.article- no footage. The article text is inassets/source-article.md(fetched frombrief.sourceUrl; if the file is missing,POST /api/projects/:id/source/articlefirst). Build a motion graphics video that retells its key points.request- no footage.brief.sourceTextis the user's request/content; build a motion graphics video for it. Never invent figures, names or quotes it does not give.
Voice-over for the two no-footage kinds is brief.narrationMode (the prompt prints it in all three directions):
none- no voice, no transcript: auto-cut does not apply and karaoke has no speech to sync to.tts- write the narration script, thenPOST /api/projects/:id/narration {"script": ...}and poll the task. The server reads it in EXACTLYbrief.narrationVoice(the body cannot pick a voice - the user chose it), writesassets/narration.wav+assets/narration.transcript.jsonand setsmeta.audio.voice.file- the user's recordingbrief.narrationFile:POST /narration {}only transcribes it. The recording is the timeline and the main content. With a voice, the voice is the timeline: video length = voice length, scenes and karaoke follow the transcript, music ducks under it. Images come from AI illustrations only whenbrief.autoIllustrationsis on (the server refuses otherwise) - off means text + HTML/SVG graphics + images already inassets/. Prefernoti-tiktok-full-text(vertical) ornoti-youtube-editmode B (landscape) over talking-head skills.
Finish mode: stop at the draft, or go all the way to final
brief.finishMode decides where an AI edit session stops. The edit prompt always states the mode, in both directions - follow the prompt over the step list above when they disagree.
finishMode |
Session goal | Where you stop | Done means |
|---|---|---|---|
"draft" (default) |
draft |
after step 5 (QC) | a NEW assemble-draft job finished during this run, and QC ran on that exact outputs/<id>-draft.mp4 |
"final" |
final |
after step 7 | outputs/<id>-v<N>.mp4 exists and meta.json has status: "done" + output |
In draft mode:
- Never render final: no
assemble-final/scene-finaljob, no--quality standardCLI render, no thumbnail, no publish package, do not setstatus: "done"oroutput- even though steps 6-7 above say so. The user reviews the draft, edits it by hand, and clicks "Xuất final" themselves. - The final full draft must come from a queued job (
POST /api/projects/<id>/editor/renderwith{"quality":"draft"}queues the stale scene drafts +assemble-draft). A draft rendered by hand with the CLI has no job row, so the server gate never sees it and keeps auto-resuming the session. - Run QC on that draft. A
failstill has to be fixed (re-render the draft, QC again) - the gate only checks that QC ran, but the user will hitQC_FAILEDon export. - End with a report: what was built, the QC result, and that the draft is waiting for review in the editor.
The batch flows (short clips, aspect-ratio repurposing, Text to video, Auto cut) still open final sessions, whatever the child project's brief says.
Handoff from the editor ("Giao cho AI")
POST /api/projects/<id>/editor/handoff {instruction, finishMode?} opens a new session (default goal draft) that continues from the current timeline. The prompt opens with a "BÀN GIAO TỪ TRÌNH CHỈNH SỬA" block. The user has already edited by hand, so:
- Re-read
meta.jsonfirst and before every edit. Treat what is there as approved. - Keep every manual edit: scene order/length/
from/to, cues, sfx, music, and the v2 fieldslayers[](withanimation,keyframes),fit,transform,crop,color,volume,fadeInSec/fadeOutSec,speed,transition. Change only what the instruction needs. Unknown fields stay. - Do not redo brief steps that are already done (auto-cut already applied - check
assets/auto-trim-report.json/transcript.cut.json; subtitles already there; music already chosen). Do the missing part plus the instruction. - Do not delete and rebuild existing HyperFrames compositions; edit only the scenes that need it.
- The server snapshots an
ai-beforerevision before you start, so the user can undo your whole run in one click. That is no excuse for sloppy edits.
The user may edit between turns. In any session, the editor can write meta.json while you are idle (between turns, or while you wait for a job). Never keep meta.json in memory across a wait: Read it again right before each Edit, edit in place, never Write the whole file from memory.
Render queue rules (backend)
- Every render goes through the queue - including the ones Claude runs by hand. Jobs are written to SQLite:
id, projectId, type (scene-draft|scene-final|assemble-draft|assemble-final|image-gen), status, progress, log, startedAt, finishedAt. - Jobs run in parallel up to
QUEUE_CONCURRENCY(default 2, configurable via env; set 1 on weak machines). Safety constraint: two jobs from the same project never run at the same time (to avoid trampling renders/meta) - parallelism only happens across different projects. - Progress: parse the CLI stdout (both engines print frame progress) -> update the DB -> push SSE to the web UI.
- Failed jobs: keep the complete log in the DB, show it in the UI, and do not auto-retry more than once.
- Draft always comes before final. The backend rejects a final job if the project has no successful draft for the current version of the scenes.
Sound effects
- Shared library:
assets/sound-effects/, every file has an entry inassets/sound-effects/library.json(file,tags,durationMs, and adescriptionwritten in Vietnamese). - When using one in a video: copy it into
video-projects/<name>/assets/sound-effects/then declare it inmeta.json- a project must contain all of its own assets (re-rendering must not depend on a library that may change). - The Sound Effects page in the web UI reads
library.json, plays previews inline, and allows uploading new files (the backend updates the json).
Speeding up renders (this machine has a GTX 1660 - verified 2026-07)
The number one reason the pipeline burns an entire hour: re-rendering a draft of the WHOLE video after every small edit. The rules:
- Verify layout/text with
npx hyperframes snapshotorinspect(a few seconds) instead of rendering a full draft (tens of minutes). Render a full draft only ONCE, when every snapshot is good, and final ONCE. - Small edit -> re-render only what changed: if you edited one scene, re-render that scene (
render -c <scene>), do not re-render the whole composition. The split-scene + Remotion-assemble pipeline is built exactly for this. - HyperFrames speed flags (the backend queue adds them automatically; when running by hand you MUST remember them):
-w 8 --browser-gpufor every render; add--gpu(NVENC) for drafts. Final keeps CPU encoding (libx264) for maximum quality. Measured in practice: ~30% faster on short scenes, more on long ones. - Transcription runs only ONCE - if
transcript.jsonalready exists, reuse it, NEVER transcribe again. - Remotion speed flags (when running
npx remotion render/stillby hand you MUST add):--concurrency 8 --gl angle. WITHOUT--gl angle, Remotion's Chrome renders with the software renderer (SwANGLE) - the CPU carries 100% while the GPU idles (symptom: Task Manager CPU ~95%, GPU ~5%). On Linux use--gl angle-eglinstead ofangle.
Known issues (verified in production 2026-07)
meta.jsonmust respect the data-type contract (the web UI reads it directly - a wrong type crashes the page):outputis a path STRING (e.g."outputs/<id>-v1.mp4"), NOT an object. Extra metadata (duration, quality, renderedAt, ...) goes into a separateoutputInfofield if needed.scenes[].durationInFramesis a number; sfx are placed withatFrame(a frame number) as in the schema at the top of this file.Rendering a single scene: use the
-cflag, not a positional argument:npx hyperframes render -c compositions/s01.html --quality draft --output renders/s01.draft.mp4. A<template>sub-composition must be referenced by index.html throughdata-composition-srcfor-cto be able to render it.Words running together on per-word reveals: the HyperFrames pipeline swallows the whitespace between inline-block
<span>s - separate words withmargin: 0 0.14emon.word, do not rely on whitespace in the HTML.The
sub_timeline_readiness_timeoutwarning when rendering a template file with-c: the render still comes out correct (best-effort) but wastes another 45s waiting - acceptable for drafts; if you want it gone entirely, render through index.html.A TS comment containing the glob path
*/(e.g.video-projects/*/meta.json) closes the block comment early -> a baffling compile error. Writevideo-projects/<id>/meta.jsoninstead.Vietnamese JSON sent with curl -d from Git Bash on Windows loses its diacritics (verified 2026-08: the thumbnail title arrived at the server as "B?n th? kh?ch..." and rendered broken). The console codepage mangles UTF-8 before curl sends it. For any API body containing Vietnamese, POST from Node instead:
node -e "fetch(url, { method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify({...}) })"(or write the JSON to a file and usecurl --data-binary @file).
Agent sandbox (what the policy blocks)
Every tool call from the in-app agent passes through apps/server/src/agentPolicy.ts (a PreToolUse hook) before it runs. The prompt always carries untrusted text (transcripts, imported articles, file descriptions), so these are refused no matter what the prompt says:
- Network: only the local API (
http://localhost:6869) -curlto any other host,wget,Invoke-WebRequest, proxies/--resolve, external URLs insidenode -e/scripts, and UNC paths. Brand logos come fromPOST /api/brand-logos, images from/api/illustrations; never download them yourself. - Installs:
npm install/i/ci/exec,npx <anything except hyperframes/remotion/tsc/tsx>(or with@version/-y),pip/python -m pip,winget/choco. Missing dependency = ask the user to run Doctor. - Secrets:
.env,~/.claude/.credentials.json,~/.ssh... API keys are not in your environment either - the server holds them. - Self-escalation: writing
.claude/settings*.json,.mcp.json,agentPolicy.ts,.git/hooks; user-only APIs (/api/connections,/api/tunnel,/api/update,/api/health,/api/chat, ...); WebFetch/WebSearch and MCP tools.
A refusal comes back as the tool error with the reason - change approach, do not try to get around it. To allow something new, edit the lists at the top of agentPolicy.ts (a human change, never the agent's).
Checklist before reporting completion
- If the brief enables autoCut: the cut was done per the
auto-cutskill, a second silencedetect pass verified the cut version, and the report states the seconds/segments removed - Every scene passed frame verification, Vietnamese text keeps all its diacritics
- Audio is in sync at the start/middle/end (check 3 random points)
- Sound effects land on the right frames, and their volume does not bury the voice (sfx ~10dB below voice)
- The output matches the dimensions/fps in
meta.json - Automated QC ran on the draft with no remaining
failchecks (the report spells out any leftover warn checks) -
finishMode: "final"only:meta.jsonhas updatedstatus+output -
finishMode: "final"only: the thumbnail was generated and Read to verify the diacritics are complete -
finishMode: "final"only: the publish package was generated (.srt/.vtt+ metadata), using the FINAL post-cut transcript -
finishMode: "draft": the draft came from anassemble-draftjob of this run, QC ran on it, and nothing final was rendered - Any new lesson learned was written into the relevant skill