# Caption Desk > THE CAPTION CHANNEL IS TWO BYTES A FRAME. CEA-608 rides 2 bytes per field on > line 21; field 1 goes past once a frame and carries services CC1 and CC2, so ONE > caption service gets 2 bytes every frame and nothing more - 59.94 B/s at 29.97 fps. > A basic character is one byte, so a caption is TYPED onto the screen at 2 characters > a frame: a full 2 x 32 pop-on is 64 characters plus 20 bytes of doubled > control codes, 42 frames, 1.4 s - and in a buffered mode all of that > passes BEFORE anything appears. The wire is a QUEUE, so lateness accumulates; a > second service on the same field halves the first one's rate; and readability and > deliverability are different tests. Five lanes over one caption file, plus a free > browser-side engine doing exact integer arithmetic on the standard's own numbers. URL: https://caption-desk.skillsafe.ai/ API: https://caption-desk.skillsafe.ai/api.html Token panel: https://caption-desk.skillsafe.ai/tokens.html (noindex) Platform: SkillSafe (https://skillsafe.ai/) Derived from the `baoyu-youtube-transcript` skill in https://github.com/jimliu/baoyu-skills, which downloads transcripts and subtitle files with their timings, in several languages, with speaker identification. That file is exactly the work object here. Not affiliated with or endorsed by the authors of that repository. ## The one thing to know **THE CAPTION CHANNEL IS TWO BYTES A FRAME.** CEA-608 rides 2 bytes per field on line 21. Field 1 goes past once a frame and carries services CC1 and CC2, so one caption service gets 2 bytes every frame and nothing more — 59.94 B/s at 29.97 fps. A basic character is one byte. **So a caption is TYPED onto the screen at 2 characters per frame:** ``` a full 2 x 32 caption 64 chars + 20 control = 84 bytes = 42 frames = 1.4 s a 12-character caption 12 chars + 16 control = 28 bytes = 14 frames = 0.47 s ``` In a buffered mode all of that time passes **before anything appears**. And look at the second row: on a short caption the control codes are 57.1% of the cost, because **every control code is sent TWICE** and a pop-on cycle pays 5 pairs before a single character of text. **THE WIRE IS A QUEUE, NOT A BUDGET.** It carries one caption at a time, so a caption whose transmission has not finished when its cue time arrives is simply late — and the wire is still busy when the next one wants to start, so the lateness ACCUMULATES down a run of dense dialogue. Which is why checking cues one at a time gives the wrong answer, and why the first caption in a file is the one that usually fails: it has to start crossing the wire before the programme starts. **AND A SECOND SERVICE HALVES THE FIRST ONE.** CC1 and CC2 share the field's two bytes, so adding Spanish on CC2 takes 1 byte a frame out of English on CC1: 29.97 B/s each instead of 59.94 B/s, and every caption takes twice as long to send. Nothing in the English file says so. **FINALLY, READABILITY AND DELIVERABILITY ARE DIFFERENT TESTS.** A reader tops out around 21 chars/s and the wire types at 59.9 chars/s — so a file nobody could read too fast to follow spends at most 35% of the channel on TEXT. The channel is not filled by words. It is filled by control codes, extra rows and second services, none of which a subtitle checker measures — which is how a file passes every reading rule and still cannot be delivered. ## What a caption costs | Caption | Characters | Control bytes | Total | Frames of wire | At 29.97 fps | | --- | --- | --- | --- | --- | --- | | 1 row of 12 | 12 | 16 | 28 | 14 frames | 0.47 s | | 1 row of 24 | 24 | 16 | 40 | 20 frames | 0.67 s | | 1 row of 32 | 32 | 16 | 48 | 24 frames | 0.8 s | | 2 rows of 24 | 48 | 20 | 68 | 34 frames | **1.13 s** | | 2 rows of 32 | 64 | 20 | 84 | 42 frames | **1.4 s** | | 3 rows of 32 | 96 | 24 | 120 | 60 frames | **2 s** | | 4 rows of 32 | 128 | 28 | 156 | 78 frames | **2.6 s** | In pop-on, at 29.97 fps, one service. **A full two-row caption is 84 bytes and 1.4 s of wire** — and in a buffered mode all of that passes before anything appears on screen. Note the other end of the table: a 12-character caption is 28 bytes, of which 16 bytes — 57.1% — is control codes. **On short captions the overhead IS the cost.** ## What the wire carries at each rate | Rate | One service | Characters a second | Whole line 21 | A full two-row pop-on | | --- | --- | --- | --- | --- | | 24000/1001 (23.976) fps | 47.95 B/s | 48 chars/s | 767.23 bit/s | 42 frames, 1.75 s | | 24 fps | 48 B/s | 48 chars/s | 768 bit/s | 42 frames, 1.75 s | | 25 fps | 50 B/s | 50 chars/s | 800 bit/s | 42 frames, 1.68 s | | 30000/1001 (29.97) fps | 59.94 B/s | 59.9 chars/s | 959.04 bit/s | 42 frames, 1.4 s | | 30 fps | 60 B/s | 60 chars/s | 960 bit/s | 42 frames, 1.4 s | | 50 fps | 100 B/s | 100 chars/s | 1600 bit/s | 42 frames, 0.84 s | | 60000/1001 (59.94) fps | 119.88 B/s | 119.9 chars/s | 1918.08 bit/s | 42 frames, 0.7 s | | 60 fps | 120 B/s | 120 chars/s | 1920 bit/s | 42 frames, 0.7 s | `2 bytes per field x 2 fields x the frame rate` is the whole line-21 stream, which at 29.97 fps is 959.04 bit/s — the standard's familiar 960, derived rather than quoted. **One caption service draws from ONE field**, which goes past once a frame, so it gets 2 bytes a frame and 59.94 B/s at 29.97 fps. A basic character is one byte, so that column is also the rate at which a caption is TYPED onto the screen. ## What each mode costs | Mode | Control pairs | Control bytes | A full caption | Positionable | What it does | | --- | --- | --- | --- | --- | --- | | pop-on | 5 | 20 | 84 bytes, 42 frames | yes | loaded into the back buffer and flipped, so the whole transmission happens before anything appears | | roll-up | 3 | 12 | 76 bytes, 38 frames | **no** | the base row is set once and each new line rolls the others up, so text appears as it arrives | | paint-on | 3 | 12 | 76 bytes, 38 frames | yes | written straight to the displayed memory, so the viewer watches it type across the screen | On a full 2-row, 32-column caption. The modes differ ONLY in their control codes — the text is the same — so the difference is 8 bytes a caption, which on a busy channel is what decides whether cues arrive. **Every control code is sent twice**, so each pair costs 4 bytes rather than 2 bytes. ## What a second service costs the first | Services on the field | Bytes a frame each | Bytes a second each | Characters a second | A full pop-on | | --- | --- | --- | --- | --- | | 1 | 2 | 59.94 B/s | 59.9 chars/s | 1.4 s | | 2 | 1 | 29.97 B/s | 30 chars/s | 2.8 s | **A second language on CC2 is not extra capacity — it is taken out of CC1.** Both services share the two bytes the field carries each frame, so each gets one, everything takes twice as long to send, and a file that was comfortable at one service is over the wire at two. Nothing about the second language is visible in the first language's file. ## The reading limit against the wire | Reading limit | Text share of the wire | With 2 services | What is left for control codes | | --- | --- | --- | --- | | 12 chars/s | 20% | 40% | 80% | | 15 chars/s | 25% | 50.1% | 75% | | 17 chars/s | 28.4% | 56.7% | 71.6% | | 21 chars/s | 35% | 70.1% | 65% | | 25 chars/s | 41.7% | 83.4% | 58.3% | | 30 chars/s | 50.1% | **100.1%** | 49.9% | At 29.97 fps, one service, the wire types at 59.9 chars/s. **So a file nobody could read too fast spends only 35% of the channel on text** — which is the most useful thing on this page. It means the wire is almost never filled by words. It is filled by control codes, by extra rows, and by a second service; and those are the three things a style guide never mentions. ## Constants and thresholds | Constant | Value | What it decides | | --- | --- | --- | | bytes per field | 2 | the whole budget. Everything else in this app is division | | fields per frame | 2 | so the line-21 stream is 959.04 bit/s at 29.97 fps | | services per field | 2 | CC1 and CC2 share field 1, and share its two bytes | | bits per byte | 8 | seven data bits and an odd parity bit | | columns | 32 | the display width. There is no 33rd column | | rows in the grid | 15 | the display height | | a control pair | 2 bytes | the size of one code | | sent twice | x2 | so a code costs 4 bytes, not 2 bytes | | an extended character | 2 bytes | an accent or a curly quote is a two-byte code | | rows a caption | 2 | convention, not the standard. Each extra row is another doubled pair | | reading limit | 21 chars/s | above this a cue is reported as too fast to follow | | shortest cue | 20 frames | below this a caption reads as a flash | | smallest gap | 2 frames | below this consecutive captions read as one block | | lateness worth naming | 2 frames | below this an inserter would absorb it | | a busy wire | 80% | above this the average fits and the peaks will not | **The first nine are CEA-608's, not this page's policy** — the byte rate, the doubling, the grid and the two-byte extended characters are what the standard is, and they are in this table so a reader can check them rather than take them on trust. Everything below them is guidance this page chose, and the harness proves each one is load-bearing by moving it and requiring the report to move with it. ## File grammar Paste the caption file itself. **SRT and WebVTT are both read** - the difference between them is a comma and a dot - and a few optional `KEY: value` lines above the cues set the channel. ``` PROGRAMME: episode 4, English CC1 a name for the record FPS: 29.97 29.97 is read as 30000/1001 MODE: pop-on pop-on, roll-up or paint-on SERVICES: 2 how many share the field ROWS: 2 the row budget a caption CPS: 21 the reading limit to score against DURATION: 24:00 optional; else the cues' own span 1 00:00:02.002 --> 00:00:05.005 The stock has to be cold when it goes in, or the fat will not go ``` **THE RATE IS THE ONE LINE THAT MATTERS MOST.** The byte budget is 2 bytes times the frame rate, so it is directly proportional: the same file has -16.6% more wire at 25 fps than at 29.97 fps. And `29.97` is read as the exact 30000/1001 rather than the decimal, because the decimal makes the wire 0.1% faster than it is. **TIMECODE WORKS, BUT ONLY WITH A RATE.** `00:00:04:12` is hours, minutes, seconds and FRAMES, and a frame is only a duration once FPS says how long one is - so a timecoded file with no `FPS` line gets an error rather than an assumption. A semicolon (`00:00:04;12`) means drop-frame, which skips LABELS and never a frame of picture, so it does not change the byte budget at all. **MARKUP IS COUNTED, NOT IGNORED.** ``, ``, `` and `{\i1}` are each a mid-row control code, so each one is another 4 bytes on the wire. WebVTT cue settings such as `line:` and `align:` are read and reported, because the 32 by 15 grid has rows and columns and nothing else. **A BARE NUMBER BEFORE A TIMING IS A CUE INDEX** and is ignored, so an SRT file pastes in unmodified. A `WEBVTT` line, a `NOTE` block and a `STYLE` block are skipped the same way. ## Lanes | Lane | What it answers | Sections | | --- | --- | --- | | `plan` | Choose the channel before the file is written | Summary, The Header, The Numbers, Reasoning, Next Step | | `check` (primary) | Whether this file can actually be delivered, and whether it reads | Summary, Verdict, Findings, Corrected File, Next Step | | `wire` | The wire question: what each caption costs and when it has to start | Summary, The Budget, Cue By Cue, What Each Mode Costs, Next Step | | `grid` | The grid question: 32 columns, 15 rows, and no thirty-third column | Summary, The Grid, Row By Row, What Positioning Costs, Next Step | | `deliver` | Decide what changes: the edit, the channel, or nothing | Summary, An Edit Fixes, Only The Channel Fixes, Nothing Fixes, Next Step | | If the question is | Ask | Because | | --- | --- | --- | | nothing is written yet | `plan` | it derives the byte budget, the mode and the service layout from the delivery spec, instead of writing to a style guide and discovering the channel at QC. | | the QC report says captions late | `check` | the full read: the queue, the budget, the grid and the reading speeds, and which of them is actually failing. | | what does each caption cost | `wire` | bytes, frames, and the instant each one has to start crossing the channel. | | a row will not fit | `grid` | 32 columns and 15 rows, and what positioning costs in control codes. | | we know it is wrong, what do we change | `deliver` | sorts the findings into the edit, the channel, and nothing. | ## Findings All 46 are computed in the browser and cost nothing: 4 errors, 18 warnings, 24 notes. | Code | Severity | Scope | What it says | | --- | --- | --- | --- | | `NOTHING-READABLE` | error | file | not one line parsed as a cue | | `NO-CUES` | error | file | no cues, so there is nothing to send | | `TIMECODE-WITHOUT-A-RATE` | error | file | frames in the timecode and no frame rate | | `TWO-BYTES-A-FRAME` | note | wire | the byte budget, from the standard's own numbers | | `THE-GRID-IS-32-BY-15` | note | grid | the display is 32 columns by 15 rows | | `CONTROL-CODES-ARE-SENT-TWICE` | note | wire | every control code costs four bytes, not two | | `A-FULL-POP-ON-COSTS` | note | wire | what a full two-row caption costs on the wire | | `THE-READING-LIMIT-PROTECTS-THE-WIRE` | note | reading | a readable file spends only a third of the channel on text | | `READABILITY-IS-A-DIFFERENT-QUESTION` | note | reading | the two tests, stated side by side | | `OVER-THE-WIRE` | warn | wire | the file needs more bytes than the channel carries | | `THE-WIRE-IS-BUSY` | warn | wire | the average fits and the peaks will not | | `THE-WIRE-HAS-ROOM` | note | wire | how much of the channel is in use | | `A-SECOND-SERVICE-HALVES-THE-RATE` | warn | wire | two services share two bytes a frame | | `THE-OTHER-FIELD-IS-EMPTY` | note | wire | the second field is empty, and the second language belongs on it | | `MOST-OF-THE-BYTES-ARE-CONTROL-CODES` | note | wire | the overhead is larger than the text | | `CONTROL-CODES-ARE-A-FIFTH-OF-IT` | note | wire | what share of the bytes is overhead | | `CUES-ARRIVE-LATE` | warn | queue | a cue cannot be transmitted before its start time | | `A-CUE-IS-A-FRAME-LATE` | note | queue | lateness inside the rounding | | `EVERY-CUE-ARRIVES-ON-TIME` | note | queue | the queue never falls behind | | `NO-IDLE-WIRE-AND-STILL-ON-TIME` | note | queue | a caption with no idle wire before it, arriving exactly on time | | `THE-LONGEST-TRANSMISSION` | note | queue | the heaviest cue, and when it has to start | | `THE-FIRST-CUE-HAS-NO-RUNWAY` | warn | queue | the first caption would have to start before the file does | | `THE-FIRST-CUE-IS-TIGHT` | note | queue | the first caption has very little runway | | `THE-CUE-RATE-IS-FASTER-THAN-THE-WIRE` | warn | queue | captions arrive faster than the channel can send them | | `IT-PASSES-EVERY-READING-RULE-AND-CANNOT-BE-DELIVERED` | warn | reading | clean on paper, late on the wire | | `IT-DELIVERS-AND-IT-DOES-NOT-READ` | note | reading | the other way round | | `READING-SPEED-OVER-THE-LIMIT` | warn | reading | a cue faster than a reader can follow | | `ROW-WIDER-THAN-THE-GRID` | warn | grid | a row longer than 32 characters | | `MORE-ROWS-THAN-THE-BUDGET` | warn | grid | more rows than the convention allows | | `CUE-SHORTER-THAN-THE-MINIMUM` | warn | reading | a caption too short to read | | `CUES-OVERLAP` | warn | reading | a cue starts before the previous one ends | | `GAPS-UNDER-TWO-FRAMES` | note | reading | consecutive captions will read as one block | | `CUE-TIMES-NOT-ON-A-FRAME` | warn | grid | a time that is not a whole frame | | `CUE-ENDS-BEFORE-IT-STARTS` | warn | file | a cue holding no time at all | | `THE-FILE-IS-OUT-OF-ORDER` | error | file | a cue starts before the one above it | | `STYLE-CODES-EAT-THE-BUDGET` | warn | wire | markup is another doubled pair each | | `EXTENDED-CHARACTERS-COST-DOUBLE` | warn | wire | accents and curly quotes are two-byte codes | | `EVERY-CHARACTER-IS-ONE-BYTE` | note | wire | nothing outside the basic set | | `A-DIFFERENT-MODE-WOULD-FIT` | warn | mode | another caption mode carries this file better | | `THE-MODE-IS-THE-RIGHT-ONE` | note | mode | the chosen mode is the cheapest here | | `THIS-MODE-CANNOT-BE-POSITIONED` | note | mode | roll-up sets a base row and rolls into it | | `DROP-FRAME-TIMECODE` | note | file | drop-frame skips labels, never frames | | `CUE-SETTINGS-HAVE-NO-EQUIVALENT-HERE` | note | grid | WebVTT positioning does not survive | | `DURATION-TAKEN-FROM-THE-LAST-CUE` | note | file | no duration stated | | `LINES-COULD-NOT-BE-READ` | warn | file | lines the parser could not use | | `ASSUMED-VALUES` | note | file | what was not stated, and what was assumed | **Severity means one thing here.** An `error` is something the engine cannot compute at all — there are 4 of those. A `warn` is a consequence somebody has to decide about. A `note` is the arithmetic itself. Nothing that fires on every usable file is a warning. ## What this page cannot do This page computes from the file you paste and the header above it. It has not opened a video, encoded anything, or spoken to a caption inserter. - **The byte rate, the doubling and the grid are CEA-608's.** A 708-native service has far more bandwidth and a different code set; what 708 also does is carry 608 inside it for compatibility, and it is that 608 service this page is about. If your deliverable is 708-only, the wire arithmetic here does not constrain you and the grid arithmetic still might. - **The control-code counts are the standard cycles, not your encoder's.** A real inserter may send an extra erase, may not double a code it considers safe, and may pad with nulls when idle. Those change the figures by a code or two a caption, not by an order of magnitude. - **The queue model gives each caption the whole service.** It does not model an encoder that interleaves two services frame by frame, or one that drops a caption rather than delay it. Both of those make the answer worse, not better. - **Reading speed is guidance and it varies by broadcaster and by audience.** 21 chars/s is a common adult figure; children's programming is often nearer 12, and some specs measure words a minute instead. The wire arithmetic is exact; the reading limit is a policy you should set. - **The row budget is a convention.** The grid holds 15 rows, so a three-row caption displays perfectly well - it just costs another doubled pair and covers more picture. - **Nothing here checks the words.** Whether a caption is a good caption - whether it breaks at a sensible phrase, identifies the speaker, or describes the sound - is a judgement this page has no view on. - Nothing here reaches the network, reads a file, or uploads anything. ## API `POST https://api.skillsafe.ai/v1/app-api/run` with an app session token. The body is the input object itself - there is no `input` wrapper and no `X-App-Slug` header. Every lane takes `task` plus its own fields: | Lane | Fields | | --- | --- | | `plan` | `brief` (required), `known` | | `check` | `sheet` (required), `worry` | | `wire` | `sheet` (required) | | `grid` | `sheet` (required) | | `deliver` | `sheet` (required), `fixed` | `sheet` is the caption file itself: SRT or WebVTT, pasted unmodified, optionally with KEY: value lines above the cues. `GET /v1/app-api/estimate` prices a lane for nothing and is the way to check an input shape without spending credits. Full worked examples in eight languages are at https://caption-desk.skillsafe.ai/api.html. ## Provenance The arithmetic, the queue model, the thresholds, the file grammar and every finding are this app's own. The byte rate, the doubling of control codes, the two-byte extended characters and the 32 by 15 grid are CEA-608's, and they are in the constants table above so a reader can check them rather than take them on trust. _Generated from `cc.js` by `build-skill.js`, like SKILL.md, so the two documents and the app cannot disagree._