# 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}/generate
```
Resource 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 filename
- `mode`: `"speed"` or `"precision"` — `precision` is recommended; final render quality follows from this
- `speaker_num`: integer, sets diarization expectation
- `enable_speech_enhancement`, `disable_music_track`, `enable_video_stretching`, `keep_the_same_format`: booleans matching the same flags on `create`
- `brand_voice_id`: optional brand voice
- `folder_id`: optional project folder
- `srt`: 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 via `heygen asset create` — see Caveats below)
**What the engine does after submission:**
1. Downloads the video.
2. Runs ASR on the source audio → produces an **original SRT** in the source language (this is fetched via `srt get` as `original_srt_url`).
3. Translates that SRT into each target language → produces an **editable SRT** per language (returned via `srt get` as `srt_url`).
4. 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):**
```json
{
"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`):**
```json
{
"data": {
"created_at": 1777334587,
"id": "8ce0fba6c1bf41339f991dcc0a0fc3e6-es",
"output_language": "Spanish",
"status": "processing",
"title": "PR82 sintel proofread"
}
}
```
**While `failed`:**
```json
{
"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):**
```json
{
"data": {
"original_srt_url": "https://resource2.heygen.ai/video_translate/.../
_proofread_original.srt?...",
"srt_url": "https://resource2.heygen.ai/video_translate/.../_proofread.srt?..."
}
}
```
Both URLs are **presigned and short-lived**. Download immediately:
```bash
SRT_URL=$(heygen video-translate proofreads srt get "$PR_ID" \
| jq -r '.data.srt_url')
curl -s "$SRT_URL" -o /tmp/proofread.srt
```
**`srt_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-id` as positional argument
- `srt` body: `{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:"..."}`:
```bash
ASSET_ID=$(heygen asset create --file /tmp/proofread.srt | jq -r '.data.asset_id')
heygen video-translate proofreads srt update \
-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///raw/.srt` |
| GitHub repo | Use the raw URL: `https://raw.githubusercontent.com////.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-id` as 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:**
1. Reads the proofread session's current SRT (the most recently uploaded one).
2. Runs the actual translation render (precision-mode avatar inference + voice clone + lip-sync).
3. Returns a `video_translation_id` that is then polled via the regular `heygen video-translate get` (NOT `proofreads get` — at this point the resource graduates from "proofread" to "translation").
**Response (verified):**
```json
{
"data": {
"status": "processing",
"video_translation_id": "10c0808c39d847c2b1412af252ce50b5-es"
}
}
```
**After this call**, polling shifts to:
```bash
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 `` and `` with the actual ids you receive.
```bash
# 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":[""],"status":"processing"}}
# 2. Poll until completed (typical: 3–5 min for short videos)
heygen video-translate proofreads get
# → status: processing → completed (or failed + failure_message)
# 3. Fetch the editable SRT and the original-language transcript
heygen video-translate proofreads srt get > /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 \
-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 --captions
# → {"data":{"video_translation_id":"","status":"processing"}}
# 7. Poll the resulting translation to completion
heygen video-translate get
# → status: running → completed; data.video_url has the final mp4
```
---
## When 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
```bash
# 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 intended
```
For 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,000` may render as `mil dólares`, `US$1.000`, or `1.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:
1. Run the proofread workflow normally.
2. On `generate`, **omit `--captions`**.
3. Deliver the dubbed video AND the corrected SRT (which you already have at `/tmp/proofread.srt` from 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 ` 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](troubleshooting.md#errors--action-mapping).
---
## Caveats and known quirks (verified)
- **`heygen asset create` accepts 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",...}` to `srt update` (preferred); the URL route remains a fallback.
- **`proofreads create` returns `proofread_ids` (plural, one per language) and a `status` at the SESSION level.** The session-level `status` is just the submit ack; per-id status comes from `proofreads get`.
- **`status` enum is `processing | completed | failed`.** Not `pending` / `running`. (The translation-render endpoint uses `pending | running | completed | failed` — these are different state machines.)
- **`original_srt_url`** is 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 `_proofread.srt` and `_proofread_original.srt`. Pick a clean, fileNAME-safe `--title`.
- **Polling shifts after `generate`.** Up through `srt update`, you poll `heygen video-translate proofreads get`. After `generate` returns a `video_translation_id`, you poll `heygen video-translate get` instead — same translation poll loop as a non-proofread workflow.
- **Captions on `generate` are 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 `--captions` on `generate`.