ttconv
TypeScript subtitle conversion library. Converts between SCC, STL, SRT, VTT, TTML/IMSC, and ASS/SSA formats.
Installation
npm install omakase-ttconv-ts
Quick Start
Universal convert function
The simplest API — pass input content, input format, and output format:
import { convert } from 'omakase-ttconv-ts';
import type { ConvertConfig } from 'omakase-ttconv-ts';
import { readFileSync } from 'fs';
// SCC → VTT
const sccContent = readFileSync('captions.scc', 'utf8');
const vtt = convert(sccContent, 'scc', 'vtt');
// STL → SRT
const stlData = readFileSync('subtitles.stl'); // Buffer
const srt = convert(stlData, 'stl', 'srt');
// SRT → IMSC/TTML
const srtContent = readFileSync('captions.srt', 'utf8');
const ttml = convert(srtContent, 'srt', 'ttml');
// With reader/writer config and progress callback
const config: ConvertConfig = {
reader: { textAlign: 'center' }, // SccReaderConfiguration
writer: { textFormatting: false }, // SrtWriterConfig / VttWriterConfig / ImscWriterConfig
};
const result = convert(sccContent, 'scc', 'srt', config, (p) => console.log(`${Math.round(p * 100)}%`));
Supported input formats: 'scc' | 'stl' | 'srt' | 'vtt' | 'ttml' | 'ass' | 'ssa'
Supported output formats: 'srt' | 'vtt' | 'ttml'
input is string for all text formats and Buffer for 'stl'.
Universal parse function
Parses any supported input format into a simplified, format-agnostic ParsedFile — a flat list of cues plus any document-level metadata — instead of the full ContentDocument model. Throws if the reader recorded any spec-conformance errors.
import { parse } from 'omakase-ttconv-ts';
import type { ParsedFile } from 'omakase-ttconv-ts';
import { readFileSync } from 'fs';
const vttContent = readFileSync('captions.vtt', 'utf8');
const parsedFile: ParsedFile = parse(vttContent, 'vtt');
console.log(parsedFile.valid); // true if no parse errors were recorded
console.log(parsedFile.cues); // ParsedCue[]: { id, start, end, text }
console.log(parsedFile.meta); // string[] | undefined — VTT-only: WEBVTT header text / X-TIMESTAMP-MAP
console.log(parsedFile.notes); // string[] | undefined — VTT-only: NOTE block bodies
// throws if the source had any recorded parse errors
try {
parse('not a valid VTT file', 'vtt');
} catch (e) {
console.error(e); // "Errors found while parsing file: ..."
}
ParsedCue.start/.end are number | undefined — a cue whose begin/end couldn't be resolved from the source (e.g. a TTML <p> that inherits timing from an ancestor instead of specifying it directly, or a pop-on SCC caption with no closing/clear command) is left undefined rather than defaulted; apply any fallback (e.g. clamping to 0 or to the document's overall duration) on your own side if you need one.
meta/notes are currently populated only by the VTT reader (WEBVTT header text, including the HLS X-TIMESTAMP-MAP extension, and NOTE blocks respectively) — every other format leaves them undefined. Each is deduped by exact text and kept in order of first appearance, so parsing several concatenated VTT segments that repeat the same header/notes collapses them to one entry each.
parse is a thin composition of two lower-level functions, also exported individually:
import { toModel, toParsedFile } from 'omakase-ttconv-ts';
const doc = toModel(vttContent, 'vtt'); // same as vttToModel, dispatched by format
const parsedFile = toParsedFile(doc); // ContentDocument -> ParsedFile, doesn't throw
Use toModel/toParsedFile directly instead of parse when you want to inspect doc.getErrors() yourself (e.g. to log parse problems without aborting), or to convert an already-parsed ContentDocument (from any reader, or one you built by hand) into a ParsedFile.
Universal concatenate function
Merges multiple same-format inputs (raw text) into a single output string of that same format — parsing each one, appending their content into a single document, and writing it back out once. Only supports the three round-trippable output formats ('srt' | 'vtt' | 'ttml'); the reader and writer for a given call always match.
import { concatenate } from 'omakase-ttconv-ts';
import { readFileSync } from 'fs';
const segments = ['segment1.vtt', 'segment2.vtt', 'segment3.vtt']
.map(f => readFileSync(f, 'utf8'));
const merged = concatenate(segments, 'vtt');
- A single input is returned verbatim — it isn't parsed or re-serialized at all.
- An empty array returns
undefined. readerConfig/writerConfigare forwarded to every input's parse and the final write respectively.- For VTT specifically, repeated header metadata /
NOTE/REGIONblocks across the concatenated segments are deduped automatically (seeparseabove) — this is the intended way to reassemble a subtitle file that arrived split into HLS-style segments, since each chunk typically repeats the sameWEBVTTheader. - If a non-first input fails to parse cleanly,
concatenatethrows; a failure in the first input is logged (viaconsole.error) but doesn't abort — its (possibly empty) document is still used as the base the other inputs get merged into.
concatenate is built on the same toModel/fromModel functions parse uses on the reader side:
import { toModel, fromModel } from 'omakase-ttconv-ts';
const doc = toModel(vttContent, 'vtt');
const vttOut = fromModel(doc, 'vtt'); // same as vttFromModel, dispatched by format
Format-specific convenience functions (async)
import { convertScc, convertStl } from 'omakase-ttconv-ts';
import { readFileSync } from 'fs';
// SCC → VTT
const sccContent = readFileSync('captions.scc', 'utf8');
const vtt = await convertScc(sccContent, 'vtt');
// SCC → SRT
const srt = await convertScc(sccContent, 'srt');
// SCC → IMSC/TTML
const ttml = await convertScc(sccContent, 'imsc');
// STL → VTT
const stlData = readFileSync('subtitles.stl');
const vttFromStl = await convertStl(stlData, 'vtt');
Two-step conversion (reader → writer)
Use this approach to apply configuration or perform multiple conversions from the same parsed document.
import { sccToModel, srtFromModel, vttFromModel, imscFromModel } from 'omakase-ttconv-ts';
import { readFileSync } from 'fs';
const sccContent = readFileSync('captions.scc', 'utf8');
const doc = sccToModel(sccContent);
const srt = srtFromModel(doc);
const vtt = vttFromModel(doc);
const ttml = imscFromModel(doc);
Readers
SCC (CTA-608 Closed Captions)
import { sccToModel } from 'omakase-ttconv-ts';
import type { SccReaderConfiguration } from 'omakase-ttconv-ts';
const config: SccReaderConfiguration = {
textAlign: 'auto', // 'auto' | 'left' | 'center' | 'right'
};
const doc = sccToModel(sccContent, config);
STL (EBU STL binary)
import { stlToModel } from 'omakase-ttconv-ts';
import { readFileSync } from 'fs';
const stlData = readFileSync('subtitles.stl'); // Buffer
const doc = stlToModel(stlData);
SRT
import { srtToModel } from 'omakase-ttconv-ts';
const doc = srtToModel(srtContent);
Supports inline HTML tags: <b>, <i>, <u>, <font color="...">.
VTT (WebVTT)
import { vttToModel } from 'omakase-ttconv-ts';
const doc = vttToModel(vttContent);
TTML / IMSC
import { ttmlToModel } from 'omakase-ttconv-ts';
const doc = ttmlToModel(ttmlContent);
ASS / SSA
import { assToModel } from 'omakase-ttconv-ts'; // ssaToModel is the same function
const doc = assToModel(assContent);
Writers
SRT
import { srtFromModel } from 'omakase-ttconv-ts';
import type { SrtWriterConfig } from 'omakase-ttconv-ts';
const config: SrtWriterConfig = {
textFormatting: true, // default true; set false to strip all HTML tags
};
const srt = srtFromModel(doc, config);
VTT (WebVTT)
import { vttFromModel } from 'omakase-ttconv-ts';
import type { VttWriterConfig } from 'omakase-ttconv-ts';
const config: VttWriterConfig = {
styleRegion: false, // default false; set true to emit VTT region/line position cues
};
const vtt = vttFromModel(doc, config);
IMSC / TTML
import { imscFromModel } from 'omakase-ttconv-ts';
import type { ImscWriterConfig } from 'omakase-ttconv-ts';
import { Fraction } from 'omakase-ttconv-ts';
const config: ImscWriterConfig = {
frameRate: new Fraction(30), // output frame rate
timeExpressionSyntax: 'clock_time', // 'clock_time' | 'frames' | 'clock_time_with_frames'
contentProfilesSignaling: 'content_profiles', // 'none' | 'content_profiles'
};
const ttml = imscFromModel(doc, config);
Progress Callbacks
All writers accept an optional progress callback as the third argument:
const srt = srtFromModel(doc, {}, (progress) => {
console.log(`${Math.round(progress * 100)}%`);
});
Browser Bundle
A pre-built IIFE bundle is available at dist/ttconv.browser.js. It exposes the global ttconvModule:
<script src="ttconv.browser.js"></script>
<script>
const doc = ttconvModule.sccToModel(sccContent);
const vtt = ttconvModule.vttFromModel(doc);
</script>
Working with the Document Model
Readers return a ContentDocument and writers consume one. You can also build or inspect a document directly using the model classes.
import {
ContentDocument, Body, Div, P, Span, Text, Region, Fraction,
StyleProperties, FontStyleType, FontWeightType, makeColor, LengthUnits, makeLengthType
} from 'omakase-ttconv-ts';
// Build a document from scratch
const doc = new ContentDocument();
doc.setLang('en');
const region = new Region('r1');
region.setStyle(StyleProperties.Origin, {
x: makeLengthType(10, LengthUnits.pct),
y: makeLengthType(85, LengthUnits.pct),
});
region.setStyle(StyleProperties.Extent, {
width: makeLengthType(80, LengthUnits.pct),
height: makeLengthType(10, LengthUnits.pct),
});
doc.putRegion(region);
const body = new Body(doc);
doc.setBody(body);
const div = new Div(doc);
body.pushChild(div);
const p = new P(doc);
p.setRegion(region);
p.setBegin(new Fraction(0)); // seconds as rational number
p.setEnd(new Fraction(5, 2)); // 2.5 seconds
div.pushChild(p);
const span = new Span(doc);
span.setStyle(StyleProperties.FontStyle, FontStyleType.italic);
span.setStyle(StyleProperties.Color, makeColor(255, 255, 255)); // white
span.pushChild(new Text(doc, 'Hello, world!'));
p.pushChild(span);
const srt = srtFromModel(doc);
Fraction
Fraction represents an exact rational time value (numerator/denominator in seconds).
import { Fraction } from 'omakase-ttconv-ts';
const t = new Fraction(1001, 30000); // 1001/30000 seconds
const t2 = new Fraction(5); // 5 seconds
console.log(t.toNumber()); // 0.033366...
console.log(t2.toNumber()); // 5
const sum = t.add(t2);
Style properties
import {
StyleProperties,
FontStyleType, // normal | italic | oblique
FontWeightType, // normal | bold
TextAlignType, // left | center | right | start | end | justify
DisplayAlignType, // before | center | after
NamedColors, // white, black, red, green, blue, cyan, magenta, yellow, transparent
makeColor, // makeColor(r, g, b, a?) → ColorType
} from 'omakase-ttconv-ts';
License
BSD-2-Clause. See THIRD_PARTY_NOTICES.md for third-party attributions.