Proofreads Workflow
The proofreads endpoint extracts editable subtitles BEFORE the engine commits to the final translated render. Use it for high-stakes content where a re-translation would be expensive (time, plan credits, disappointment) compared with the proofread overhead.
This page documents what the proofread CLI actually performs, verified
against the live heygen video-translate proofreads commands and the v3
HeyGen API as of skill v3.1.0. Where API docs and live behavior diverge,
this page reflects live behavior.
What the proofread workflow does
Five subcommands and three resources:
heygen video-translate proofreads
├── create POST /v3/video-translations/proofreads
├── get GET /v3/video-translations/proofreads/{id}
├── srt get GET /v3/video-translations/proofreads/{id}/srt
├── srt update PUT /v3/video-translations/proofreads/{id}/srt
└── generate POST /v3/video-translations/proofreads/{id}/generateResource lifecycle:
create srt get srt update generate
| | | |
v v v v
proofread session -----> edited SRT replace SRT video_translation_id
(status: + original (continues as a
processing→completed) auto-extracted normal translation
transcript render — poll via
video-translate get)Each language gets its own proofread_id. Submitting output_languages: ["Spanish", "French"] returns two ids; each is reviewed and finalized independently.
Step-by-step: what each command actually does
proofreads create
Inputs (per CLI --request-schema):
video(required):{type: "url", url: "..."}or{type: "asset_id", asset_id: "..."}output_languages(required): array of canonical language strings (e.g.["Spanish"])title(required): short label that appears in the dashboard and in the downloaded SRT filenamemode:"speed"or"precision"—precisionis recommended; final render quality follows from thisspeaker_num: integer, sets diarization expectationenable_speech_enhancement,disable_music_track,enable_video_stretching,keep_the_same_format: booleans matching the same flags oncreatebrand_voice_id: optional brand voicefolder_id: optional project foldersrt: OPTIONAL initial SRT to seed the proofread session —{type:"url",url:"..."}(asset_id route is also accepted by the schema but only specific media types upload viaheygen asset create— see Caveats below)
What the engine does after submission:
- Downloads the video.
- Runs ASR on the source audio → produces an original SRT in the source language (this is fetched via
srt getasoriginal_srt_url). - Translates that SRT into each target language → produces an editable SRT per language (returned via
srt getassrt_url). - Marks the proofread
status: completed.
No video render happens at this stage. That's the point — all the upfront ASR + translation cost is paid, but no avatar inference / lip-sync work is done until you generate.
Response (verified):
{
"data": {
"proofread_ids": ["b84c8e8d8a18418a8f6a001f8c8b2423-es"],
"status": "processing"
}
}status is the session-level submit status, not per-id. Each id is then independently checked via proofreads get.
proofreads get
Inputs: the proofread id.
What it returns (verified, while processing):
{
"data": {
"created_at": 1777334587,
"id": "8ce0fba6c1bf41339f991dcc0a0fc3e6-es",
"output_language": "Spanish",
"status": "processing",
"title": "PR82 sintel proofread"
}
}While failed:
{
"data": {
"created_at": 1777334317,
"failure_message": "Your video's audio is missing or corrupted, please try with another video",
"id": "b84c8e8d8a18418a8f6a001f8c8b2423-es",
"output_language": "Spanish",
"status": "failed",
"title": "..."
}
}While completed: same shape as processing, just status: "completed". submitted_for_review and input_language are nullable schema fields and may not appear on every response.
Polling cadence (verified empirically): typical extraction time for a 50-second source video is 3–5 minutes. Poll every 60 s after the first 30 s. Hard timeout: 30 min — beyond that, treat the session as stuck.
proofreads srt get
Inputs: the proofread id (must be completed).
What it returns (verified):
{
"data": {
"original_srt_url": "https://resource2.heygen.ai/video_translate/.../<title>_proofread_original.srt?...",
"srt_url": "https://resource2.heygen.ai/video_translate/.../<title>_proofread.srt?..."
}
}Both URLs are presigned and short-lived. Download immediately:
SRT_URL=$(heygen video-translate proofreads srt get "$PR_ID" \
| jq -r '.data.srt_url')
curl -s "$SRT_URL" -o /tmp/proofread.srtsrt_url = the editable, target-language SRT that the engine produced. This is what you edit and re-upload.
original_srt_url = the engine's transcription of the source audio in the source language. Useful as ground-truth context when the user is verifying the translation against what was actually said in the source. NEVER upload this back as a target-language SRT — it'll fail or render the wrong language.
Verified format of the SRT files: standard SRT (UTF-8), well-formed timecodes, line numbers, separator blank lines. No HeyGen-proprietary format. Editable by hand, by sed, or by any subtitle tool.
Example (real output from a Sintel trailer test):
1
00:00:12,220 --> 00:00:14,320
¿Qué te trae a la tierra de los guardianes?
2
00:00:18,660 --> 00:00:19,900
Busco a alguien.proofreads srt update
Inputs:
proofread-idas positional argumentsrtbody:{type: "url", url: "..."}or{type: "asset_id", asset_id: "..."}
Upload behavior:
✅ asset_id route works (preferred). heygen asset create accepts SRT (supported types: png, jpeg, mp4, webm, mp3, wav, pdf, srt). Upload the edited SRT, capture .data.asset_id, and pass {type:"asset_id", asset_id:"..."}:
ASSET_ID=$(heygen asset create --file /tmp/proofread.srt | jq -r '.data.asset_id')
heygen video-translate proofreads srt update <proofread-id> \
-d "{\"srt\":{\"type\":\"asset_id\",\"asset_id\":\"$ASSET_ID\"}}"✅ URL route works (fallback). Host the edited SRT at any publicly reachable URL and pass {type:"url", url:"..."}. (Non-subtitle content-type mismatches still error on upload.)
Practical patterns for hosting the edited SRT (URL route):
| Where the SRT lives | How to make it reachable |
|---|---|
| Local file | Upload to S3 / Cloud Storage with public-read OR a presigned URL ≥2 h |
| GitHub gist | Use the raw URL: https://gist.githubusercontent.com/<user>/<id>/raw/<file>.srt |
| GitHub repo | Use the raw URL: https://raw.githubusercontent.com/<owner>/<repo>/<branch>/<path>.srt |
| Vercel / static host | Drop into public/ and reference the absolute URL |
| Already-uploaded HeyGen presigned URL | The presigned URLs from srt get work for round-tripping, but they expire — use only for very short edit cycles |
The URL must return Content-Type of application/x-subrip or text/plain and the SRT body. Verify with curl -sI "$URL" before submitting.
Response (verified): the full proofread resource shape. The session is still completed; the SRT replacement takes effect immediately for the next generate call.
proofreads generate
Inputs:
proofread-idas positional argument--captions(default false): burn the corrected captions into the final video--translate-audio-only(default false): skip lip-sync, output dubbed audio only--callback-url,--callback-id: optional webhook for completion
What the engine does:
- Reads the proofread session's current SRT (the most recently uploaded one).
- Runs the actual translation render (precision-mode avatar inference + voice clone + lip-sync).
- Returns a
video_translation_idthat is then polled via the regularheygen video-translate get(NOTproofreads get— at this point the resource graduates from "proofread" to "translation").
Response (verified):
{
"data": {
"status": "processing",
"video_translation_id": "10c0808c39d847c2b1412af252ce50b5-es"
}
}After this call, polling shifts to:
heygen video-translate get "$VID_ID"…which returns the standard translation response shape (status, eventual video_url, failure_message, etc.).
Full CLI walkthrough (verified end-to-end)
Substitute <proofread-id> and <vid-id> with the actual ids you receive.
# 1. Submit proofread session
heygen video-translate proofreads create \
-d '{"video":{"type":"url","url":"https://example.com/source.mp4"}}' \
--output-languages "Spanish" \
--mode precision \
--enable-speech-enhancement \
--keep-the-same-format \
--speaker-num 1 \
--title "Q4 launch — Spanish proofread"
# → {"data":{"proofread_ids":["<id-es>"],"status":"processing"}}
# 2. Poll until completed (typical: 3–5 min for short videos)
heygen video-translate proofreads get <proofread-id>
# → status: processing → completed (or failed + failure_message)
# 3. Fetch the editable SRT and the original-language transcript
heygen video-translate proofreads srt get <proofread-id> > /tmp/srt-resp.json
SRT_URL=$(jq -r '.data.srt_url' /tmp/srt-resp.json)
ORIG_URL=$(jq -r '.data.original_srt_url' /tmp/srt-resp.json)
curl -s "$SRT_URL" -o /tmp/proofread.srt
curl -s "$ORIG_URL" -o /tmp/proofread-original.srt
# 4. Edit /tmp/proofread.srt — by hand, by sed, by tool
# Example: brand glossary find-replace
sed -i 's/Centro Garra/ClawHub/g' /tmp/proofread.srt
sed -i 's/José Joshua/Joshua Xu/g' /tmp/proofread.srt
# 5. Upload the edited SRT as an asset, then reference it by asset_id
# (fallback: host at a public URL and use {"type":"url","url":"..."})
ASSET_ID=$(heygen asset create --file /tmp/proofread.srt | jq -r '.data.asset_id')
heygen video-translate proofreads srt update <proofread-id> \
-d "{\"srt\":{\"type\":\"asset_id\",\"asset_id\":\"$ASSET_ID\"}}"
# → returns the proofread resource (status still completed)
# 6. Kick off the final render with the corrected captions
heygen video-translate proofreads generate <proofread-id> --captions
# → {"data":{"video_translation_id":"<vid-id>","status":"processing"}}
# 7. Poll the resulting translation to completion
heygen video-translate get <vid-id>
# → status: running → completed; data.video_url has the final mp4When to insist on proofreads
- Long videos (>3 minutes). The cost of re-rendering a 10-minute mistranslation is high; ASR + glossary fixes are cheap.
- Corporate / branded content. Brand names, product names, slogans, claims need to land exactly.
- Legal, medical, educational content. Wrong terminology has consequences beyond user disappointment.
- The user reads the target language natively. They'll catch things you can't.
- Source has named entities (people, companies, products, places) that auto-translation will likely mangle.
- High-formality languages (ja, ko, th, de) where the source is conversational and the user wants register-matched output.
- RTL languages (ar, he, ur, fa) where caption styling matters — sidecar SRT delivery via proofread is cleaner than burned-in captions.
If two or more apply: default proofreads ON. Don't ask — propose: "For [reason], let's do a quick proofread before final render."
When to skip proofreads
- Short videos (<60 s) that aren't high-stakes.
- The user wants speed and is willing to re-translate if quality is off.
- Single language, single speaker, neutral content.
- The user explicitly says "just go" or "fast as possible".
When the user CAN'T review the SRT
If neither the user nor a reachable native speaker reads the target language:
- Skip proofreads — they can't add value to a review nobody performs.
- Set lower expectations on the result. Suggest a test clip first.
- Offer to deliver the dubbed video plus the SRT as a sidecar so they can share with a native-speaking colleague to review BEFORE distribution.
Don't run proofreads as a checkbox if no one will actually read the SRT — it just adds time without adding accuracy.
Common SRT edits
The engine WILL translate proper nouns and jargon by default. Find-and-replace is the fastest way to surgically restore them.
Brand glossary / do-not-translate
# Names, products, companies, slogans
sed -i 's/Centro Garra/ClawHub/g' /tmp/proofread.srt
sed -i 's/José Joshua/Joshua Xu/g' /tmp/proofread.srt
sed -i 's/Garra/Claw/g' /tmp/proofread.srt # only if "Garra" wasn't intendedFor non-trivial glossaries, write a small Python script with the full list. Show the user the list first; have them confirm before applying.
Register / formality fixes
For ja/ko/de/fr/es/th/hi conversational content where the engine landed too formal:
| Language | Find | Replace |
|---|---|---|
| German | Sie |
du |
| German | Ihr |
dein |
| German | Ihre |
deine |
| German | Sie haben |
du hast |
| French | vous |
tu (context-aware — see caveat below) |
| French | votre |
ton / ta |
| French | vos |
tes |
| Japanese | です (declarative) → strip |
per-line review needed |
| Japanese | ます (verb suffix) → strip |
per-line review needed |
| Korean | ㅂ니다 → 요 |
per-line review needed |
| Thai | ค่ะ / ครับ (particles) → strip |
per-line review needed |
| Spanish | usted → tú |
LATAM informal target |
| Spanish | vosotros → ustedes |
es-ES → LATAM |
Caveat: bulk sed-replace on register markers can break sentences in CJK/SEA languages where the marker is part of a verb. Walk through line-by-line for ja/ko/th, batch with confirmation for de/fr/es.
Numbers, dates, units
The engine usually localizes numbers and dates correctly. Spot-check:
- Currency:
$1,000may render asmil dólares,US$1.000, or1.000 $. Pick the audience convention. - Dates:
4/27→27 de abril(es),4月27日(ja),27.04.(de),27/4(UK). - Units: imperial → metric is NOT done. Surface to the user if the source uses miles/feet/Fahrenheit and the audience expects metric.
Cultural references
Idioms, sports references, and pop-culture callouts often translate literally and lose meaning. Flag affected lines, ask whether to adapt or keep literal.
Sidecar SRT delivery
If the user wants captions as a sidecar (.srt) instead of burned-in:
- Run the proofread workflow normally.
- On
generate, omit--captions. - Deliver the dubbed video AND the corrected SRT (which you already have at
/tmp/proofread.srtfrom Step 4) to the user.
This is the cleanest path for: RTL languages, brand-styled captions (their editor styles), or accessibility workflows where SRT is required.
Cost / time math
Proofreads add roughly:
- 3–5 min for SRT extraction (Step 2). Verified empirically on a 50-second source video.
- 2–10 min for review/edit (Step 4) — depends on length and editing depth.
- Same render time as a normal translation for the final generate (Step 6–7).
Total proofreads overhead: ~10–20 minutes. For high-stakes content, trivially worth it. For a 30-second product demo, overkill.
Cost: HeyGen bills the final render the same as a non-proofread translation. The proofread session itself is light — no avatar inference happens until generate. So you effectively pay once (for the final render) regardless of how many SRT-edit cycles you ran.
Failure modes specific to proofreads
| Symptom | Verified failure_message |
Cause | Fix |
|---|---|---|---|
failed immediately on submit |
Failed to download video from url, please check the url is valid or the video is public |
Source URL was 401/403/404, returned HTML, redirected to a login page, or had wrong MIME | HEAD-check the URL with curl -sI; ask user for public URL or local file → upload route |
failed after ~30 s of processing |
Your video's audio is missing or corrupted, please try with another video |
Source has no audible speech, audio track is silent / missing, or codec is unparseable | Verify the source has speech; consider re-encoding to MP4 + AAC; for silent / animation-only sources, route to a different workflow (this skill won't help) |
asset create returns Content type not supported ... |
(CLI error envelope, not API status) | The uploaded file's sniffed content type isn't a supported type (png/jpeg/mp4/webm/mp3/wav/pdf/srt) — e.g. a mislabeled or non-subtitle file |
Upload a real .srt via heygen asset create --file <x.srt> and pass {type:"asset_id",...}, or use the URL route |
| Uploaded SRT timing is off | n/a — bad render result | SRT timecodes were edited or shifted | Re-download the SRT from proofreads srt get and edit text only, never timecodes; re-upload |
generate runs but final render uses old text |
n/a | srt update didn't take or you forgot to call it before generate |
Re-upload, then re-call proofreads generate |
| Proofread session expired | session not found | Sessions have a TTL (typically 24 h) | Re-create the proofread; don't try to revive an expired one |
For other failure_message strings, see troubleshooting.md.
Caveats and known quirks (verified)
heygen asset createaccepts SRT files. Supported types:png/jpeg/mp4/webm/mp3/wav/pdf/srt. Upload the edited SRT, capture.data.asset_id, and pass{type:"asset_id",...}tosrt update(preferred); the URL route remains a fallback.proofreads createreturnsproofread_ids(plural, one per language) and astatusat the SESSION level. The session-levelstatusis just the submit ack; per-id status comes fromproofreads get.statusenum isprocessing | completed | failed. Notpending/running. (The translation-render endpoint usespending | running | completed | failed— these are different state machines.)original_srt_urlis auto-populated even when no SRT was provided at create time. It's the engine's source-language transcription, not a copy of an SRT you uploaded. Useful for verification, never re-upload as a target-language SRT.- SRT filenames carry the title. The downloaded SRTs are named
<title>_proofread.srtand<title>_proofread_original.srt. Pick a clean, fileNAME-safe--title. - Polling shifts after
generate. Up throughsrt update, you pollheygen video-translate proofreads get. Aftergeneratereturns avideo_translation_id, you pollheygen video-translate getinstead — same translation poll loop as a non-proofread workflow. - Captions on
generateare independent of captions in the proofread session. The proofread session always produces an editable SRT (that's its purpose); whether the FINAL video has captions burned in is controlled by--captionsongenerate.