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 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
- # Temporary Holding Version
1
+ # subcraft
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ > **Zero-dependency, isomorphic TypeScript engine for subtitle parsing, frame-rate retiming, and Unicode grapheme-cluster quality linting (powered by `Intl.Segmenter`).**
4
+
5
+ [![npm version](https://img.shields.io/badge/npm-v0.2.0-blue.svg)](https://www.npmjs.com/package/subcraft)
6
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.8-blue.svg)](https://www.typescriptlang.org/)
7
+ [![Zero Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen.svg)](https://www.npmjs.com/package/subcraft)
8
+ [![Tests](https://img.shields.io/badge/tests-51%20passed-brightgreen.svg)](https://vitest.dev/)
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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)