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 +21 -0
- package/README.md +151 -0
- package/dist/index.cjs +16 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +70 -0
- package/dist/index.d.ts +70 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/package.json +63 -0
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
|
+
[](https://www.npmjs.com/package/captionkit)
|
|
8
|
+
[](https://bundlephobia.com/package/captionkit)
|
|
9
|
+
[](https://github.com/didrod205/captionkit/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/captionkit)
|
|
11
|
+
[](./LICENSE)
|
|
12
|
+
|
|
13
|
+
**[π Try the free web app β](https://didrod205.github.io/captionkit/)** Β· 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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|