subcraft 0.0.0-stage → 0.2.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 +315 -2
- package/dist/index.cjs +417 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +228 -0
- package/dist/index.d.ts +228 -0
- package/dist/index.js +401 -0
- package/dist/index.js.map +1 -0
- package/package.json +67 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ranjith Gowda
|
|
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
CHANGED
|
@@ -1,3 +1,316 @@
|
|
|
1
|
-
#
|
|
1
|
+
# subcraft
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
> **Zero-dependency, isomorphic TypeScript engine for subtitle parsing, frame-rate retiming, and Unicode grapheme-cluster quality linting (powered by `Intl.Segmenter`).**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/subcraft)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](https://www.npmjs.com/package/subcraft)
|
|
8
|
+
[](https://vitest.dev/)
|
|
9
|
+
[](https://opensource.org/licenses/MIT)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Highlights
|
|
14
|
+
|
|
15
|
+
* **100% Zero Dependencies:** Lightweight and fast. Runs natively in any JavaScript runtime (Browsers, Web Workers, Node.js 20+, and Edge runtimes) without polyfills.
|
|
16
|
+
* **Resilient Parsing:** Strips UTF-8 BOM (`\uFEFF`), normalizes mixed line endings (`CRLF`, `CR`, `LF`), and captures syntax errors without crashing or throwing unhandled exceptions.
|
|
17
|
+
* **Deterministic Timecode Math:** Lossless millisecond arithmetic with integer precision (`Math.round`), frame-rate drift scaling (`retime`), and linear audio-sync shifting (`shiftTimecodes`).
|
|
18
|
+
* **Grapheme-Aware Quality Linter:** Uses the native ECMAScript `Intl.Segmenter` API to measure true visual characters rather than UTF-16 code units—eliminating false reading-speed warnings in Indic scripts (Kannada, Hindi) and emoji-heavy text.
|
|
19
|
+
* **First-Class TypeScript:** Authored in strict TypeScript with comprehensive type declarations (`.d.ts` and `.d.cts`) and verified dual ESM/CJS exports via `publint`.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Table of Contents
|
|
24
|
+
|
|
25
|
+
1. [Installation](#installation)
|
|
26
|
+
2. [Quick Start](#quick-start)
|
|
27
|
+
3. [Core Modules](#core-modules)
|
|
28
|
+
- [1. SRT Parsing & Resilient Error Collection](#1-srt-parsing--resilient-error-collection)
|
|
29
|
+
- [2. SRT Serialization](#2-srt-serialization)
|
|
30
|
+
- [3. Timecode Conversions](#3-timecode-conversions)
|
|
31
|
+
- [4. Audio Sync & Frame-Rate Drift Retiming](#4-audio-sync--frame-rate-drift-retiming)
|
|
32
|
+
- [5. Locale-Aware Quality Linter](#5-locale-aware-quality-linter)
|
|
33
|
+
4. [The Indic / Unicode Grapheme Problem](#the-indic--unicode-grapheme-problem)
|
|
34
|
+
5. [Comparison with Legacy Libraries](#comparison-with-legacy-libraries)
|
|
35
|
+
6. [API Reference & Types](#api-reference--types)
|
|
36
|
+
7. [License](#license)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# Using npm
|
|
44
|
+
npm install subcraft
|
|
45
|
+
|
|
46
|
+
# Using pnpm
|
|
47
|
+
pnpm add subcraft
|
|
48
|
+
|
|
49
|
+
# Using yarn
|
|
50
|
+
yarn add subcraft
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Quick Start
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
import { parseSRT, stringifySRT, lintCues, shiftTimecodes } from 'subcraft';
|
|
59
|
+
|
|
60
|
+
const rawSRT = `1
|
|
61
|
+
00:00:01,000 --> 00:00:04,000
|
|
62
|
+
Welcome to subcraft!
|
|
63
|
+
|
|
64
|
+
2
|
|
65
|
+
00:00:03,800 --> 00:00:06,000
|
|
66
|
+
Overlapping subtitle cue.`;
|
|
67
|
+
|
|
68
|
+
// 1. Parse SRT text resiliently
|
|
69
|
+
const { cues, errors } = parseSRT(rawSRT);
|
|
70
|
+
console.log(`Parsed ${cues.length} cues with ${errors.length} syntax errors.`);
|
|
71
|
+
|
|
72
|
+
// 2. Lint for quality defects (overlaps, reading speed, duration)
|
|
73
|
+
const diagnostics = lintCues(cues);
|
|
74
|
+
console.log(diagnostics);
|
|
75
|
+
// Output flags: Cue 2 overlaps with Cue 1 by 200ms!
|
|
76
|
+
|
|
77
|
+
// 3. Shift timings by +500ms (to fix audio desync)
|
|
78
|
+
const shiftedCues = shiftTimecodes(cues, 500);
|
|
79
|
+
|
|
80
|
+
// 4. Serialize back to standard SRT string
|
|
81
|
+
const updatedSRT = stringifySRT(shiftedCues);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Core Modules
|
|
87
|
+
|
|
88
|
+
### 1. SRT Parsing & Resilient Error Collection
|
|
89
|
+
|
|
90
|
+
Unlike traditional parsers that throw fatal exceptions when encountering corrupted blocks or missing line numbers, `parseSRT` collects syntax errors alongside successfully parsed cues:
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
import { parseSRT } from 'subcraft';
|
|
94
|
+
|
|
95
|
+
const { cues, errors } = parseSRT(dirtySubtitleContent);
|
|
96
|
+
|
|
97
|
+
if (errors.length > 0) {
|
|
98
|
+
errors.forEach(err => {
|
|
99
|
+
console.warn(`Block error around index ${err.index}: ${err.message}`);
|
|
100
|
+
console.warn(`Raw block: "${err.rawBlock}"`);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
* Automatically strips UTF-8 BOM (`\uFEFF`).
|
|
106
|
+
* Normalizes Windows (`\r\n`) and Unix (`\n`) newlines.
|
|
107
|
+
* Handles missing or non-numeric cue IDs by assigning fallback sequential numbers.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
### 2. SRT Serialization
|
|
112
|
+
|
|
113
|
+
Formats structured `SubtitleCue[]` objects back into strict standard SubRip (.srt) syntax:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
import { stringifySRT, SubtitleCue } from 'subcraft';
|
|
117
|
+
|
|
118
|
+
const cues: SubtitleCue[] = [
|
|
119
|
+
{ id: '1', start: 1000, end: 4000, text: 'Hello world' }
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
const srtString = stringifySRT(cues);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### 3. WebVTT Parsing, Serialization & Format Conversion
|
|
128
|
+
|
|
129
|
+
Parse standard WebVTT (`.vtt`) files with headers, `NOTE` comments, and placement settings (`align:middle line:90%`):
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { parseVTT, stringifyVTT, srtToVTT, vttToSRT } from 'subcraft';
|
|
133
|
+
|
|
134
|
+
// 1. Parse WebVTT
|
|
135
|
+
const { cues, errors } = parseVTT(vttText);
|
|
136
|
+
|
|
137
|
+
// 2. Serialize to WebVTT with custom header
|
|
138
|
+
const vttOutput = stringifyVTT(cues, { header: 'English SDH' });
|
|
139
|
+
|
|
140
|
+
// 3. Instant one-line bidirectional format conversion
|
|
141
|
+
const webVTT = srtToVTT(rawSRT); // Convert SRT to WebVTT
|
|
142
|
+
const srtText = vttToSRT(rawVTT); // Convert WebVTT to SRT
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
### 3. Timecode Conversions
|
|
148
|
+
|
|
149
|
+
Convert between standard timecode strings and integer milliseconds without floating-point drift:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { timecodeToMs, msToTimecode } from 'subcraft';
|
|
153
|
+
|
|
154
|
+
// Convert string to milliseconds
|
|
155
|
+
timecodeToMs('00:01:23,456'); // Returns: 83456
|
|
156
|
+
timecodeToMs('01:23.456'); // WebVTT short form -> Returns: 83456
|
|
157
|
+
|
|
158
|
+
// Convert milliseconds to formatted timecode
|
|
159
|
+
msToTimecode(83456, 'srt'); // "00:01:23,456"
|
|
160
|
+
msToTimecode(83456, 'vtt'); // "00:01:23.456"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### 4. Audio Sync & Frame-Rate Drift Retiming
|
|
166
|
+
|
|
167
|
+
#### Linear Shift (`shiftTimecodes`)
|
|
168
|
+
Add or subtract a constant offset across all cues (e.g. delaying subtitles by 500ms). Negative results are safely clamped to `0ms`:
|
|
169
|
+
|
|
170
|
+
```typescript
|
|
171
|
+
import { shiftTimecodes } from 'subcraft';
|
|
172
|
+
|
|
173
|
+
// Shift all cues forward by 1.5 seconds
|
|
174
|
+
const delayed = shiftTimecodes(cues, 1500);
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Frame-Rate Scaling (`retime`)
|
|
178
|
+
When videos are converted between frame rates (e.g., 24 fps Film to 25 fps PAL television), timecodes drift multiplicatively over time. `retime` applies a scaling ratio, strictly rounding each millisecond with `Math.round`:
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
import { retime } from 'subcraft';
|
|
182
|
+
|
|
183
|
+
// 24 fps to 25 fps (PAL speed-up): video runs ~4% faster, timecodes compress
|
|
184
|
+
const palCues = retime(filmCues, 24 / 25); // ratio: 0.96
|
|
185
|
+
|
|
186
|
+
// 25 fps to 24 fps (Slowdown): video runs slower, timecodes stretch
|
|
187
|
+
const filmCues = retime(palCues, 25 / 24); // ratio: 1.04167
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
### 5. Locale-Aware Quality Linter
|
|
193
|
+
|
|
194
|
+
Inspects subtitles against broadcaster standards (Netflix / BBC style guides):
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
import { lintCues, DEFAULT_PROFILES } from 'subcraft';
|
|
198
|
+
|
|
199
|
+
// Lint using standard Latin rules (default)
|
|
200
|
+
const diagnostics = lintCues(cues);
|
|
201
|
+
|
|
202
|
+
// Lint using Indic (Kannada) profile
|
|
203
|
+
const kannadaDiagnostics = lintCues(cues, DEFAULT_PROFILES.indicKannada);
|
|
204
|
+
|
|
205
|
+
// Custom profile overrides
|
|
206
|
+
const customDiagnostics = lintCues(cues, {
|
|
207
|
+
maxCps: 18,
|
|
208
|
+
maxCpl: 40,
|
|
209
|
+
minDurationMs: 1000,
|
|
210
|
+
maxDurationMs: 6000,
|
|
211
|
+
includeSpaces: true,
|
|
212
|
+
locale: 'kn',
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
#### Rules Enforced:
|
|
217
|
+
| Rule | Severity | Description | Default Threshold |
|
|
218
|
+
| :--- | :---: | :--- | :--- |
|
|
219
|
+
| `out-of-order` | `error` | Cue start timestamp is earlier than previous cue start | Ascending chronological order |
|
|
220
|
+
| `zero-or-negative-duration` | `error` | Cue duration is zero or negative (`end <= start`) | `end > start` |
|
|
221
|
+
| `overlap` | `error` | Cue start timestamp precedes the previous cue's end timestamp | `start >= prevEnd` |
|
|
222
|
+
| `min-duration` | `warning` | Cue flashes too quickly for human cognitive reading | Min `800ms` |
|
|
223
|
+
| `max-duration` | `warning` | Cue stays on screen for too long, distracting from video | Max `7,000ms` |
|
|
224
|
+
| `max-cpl` | `warning` | Line length exceeds display boundary limits | Max `42` CPL (Latin) / `35` CPL (Indic) |
|
|
225
|
+
| `max-cps` | `warning` | Reading speed exceeds human reading speed capacity | Max `20` CPS (Latin) / `15` CPS (Indic) |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## The Indic / Unicode Grapheme Problem
|
|
230
|
+
|
|
231
|
+
In standard JavaScript, `string.length` counts **UTF-16 code units**, not visual characters.
|
|
232
|
+
|
|
233
|
+
In Indic scripts (such as Kannada, Hindi, Telugu, Tamil), conjunct consonants, viramas, and vowel modifiers (matras) are stored across multiple code units:
|
|
234
|
+
|
|
235
|
+
```javascript
|
|
236
|
+
// Standard JavaScript string.length:
|
|
237
|
+
console.log('ವಿದ್ಯಾರ್ಥಿ'.length); // 10 code units!
|
|
238
|
+
console.log('ಬೆಂಗಳೂರು'.length); // 8 code units!
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
If a subtitle validation tool calculates **Characters Per Second (CPS)** using `string.length`, it counts **10 characters** for a 5-character word like `"ವಿದ್ಯಾರ್ಥಿ"`. This **doubles the reading speed calculation**, triggering false-positive "Reading speed too fast!" warnings.
|
|
242
|
+
|
|
243
|
+
### How `subcraft` Solves This:
|
|
244
|
+
`subcraft` uses the native ECMAScript standard **`Intl.Segmenter`** (UAX #29 Unicode grapheme clustering):
|
|
245
|
+
|
|
246
|
+
```typescript
|
|
247
|
+
import { countGraphemes } from 'subcraft';
|
|
248
|
+
|
|
249
|
+
countGraphemes('ವಿದ್ಯಾರ್ಥಿ', 'kn'); // Accurately returns: 5 graphemes
|
|
250
|
+
countGraphemes('ಬೆಂಗಳೂರು', 'kn'); // Accurately returns: 4 graphemes
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
`subcraft` also automatically strips HTML markup (`<i>`, `<b>`, `<font...>`) before counting characters, ensuring that visual styling does not artificially penalize reading speed metrics.
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
## Comparison with Legacy Libraries
|
|
258
|
+
|
|
259
|
+
| Feature | `subcraft` | `subtitle.js` (legacy) | `srt-parser-2` |
|
|
260
|
+
| :--- | :---: | :---: | :---: |
|
|
261
|
+
| **Dependencies** | **0 (Zero)** | Depends on Node.js `stream` | 0 |
|
|
262
|
+
| **Browser & Web Worker Native** | ✅ **100% Native** | ❌ Requires Node polyfills | ✅ |
|
|
263
|
+
| **Built-in Quality Linter** | ✅ **Comprehensive** | ❌ None | ❌ None |
|
|
264
|
+
| **Unicode Grapheme CPS / CPL** | ✅ **`Intl.Segmenter`** | ❌ None | ❌ None |
|
|
265
|
+
| **Overlap & Chronology Checks** | ✅ **Yes** | ❌ None | ❌ None |
|
|
266
|
+
| **Frame-rate Drift Retiming** | ✅ **Yes (`retime`)** | ❌ Only linear shift | ❌ None |
|
|
267
|
+
| **Resilient Error Collection** | ✅ **Returns `{ cues, errors }`** | ❌ Throws errors | ❌ Incomplete |
|
|
268
|
+
| **Type Definitions** | ✅ **Strict ESM + CJS `.d.ts`** | ⚠️ Partial | ⚠️ Basic |
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## API Reference & Types
|
|
273
|
+
|
|
274
|
+
### Data Models
|
|
275
|
+
|
|
276
|
+
```typescript
|
|
277
|
+
export interface SubtitleCue {
|
|
278
|
+
id: string; // Unique identifier (e.g., "1", "2")
|
|
279
|
+
start: number; // Integer start time in milliseconds
|
|
280
|
+
end: number; // Integer end time in milliseconds
|
|
281
|
+
text: string; // Raw subtitle text payload (may contain newlines)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface ParseResult {
|
|
285
|
+
cues: SubtitleCue[];
|
|
286
|
+
errors: ParseError[];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface ParseError {
|
|
290
|
+
index?: number;
|
|
291
|
+
rawBlock: string;
|
|
292
|
+
message: string;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export interface LintDiagnostic {
|
|
296
|
+
cueId: string;
|
|
297
|
+
rule:
|
|
298
|
+
| 'out-of-order'
|
|
299
|
+
| 'zero-or-negative-duration'
|
|
300
|
+
| 'overlap'
|
|
301
|
+
| 'min-duration'
|
|
302
|
+
| 'max-duration'
|
|
303
|
+
| 'max-cpl'
|
|
304
|
+
| 'max-cps';
|
|
305
|
+
severity: 'error' | 'warning';
|
|
306
|
+
message: string;
|
|
307
|
+
expected?: number | string;
|
|
308
|
+
actual?: number | string;
|
|
309
|
+
}
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
## License
|
|
315
|
+
|
|
316
|
+
MIT © 2026 [Ranjith Gowda](https://github.com/Ranjithgowda007)
|