captionkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 captionkit contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,151 @@
1
+ <div align="center">
2
+
3
+ # 🎬 captionkit
4
+
5
+ ### Convert and re-sync subtitles β€” SRT ⇄ WebVTT, shift, fix drift β€” locally.
6
+
7
+ [![npm version](https://img.shields.io/npm/v/captionkit.svg?color=success)](https://www.npmjs.com/package/captionkit)
8
+ [![bundle size](https://img.shields.io/bundlephobia/minzip/captionkit?label=gzip)](https://bundlephobia.com/package/captionkit)
9
+ [![CI](https://github.com/didrod205/captionkit/actions/workflows/ci.yml/badge.svg)](https://github.com/didrod205/captionkit/actions/workflows/ci.yml)
10
+ [![types](https://img.shields.io/npm/types/captionkit.svg)](https://www.npmjs.com/package/captionkit)
11
+ [![license](https://img.shields.io/npm/l/captionkit.svg)](./LICENSE)
12
+
13
+ **[🌐 Try the free web app β†’](https://didrod205.github.io/captionkit/)** &nbsp;Β·&nbsp; drop a `.srt` or `.vtt`, fix the sync, download. Nothing uploaded.
14
+
15
+ </div>
16
+
17
+ ---
18
+
19
+ You exported captions and the player won't take them (it wants WebVTT, you have
20
+ SRT). Or the subtitles play **2 seconds late**. Or they slowly **drift out of
21
+ sync** by the end of the video. Fixing this by hand means editing dozens of
22
+ `00:01:23,456` timestamps without breaking the format β€” and one stray comma or
23
+ missing millisecond makes the whole file invalid.
24
+
25
+ **captionkit** does it correctly: convert **SRT ⇄ WebVTT**, **shift** every cue,
26
+ **scale**/**resync** to fix drift, and **fix overlaps** β€” with exact
27
+ millisecond timecode math, **zero dependencies**, and **100% locally**.
28
+
29
+ > πŸ“Έ _Screenshot / demo GIF:_ `./web/screenshot.png` β€” record the [live app](https://didrod205.github.io/captionkit/) dropping an out-of-sync SRT and nudging it into place.
30
+
31
+ ## Why it exists
32
+
33
+ - **AI can't reliably do this.** Re-timing every cue and emitting spec-valid
34
+ SRT/WebVTT (comma vs dot, `WEBVTT` header, numbering rules, CRLF) is exact,
35
+ fiddly format work β€” a chatbot will quietly corrupt a timestamp. It's a job for
36
+ a small, tested, deterministic tool.
37
+ - **Privacy & friction.** Online subtitle converters make you upload your file
38
+ and wait. captionkit runs on your machine, instantly.
39
+ - **The drift fix is the "aha".** `resync` linearly maps the first and last cue
40
+ to where they *should* be β€” repairing subtitles that slip out of sync as the
41
+ video goes on. Most tools only do a flat shift.
42
+
43
+ ## Who it's for
44
+
45
+ **Video creators & YouTubers**, **marketers** (captioned social video),
46
+ **educators**, **accessibility teams**, translators, and **developers** building
47
+ media tooling who want a tiny subtitle library.
48
+
49
+ ## Install
50
+
51
+ **No install β€”** just open the **[web app](https://didrod205.github.io/captionkit/)**.
52
+
53
+ For the library:
54
+
55
+ ```bash
56
+ npm install captionkit
57
+ ```
58
+
59
+ Zero dependencies. ESM + CJS + TypeScript types. Runs in the browser, Node, Deno and Bun.
60
+
61
+ ## Usage
62
+
63
+ ```ts
64
+ import { parse, convert, shift, resync, toVTT } from "captionkit";
65
+
66
+ // Convert SRT β†’ WebVTT (auto-detects the input)
67
+ convert(srtText, "vtt");
68
+
69
+ // Parse, then re-time
70
+ const cues = parse(srtText);
71
+ shift(cues, 2500); // everything 2.5s later
72
+ shift(cues, -1000); // …or earlier (clamped at 0)
73
+
74
+ // Fix drift: map the first cue to 1.0s and the last to 10:00.0
75
+ resync(cues, { firstStart: 1000, lastStart: 600000 });
76
+
77
+ toVTT(cues); // serialize back out
78
+ ```
79
+
80
+ ### More re-timing
81
+
82
+ ```ts
83
+ import { scale, fixOverlaps, totalDuration } from "captionkit";
84
+
85
+ scale(cues, 25 / 23.976); // framerate conversion
86
+ fixOverlaps(cues, 40); // no two cues overlap (40ms min gap)
87
+ totalDuration(cues); // total on-screen time (ms)
88
+ ```
89
+
90
+ ## API
91
+
92
+ | Function | Description |
93
+ | -------- | ----------- |
94
+ | `parse(text)` | Parse SRT or WebVTT β†’ `Cue[]` (`{ index, start, end, text }`, ms). |
95
+ | `toSRT(cues)` / `toVTT(cues)` / `convert(text, to)` | Serialize / convert. |
96
+ | `detectFormat(text)` | `"srt"` or `"vtt"`. |
97
+ | `shift(cues, ms)` | Offset all cues. |
98
+ | `scale(cues, factor)` | Multiply all timestamps. |
99
+ | `resync(cues, { firstStart, lastStart })` | Linear drift correction. |
100
+ | `fixOverlaps(cues, minGap?)` | Remove overlaps. |
101
+ | `renumber` / `totalDuration` | Helpers. |
102
+ | `parseTimecode` / `formatSRT` / `formatVTT` | Timecode ⇄ ms. |
103
+
104
+ ## FAQ
105
+
106
+ **Is my subtitle file uploaded anywhere?**
107
+ No. Everything runs on your device β€” no server, no telemetry, works offline.
108
+
109
+ **What formats are supported?**
110
+ SRT and WebVTT today. ASS/SSA and SBV are on the roadmap β€” [open an issue](https://github.com/didrod205/captionkit/issues).
111
+
112
+ **What's the difference between shift, scale and resync?**
113
+ *Shift* adds a fixed offset (captions are uniformly early/late). *Scale*
114
+ multiplies timestamps (framerate mismatch). *Resync* pins the first and last cue
115
+ to target times and interpolates the rest β€” the fix for gradual drift.
116
+
117
+ **Will it keep my line breaks and styling?**
118
+ Multi-line cue text is preserved. Inline WebVTT tags pass through as text; full
119
+ ASS styling is not converted (yet).
120
+
121
+ **Can I use it in a build or batch script?**
122
+ Yes β€” the library works in Node; map the functions over your files.
123
+
124
+ ## Contributing
125
+
126
+ Contributions welcome! See [CONTRIBUTING.md](./CONTRIBUTING.md) and the
127
+ [Code of Conduct](./CODE_OF_CONDUCT.md).
128
+
129
+ ```bash
130
+ git clone https://github.com/didrod205/captionkit.git
131
+ cd captionkit
132
+ npm install
133
+ npm test # run the suite
134
+ npm run dev # run the web app locally
135
+ ```
136
+
137
+ ## πŸ’– Sponsor
138
+
139
+ captionkit is free, MIT-licensed, and built in spare time. If it saved you from
140
+ hand-editing timestamps, please consider supporting it:
141
+
142
+ - ⭐ **Star this repo** β€” free, and it genuinely helps others find it.
143
+ - πŸ‹ **[Sponsor via Lemon Squeezy](https://elab-studio.lemonsqueezy.com/checkout/buy/5d059b89-51d0-456b-b33a-ed56994f7010)** β€” one-time or recurring support.
144
+
145
+ **Where your support goes:** more formats (ASS/SSA, SBV, SAMI), split/merge,
146
+ characters-per-second warnings, an auto-shift-by-waveform helper, a CLI,
147
+ keeping the free web app online, and fast issue responses.
148
+
149
+ ## License
150
+
151
+ [MIT](./LICENSE) Β© captionkit contributors
package/dist/index.cjs ADDED
@@ -0,0 +1,16 @@
1
+ 'use strict';function d(n){let t=/^\s*(?:(\d+):)?(\d{1,2}):(\d{2})[.,](\d{1,3})\s*$/.exec(n);if(!t)throw new SyntaxError(`captionkit: invalid timecode "${n}"`);let r=t[1]?Number(t[1]):0,e=Number(t[2]),s=Number(t[3]),o=Number(t[4].padEnd(3,"0"));return ((r*60+e)*60+s)*1e3+o}var i=(n,t)=>String(n).padStart(t,"0");function l(n){let t=Math.max(0,Math.round(n));return {h:Math.floor(t/36e5),m:Math.floor(t%36e5/6e4),s:Math.floor(t%6e4/1e3),ms:t%1e3}}function c(n){let t=l(n);return `${i(t.h,2)}:${i(t.m,2)}:${i(t.s,2)},${i(t.ms,3)}`}function f(n){let t=l(n);return `${i(t.h,2)}:${i(t.m,2)}:${i(t.s,2)}.${i(t.ms,3)}`}var x=/(\d{1,2}:\d{1,2}:\d{2}[.,]\d{1,3}|\d{1,2}:\d{2}[.,]\d{1,3})\s*-->\s*(\d{1,2}:\d{1,2}:\d{2}[.,]\d{1,3}|\d{1,2}:\d{2}[.,]\d{1,3})/;function N(n){return /^ο»Ώ?\s*WEBVTT/.test(n)?"vtt":/\d{1,2}:\d{2}[.,]\d{1,3}\s*-->/.test(n)?/,\d{1,3}\s*-->/.test(n)?"srt":"vtt":"srt"}function C(n){let r=n.replace(/^ο»Ώ/,"").replace(/\r\n?/g,`
2
+ `).split(/\n{2,}/),e=[];for(let s of r){let o=s.split(`
3
+ `),u=s.trim();if(!u||/^WEBVTT/.test(u)||/^(NOTE|STYLE|REGION)\b/.test(u))continue;let a=-1;for(let m=0;m<o.length;m++)if(x.test(o[m])){a=m;break}if(a===-1)continue;let p=x.exec(o[a]);if(!p)continue;let h=d(p[1]),T=d(p[2]),g=o.slice(a+1).join(`
4
+ `).trim();e.push({index:e.length+1,start:h,end:T,text:g});}return e}function b(n){return n.map((t,r)=>({...t,index:r+1}))}function S(n){return b(n).map(t=>`${t.index}
5
+ ${c(t.start)} --> ${c(t.end)}
6
+ ${t.text}`).join(`
7
+
8
+ `)+`
9
+ `}function $(n){return `WEBVTT
10
+
11
+ ${n.map(r=>`${f(r.start)} --> ${f(r.end)}
12
+ ${r.text}`).join(`
13
+
14
+ `)}
15
+ `}function V(n,t){let r=C(n);return t==="vtt"?$(r):S(r)}function M(n,t){return n.map(r=>({...r,start:Math.max(0,r.start+t),end:Math.max(0,r.end+t)}))}function _(n,t){if(!(t>0))throw new RangeError("captionkit: scale factor must be positive");return n.map(r=>({...r,start:Math.max(0,Math.round(r.start*t)),end:Math.max(0,Math.round(r.end*t))}))}function R(n,t){if(n.length===0)return [];let r=n[0],s=n[n.length-1].start-r.start;if(s===0)return M(n,t.firstStart-r.start);let o=(t.lastStart-t.firstStart)/s,u=a=>Math.max(0,Math.round(t.firstStart+(a-r.start)*o));return n.map(a=>({...a,start:u(a.start),end:u(a.end)}))}function k(n,t=0){let r=[...n].sort((e,s)=>e.start-s.start);for(let e=1;e<r.length;e++){let s=r[e-1],o=r[e];o.start<s.end+t&&(r[e]={...o,start:s.end+t},r[e].end<r[e].start&&(r[e]={...r[e],end:r[e].start}));}return b(r)}function y(n){return n.reduce((t,r)=>t+Math.max(0,r.end-r.start),0)}exports.convert=V;exports.detectFormat=N;exports.fixOverlaps=k;exports.formatSRT=c;exports.formatVTT=f;exports.parse=C;exports.parseTimecode=d;exports.renumber=b;exports.resync=R;exports.scale=_;exports.shift=M;exports.toSRT=S;exports.toVTT=$;exports.totalDuration=y;//# sourceMappingURL=index.cjs.map
16
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/timecode.ts","../src/index.ts"],"names":["parseTimecode","tc","m","hours","minutes","seconds","millis","pad","len","parts","ms","clamped","formatSRT","p","formatVTT","TIME_LINE","detectFormat","text","parse","blocks","cues","block","lines","trimmed","timeIdx","i","start","end","textLines","renumber","c","toSRT","toVTT","convert","to","shift","scale","factor","resync","target","first","srcSpan","map","t","fixOverlaps","minGap","sorted","a","b","prev","cur","totalDuration","sum"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Timecode parsing/formatting for SRT (`00:01:23,456`) and WebVTT\n * (`00:01:23.456`, with an optional hour part). All times are tracked in\n * integer milliseconds so the math stays exact.\n */\n\n/** Parse `HH:MM:SS,mmm` / `MM:SS.mmm` (SRT or VTT) into milliseconds. */\nexport function parseTimecode(tc: string): number {\n const m = /^\\s*(?:(\\d+):)?(\\d{1,2}):(\\d{2})[.,](\\d{1,3})\\s*$/.exec(tc);\n if (!m) throw new SyntaxError(`captionkit: invalid timecode \"${tc}\"`);\n const hours = m[1] ? Number(m[1]) : 0;\n const minutes = Number(m[2]);\n const seconds = Number(m[3]);\n const millis = Number((m[4] as string).padEnd(3, \"0\"));\n return ((hours * 60 + minutes) * 60 + seconds) * 1000 + millis;\n}\n\nconst pad = (n: number, len: number): string => String(n).padStart(len, \"0\");\n\nfunction parts(ms: number): { h: number; m: number; s: number; ms: number } {\n const clamped = Math.max(0, Math.round(ms));\n return {\n h: Math.floor(clamped / 3_600_000),\n m: Math.floor((clamped % 3_600_000) / 60_000),\n s: Math.floor((clamped % 60_000) / 1000),\n ms: clamped % 1000,\n };\n}\n\n/** Format milliseconds as an SRT timecode `HH:MM:SS,mmm`. */\nexport function formatSRT(ms: number): string {\n const p = parts(ms);\n return `${pad(p.h, 2)}:${pad(p.m, 2)}:${pad(p.s, 2)},${pad(p.ms, 3)}`;\n}\n\n/** Format milliseconds as a WebVTT timecode `HH:MM:SS.mmm`. */\nexport function formatVTT(ms: number): string {\n const p = parts(ms);\n return `${pad(p.h, 2)}:${pad(p.m, 2)}:${pad(p.s, 2)}.${pad(p.ms, 3)}`;\n}\n","/**\n * captionkit β€” parse, convert and re-time subtitle files (SRT ⇄ WebVTT)\n * entirely locally. Exact millisecond timecode math; zero dependencies.\n */\n\nimport { formatSRT, formatVTT, parseTimecode } from \"./timecode.js\";\n\nexport { parseTimecode, formatSRT, formatVTT } from \"./timecode.js\";\n\nexport type SubtitleFormat = \"srt\" | \"vtt\";\n\nexport interface Cue {\n /** Sequential number (1-based). */\n index: number;\n /** Start time, milliseconds. */\n start: number;\n /** End time, milliseconds. */\n end: number;\n /** Cue text (may contain multiple lines). */\n text: string;\n}\n\nconst TIME_LINE = /(\\d{1,2}:\\d{1,2}:\\d{2}[.,]\\d{1,3}|\\d{1,2}:\\d{2}[.,]\\d{1,3})\\s*-->\\s*(\\d{1,2}:\\d{1,2}:\\d{2}[.,]\\d{1,3}|\\d{1,2}:\\d{2}[.,]\\d{1,3})/;\n\n/** Detect whether text looks like WebVTT or SRT. */\nexport function detectFormat(text: string): SubtitleFormat {\n if (/^ο»Ώ?\\s*WEBVTT/.test(text)) return \"vtt\";\n // SRT uses a comma before the milliseconds; VTT uses a dot.\n if (/\\d{1,2}:\\d{2}[.,]\\d{1,3}\\s*-->/.test(text)) {\n return /,\\d{1,3}\\s*-->/.test(text) ? \"srt\" : \"vtt\";\n }\n return \"srt\";\n}\n\n/**\n * Parse an SRT or WebVTT string into cues. Tolerant of CRLF, a BOM, a WEBVTT\n * header, blank lines, and missing cue numbers.\n *\n * ```ts\n * const cues = parse(srtText);\n * cues[0]; // { index: 1, start: 1000, end: 4000, text: \"Hello\" }\n * ```\n */\nexport function parse(text: string): Cue[] {\n const normalized = text.replace(/^ο»Ώ/, \"\").replace(/\\r\\n?/g, \"\\n\");\n const blocks = normalized.split(/\\n{2,}/);\n const cues: Cue[] = [];\n\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n const trimmed = block.trim();\n if (!trimmed || /^WEBVTT/.test(trimmed) || /^(NOTE|STYLE|REGION)\\b/.test(trimmed)) continue;\n\n // Find the line with the timing arrow.\n let timeIdx = -1;\n for (let i = 0; i < lines.length; i++) {\n if (TIME_LINE.test(lines[i] as string)) {\n timeIdx = i;\n break;\n }\n }\n if (timeIdx === -1) continue;\n\n const m = TIME_LINE.exec(lines[timeIdx] as string);\n if (!m) continue;\n const start = parseTimecode(m[1] as string);\n const end = parseTimecode(m[2] as string);\n const textLines = lines.slice(timeIdx + 1).join(\"\\n\").trim();\n\n cues.push({ index: cues.length + 1, start, end, text: textLines });\n }\n\n return cues;\n}\n\n/** Re-number cues 1..n in place-order (returns a new array). */\nexport function renumber(cues: Cue[]): Cue[] {\n return cues.map((c, i) => ({ ...c, index: i + 1 }));\n}\n\n/** Serialize cues to SRT. */\nexport function toSRT(cues: Cue[]): string {\n return (\n renumber(cues)\n .map((c) => `${c.index}\\n${formatSRT(c.start)} --> ${formatSRT(c.end)}\\n${c.text}`)\n .join(\"\\n\\n\") + \"\\n\"\n );\n}\n\n/** Serialize cues to WebVTT. */\nexport function toVTT(cues: Cue[]): string {\n const body = cues\n .map((c) => `${formatVTT(c.start)} --> ${formatVTT(c.end)}\\n${c.text}`)\n .join(\"\\n\\n\");\n return `WEBVTT\\n\\n${body}\\n`;\n}\n\n/** Convert subtitle text from one format to the other (auto-detects input). */\nexport function convert(text: string, to: SubtitleFormat): string {\n const cues = parse(text);\n return to === \"vtt\" ? toVTT(cues) : toSRT(cues);\n}\n\n/** Shift every cue by `ms` (can be negative). Times never go below 0. */\nexport function shift(cues: Cue[], ms: number): Cue[] {\n return cues.map((c) => ({\n ...c,\n start: Math.max(0, c.start + ms),\n end: Math.max(0, c.end + ms),\n }));\n}\n\n/** Multiply every timestamp by `factor` (e.g. 25/23.976 for a framerate change). */\nexport function scale(cues: Cue[], factor: number): Cue[] {\n if (!(factor > 0)) throw new RangeError(\"captionkit: scale factor must be positive\");\n return cues.map((c) => ({\n ...c,\n start: Math.max(0, Math.round(c.start * factor)),\n end: Math.max(0, Math.round(c.end * factor)),\n }));\n}\n\n/**\n * Linearly re-time so the first cue starts at `firstStart` and the last cue\n * starts at `lastStart` β€” the fix for subtitles that slowly drift out of sync.\n *\n * ```ts\n * resync(cues, { firstStart: 1000, lastStart: 600000 });\n * ```\n */\nexport function resync(cues: Cue[], target: { firstStart: number; lastStart: number }): Cue[] {\n if (cues.length === 0) return [];\n const first = cues[0] as Cue;\n const last = cues[cues.length - 1] as Cue;\n const srcSpan = last.start - first.start;\n if (srcSpan === 0) return shift(cues, target.firstStart - first.start);\n\n const factor = (target.lastStart - target.firstStart) / srcSpan;\n const map = (t: number) => Math.max(0, Math.round(target.firstStart + (t - first.start) * factor));\n return cues.map((c) => ({ ...c, start: map(c.start), end: map(c.end) }));\n}\n\n/** Ensure cues are in order and never overlap (keeps a `minGap` ms between them). */\nexport function fixOverlaps(cues: Cue[], minGap = 0): Cue[] {\n const sorted = [...cues].sort((a, b) => a.start - b.start);\n for (let i = 1; i < sorted.length; i++) {\n const prev = sorted[i - 1] as Cue;\n const cur = sorted[i] as Cue;\n if (cur.start < prev.end + minGap) {\n sorted[i] = { ...cur, start: prev.end + minGap };\n if (sorted[i]!.end < sorted[i]!.start) sorted[i] = { ...sorted[i]!, end: sorted[i]!.start };\n }\n }\n return renumber(sorted);\n}\n\n/** Total on-screen caption duration in milliseconds. */\nexport function totalDuration(cues: Cue[]): number {\n return cues.reduce((sum, c) => sum + Math.max(0, c.end - c.start), 0);\n}\n"]}
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Timecode parsing/formatting for SRT (`00:01:23,456`) and WebVTT
3
+ * (`00:01:23.456`, with an optional hour part). All times are tracked in
4
+ * integer milliseconds so the math stays exact.
5
+ */
6
+ /** Parse `HH:MM:SS,mmm` / `MM:SS.mmm` (SRT or VTT) into milliseconds. */
7
+ declare function parseTimecode(tc: string): number;
8
+ /** Format milliseconds as an SRT timecode `HH:MM:SS,mmm`. */
9
+ declare function formatSRT(ms: number): string;
10
+ /** Format milliseconds as a WebVTT timecode `HH:MM:SS.mmm`. */
11
+ declare function formatVTT(ms: number): string;
12
+
13
+ /**
14
+ * captionkit β€” parse, convert and re-time subtitle files (SRT ⇄ WebVTT)
15
+ * entirely locally. Exact millisecond timecode math; zero dependencies.
16
+ */
17
+
18
+ type SubtitleFormat = "srt" | "vtt";
19
+ interface Cue {
20
+ /** Sequential number (1-based). */
21
+ index: number;
22
+ /** Start time, milliseconds. */
23
+ start: number;
24
+ /** End time, milliseconds. */
25
+ end: number;
26
+ /** Cue text (may contain multiple lines). */
27
+ text: string;
28
+ }
29
+ /** Detect whether text looks like WebVTT or SRT. */
30
+ declare function detectFormat(text: string): SubtitleFormat;
31
+ /**
32
+ * Parse an SRT or WebVTT string into cues. Tolerant of CRLF, a BOM, a WEBVTT
33
+ * header, blank lines, and missing cue numbers.
34
+ *
35
+ * ```ts
36
+ * const cues = parse(srtText);
37
+ * cues[0]; // { index: 1, start: 1000, end: 4000, text: "Hello" }
38
+ * ```
39
+ */
40
+ declare function parse(text: string): Cue[];
41
+ /** Re-number cues 1..n in place-order (returns a new array). */
42
+ declare function renumber(cues: Cue[]): Cue[];
43
+ /** Serialize cues to SRT. */
44
+ declare function toSRT(cues: Cue[]): string;
45
+ /** Serialize cues to WebVTT. */
46
+ declare function toVTT(cues: Cue[]): string;
47
+ /** Convert subtitle text from one format to the other (auto-detects input). */
48
+ declare function convert(text: string, to: SubtitleFormat): string;
49
+ /** Shift every cue by `ms` (can be negative). Times never go below 0. */
50
+ declare function shift(cues: Cue[], ms: number): Cue[];
51
+ /** Multiply every timestamp by `factor` (e.g. 25/23.976 for a framerate change). */
52
+ declare function scale(cues: Cue[], factor: number): Cue[];
53
+ /**
54
+ * Linearly re-time so the first cue starts at `firstStart` and the last cue
55
+ * starts at `lastStart` β€” the fix for subtitles that slowly drift out of sync.
56
+ *
57
+ * ```ts
58
+ * resync(cues, { firstStart: 1000, lastStart: 600000 });
59
+ * ```
60
+ */
61
+ declare function resync(cues: Cue[], target: {
62
+ firstStart: number;
63
+ lastStart: number;
64
+ }): Cue[];
65
+ /** Ensure cues are in order and never overlap (keeps a `minGap` ms between them). */
66
+ declare function fixOverlaps(cues: Cue[], minGap?: number): Cue[];
67
+ /** Total on-screen caption duration in milliseconds. */
68
+ declare function totalDuration(cues: Cue[]): number;
69
+
70
+ export { type Cue, type SubtitleFormat, convert, detectFormat, fixOverlaps, formatSRT, formatVTT, parse, parseTimecode, renumber, resync, scale, shift, toSRT, toVTT, totalDuration };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Timecode parsing/formatting for SRT (`00:01:23,456`) and WebVTT
3
+ * (`00:01:23.456`, with an optional hour part). All times are tracked in
4
+ * integer milliseconds so the math stays exact.
5
+ */
6
+ /** Parse `HH:MM:SS,mmm` / `MM:SS.mmm` (SRT or VTT) into milliseconds. */
7
+ declare function parseTimecode(tc: string): number;
8
+ /** Format milliseconds as an SRT timecode `HH:MM:SS,mmm`. */
9
+ declare function formatSRT(ms: number): string;
10
+ /** Format milliseconds as a WebVTT timecode `HH:MM:SS.mmm`. */
11
+ declare function formatVTT(ms: number): string;
12
+
13
+ /**
14
+ * captionkit β€” parse, convert and re-time subtitle files (SRT ⇄ WebVTT)
15
+ * entirely locally. Exact millisecond timecode math; zero dependencies.
16
+ */
17
+
18
+ type SubtitleFormat = "srt" | "vtt";
19
+ interface Cue {
20
+ /** Sequential number (1-based). */
21
+ index: number;
22
+ /** Start time, milliseconds. */
23
+ start: number;
24
+ /** End time, milliseconds. */
25
+ end: number;
26
+ /** Cue text (may contain multiple lines). */
27
+ text: string;
28
+ }
29
+ /** Detect whether text looks like WebVTT or SRT. */
30
+ declare function detectFormat(text: string): SubtitleFormat;
31
+ /**
32
+ * Parse an SRT or WebVTT string into cues. Tolerant of CRLF, a BOM, a WEBVTT
33
+ * header, blank lines, and missing cue numbers.
34
+ *
35
+ * ```ts
36
+ * const cues = parse(srtText);
37
+ * cues[0]; // { index: 1, start: 1000, end: 4000, text: "Hello" }
38
+ * ```
39
+ */
40
+ declare function parse(text: string): Cue[];
41
+ /** Re-number cues 1..n in place-order (returns a new array). */
42
+ declare function renumber(cues: Cue[]): Cue[];
43
+ /** Serialize cues to SRT. */
44
+ declare function toSRT(cues: Cue[]): string;
45
+ /** Serialize cues to WebVTT. */
46
+ declare function toVTT(cues: Cue[]): string;
47
+ /** Convert subtitle text from one format to the other (auto-detects input). */
48
+ declare function convert(text: string, to: SubtitleFormat): string;
49
+ /** Shift every cue by `ms` (can be negative). Times never go below 0. */
50
+ declare function shift(cues: Cue[], ms: number): Cue[];
51
+ /** Multiply every timestamp by `factor` (e.g. 25/23.976 for a framerate change). */
52
+ declare function scale(cues: Cue[], factor: number): Cue[];
53
+ /**
54
+ * Linearly re-time so the first cue starts at `firstStart` and the last cue
55
+ * starts at `lastStart` β€” the fix for subtitles that slowly drift out of sync.
56
+ *
57
+ * ```ts
58
+ * resync(cues, { firstStart: 1000, lastStart: 600000 });
59
+ * ```
60
+ */
61
+ declare function resync(cues: Cue[], target: {
62
+ firstStart: number;
63
+ lastStart: number;
64
+ }): Cue[];
65
+ /** Ensure cues are in order and never overlap (keeps a `minGap` ms between them). */
66
+ declare function fixOverlaps(cues: Cue[], minGap?: number): Cue[];
67
+ /** Total on-screen caption duration in milliseconds. */
68
+ declare function totalDuration(cues: Cue[]): number;
69
+
70
+ export { type Cue, type SubtitleFormat, convert, detectFormat, fixOverlaps, formatSRT, formatVTT, parse, parseTimecode, renumber, resync, scale, shift, toSRT, toVTT, totalDuration };
package/dist/index.js ADDED
@@ -0,0 +1,16 @@
1
+ function d(n){let t=/^\s*(?:(\d+):)?(\d{1,2}):(\d{2})[.,](\d{1,3})\s*$/.exec(n);if(!t)throw new SyntaxError(`captionkit: invalid timecode "${n}"`);let r=t[1]?Number(t[1]):0,e=Number(t[2]),s=Number(t[3]),o=Number(t[4].padEnd(3,"0"));return ((r*60+e)*60+s)*1e3+o}var i=(n,t)=>String(n).padStart(t,"0");function l(n){let t=Math.max(0,Math.round(n));return {h:Math.floor(t/36e5),m:Math.floor(t%36e5/6e4),s:Math.floor(t%6e4/1e3),ms:t%1e3}}function c(n){let t=l(n);return `${i(t.h,2)}:${i(t.m,2)}:${i(t.s,2)},${i(t.ms,3)}`}function f(n){let t=l(n);return `${i(t.h,2)}:${i(t.m,2)}:${i(t.s,2)}.${i(t.ms,3)}`}var x=/(\d{1,2}:\d{1,2}:\d{2}[.,]\d{1,3}|\d{1,2}:\d{2}[.,]\d{1,3})\s*-->\s*(\d{1,2}:\d{1,2}:\d{2}[.,]\d{1,3}|\d{1,2}:\d{2}[.,]\d{1,3})/;function N(n){return /^ο»Ώ?\s*WEBVTT/.test(n)?"vtt":/\d{1,2}:\d{2}[.,]\d{1,3}\s*-->/.test(n)?/,\d{1,3}\s*-->/.test(n)?"srt":"vtt":"srt"}function C(n){let r=n.replace(/^ο»Ώ/,"").replace(/\r\n?/g,`
2
+ `).split(/\n{2,}/),e=[];for(let s of r){let o=s.split(`
3
+ `),u=s.trim();if(!u||/^WEBVTT/.test(u)||/^(NOTE|STYLE|REGION)\b/.test(u))continue;let a=-1;for(let m=0;m<o.length;m++)if(x.test(o[m])){a=m;break}if(a===-1)continue;let p=x.exec(o[a]);if(!p)continue;let h=d(p[1]),T=d(p[2]),g=o.slice(a+1).join(`
4
+ `).trim();e.push({index:e.length+1,start:h,end:T,text:g});}return e}function b(n){return n.map((t,r)=>({...t,index:r+1}))}function S(n){return b(n).map(t=>`${t.index}
5
+ ${c(t.start)} --> ${c(t.end)}
6
+ ${t.text}`).join(`
7
+
8
+ `)+`
9
+ `}function $(n){return `WEBVTT
10
+
11
+ ${n.map(r=>`${f(r.start)} --> ${f(r.end)}
12
+ ${r.text}`).join(`
13
+
14
+ `)}
15
+ `}function V(n,t){let r=C(n);return t==="vtt"?$(r):S(r)}function M(n,t){return n.map(r=>({...r,start:Math.max(0,r.start+t),end:Math.max(0,r.end+t)}))}function _(n,t){if(!(t>0))throw new RangeError("captionkit: scale factor must be positive");return n.map(r=>({...r,start:Math.max(0,Math.round(r.start*t)),end:Math.max(0,Math.round(r.end*t))}))}function R(n,t){if(n.length===0)return [];let r=n[0],s=n[n.length-1].start-r.start;if(s===0)return M(n,t.firstStart-r.start);let o=(t.lastStart-t.firstStart)/s,u=a=>Math.max(0,Math.round(t.firstStart+(a-r.start)*o));return n.map(a=>({...a,start:u(a.start),end:u(a.end)}))}function k(n,t=0){let r=[...n].sort((e,s)=>e.start-s.start);for(let e=1;e<r.length;e++){let s=r[e-1],o=r[e];o.start<s.end+t&&(r[e]={...o,start:s.end+t},r[e].end<r[e].start&&(r[e]={...r[e],end:r[e].start}));}return b(r)}function y(n){return n.reduce((t,r)=>t+Math.max(0,r.end-r.start),0)}export{V as convert,N as detectFormat,k as fixOverlaps,c as formatSRT,f as formatVTT,C as parse,d as parseTimecode,b as renumber,R as resync,_ as scale,M as shift,S as toSRT,$ as toVTT,y as totalDuration};//# sourceMappingURL=index.js.map
16
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/timecode.ts","../src/index.ts"],"names":["parseTimecode","tc","m","hours","minutes","seconds","millis","pad","len","parts","ms","clamped","formatSRT","p","formatVTT","TIME_LINE","detectFormat","text","parse","blocks","cues","block","lines","trimmed","timeIdx","i","start","end","textLines","renumber","c","toSRT","toVTT","convert","to","shift","scale","factor","resync","target","first","srcSpan","map","t","fixOverlaps","minGap","sorted","a","b","prev","cur","totalDuration","sum"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Timecode parsing/formatting for SRT (`00:01:23,456`) and WebVTT\n * (`00:01:23.456`, with an optional hour part). All times are tracked in\n * integer milliseconds so the math stays exact.\n */\n\n/** Parse `HH:MM:SS,mmm` / `MM:SS.mmm` (SRT or VTT) into milliseconds. */\nexport function parseTimecode(tc: string): number {\n const m = /^\\s*(?:(\\d+):)?(\\d{1,2}):(\\d{2})[.,](\\d{1,3})\\s*$/.exec(tc);\n if (!m) throw new SyntaxError(`captionkit: invalid timecode \"${tc}\"`);\n const hours = m[1] ? Number(m[1]) : 0;\n const minutes = Number(m[2]);\n const seconds = Number(m[3]);\n const millis = Number((m[4] as string).padEnd(3, \"0\"));\n return ((hours * 60 + minutes) * 60 + seconds) * 1000 + millis;\n}\n\nconst pad = (n: number, len: number): string => String(n).padStart(len, \"0\");\n\nfunction parts(ms: number): { h: number; m: number; s: number; ms: number } {\n const clamped = Math.max(0, Math.round(ms));\n return {\n h: Math.floor(clamped / 3_600_000),\n m: Math.floor((clamped % 3_600_000) / 60_000),\n s: Math.floor((clamped % 60_000) / 1000),\n ms: clamped % 1000,\n };\n}\n\n/** Format milliseconds as an SRT timecode `HH:MM:SS,mmm`. */\nexport function formatSRT(ms: number): string {\n const p = parts(ms);\n return `${pad(p.h, 2)}:${pad(p.m, 2)}:${pad(p.s, 2)},${pad(p.ms, 3)}`;\n}\n\n/** Format milliseconds as a WebVTT timecode `HH:MM:SS.mmm`. */\nexport function formatVTT(ms: number): string {\n const p = parts(ms);\n return `${pad(p.h, 2)}:${pad(p.m, 2)}:${pad(p.s, 2)}.${pad(p.ms, 3)}`;\n}\n","/**\n * captionkit β€” parse, convert and re-time subtitle files (SRT ⇄ WebVTT)\n * entirely locally. Exact millisecond timecode math; zero dependencies.\n */\n\nimport { formatSRT, formatVTT, parseTimecode } from \"./timecode.js\";\n\nexport { parseTimecode, formatSRT, formatVTT } from \"./timecode.js\";\n\nexport type SubtitleFormat = \"srt\" | \"vtt\";\n\nexport interface Cue {\n /** Sequential number (1-based). */\n index: number;\n /** Start time, milliseconds. */\n start: number;\n /** End time, milliseconds. */\n end: number;\n /** Cue text (may contain multiple lines). */\n text: string;\n}\n\nconst TIME_LINE = /(\\d{1,2}:\\d{1,2}:\\d{2}[.,]\\d{1,3}|\\d{1,2}:\\d{2}[.,]\\d{1,3})\\s*-->\\s*(\\d{1,2}:\\d{1,2}:\\d{2}[.,]\\d{1,3}|\\d{1,2}:\\d{2}[.,]\\d{1,3})/;\n\n/** Detect whether text looks like WebVTT or SRT. */\nexport function detectFormat(text: string): SubtitleFormat {\n if (/^ο»Ώ?\\s*WEBVTT/.test(text)) return \"vtt\";\n // SRT uses a comma before the milliseconds; VTT uses a dot.\n if (/\\d{1,2}:\\d{2}[.,]\\d{1,3}\\s*-->/.test(text)) {\n return /,\\d{1,3}\\s*-->/.test(text) ? \"srt\" : \"vtt\";\n }\n return \"srt\";\n}\n\n/**\n * Parse an SRT or WebVTT string into cues. Tolerant of CRLF, a BOM, a WEBVTT\n * header, blank lines, and missing cue numbers.\n *\n * ```ts\n * const cues = parse(srtText);\n * cues[0]; // { index: 1, start: 1000, end: 4000, text: \"Hello\" }\n * ```\n */\nexport function parse(text: string): Cue[] {\n const normalized = text.replace(/^ο»Ώ/, \"\").replace(/\\r\\n?/g, \"\\n\");\n const blocks = normalized.split(/\\n{2,}/);\n const cues: Cue[] = [];\n\n for (const block of blocks) {\n const lines = block.split(\"\\n\");\n const trimmed = block.trim();\n if (!trimmed || /^WEBVTT/.test(trimmed) || /^(NOTE|STYLE|REGION)\\b/.test(trimmed)) continue;\n\n // Find the line with the timing arrow.\n let timeIdx = -1;\n for (let i = 0; i < lines.length; i++) {\n if (TIME_LINE.test(lines[i] as string)) {\n timeIdx = i;\n break;\n }\n }\n if (timeIdx === -1) continue;\n\n const m = TIME_LINE.exec(lines[timeIdx] as string);\n if (!m) continue;\n const start = parseTimecode(m[1] as string);\n const end = parseTimecode(m[2] as string);\n const textLines = lines.slice(timeIdx + 1).join(\"\\n\").trim();\n\n cues.push({ index: cues.length + 1, start, end, text: textLines });\n }\n\n return cues;\n}\n\n/** Re-number cues 1..n in place-order (returns a new array). */\nexport function renumber(cues: Cue[]): Cue[] {\n return cues.map((c, i) => ({ ...c, index: i + 1 }));\n}\n\n/** Serialize cues to SRT. */\nexport function toSRT(cues: Cue[]): string {\n return (\n renumber(cues)\n .map((c) => `${c.index}\\n${formatSRT(c.start)} --> ${formatSRT(c.end)}\\n${c.text}`)\n .join(\"\\n\\n\") + \"\\n\"\n );\n}\n\n/** Serialize cues to WebVTT. */\nexport function toVTT(cues: Cue[]): string {\n const body = cues\n .map((c) => `${formatVTT(c.start)} --> ${formatVTT(c.end)}\\n${c.text}`)\n .join(\"\\n\\n\");\n return `WEBVTT\\n\\n${body}\\n`;\n}\n\n/** Convert subtitle text from one format to the other (auto-detects input). */\nexport function convert(text: string, to: SubtitleFormat): string {\n const cues = parse(text);\n return to === \"vtt\" ? toVTT(cues) : toSRT(cues);\n}\n\n/** Shift every cue by `ms` (can be negative). Times never go below 0. */\nexport function shift(cues: Cue[], ms: number): Cue[] {\n return cues.map((c) => ({\n ...c,\n start: Math.max(0, c.start + ms),\n end: Math.max(0, c.end + ms),\n }));\n}\n\n/** Multiply every timestamp by `factor` (e.g. 25/23.976 for a framerate change). */\nexport function scale(cues: Cue[], factor: number): Cue[] {\n if (!(factor > 0)) throw new RangeError(\"captionkit: scale factor must be positive\");\n return cues.map((c) => ({\n ...c,\n start: Math.max(0, Math.round(c.start * factor)),\n end: Math.max(0, Math.round(c.end * factor)),\n }));\n}\n\n/**\n * Linearly re-time so the first cue starts at `firstStart` and the last cue\n * starts at `lastStart` β€” the fix for subtitles that slowly drift out of sync.\n *\n * ```ts\n * resync(cues, { firstStart: 1000, lastStart: 600000 });\n * ```\n */\nexport function resync(cues: Cue[], target: { firstStart: number; lastStart: number }): Cue[] {\n if (cues.length === 0) return [];\n const first = cues[0] as Cue;\n const last = cues[cues.length - 1] as Cue;\n const srcSpan = last.start - first.start;\n if (srcSpan === 0) return shift(cues, target.firstStart - first.start);\n\n const factor = (target.lastStart - target.firstStart) / srcSpan;\n const map = (t: number) => Math.max(0, Math.round(target.firstStart + (t - first.start) * factor));\n return cues.map((c) => ({ ...c, start: map(c.start), end: map(c.end) }));\n}\n\n/** Ensure cues are in order and never overlap (keeps a `minGap` ms between them). */\nexport function fixOverlaps(cues: Cue[], minGap = 0): Cue[] {\n const sorted = [...cues].sort((a, b) => a.start - b.start);\n for (let i = 1; i < sorted.length; i++) {\n const prev = sorted[i - 1] as Cue;\n const cur = sorted[i] as Cue;\n if (cur.start < prev.end + minGap) {\n sorted[i] = { ...cur, start: prev.end + minGap };\n if (sorted[i]!.end < sorted[i]!.start) sorted[i] = { ...sorted[i]!, end: sorted[i]!.start };\n }\n }\n return renumber(sorted);\n}\n\n/** Total on-screen caption duration in milliseconds. */\nexport function totalDuration(cues: Cue[]): number {\n return cues.reduce((sum, c) => sum + Math.max(0, c.end - c.start), 0);\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "captionkit",
3
+ "version": "0.1.0",
4
+ "description": "Convert and re-time subtitles locally β€” SRT ⇄ WebVTT, shift, scale, resync drift, fix overlaps. Exact millisecond timecode math, zero dependencies, no upload.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "sideEffects": false,
20
+ "engines": {
21
+ "node": ">=18"
22
+ },
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "build:web": "vite build",
26
+ "dev": "vite",
27
+ "test": "vitest run",
28
+ "test:watch": "vitest",
29
+ "typecheck": "tsc --noEmit",
30
+ "lint": "tsc --noEmit",
31
+ "prepublishOnly": "npm run build"
32
+ },
33
+ "keywords": [
34
+ "subtitles",
35
+ "srt",
36
+ "vtt",
37
+ "webvtt",
38
+ "captions",
39
+ "srt-to-vtt",
40
+ "subtitle-sync",
41
+ "subtitle-converter",
42
+ "timecode",
43
+ "video",
44
+ "accessibility",
45
+ "zero-dependency"
46
+ ],
47
+ "author": "didrod205 (https://github.com/didrod205)",
48
+ "license": "MIT",
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/didrod205/captionkit.git"
52
+ },
53
+ "bugs": {
54
+ "url": "https://github.com/didrod205/captionkit/issues"
55
+ },
56
+ "homepage": "https://didrod205.github.io/captionkit/",
57
+ "devDependencies": {
58
+ "tsup": "^8.3.5",
59
+ "typescript": "^5.7.2",
60
+ "vite": "^6.0.0",
61
+ "vitest": "^2.1.8"
62
+ }
63
+ }