subcraft 0.1.0 → 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/README.md +22 -2
- package/dist/index.cjs +125 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -1
- package/dist/index.d.ts +49 -1
- package/dist/index.js +122 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
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
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/subcraft)
|
|
6
6
|
[](https://www.typescriptlang.org/)
|
|
7
7
|
[](https://www.npmjs.com/package/subcraft)
|
|
8
|
-
[](https://vitest.dev/)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
|
|
11
11
|
---
|
|
@@ -124,6 +124,26 @@ const srtString = stringifySRT(cues);
|
|
|
124
124
|
|
|
125
125
|
---
|
|
126
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
|
+
|
|
127
147
|
### 3. Timecode Conversions
|
|
128
148
|
|
|
129
149
|
Convert between standard timecode strings and integer milliseconds without floating-point drift:
|
package/dist/index.cjs
CHANGED
|
@@ -133,6 +133,127 @@ ${text}`;
|
|
|
133
133
|
return blocks.join("\n\n") + "\n";
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
// src/vtt.ts
|
|
137
|
+
var VTT_ARROW_REGEX = /((?:\d{1,2}:)?\d{2}:\d{2}[,.]\d{3})\s*-->\s*((?:\d{1,2}:)?\d{2}:\d{2}[,.]\d{3})(?:[ \t]+([^\r\n]+))?/;
|
|
138
|
+
function parseVTT(rawText) {
|
|
139
|
+
const cues = [];
|
|
140
|
+
const errors = [];
|
|
141
|
+
if (!rawText || !rawText.trim()) {
|
|
142
|
+
return { cues, errors };
|
|
143
|
+
}
|
|
144
|
+
const clean = rawText.replace(/^\uFEFF/, "");
|
|
145
|
+
const normalized = clean.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
146
|
+
if (!normalized.startsWith("WEBVTT")) {
|
|
147
|
+
errors.push({
|
|
148
|
+
rawBlock: normalized.slice(0, 50),
|
|
149
|
+
message: 'Missing standard "WEBVTT" signature at the beginning of the file.'
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
const rawBlocks = normalized.trim().split(/\n\s*\n+/);
|
|
153
|
+
let fallbackIndex = 1;
|
|
154
|
+
for (let bIndex = 0; bIndex < rawBlocks.length; bIndex++) {
|
|
155
|
+
const rawBlock = rawBlocks[bIndex]?.trim();
|
|
156
|
+
if (!rawBlock) continue;
|
|
157
|
+
if (bIndex === 0 && rawBlock.startsWith("WEBVTT")) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (rawBlock.startsWith("NOTE")) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (rawBlock.startsWith("STYLE") || rawBlock.startsWith("REGION")) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const lines = rawBlock.split("\n");
|
|
167
|
+
let id;
|
|
168
|
+
let timecodeLine;
|
|
169
|
+
let textLines;
|
|
170
|
+
if (lines[0] && VTT_ARROW_REGEX.test(lines[0])) {
|
|
171
|
+
id = String(fallbackIndex);
|
|
172
|
+
timecodeLine = lines[0];
|
|
173
|
+
textLines = lines.slice(1);
|
|
174
|
+
} else if (lines[1] && VTT_ARROW_REGEX.test(lines[1])) {
|
|
175
|
+
id = lines[0]?.trim() || String(fallbackIndex);
|
|
176
|
+
timecodeLine = lines[1];
|
|
177
|
+
textLines = lines.slice(2);
|
|
178
|
+
} else {
|
|
179
|
+
errors.push({
|
|
180
|
+
index: fallbackIndex,
|
|
181
|
+
rawBlock,
|
|
182
|
+
message: 'Invalid WebVTT cue block. Missing timecode arrow "-->".'
|
|
183
|
+
});
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
const match = timecodeLine.match(VTT_ARROW_REGEX);
|
|
187
|
+
if (!match || !match[1] || !match[2]) {
|
|
188
|
+
errors.push({
|
|
189
|
+
index: fallbackIndex,
|
|
190
|
+
rawBlock,
|
|
191
|
+
message: `Failed to parse timestamps from WebVTT cue: "${timecodeLine}".`
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
try {
|
|
196
|
+
const start = timecodeToMs(match[1]);
|
|
197
|
+
const end = timecodeToMs(match[2]);
|
|
198
|
+
const settings = match[3]?.trim();
|
|
199
|
+
const text = textLines.join("\n").trim();
|
|
200
|
+
const cue = {
|
|
201
|
+
id,
|
|
202
|
+
start,
|
|
203
|
+
end,
|
|
204
|
+
text
|
|
205
|
+
};
|
|
206
|
+
if (settings) {
|
|
207
|
+
cue.settings = settings;
|
|
208
|
+
}
|
|
209
|
+
cues.push(cue);
|
|
210
|
+
fallbackIndex++;
|
|
211
|
+
} catch (err) {
|
|
212
|
+
errors.push({
|
|
213
|
+
index: fallbackIndex,
|
|
214
|
+
rawBlock,
|
|
215
|
+
message: err instanceof Error ? err.message : String(err)
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return { cues, errors };
|
|
220
|
+
}
|
|
221
|
+
function stringifyVTT(cues, options) {
|
|
222
|
+
const header = options?.header ? `WEBVTT - ${options.header}` : "WEBVTT";
|
|
223
|
+
if (!cues || cues.length === 0) {
|
|
224
|
+
return `${header}
|
|
225
|
+
`;
|
|
226
|
+
}
|
|
227
|
+
const includeIds = options?.includeIds ?? false;
|
|
228
|
+
const blocks = cues.map((cue, idx) => {
|
|
229
|
+
const id = cue.id ? String(cue.id) : String(idx + 1);
|
|
230
|
+
const startStr = msToTimecode(cue.start, "vtt");
|
|
231
|
+
const endStr = msToTimecode(cue.end, "vtt");
|
|
232
|
+
const settingsStr = cue.settings ? ` ${cue.settings}` : "";
|
|
233
|
+
const timeLine = `${startStr} --> ${endStr}${settingsStr}`;
|
|
234
|
+
const text = cue.text ?? "";
|
|
235
|
+
if (includeIds) {
|
|
236
|
+
return `${id}
|
|
237
|
+
${timeLine}
|
|
238
|
+
${text}`;
|
|
239
|
+
}
|
|
240
|
+
return `${timeLine}
|
|
241
|
+
${text}`;
|
|
242
|
+
});
|
|
243
|
+
return `${header}
|
|
244
|
+
|
|
245
|
+
${blocks.join("\n\n")}
|
|
246
|
+
`;
|
|
247
|
+
}
|
|
248
|
+
function srtToVTT(srtText, options) {
|
|
249
|
+
const { cues } = parseSRT(srtText);
|
|
250
|
+
return stringifyVTT(cues, options);
|
|
251
|
+
}
|
|
252
|
+
function vttToSRT(vttText) {
|
|
253
|
+
const { cues } = parseVTT(vttText);
|
|
254
|
+
return stringifySRT(cues);
|
|
255
|
+
}
|
|
256
|
+
|
|
136
257
|
// src/linter.ts
|
|
137
258
|
var DEFAULT_PROFILES = {
|
|
138
259
|
standardLatin: {
|
|
@@ -283,10 +404,14 @@ exports.getCueDuration = getCueDuration;
|
|
|
283
404
|
exports.lintCues = lintCues;
|
|
284
405
|
exports.msToTimecode = msToTimecode;
|
|
285
406
|
exports.parseSRT = parseSRT;
|
|
407
|
+
exports.parseVTT = parseVTT;
|
|
286
408
|
exports.retime = retime;
|
|
287
409
|
exports.shiftTimecodes = shiftTimecodes;
|
|
410
|
+
exports.srtToVTT = srtToVTT;
|
|
288
411
|
exports.stringifySRT = stringifySRT;
|
|
412
|
+
exports.stringifyVTT = stringifyVTT;
|
|
289
413
|
exports.stripFormattingTags = stripFormattingTags;
|
|
290
414
|
exports.timecodeToMs = timecodeToMs;
|
|
415
|
+
exports.vttToSRT = vttToSRT;
|
|
291
416
|
//# sourceMappingURL=index.cjs.map
|
|
292
417
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/timecode.ts","../src/parser.ts","../src/serializer.ts","../src/linter.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Core normalized representation of a single subtitle cue.\n * Timestamps are strictly stored in integer milliseconds.\n */\nexport interface SubtitleCue {\n /** Unique sequential identifier (e.g. \"1\", \"2\") */\n id: string;\n /** Start time in integer milliseconds */\n start: number;\n /** End time in integer milliseconds */\n end: number;\n /** Raw subtitle text content (may contain newlines) */\n text: string;\n}\n\n/**\n * Calculates the duration of a cue in milliseconds.\n * Derived helper function to keep the core data model minimal and immutable.\n */\nexport const getCueDuration = (cue: SubtitleCue): number => cue.end - cue.start;\n\n/** Supported subtitle file formats */\nexport type TimecodeFormat = 'srt' | 'vtt';\n\n/**\n * Detailed error reported when a cue block fails parsing syntax rules.\n */\nexport interface ParseError {\n index?: number;\n rawBlock: string;\n message: string;\n}\n\n/**\n * Resilient parser result returning successfully parsed cues\n * alongside any syntax errors encountered without throwing.\n */\nexport interface ParseResult {\n cues: SubtitleCue[];\n errors: ParseError[];\n}\n\n/** Severity levels for lint diagnostics */\nexport type LintSeverity = 'error' | 'warning';\n\n/** Quality rules enforced by the linter */\nexport type LintRule =\n | 'out-of-order'\n | 'overlap'\n | 'zero-or-negative-duration'\n | 'min-duration'\n | 'max-duration'\n | 'max-cpl'\n | 'max-cps';\n\n/**\n * Diagnostic report produced when a cue violates a quality rule.\n */\nexport interface LintDiagnostic {\n cueId: string;\n rule: LintRule;\n severity: LintSeverity;\n message: string;\n expected?: number | string;\n actual?: number | string;\n}\n\n/**\n * Configuration options for locale-aware subtitle validation.\n */\nexport interface LintProfile {\n name: string;\n locale?: string;\n maxCps: number;\n maxCpl: number;\n minDurationMs: number;\n maxDurationMs: number;\n includeSpaces?: boolean;\n}\n","import type { SubtitleCue, TimecodeFormat } from './types.js';\n\n/**\n * Regular expression to match both SRT (comma) and WebVTT (dot) timecodes.\n * Supports:\n * - HH:MM:SS,mmm or HH:MM:SS.mmm\n * - MM:SS.mmm (WebVTT short format)\n */\nconst TIMECODE_REGEX = /^(?:(?:(\\d{1,2}):)?(\\d{2}):)?(\\d{2})[,.](\\d{3})$/;\n\n/**\n * Converts a formatted timecode string into integer milliseconds.\n *\n * Supported formats:\n * - \"01:23:45,678\" (SRT standard)\n * - \"01:23:45.678\" (WebVTT standard)\n * - \"23:45.678\" (WebVTT short form without hours)\n *\n * @throws {Error} if timecode format is invalid\n */\nexport function timecodeToMs(timecode: string): number {\n const trimmed = timecode.trim();\n const match = trimmed.match(TIMECODE_REGEX);\n\n if (!match) {\n throw new Error(\n `Invalid timecode format: \"${timecode}\". Expected \"HH:MM:SS,mmm\" or \"HH:MM:SS.mmm\".`\n );\n }\n\n const hours = match[1] ? parseInt(match[1], 10) : 0;\n const minutes = match[2] ? parseInt(match[2], 10) : 0;\n const seconds = parseInt(match[3] ?? '0', 10);\n const milliseconds = parseInt(match[4] ?? '0', 10);\n\n return (\n hours * 3_600_000 +\n minutes * 60_000 +\n seconds * 1_000 +\n milliseconds\n );\n}\n\n/**\n * Converts integer milliseconds into a standard formatted timecode string.\n *\n * @param ms - Non-negative integer milliseconds (negative values clamped to 0)\n * @param format - 'srt' (uses comma delimiter) or 'vtt' (uses dot delimiter)\n *\n * @example\n * msToTimecode(83456, 'srt') // \"00:01:23,456\"\n * msToTimecode(83456, 'vtt') // \"00:01:23.456\"\n */\nexport function msToTimecode(ms: number, format: TimecodeFormat = 'srt'): string {\n const safeMs = Math.max(0, Math.floor(ms));\n\n const hours = Math.floor(safeMs / 3_600_000);\n const minutes = Math.floor((safeMs % 3_600_000) / 60_000);\n const seconds = Math.floor((safeMs % 60_000) / 1_000);\n const remainderMs = safeMs % 1_000;\n\n const hh = String(hours).padStart(2, '0');\n const mm = String(minutes).padStart(2, '0');\n const ss = String(seconds).padStart(2, '0');\n const mmm = String(remainderMs).padStart(3, '0');\n\n const delimiter = format === 'vtt' ? '.' : ',';\n return `${hh}:${mm}:${ss}${delimiter}${mmm}`;\n}\n\n/**\n * Linearly shifts timecodes by a constant millisecond offset.\n * Used to correct audio-sync delay or advance.\n * Clamps negative results to 0.\n *\n * @param cues - Array of SubtitleCue objects\n * @param offsetMs - Offset in milliseconds (positive advances, negative delays)\n * @returns A new immutable array of shifted cues\n */\nexport function shiftTimecodes(\n cues: readonly SubtitleCue[],\n offsetMs: number\n): SubtitleCue[] {\n return cues.map((cue) => ({\n ...cue,\n start: Math.max(0, cue.start + offsetMs),\n end: Math.max(0, cue.end + offsetMs),\n }));\n}\n\n/**\n * Scales all cue timecodes by a multiplicative ratio.\n * Used for frame-rate drift compensation between video standards.\n * All results are strictly rounded to integer milliseconds with Math.round.\n *\n * Direction definition:\n * targetTimestamp = Math.round(sourceTimestamp * ratio)\n *\n * @example\n * // 24 fps to 25 fps (PAL speed-up): video is ~4% faster, timecodes compress\n * const palCues = retime(filmCues, 24 / 25); // ratio = 0.96\n *\n * @example\n * // 25 fps to 24 fps (Slow-down): video is ~4% slower, timecodes stretch\n * const filmCues = retime(palCues, 25 / 24); // ratio = 1.04167\n *\n * @param cues - Array of SubtitleCue objects\n * @param ratio - Positive scaling multiplier (e.g., 24/25 or 25/24)\n * @returns A new immutable array of retimed cues\n */\nexport function retime(\n cues: readonly SubtitleCue[],\n ratio: number\n): SubtitleCue[] {\n if (ratio <= 0) {\n throw new Error(`Retime ratio must be greater than 0. Received: ${ratio}`);\n }\n\n return cues.map((cue) => ({\n ...cue,\n start: Math.round(cue.start * ratio),\n end: Math.round(cue.end * ratio),\n }));\n}\n","import { timecodeToMs } from './timecode.js';\nimport type { SubtitleCue, ParseError, ParseResult } from './types.js';\n\n/** Matches the standard SRT timestamp line: 00:00:00,000 --> 00:00:00,000 */\nconst ARROW_REGEX =\n /((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})\\s*-->\\s*((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})/;\n\n/**\n * Parses raw SRT subtitle text into structured SubtitleCue objects.\n * Resilient parser that collects syntax errors without throwing unhandled exceptions.\n *\n * Handles:\n * - UTF-8 Byte Order Marks (BOM `\\uFEFF`)\n * - Windows (CRLF), Unix (LF), and legacy Mac (CR) line endings\n * - Optional or missing cue numeric index headers\n * - Multi-line subtitle texts\n * - Trailing and irregular blank lines\n *\n * @param rawText - Raw SRT file content as a string\n * @returns {ParseResult} Object containing successfully parsed cues and any block errors\n */\nexport function parseSRT(rawText: string): ParseResult {\n const cues: SubtitleCue[] = [];\n const errors: ParseError[] = [];\n\n if (!rawText || !rawText.trim()) {\n return { cues, errors };\n }\n\n // 1. Strip UTF-8 Byte Order Mark (BOM)\n const cleanText = rawText.replace(/^\\uFEFF/, '');\n\n // 2. Normalize CRLF and CR to standard Unix LF\n const normalized = cleanText.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n // 3. Split by 2 or more consecutive newlines into cue blocks\n const rawBlocks = normalized.trim().split(/\\n\\s*\\n+/);\n\n let fallbackIndex = 1;\n\n for (let bIndex = 0; bIndex < rawBlocks.length; bIndex++) {\n const rawBlock = rawBlocks[bIndex]?.trim();\n if (!rawBlock) continue;\n\n const lines = rawBlock.split('\\n');\n\n let id: string;\n let timecodeLine: string;\n let textLines: string[];\n\n // Check if line 0 is the timecode arrow (missing numeric index)\n if (lines[0] && ARROW_REGEX.test(lines[0])) {\n id = String(fallbackIndex);\n timecodeLine = lines[0];\n textLines = lines.slice(1);\n } else if (lines[1] && ARROW_REGEX.test(lines[1])) {\n // Standard SRT: line 0 is ID, line 1 is timecode\n id = lines[0]?.trim() || String(fallbackIndex);\n timecodeLine = lines[1];\n textLines = lines.slice(2);\n } else {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message:\n 'Invalid block format. Expected timecode arrow \"-->\" on line 1 or 2.',\n });\n continue;\n }\n\n const match = timecodeLine.match(ARROW_REGEX);\n if (!match || !match[1] || !match[2]) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: `Failed to extract start and end timestamps from: \"${timecodeLine}\".`,\n });\n continue;\n }\n\n try {\n const start = timecodeToMs(match[1]);\n const end = timecodeToMs(match[2]);\n const text = textLines.join('\\n').trim();\n\n cues.push({\n id,\n start,\n end,\n text,\n });\n\n fallbackIndex++;\n } catch (err) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { cues, errors };\n}\n","import { msToTimecode } from './timecode.js';\nimport type { SubtitleCue } from './types.js';\n\n/**\n * Serializes an array of SubtitleCue objects into standard SRT file format.\n *\n * Output format:\n * 1\n * 00:00:20,000 --> 00:00:24,000\n * Subtitle text\n *\n * @param cues - Array of SubtitleCue objects to format\n * @returns Standard formatted SRT string\n */\nexport function stringifySRT(cues: readonly SubtitleCue[]): string {\n if (!cues || cues.length === 0) {\n return '';\n }\n\n const blocks = cues.map((cue, idx) => {\n const id = cue.id ? String(cue.id) : String(idx + 1);\n const startStr = msToTimecode(cue.start, 'srt');\n const endStr = msToTimecode(cue.end, 'srt');\n const text = cue.text ?? '';\n\n return `${id}\\n${startStr} --> ${endStr}\\n${text}`;\n });\n\n return blocks.join('\\n\\n') + '\\n';\n}\n","import type {\n SubtitleCue,\n LintDiagnostic,\n LintProfile,\n} from './types.js';\n\n/** Default quality profiles for subtitle verification */\nexport const DEFAULT_PROFILES: Record<string, LintProfile> = {\n standardLatin: {\n name: 'Standard Latin (Netflix/BBC Style Guide)',\n locale: 'en',\n maxCps: 20,\n maxCpl: 42,\n minDurationMs: 800,\n maxDurationMs: 7000,\n includeSpaces: true,\n },\n indicKannada: {\n name: 'Indic - Kannada Locale',\n locale: 'kn',\n maxCps: 15,\n maxCpl: 35,\n minDurationMs: 800,\n maxDurationMs: 7000,\n includeSpaces: true,\n },\n};\n\n/**\n * Strips HTML formatting tags from subtitle text before measuring character limits.\n * Handles <i>, <b>, <u>, <font color=\"...\">, etc.\n */\nexport function stripFormattingTags(text: string): string {\n return text.replace(/<[^>]*>/g, '');\n}\n\n/**\n * Counts user-perceived grapheme clusters using native ECMAScript Intl.Segmenter.\n * Accurately measures multi-byte conjuncts and vowel modifiers in Indic scripts (Kannada, Hindi),\n * as well as emojis and complex Unicode characters without UTF-16 code unit inflation.\n */\nexport function countGraphemes(text: string, locale: string = 'en'): number {\n if (!text) return 0;\n\n if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {\n const segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });\n let count = 0;\n // Iterate over segments efficiently without allocating full array\n for (const _ of segmenter.segment(text)) {\n count++;\n }\n return count;\n }\n\n // Fallback for environments lacking Intl.Segmenter\n return [...text].length;\n}\n\n/**\n * Inspects an array of SubtitleCues against timing, structural, and reading speed rules.\n *\n * Rules verified:\n * - out-of-order: Cues not in ascending chronological order\n * - zero-or-negative-duration: Cues where end <= start\n * - overlap: Cues overlapping chronologically with preceding cues\n * - min-duration: Cues flashing too fast for comprehension (< 800ms)\n * - max-duration: Cues remaining on screen too long (> 7000ms)\n * - max-cpl: Any line exceeding characters-per-line limit\n * - max-cps: Reading speed exceeding characters-per-second limit\n *\n * @param cues - Array of SubtitleCue objects to inspect\n * @param customProfile - Optional custom thresholds or language profile\n * @returns Array of LintDiagnostic reports\n */\nexport function lintCues(\n cues: readonly SubtitleCue[],\n customProfile?: Partial<LintProfile>\n): LintDiagnostic[] {\n const diagnostics: LintDiagnostic[] = [];\n if (!cues || cues.length === 0) return diagnostics;\n\n const profile: LintProfile = {\n ...DEFAULT_PROFILES['standardLatin']!,\n ...customProfile,\n };\n\n const {\n locale = 'en',\n maxCps,\n maxCpl,\n minDurationMs,\n maxDurationMs,\n includeSpaces = true,\n } = profile;\n\n for (let i = 0; i < cues.length; i++) {\n const cue = cues[i];\n if (!cue) continue;\n\n const prevCue = i > 0 ? cues[i - 1] : undefined;\n const duration = cue.end - cue.start;\n\n // --- TIMING RULES ---\n\n // 1. Out-of-order check\n if (prevCue && cue.start < prevCue.start) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'out-of-order',\n severity: 'error',\n message: `Cue starts at ${cue.start}ms before previous cue start of ${prevCue.start}ms.`,\n expected: `start >= ${prevCue.start}`,\n actual: `start = ${cue.start}`,\n });\n }\n\n // 2. Zero or negative duration check\n if (cue.end <= cue.start) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'zero-or-negative-duration',\n severity: 'error',\n message: `Cue has invalid duration (${duration}ms). End timestamp must be greater than start timestamp.`,\n expected: `end > ${cue.start}`,\n actual: `end = ${cue.end}`,\n });\n }\n\n // 3. Overlap check (only if not out-of-order)\n if (prevCue && cue.start < prevCue.end && cue.start >= prevCue.start) {\n const overlapMs = prevCue.end - cue.start;\n diagnostics.push({\n cueId: cue.id,\n rule: 'overlap',\n severity: 'error',\n message: `Cue overlaps with previous cue by ${overlapMs}ms.`,\n expected: `start >= ${prevCue.end}`,\n actual: `start = ${cue.start}`,\n });\n }\n\n // 4. Min duration check (only if duration is positive)\n if (duration > 0 && duration < minDurationMs) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'min-duration',\n severity: 'warning',\n message: `Cue duration of ${duration}ms is below the minimum threshold of ${minDurationMs}ms.`,\n expected: minDurationMs,\n actual: duration,\n });\n }\n\n // 5. Max duration check\n if (duration > maxDurationMs) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-duration',\n severity: 'warning',\n message: `Cue duration of ${duration}ms exceeds the maximum threshold of ${maxDurationMs}ms.`,\n expected: maxDurationMs,\n actual: duration,\n });\n }\n\n // --- TEXT & PERCEPTION RULES ---\n\n const rawText = cue.text || '';\n const cleanText = stripFormattingTags(rawText);\n\n // 6. Characters Per Line (CPL) check\n const lines = cleanText.split('\\n');\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex];\n if (!line) continue;\n\n const cpl = countGraphemes(line, locale);\n if (cpl > maxCpl) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-cpl',\n severity: 'warning',\n message: `Line ${lineIndex + 1} has ${cpl} characters, exceeding the maximum limit of ${maxCpl} CPL.`,\n expected: maxCpl,\n actual: cpl,\n });\n }\n }\n\n // 7. Characters Per Second (CPS) reading speed check\n if (duration > 0) {\n const textToMeasure = includeSpaces\n ? cleanText\n : cleanText.replace(/\\s+/g, '');\n\n const totalGraphemes = countGraphemes(textToMeasure, locale);\n const cps = Number(((totalGraphemes / duration) * 1000).toFixed(1));\n\n if (cps > maxCps) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-cps',\n severity: 'warning',\n message: `Reading speed of ${cps} CPS exceeds maximum limit of ${maxCps} CPS.`,\n expected: maxCps,\n actual: cps,\n });\n }\n }\n }\n\n return diagnostics;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/timecode.ts","../src/parser.ts","../src/serializer.ts","../src/vtt.ts","../src/linter.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Core normalized representation of a single subtitle cue.\n * Timestamps are strictly stored in integer milliseconds.\n */\nexport interface SubtitleCue {\n /** Unique sequential identifier (e.g. \"1\", \"2\") */\n id: string;\n /** Start time in integer milliseconds */\n start: number;\n /** End time in integer milliseconds */\n end: number;\n /** Raw subtitle text content (may contain newlines) */\n text: string;\n /** Optional WebVTT cue placement/styling settings (e.g., \"align:middle line:90%\") */\n settings?: string;\n}\n\n/**\n * Calculates the duration of a cue in milliseconds.\n * Derived helper function to keep the core data model minimal and immutable.\n */\nexport const getCueDuration = (cue: SubtitleCue): number => cue.end - cue.start;\n\n/** Supported subtitle file formats */\nexport type TimecodeFormat = 'srt' | 'vtt';\n\n/**\n * Detailed error reported when a cue block fails parsing syntax rules.\n */\nexport interface ParseError {\n index?: number;\n rawBlock: string;\n message: string;\n}\n\n/**\n * Resilient parser result returning successfully parsed cues\n * alongside any syntax errors encountered without throwing.\n */\nexport interface ParseResult {\n cues: SubtitleCue[];\n errors: ParseError[];\n}\n\n/** Severity levels for lint diagnostics */\nexport type LintSeverity = 'error' | 'warning';\n\n/** Quality rules enforced by the linter */\nexport type LintRule =\n | 'out-of-order'\n | 'overlap'\n | 'zero-or-negative-duration'\n | 'min-duration'\n | 'max-duration'\n | 'max-cpl'\n | 'max-cps';\n\n/**\n * Diagnostic report produced when a cue violates a quality rule.\n */\nexport interface LintDiagnostic {\n cueId: string;\n rule: LintRule;\n severity: LintSeverity;\n message: string;\n expected?: number | string;\n actual?: number | string;\n}\n\n/**\n * Configuration options for locale-aware subtitle validation.\n */\nexport interface LintProfile {\n name: string;\n locale?: string;\n maxCps: number;\n maxCpl: number;\n minDurationMs: number;\n maxDurationMs: number;\n includeSpaces?: boolean;\n}\n","import type { SubtitleCue, TimecodeFormat } from './types.js';\n\n/**\n * Regular expression to match both SRT (comma) and WebVTT (dot) timecodes.\n * Supports:\n * - HH:MM:SS,mmm or HH:MM:SS.mmm\n * - MM:SS.mmm (WebVTT short format)\n */\nconst TIMECODE_REGEX = /^(?:(?:(\\d{1,2}):)?(\\d{2}):)?(\\d{2})[,.](\\d{3})$/;\n\n/**\n * Converts a formatted timecode string into integer milliseconds.\n *\n * Supported formats:\n * - \"01:23:45,678\" (SRT standard)\n * - \"01:23:45.678\" (WebVTT standard)\n * - \"23:45.678\" (WebVTT short form without hours)\n *\n * @throws {Error} if timecode format is invalid\n */\nexport function timecodeToMs(timecode: string): number {\n const trimmed = timecode.trim();\n const match = trimmed.match(TIMECODE_REGEX);\n\n if (!match) {\n throw new Error(\n `Invalid timecode format: \"${timecode}\". Expected \"HH:MM:SS,mmm\" or \"HH:MM:SS.mmm\".`\n );\n }\n\n const hours = match[1] ? parseInt(match[1], 10) : 0;\n const minutes = match[2] ? parseInt(match[2], 10) : 0;\n const seconds = parseInt(match[3] ?? '0', 10);\n const milliseconds = parseInt(match[4] ?? '0', 10);\n\n return (\n hours * 3_600_000 +\n minutes * 60_000 +\n seconds * 1_000 +\n milliseconds\n );\n}\n\n/**\n * Converts integer milliseconds into a standard formatted timecode string.\n *\n * @param ms - Non-negative integer milliseconds (negative values clamped to 0)\n * @param format - 'srt' (uses comma delimiter) or 'vtt' (uses dot delimiter)\n *\n * @example\n * msToTimecode(83456, 'srt') // \"00:01:23,456\"\n * msToTimecode(83456, 'vtt') // \"00:01:23.456\"\n */\nexport function msToTimecode(ms: number, format: TimecodeFormat = 'srt'): string {\n const safeMs = Math.max(0, Math.floor(ms));\n\n const hours = Math.floor(safeMs / 3_600_000);\n const minutes = Math.floor((safeMs % 3_600_000) / 60_000);\n const seconds = Math.floor((safeMs % 60_000) / 1_000);\n const remainderMs = safeMs % 1_000;\n\n const hh = String(hours).padStart(2, '0');\n const mm = String(minutes).padStart(2, '0');\n const ss = String(seconds).padStart(2, '0');\n const mmm = String(remainderMs).padStart(3, '0');\n\n const delimiter = format === 'vtt' ? '.' : ',';\n return `${hh}:${mm}:${ss}${delimiter}${mmm}`;\n}\n\n/**\n * Linearly shifts timecodes by a constant millisecond offset.\n * Used to correct audio-sync delay or advance.\n * Clamps negative results to 0.\n *\n * @param cues - Array of SubtitleCue objects\n * @param offsetMs - Offset in milliseconds (positive advances, negative delays)\n * @returns A new immutable array of shifted cues\n */\nexport function shiftTimecodes(\n cues: readonly SubtitleCue[],\n offsetMs: number\n): SubtitleCue[] {\n return cues.map((cue) => ({\n ...cue,\n start: Math.max(0, cue.start + offsetMs),\n end: Math.max(0, cue.end + offsetMs),\n }));\n}\n\n/**\n * Scales all cue timecodes by a multiplicative ratio.\n * Used for frame-rate drift compensation between video standards.\n * All results are strictly rounded to integer milliseconds with Math.round.\n *\n * Direction definition:\n * targetTimestamp = Math.round(sourceTimestamp * ratio)\n *\n * @example\n * // 24 fps to 25 fps (PAL speed-up): video is ~4% faster, timecodes compress\n * const palCues = retime(filmCues, 24 / 25); // ratio = 0.96\n *\n * @example\n * // 25 fps to 24 fps (Slow-down): video is ~4% slower, timecodes stretch\n * const filmCues = retime(palCues, 25 / 24); // ratio = 1.04167\n *\n * @param cues - Array of SubtitleCue objects\n * @param ratio - Positive scaling multiplier (e.g., 24/25 or 25/24)\n * @returns A new immutable array of retimed cues\n */\nexport function retime(\n cues: readonly SubtitleCue[],\n ratio: number\n): SubtitleCue[] {\n if (ratio <= 0) {\n throw new Error(`Retime ratio must be greater than 0. Received: ${ratio}`);\n }\n\n return cues.map((cue) => ({\n ...cue,\n start: Math.round(cue.start * ratio),\n end: Math.round(cue.end * ratio),\n }));\n}\n","import { timecodeToMs } from './timecode.js';\nimport type { SubtitleCue, ParseError, ParseResult } from './types.js';\n\n/** Matches the standard SRT timestamp line: 00:00:00,000 --> 00:00:00,000 */\nconst ARROW_REGEX =\n /((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})\\s*-->\\s*((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})/;\n\n/**\n * Parses raw SRT subtitle text into structured SubtitleCue objects.\n * Resilient parser that collects syntax errors without throwing unhandled exceptions.\n *\n * Handles:\n * - UTF-8 Byte Order Marks (BOM `\\uFEFF`)\n * - Windows (CRLF), Unix (LF), and legacy Mac (CR) line endings\n * - Optional or missing cue numeric index headers\n * - Multi-line subtitle texts\n * - Trailing and irregular blank lines\n *\n * @param rawText - Raw SRT file content as a string\n * @returns {ParseResult} Object containing successfully parsed cues and any block errors\n */\nexport function parseSRT(rawText: string): ParseResult {\n const cues: SubtitleCue[] = [];\n const errors: ParseError[] = [];\n\n if (!rawText || !rawText.trim()) {\n return { cues, errors };\n }\n\n // 1. Strip UTF-8 Byte Order Mark (BOM)\n const cleanText = rawText.replace(/^\\uFEFF/, '');\n\n // 2. Normalize CRLF and CR to standard Unix LF\n const normalized = cleanText.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n // 3. Split by 2 or more consecutive newlines into cue blocks\n const rawBlocks = normalized.trim().split(/\\n\\s*\\n+/);\n\n let fallbackIndex = 1;\n\n for (let bIndex = 0; bIndex < rawBlocks.length; bIndex++) {\n const rawBlock = rawBlocks[bIndex]?.trim();\n if (!rawBlock) continue;\n\n const lines = rawBlock.split('\\n');\n\n let id: string;\n let timecodeLine: string;\n let textLines: string[];\n\n // Check if line 0 is the timecode arrow (missing numeric index)\n if (lines[0] && ARROW_REGEX.test(lines[0])) {\n id = String(fallbackIndex);\n timecodeLine = lines[0];\n textLines = lines.slice(1);\n } else if (lines[1] && ARROW_REGEX.test(lines[1])) {\n // Standard SRT: line 0 is ID, line 1 is timecode\n id = lines[0]?.trim() || String(fallbackIndex);\n timecodeLine = lines[1];\n textLines = lines.slice(2);\n } else {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message:\n 'Invalid block format. Expected timecode arrow \"-->\" on line 1 or 2.',\n });\n continue;\n }\n\n const match = timecodeLine.match(ARROW_REGEX);\n if (!match || !match[1] || !match[2]) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: `Failed to extract start and end timestamps from: \"${timecodeLine}\".`,\n });\n continue;\n }\n\n try {\n const start = timecodeToMs(match[1]);\n const end = timecodeToMs(match[2]);\n const text = textLines.join('\\n').trim();\n\n cues.push({\n id,\n start,\n end,\n text,\n });\n\n fallbackIndex++;\n } catch (err) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { cues, errors };\n}\n","import { msToTimecode } from './timecode.js';\nimport type { SubtitleCue } from './types.js';\n\n/**\n * Serializes an array of SubtitleCue objects into standard SRT file format.\n *\n * Output format:\n * 1\n * 00:00:20,000 --> 00:00:24,000\n * Subtitle text\n *\n * @param cues - Array of SubtitleCue objects to format\n * @returns Standard formatted SRT string\n */\nexport function stringifySRT(cues: readonly SubtitleCue[]): string {\n if (!cues || cues.length === 0) {\n return '';\n }\n\n const blocks = cues.map((cue, idx) => {\n const id = cue.id ? String(cue.id) : String(idx + 1);\n const startStr = msToTimecode(cue.start, 'srt');\n const endStr = msToTimecode(cue.end, 'srt');\n const text = cue.text ?? '';\n\n return `${id}\\n${startStr} --> ${endStr}\\n${text}`;\n });\n\n return blocks.join('\\n\\n') + '\\n';\n}\n","import { timecodeToMs, msToTimecode } from './timecode.js';\nimport { parseSRT } from './parser.js';\nimport { stringifySRT } from './serializer.js';\nimport type { SubtitleCue, ParseError, ParseResult } from './types.js';\n\n/**\n * Matches the WebVTT timestamp line with optional cue settings:\n * 00:01:20.500 --> 00:01:23.000 align:middle line:90%\n */\nconst VTT_ARROW_REGEX =\n /((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})\\s*-->\\s*((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})(?:[ \\t]+([^\\r\\n]+))?/;\n\nexport interface StringifyVTTOptions {\n /** Optional custom header line after WEBVTT (e.g. \"WEBVTT - Commentary Track\") */\n header?: string;\n /** Whether to output cue numeric ID headers (default: false for standard clean WebVTT) */\n includeIds?: boolean;\n}\n\n/**\n * Parses raw WebVTT (.vtt) subtitle text into structured SubtitleCue objects.\n *\n * Handles:\n * - UTF-8 Byte Order Marks (BOM \\uFEFF)\n * - WEBVTT header and metadata blocks\n * - NOTE comment blocks (skipped automatically)\n * - Dot-separated milliseconds (00:01:20.500) and comma-separated fallback\n * - Short timecodes without hours (MM:SS.mmm)\n * - Cue placement/styling settings (e.g. align:middle line:90%)\n * - Optional cue identifiers\n *\n * @param rawText - Raw WebVTT file content as a string\n * @returns {ParseResult} Successfully parsed cues and any block errors\n */\nexport function parseVTT(rawText: string): ParseResult {\n const cues: SubtitleCue[] = [];\n const errors: ParseError[] = [];\n\n if (!rawText || !rawText.trim()) {\n return { cues, errors };\n }\n\n // 1. Strip BOM & normalize newlines\n const clean = rawText.replace(/^\\uFEFF/, '');\n const normalized = clean.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n // 2. Validate and strip WEBVTT header\n if (!normalized.startsWith('WEBVTT')) {\n errors.push({\n rawBlock: normalized.slice(0, 50),\n message: 'Missing standard \"WEBVTT\" signature at the beginning of the file.',\n });\n }\n\n // Split into blocks by double newlines\n const rawBlocks = normalized.trim().split(/\\n\\s*\\n+/);\n let fallbackIndex = 1;\n\n for (let bIndex = 0; bIndex < rawBlocks.length; bIndex++) {\n const rawBlock = rawBlocks[bIndex]?.trim();\n if (!rawBlock) continue;\n\n // Skip the very first block if it is just the WEBVTT header line\n if (bIndex === 0 && rawBlock.startsWith('WEBVTT')) {\n continue;\n }\n\n // Skip NOTE comment blocks\n if (rawBlock.startsWith('NOTE')) {\n continue;\n }\n\n // Skip STYLE or REGION metadata blocks if encountered\n if (rawBlock.startsWith('STYLE') || rawBlock.startsWith('REGION')) {\n continue;\n }\n\n const lines = rawBlock.split('\\n');\n let id: string;\n let timecodeLine: string;\n let textLines: string[];\n\n // Case 1: First line is the timestamp arrow (no cue ID header)\n if (lines[0] && VTT_ARROW_REGEX.test(lines[0])) {\n id = String(fallbackIndex);\n timecodeLine = lines[0];\n textLines = lines.slice(1);\n } else if (lines[1] && VTT_ARROW_REGEX.test(lines[1])) {\n // Case 2: Line 0 is ID header, line 1 is timestamp\n id = lines[0]?.trim() || String(fallbackIndex);\n timecodeLine = lines[1];\n textLines = lines.slice(2);\n } else {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: 'Invalid WebVTT cue block. Missing timecode arrow \"-->\".',\n });\n continue;\n }\n\n const match = timecodeLine.match(VTT_ARROW_REGEX);\n if (!match || !match[1] || !match[2]) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: `Failed to parse timestamps from WebVTT cue: \"${timecodeLine}\".`,\n });\n continue;\n }\n\n try {\n const start = timecodeToMs(match[1]);\n const end = timecodeToMs(match[2]);\n const settings = match[3]?.trim();\n const text = textLines.join('\\n').trim();\n\n const cue: SubtitleCue = {\n id,\n start,\n end,\n text,\n };\n\n if (settings) {\n cue.settings = settings;\n }\n\n cues.push(cue);\n fallbackIndex++;\n } catch (err) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { cues, errors };\n}\n\n/**\n * Serializes an array of SubtitleCue objects into standard WebVTT (.vtt) file format.\n *\n * @param cues - Array of SubtitleCue objects to format\n * @param options - Formatting options (header title, inclusion of cue IDs)\n * @returns Formatted WebVTT string\n */\nexport function stringifyVTT(\n cues: readonly SubtitleCue[],\n options?: StringifyVTTOptions\n): string {\n const header = options?.header ? `WEBVTT - ${options.header}` : 'WEBVTT';\n\n if (!cues || cues.length === 0) {\n return `${header}\\n`;\n }\n\n const includeIds = options?.includeIds ?? false;\n\n const blocks = cues.map((cue, idx) => {\n const id = cue.id ? String(cue.id) : String(idx + 1);\n const startStr = msToTimecode(cue.start, 'vtt');\n const endStr = msToTimecode(cue.end, 'vtt');\n const settingsStr = cue.settings ? ` ${cue.settings}` : '';\n const timeLine = `${startStr} --> ${endStr}${settingsStr}`;\n const text = cue.text ?? '';\n\n if (includeIds) {\n return `${id}\\n${timeLine}\\n${text}`;\n }\n return `${timeLine}\\n${text}`;\n });\n\n return `${header}\\n\\n${blocks.join('\\n\\n')}\\n`;\n}\n\n/**\n * Converts raw SRT subtitle text directly into a valid WebVTT (.vtt) string.\n *\n * @param srtText - Raw SubRip (.srt) text content\n * @param options - WebVTT formatting options\n * @returns Formatted WebVTT string\n */\nexport function srtToVTT(\n srtText: string,\n options?: StringifyVTTOptions\n): string {\n const { cues } = parseSRT(srtText);\n return stringifyVTT(cues, options);\n}\n\n/**\n * Converts raw WebVTT subtitle text directly into a standard SRT (.srt) string.\n *\n * @param vttText - Raw WebVTT (.vtt) text content\n * @returns Standard SubRip (.srt) string\n */\nexport function vttToSRT(vttText: string): string {\n const { cues } = parseVTT(vttText);\n return stringifySRT(cues);\n}\n","import type {\n SubtitleCue,\n LintDiagnostic,\n LintProfile,\n} from './types.js';\n\n/** Default quality profiles for subtitle verification */\nexport const DEFAULT_PROFILES: Record<string, LintProfile> = {\n standardLatin: {\n name: 'Standard Latin (Netflix/BBC Style Guide)',\n locale: 'en',\n maxCps: 20,\n maxCpl: 42,\n minDurationMs: 800,\n maxDurationMs: 7000,\n includeSpaces: true,\n },\n indicKannada: {\n name: 'Indic - Kannada Locale',\n locale: 'kn',\n maxCps: 15,\n maxCpl: 35,\n minDurationMs: 800,\n maxDurationMs: 7000,\n includeSpaces: true,\n },\n};\n\n/**\n * Strips HTML formatting tags from subtitle text before measuring character limits.\n * Handles <i>, <b>, <u>, <font color=\"...\">, etc.\n */\nexport function stripFormattingTags(text: string): string {\n return text.replace(/<[^>]*>/g, '');\n}\n\n/**\n * Counts user-perceived grapheme clusters using native ECMAScript Intl.Segmenter.\n * Accurately measures multi-byte conjuncts and vowel modifiers in Indic scripts (Kannada, Hindi),\n * as well as emojis and complex Unicode characters without UTF-16 code unit inflation.\n */\nexport function countGraphemes(text: string, locale: string = 'en'): number {\n if (!text) return 0;\n\n if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {\n const segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });\n let count = 0;\n // Iterate over segments efficiently without allocating full array\n for (const _ of segmenter.segment(text)) {\n count++;\n }\n return count;\n }\n\n // Fallback for environments lacking Intl.Segmenter\n return [...text].length;\n}\n\n/**\n * Inspects an array of SubtitleCues against timing, structural, and reading speed rules.\n *\n * Rules verified:\n * - out-of-order: Cues not in ascending chronological order\n * - zero-or-negative-duration: Cues where end <= start\n * - overlap: Cues overlapping chronologically with preceding cues\n * - min-duration: Cues flashing too fast for comprehension (< 800ms)\n * - max-duration: Cues remaining on screen too long (> 7000ms)\n * - max-cpl: Any line exceeding characters-per-line limit\n * - max-cps: Reading speed exceeding characters-per-second limit\n *\n * @param cues - Array of SubtitleCue objects to inspect\n * @param customProfile - Optional custom thresholds or language profile\n * @returns Array of LintDiagnostic reports\n */\nexport function lintCues(\n cues: readonly SubtitleCue[],\n customProfile?: Partial<LintProfile>\n): LintDiagnostic[] {\n const diagnostics: LintDiagnostic[] = [];\n if (!cues || cues.length === 0) return diagnostics;\n\n const profile: LintProfile = {\n ...DEFAULT_PROFILES['standardLatin']!,\n ...customProfile,\n };\n\n const {\n locale = 'en',\n maxCps,\n maxCpl,\n minDurationMs,\n maxDurationMs,\n includeSpaces = true,\n } = profile;\n\n for (let i = 0; i < cues.length; i++) {\n const cue = cues[i];\n if (!cue) continue;\n\n const prevCue = i > 0 ? cues[i - 1] : undefined;\n const duration = cue.end - cue.start;\n\n // --- TIMING RULES ---\n\n // 1. Out-of-order check\n if (prevCue && cue.start < prevCue.start) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'out-of-order',\n severity: 'error',\n message: `Cue starts at ${cue.start}ms before previous cue start of ${prevCue.start}ms.`,\n expected: `start >= ${prevCue.start}`,\n actual: `start = ${cue.start}`,\n });\n }\n\n // 2. Zero or negative duration check\n if (cue.end <= cue.start) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'zero-or-negative-duration',\n severity: 'error',\n message: `Cue has invalid duration (${duration}ms). End timestamp must be greater than start timestamp.`,\n expected: `end > ${cue.start}`,\n actual: `end = ${cue.end}`,\n });\n }\n\n // 3. Overlap check (only if not out-of-order)\n if (prevCue && cue.start < prevCue.end && cue.start >= prevCue.start) {\n const overlapMs = prevCue.end - cue.start;\n diagnostics.push({\n cueId: cue.id,\n rule: 'overlap',\n severity: 'error',\n message: `Cue overlaps with previous cue by ${overlapMs}ms.`,\n expected: `start >= ${prevCue.end}`,\n actual: `start = ${cue.start}`,\n });\n }\n\n // 4. Min duration check (only if duration is positive)\n if (duration > 0 && duration < minDurationMs) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'min-duration',\n severity: 'warning',\n message: `Cue duration of ${duration}ms is below the minimum threshold of ${minDurationMs}ms.`,\n expected: minDurationMs,\n actual: duration,\n });\n }\n\n // 5. Max duration check\n if (duration > maxDurationMs) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-duration',\n severity: 'warning',\n message: `Cue duration of ${duration}ms exceeds the maximum threshold of ${maxDurationMs}ms.`,\n expected: maxDurationMs,\n actual: duration,\n });\n }\n\n // --- TEXT & PERCEPTION RULES ---\n\n const rawText = cue.text || '';\n const cleanText = stripFormattingTags(rawText);\n\n // 6. Characters Per Line (CPL) check\n const lines = cleanText.split('\\n');\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex];\n if (!line) continue;\n\n const cpl = countGraphemes(line, locale);\n if (cpl > maxCpl) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-cpl',\n severity: 'warning',\n message: `Line ${lineIndex + 1} has ${cpl} characters, exceeding the maximum limit of ${maxCpl} CPL.`,\n expected: maxCpl,\n actual: cpl,\n });\n }\n }\n\n // 7. Characters Per Second (CPS) reading speed check\n if (duration > 0) {\n const textToMeasure = includeSpaces\n ? cleanText\n : cleanText.replace(/\\s+/g, '');\n\n const totalGraphemes = countGraphemes(textToMeasure, locale);\n const cps = Number(((totalGraphemes / duration) * 1000).toFixed(1));\n\n if (cps > maxCps) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-cps',\n severity: 'warning',\n message: `Reading speed of ${cps} CPS exceeds maximum limit of ${maxCps} CPS.`,\n expected: maxCps,\n actual: cps,\n });\n }\n }\n }\n\n return diagnostics;\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -11,6 +11,8 @@ interface SubtitleCue {
|
|
|
11
11
|
end: number;
|
|
12
12
|
/** Raw subtitle text content (may contain newlines) */
|
|
13
13
|
text: string;
|
|
14
|
+
/** Optional WebVTT cue placement/styling settings (e.g., "align:middle line:90%") */
|
|
15
|
+
settings?: string;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Calculates the duration of a cue in milliseconds.
|
|
@@ -146,6 +148,52 @@ declare function parseSRT(rawText: string): ParseResult;
|
|
|
146
148
|
*/
|
|
147
149
|
declare function stringifySRT(cues: readonly SubtitleCue[]): string;
|
|
148
150
|
|
|
151
|
+
interface StringifyVTTOptions {
|
|
152
|
+
/** Optional custom header line after WEBVTT (e.g. "WEBVTT - Commentary Track") */
|
|
153
|
+
header?: string;
|
|
154
|
+
/** Whether to output cue numeric ID headers (default: false for standard clean WebVTT) */
|
|
155
|
+
includeIds?: boolean;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Parses raw WebVTT (.vtt) subtitle text into structured SubtitleCue objects.
|
|
159
|
+
*
|
|
160
|
+
* Handles:
|
|
161
|
+
* - UTF-8 Byte Order Marks (BOM \uFEFF)
|
|
162
|
+
* - WEBVTT header and metadata blocks
|
|
163
|
+
* - NOTE comment blocks (skipped automatically)
|
|
164
|
+
* - Dot-separated milliseconds (00:01:20.500) and comma-separated fallback
|
|
165
|
+
* - Short timecodes without hours (MM:SS.mmm)
|
|
166
|
+
* - Cue placement/styling settings (e.g. align:middle line:90%)
|
|
167
|
+
* - Optional cue identifiers
|
|
168
|
+
*
|
|
169
|
+
* @param rawText - Raw WebVTT file content as a string
|
|
170
|
+
* @returns {ParseResult} Successfully parsed cues and any block errors
|
|
171
|
+
*/
|
|
172
|
+
declare function parseVTT(rawText: string): ParseResult;
|
|
173
|
+
/**
|
|
174
|
+
* Serializes an array of SubtitleCue objects into standard WebVTT (.vtt) file format.
|
|
175
|
+
*
|
|
176
|
+
* @param cues - Array of SubtitleCue objects to format
|
|
177
|
+
* @param options - Formatting options (header title, inclusion of cue IDs)
|
|
178
|
+
* @returns Formatted WebVTT string
|
|
179
|
+
*/
|
|
180
|
+
declare function stringifyVTT(cues: readonly SubtitleCue[], options?: StringifyVTTOptions): string;
|
|
181
|
+
/**
|
|
182
|
+
* Converts raw SRT subtitle text directly into a valid WebVTT (.vtt) string.
|
|
183
|
+
*
|
|
184
|
+
* @param srtText - Raw SubRip (.srt) text content
|
|
185
|
+
* @param options - WebVTT formatting options
|
|
186
|
+
* @returns Formatted WebVTT string
|
|
187
|
+
*/
|
|
188
|
+
declare function srtToVTT(srtText: string, options?: StringifyVTTOptions): string;
|
|
189
|
+
/**
|
|
190
|
+
* Converts raw WebVTT subtitle text directly into a standard SRT (.srt) string.
|
|
191
|
+
*
|
|
192
|
+
* @param vttText - Raw WebVTT (.vtt) text content
|
|
193
|
+
* @returns Standard SubRip (.srt) string
|
|
194
|
+
*/
|
|
195
|
+
declare function vttToSRT(vttText: string): string;
|
|
196
|
+
|
|
149
197
|
/** Default quality profiles for subtitle verification */
|
|
150
198
|
declare const DEFAULT_PROFILES: Record<string, LintProfile>;
|
|
151
199
|
/**
|
|
@@ -177,4 +225,4 @@ declare function countGraphemes(text: string, locale?: string): number;
|
|
|
177
225
|
*/
|
|
178
226
|
declare function lintCues(cues: readonly SubtitleCue[], customProfile?: Partial<LintProfile>): LintDiagnostic[];
|
|
179
227
|
|
|
180
|
-
export { DEFAULT_PROFILES, type LintDiagnostic, type LintProfile, type LintRule, type LintSeverity, type ParseError, type ParseResult, type SubtitleCue, type TimecodeFormat, countGraphemes, getCueDuration, lintCues, msToTimecode, parseSRT, retime, shiftTimecodes, stringifySRT, stripFormattingTags, timecodeToMs };
|
|
228
|
+
export { DEFAULT_PROFILES, type LintDiagnostic, type LintProfile, type LintRule, type LintSeverity, type ParseError, type ParseResult, type StringifyVTTOptions, type SubtitleCue, type TimecodeFormat, countGraphemes, getCueDuration, lintCues, msToTimecode, parseSRT, parseVTT, retime, shiftTimecodes, srtToVTT, stringifySRT, stringifyVTT, stripFormattingTags, timecodeToMs, vttToSRT };
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ interface SubtitleCue {
|
|
|
11
11
|
end: number;
|
|
12
12
|
/** Raw subtitle text content (may contain newlines) */
|
|
13
13
|
text: string;
|
|
14
|
+
/** Optional WebVTT cue placement/styling settings (e.g., "align:middle line:90%") */
|
|
15
|
+
settings?: string;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Calculates the duration of a cue in milliseconds.
|
|
@@ -146,6 +148,52 @@ declare function parseSRT(rawText: string): ParseResult;
|
|
|
146
148
|
*/
|
|
147
149
|
declare function stringifySRT(cues: readonly SubtitleCue[]): string;
|
|
148
150
|
|
|
151
|
+
interface StringifyVTTOptions {
|
|
152
|
+
/** Optional custom header line after WEBVTT (e.g. "WEBVTT - Commentary Track") */
|
|
153
|
+
header?: string;
|
|
154
|
+
/** Whether to output cue numeric ID headers (default: false for standard clean WebVTT) */
|
|
155
|
+
includeIds?: boolean;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Parses raw WebVTT (.vtt) subtitle text into structured SubtitleCue objects.
|
|
159
|
+
*
|
|
160
|
+
* Handles:
|
|
161
|
+
* - UTF-8 Byte Order Marks (BOM \uFEFF)
|
|
162
|
+
* - WEBVTT header and metadata blocks
|
|
163
|
+
* - NOTE comment blocks (skipped automatically)
|
|
164
|
+
* - Dot-separated milliseconds (00:01:20.500) and comma-separated fallback
|
|
165
|
+
* - Short timecodes without hours (MM:SS.mmm)
|
|
166
|
+
* - Cue placement/styling settings (e.g. align:middle line:90%)
|
|
167
|
+
* - Optional cue identifiers
|
|
168
|
+
*
|
|
169
|
+
* @param rawText - Raw WebVTT file content as a string
|
|
170
|
+
* @returns {ParseResult} Successfully parsed cues and any block errors
|
|
171
|
+
*/
|
|
172
|
+
declare function parseVTT(rawText: string): ParseResult;
|
|
173
|
+
/**
|
|
174
|
+
* Serializes an array of SubtitleCue objects into standard WebVTT (.vtt) file format.
|
|
175
|
+
*
|
|
176
|
+
* @param cues - Array of SubtitleCue objects to format
|
|
177
|
+
* @param options - Formatting options (header title, inclusion of cue IDs)
|
|
178
|
+
* @returns Formatted WebVTT string
|
|
179
|
+
*/
|
|
180
|
+
declare function stringifyVTT(cues: readonly SubtitleCue[], options?: StringifyVTTOptions): string;
|
|
181
|
+
/**
|
|
182
|
+
* Converts raw SRT subtitle text directly into a valid WebVTT (.vtt) string.
|
|
183
|
+
*
|
|
184
|
+
* @param srtText - Raw SubRip (.srt) text content
|
|
185
|
+
* @param options - WebVTT formatting options
|
|
186
|
+
* @returns Formatted WebVTT string
|
|
187
|
+
*/
|
|
188
|
+
declare function srtToVTT(srtText: string, options?: StringifyVTTOptions): string;
|
|
189
|
+
/**
|
|
190
|
+
* Converts raw WebVTT subtitle text directly into a standard SRT (.srt) string.
|
|
191
|
+
*
|
|
192
|
+
* @param vttText - Raw WebVTT (.vtt) text content
|
|
193
|
+
* @returns Standard SubRip (.srt) string
|
|
194
|
+
*/
|
|
195
|
+
declare function vttToSRT(vttText: string): string;
|
|
196
|
+
|
|
149
197
|
/** Default quality profiles for subtitle verification */
|
|
150
198
|
declare const DEFAULT_PROFILES: Record<string, LintProfile>;
|
|
151
199
|
/**
|
|
@@ -177,4 +225,4 @@ declare function countGraphemes(text: string, locale?: string): number;
|
|
|
177
225
|
*/
|
|
178
226
|
declare function lintCues(cues: readonly SubtitleCue[], customProfile?: Partial<LintProfile>): LintDiagnostic[];
|
|
179
227
|
|
|
180
|
-
export { DEFAULT_PROFILES, type LintDiagnostic, type LintProfile, type LintRule, type LintSeverity, type ParseError, type ParseResult, type SubtitleCue, type TimecodeFormat, countGraphemes, getCueDuration, lintCues, msToTimecode, parseSRT, retime, shiftTimecodes, stringifySRT, stripFormattingTags, timecodeToMs };
|
|
228
|
+
export { DEFAULT_PROFILES, type LintDiagnostic, type LintProfile, type LintRule, type LintSeverity, type ParseError, type ParseResult, type StringifyVTTOptions, type SubtitleCue, type TimecodeFormat, countGraphemes, getCueDuration, lintCues, msToTimecode, parseSRT, parseVTT, retime, shiftTimecodes, srtToVTT, stringifySRT, stringifyVTT, stripFormattingTags, timecodeToMs, vttToSRT };
|
package/dist/index.js
CHANGED
|
@@ -131,6 +131,127 @@ ${text}`;
|
|
|
131
131
|
return blocks.join("\n\n") + "\n";
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
+
// src/vtt.ts
|
|
135
|
+
var VTT_ARROW_REGEX = /((?:\d{1,2}:)?\d{2}:\d{2}[,.]\d{3})\s*-->\s*((?:\d{1,2}:)?\d{2}:\d{2}[,.]\d{3})(?:[ \t]+([^\r\n]+))?/;
|
|
136
|
+
function parseVTT(rawText) {
|
|
137
|
+
const cues = [];
|
|
138
|
+
const errors = [];
|
|
139
|
+
if (!rawText || !rawText.trim()) {
|
|
140
|
+
return { cues, errors };
|
|
141
|
+
}
|
|
142
|
+
const clean = rawText.replace(/^\uFEFF/, "");
|
|
143
|
+
const normalized = clean.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
144
|
+
if (!normalized.startsWith("WEBVTT")) {
|
|
145
|
+
errors.push({
|
|
146
|
+
rawBlock: normalized.slice(0, 50),
|
|
147
|
+
message: 'Missing standard "WEBVTT" signature at the beginning of the file.'
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
const rawBlocks = normalized.trim().split(/\n\s*\n+/);
|
|
151
|
+
let fallbackIndex = 1;
|
|
152
|
+
for (let bIndex = 0; bIndex < rawBlocks.length; bIndex++) {
|
|
153
|
+
const rawBlock = rawBlocks[bIndex]?.trim();
|
|
154
|
+
if (!rawBlock) continue;
|
|
155
|
+
if (bIndex === 0 && rawBlock.startsWith("WEBVTT")) {
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (rawBlock.startsWith("NOTE")) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
if (rawBlock.startsWith("STYLE") || rawBlock.startsWith("REGION")) {
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
const lines = rawBlock.split("\n");
|
|
165
|
+
let id;
|
|
166
|
+
let timecodeLine;
|
|
167
|
+
let textLines;
|
|
168
|
+
if (lines[0] && VTT_ARROW_REGEX.test(lines[0])) {
|
|
169
|
+
id = String(fallbackIndex);
|
|
170
|
+
timecodeLine = lines[0];
|
|
171
|
+
textLines = lines.slice(1);
|
|
172
|
+
} else if (lines[1] && VTT_ARROW_REGEX.test(lines[1])) {
|
|
173
|
+
id = lines[0]?.trim() || String(fallbackIndex);
|
|
174
|
+
timecodeLine = lines[1];
|
|
175
|
+
textLines = lines.slice(2);
|
|
176
|
+
} else {
|
|
177
|
+
errors.push({
|
|
178
|
+
index: fallbackIndex,
|
|
179
|
+
rawBlock,
|
|
180
|
+
message: 'Invalid WebVTT cue block. Missing timecode arrow "-->".'
|
|
181
|
+
});
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
const match = timecodeLine.match(VTT_ARROW_REGEX);
|
|
185
|
+
if (!match || !match[1] || !match[2]) {
|
|
186
|
+
errors.push({
|
|
187
|
+
index: fallbackIndex,
|
|
188
|
+
rawBlock,
|
|
189
|
+
message: `Failed to parse timestamps from WebVTT cue: "${timecodeLine}".`
|
|
190
|
+
});
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
const start = timecodeToMs(match[1]);
|
|
195
|
+
const end = timecodeToMs(match[2]);
|
|
196
|
+
const settings = match[3]?.trim();
|
|
197
|
+
const text = textLines.join("\n").trim();
|
|
198
|
+
const cue = {
|
|
199
|
+
id,
|
|
200
|
+
start,
|
|
201
|
+
end,
|
|
202
|
+
text
|
|
203
|
+
};
|
|
204
|
+
if (settings) {
|
|
205
|
+
cue.settings = settings;
|
|
206
|
+
}
|
|
207
|
+
cues.push(cue);
|
|
208
|
+
fallbackIndex++;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
errors.push({
|
|
211
|
+
index: fallbackIndex,
|
|
212
|
+
rawBlock,
|
|
213
|
+
message: err instanceof Error ? err.message : String(err)
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return { cues, errors };
|
|
218
|
+
}
|
|
219
|
+
function stringifyVTT(cues, options) {
|
|
220
|
+
const header = options?.header ? `WEBVTT - ${options.header}` : "WEBVTT";
|
|
221
|
+
if (!cues || cues.length === 0) {
|
|
222
|
+
return `${header}
|
|
223
|
+
`;
|
|
224
|
+
}
|
|
225
|
+
const includeIds = options?.includeIds ?? false;
|
|
226
|
+
const blocks = cues.map((cue, idx) => {
|
|
227
|
+
const id = cue.id ? String(cue.id) : String(idx + 1);
|
|
228
|
+
const startStr = msToTimecode(cue.start, "vtt");
|
|
229
|
+
const endStr = msToTimecode(cue.end, "vtt");
|
|
230
|
+
const settingsStr = cue.settings ? ` ${cue.settings}` : "";
|
|
231
|
+
const timeLine = `${startStr} --> ${endStr}${settingsStr}`;
|
|
232
|
+
const text = cue.text ?? "";
|
|
233
|
+
if (includeIds) {
|
|
234
|
+
return `${id}
|
|
235
|
+
${timeLine}
|
|
236
|
+
${text}`;
|
|
237
|
+
}
|
|
238
|
+
return `${timeLine}
|
|
239
|
+
${text}`;
|
|
240
|
+
});
|
|
241
|
+
return `${header}
|
|
242
|
+
|
|
243
|
+
${blocks.join("\n\n")}
|
|
244
|
+
`;
|
|
245
|
+
}
|
|
246
|
+
function srtToVTT(srtText, options) {
|
|
247
|
+
const { cues } = parseSRT(srtText);
|
|
248
|
+
return stringifyVTT(cues, options);
|
|
249
|
+
}
|
|
250
|
+
function vttToSRT(vttText) {
|
|
251
|
+
const { cues } = parseVTT(vttText);
|
|
252
|
+
return stringifySRT(cues);
|
|
253
|
+
}
|
|
254
|
+
|
|
134
255
|
// src/linter.ts
|
|
135
256
|
var DEFAULT_PROFILES = {
|
|
136
257
|
standardLatin: {
|
|
@@ -275,6 +396,6 @@ function lintCues(cues, customProfile) {
|
|
|
275
396
|
return diagnostics;
|
|
276
397
|
}
|
|
277
398
|
|
|
278
|
-
export { DEFAULT_PROFILES, countGraphemes, getCueDuration, lintCues, msToTimecode, parseSRT, retime, shiftTimecodes, stringifySRT, stripFormattingTags, timecodeToMs };
|
|
399
|
+
export { DEFAULT_PROFILES, countGraphemes, getCueDuration, lintCues, msToTimecode, parseSRT, parseVTT, retime, shiftTimecodes, srtToVTT, stringifySRT, stringifyVTT, stripFormattingTags, timecodeToMs, vttToSRT };
|
|
279
400
|
//# sourceMappingURL=index.js.map
|
|
280
401
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/types.ts","../src/timecode.ts","../src/parser.ts","../src/serializer.ts","../src/linter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Core normalized representation of a single subtitle cue.\n * Timestamps are strictly stored in integer milliseconds.\n */\nexport interface SubtitleCue {\n /** Unique sequential identifier (e.g. \"1\", \"2\") */\n id: string;\n /** Start time in integer milliseconds */\n start: number;\n /** End time in integer milliseconds */\n end: number;\n /** Raw subtitle text content (may contain newlines) */\n text: string;\n}\n\n/**\n * Calculates the duration of a cue in milliseconds.\n * Derived helper function to keep the core data model minimal and immutable.\n */\nexport const getCueDuration = (cue: SubtitleCue): number => cue.end - cue.start;\n\n/** Supported subtitle file formats */\nexport type TimecodeFormat = 'srt' | 'vtt';\n\n/**\n * Detailed error reported when a cue block fails parsing syntax rules.\n */\nexport interface ParseError {\n index?: number;\n rawBlock: string;\n message: string;\n}\n\n/**\n * Resilient parser result returning successfully parsed cues\n * alongside any syntax errors encountered without throwing.\n */\nexport interface ParseResult {\n cues: SubtitleCue[];\n errors: ParseError[];\n}\n\n/** Severity levels for lint diagnostics */\nexport type LintSeverity = 'error' | 'warning';\n\n/** Quality rules enforced by the linter */\nexport type LintRule =\n | 'out-of-order'\n | 'overlap'\n | 'zero-or-negative-duration'\n | 'min-duration'\n | 'max-duration'\n | 'max-cpl'\n | 'max-cps';\n\n/**\n * Diagnostic report produced when a cue violates a quality rule.\n */\nexport interface LintDiagnostic {\n cueId: string;\n rule: LintRule;\n severity: LintSeverity;\n message: string;\n expected?: number | string;\n actual?: number | string;\n}\n\n/**\n * Configuration options for locale-aware subtitle validation.\n */\nexport interface LintProfile {\n name: string;\n locale?: string;\n maxCps: number;\n maxCpl: number;\n minDurationMs: number;\n maxDurationMs: number;\n includeSpaces?: boolean;\n}\n","import type { SubtitleCue, TimecodeFormat } from './types.js';\n\n/**\n * Regular expression to match both SRT (comma) and WebVTT (dot) timecodes.\n * Supports:\n * - HH:MM:SS,mmm or HH:MM:SS.mmm\n * - MM:SS.mmm (WebVTT short format)\n */\nconst TIMECODE_REGEX = /^(?:(?:(\\d{1,2}):)?(\\d{2}):)?(\\d{2})[,.](\\d{3})$/;\n\n/**\n * Converts a formatted timecode string into integer milliseconds.\n *\n * Supported formats:\n * - \"01:23:45,678\" (SRT standard)\n * - \"01:23:45.678\" (WebVTT standard)\n * - \"23:45.678\" (WebVTT short form without hours)\n *\n * @throws {Error} if timecode format is invalid\n */\nexport function timecodeToMs(timecode: string): number {\n const trimmed = timecode.trim();\n const match = trimmed.match(TIMECODE_REGEX);\n\n if (!match) {\n throw new Error(\n `Invalid timecode format: \"${timecode}\". Expected \"HH:MM:SS,mmm\" or \"HH:MM:SS.mmm\".`\n );\n }\n\n const hours = match[1] ? parseInt(match[1], 10) : 0;\n const minutes = match[2] ? parseInt(match[2], 10) : 0;\n const seconds = parseInt(match[3] ?? '0', 10);\n const milliseconds = parseInt(match[4] ?? '0', 10);\n\n return (\n hours * 3_600_000 +\n minutes * 60_000 +\n seconds * 1_000 +\n milliseconds\n );\n}\n\n/**\n * Converts integer milliseconds into a standard formatted timecode string.\n *\n * @param ms - Non-negative integer milliseconds (negative values clamped to 0)\n * @param format - 'srt' (uses comma delimiter) or 'vtt' (uses dot delimiter)\n *\n * @example\n * msToTimecode(83456, 'srt') // \"00:01:23,456\"\n * msToTimecode(83456, 'vtt') // \"00:01:23.456\"\n */\nexport function msToTimecode(ms: number, format: TimecodeFormat = 'srt'): string {\n const safeMs = Math.max(0, Math.floor(ms));\n\n const hours = Math.floor(safeMs / 3_600_000);\n const minutes = Math.floor((safeMs % 3_600_000) / 60_000);\n const seconds = Math.floor((safeMs % 60_000) / 1_000);\n const remainderMs = safeMs % 1_000;\n\n const hh = String(hours).padStart(2, '0');\n const mm = String(minutes).padStart(2, '0');\n const ss = String(seconds).padStart(2, '0');\n const mmm = String(remainderMs).padStart(3, '0');\n\n const delimiter = format === 'vtt' ? '.' : ',';\n return `${hh}:${mm}:${ss}${delimiter}${mmm}`;\n}\n\n/**\n * Linearly shifts timecodes by a constant millisecond offset.\n * Used to correct audio-sync delay or advance.\n * Clamps negative results to 0.\n *\n * @param cues - Array of SubtitleCue objects\n * @param offsetMs - Offset in milliseconds (positive advances, negative delays)\n * @returns A new immutable array of shifted cues\n */\nexport function shiftTimecodes(\n cues: readonly SubtitleCue[],\n offsetMs: number\n): SubtitleCue[] {\n return cues.map((cue) => ({\n ...cue,\n start: Math.max(0, cue.start + offsetMs),\n end: Math.max(0, cue.end + offsetMs),\n }));\n}\n\n/**\n * Scales all cue timecodes by a multiplicative ratio.\n * Used for frame-rate drift compensation between video standards.\n * All results are strictly rounded to integer milliseconds with Math.round.\n *\n * Direction definition:\n * targetTimestamp = Math.round(sourceTimestamp * ratio)\n *\n * @example\n * // 24 fps to 25 fps (PAL speed-up): video is ~4% faster, timecodes compress\n * const palCues = retime(filmCues, 24 / 25); // ratio = 0.96\n *\n * @example\n * // 25 fps to 24 fps (Slow-down): video is ~4% slower, timecodes stretch\n * const filmCues = retime(palCues, 25 / 24); // ratio = 1.04167\n *\n * @param cues - Array of SubtitleCue objects\n * @param ratio - Positive scaling multiplier (e.g., 24/25 or 25/24)\n * @returns A new immutable array of retimed cues\n */\nexport function retime(\n cues: readonly SubtitleCue[],\n ratio: number\n): SubtitleCue[] {\n if (ratio <= 0) {\n throw new Error(`Retime ratio must be greater than 0. Received: ${ratio}`);\n }\n\n return cues.map((cue) => ({\n ...cue,\n start: Math.round(cue.start * ratio),\n end: Math.round(cue.end * ratio),\n }));\n}\n","import { timecodeToMs } from './timecode.js';\nimport type { SubtitleCue, ParseError, ParseResult } from './types.js';\n\n/** Matches the standard SRT timestamp line: 00:00:00,000 --> 00:00:00,000 */\nconst ARROW_REGEX =\n /((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})\\s*-->\\s*((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})/;\n\n/**\n * Parses raw SRT subtitle text into structured SubtitleCue objects.\n * Resilient parser that collects syntax errors without throwing unhandled exceptions.\n *\n * Handles:\n * - UTF-8 Byte Order Marks (BOM `\\uFEFF`)\n * - Windows (CRLF), Unix (LF), and legacy Mac (CR) line endings\n * - Optional or missing cue numeric index headers\n * - Multi-line subtitle texts\n * - Trailing and irregular blank lines\n *\n * @param rawText - Raw SRT file content as a string\n * @returns {ParseResult} Object containing successfully parsed cues and any block errors\n */\nexport function parseSRT(rawText: string): ParseResult {\n const cues: SubtitleCue[] = [];\n const errors: ParseError[] = [];\n\n if (!rawText || !rawText.trim()) {\n return { cues, errors };\n }\n\n // 1. Strip UTF-8 Byte Order Mark (BOM)\n const cleanText = rawText.replace(/^\\uFEFF/, '');\n\n // 2. Normalize CRLF and CR to standard Unix LF\n const normalized = cleanText.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n // 3. Split by 2 or more consecutive newlines into cue blocks\n const rawBlocks = normalized.trim().split(/\\n\\s*\\n+/);\n\n let fallbackIndex = 1;\n\n for (let bIndex = 0; bIndex < rawBlocks.length; bIndex++) {\n const rawBlock = rawBlocks[bIndex]?.trim();\n if (!rawBlock) continue;\n\n const lines = rawBlock.split('\\n');\n\n let id: string;\n let timecodeLine: string;\n let textLines: string[];\n\n // Check if line 0 is the timecode arrow (missing numeric index)\n if (lines[0] && ARROW_REGEX.test(lines[0])) {\n id = String(fallbackIndex);\n timecodeLine = lines[0];\n textLines = lines.slice(1);\n } else if (lines[1] && ARROW_REGEX.test(lines[1])) {\n // Standard SRT: line 0 is ID, line 1 is timecode\n id = lines[0]?.trim() || String(fallbackIndex);\n timecodeLine = lines[1];\n textLines = lines.slice(2);\n } else {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message:\n 'Invalid block format. Expected timecode arrow \"-->\" on line 1 or 2.',\n });\n continue;\n }\n\n const match = timecodeLine.match(ARROW_REGEX);\n if (!match || !match[1] || !match[2]) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: `Failed to extract start and end timestamps from: \"${timecodeLine}\".`,\n });\n continue;\n }\n\n try {\n const start = timecodeToMs(match[1]);\n const end = timecodeToMs(match[2]);\n const text = textLines.join('\\n').trim();\n\n cues.push({\n id,\n start,\n end,\n text,\n });\n\n fallbackIndex++;\n } catch (err) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { cues, errors };\n}\n","import { msToTimecode } from './timecode.js';\nimport type { SubtitleCue } from './types.js';\n\n/**\n * Serializes an array of SubtitleCue objects into standard SRT file format.\n *\n * Output format:\n * 1\n * 00:00:20,000 --> 00:00:24,000\n * Subtitle text\n *\n * @param cues - Array of SubtitleCue objects to format\n * @returns Standard formatted SRT string\n */\nexport function stringifySRT(cues: readonly SubtitleCue[]): string {\n if (!cues || cues.length === 0) {\n return '';\n }\n\n const blocks = cues.map((cue, idx) => {\n const id = cue.id ? String(cue.id) : String(idx + 1);\n const startStr = msToTimecode(cue.start, 'srt');\n const endStr = msToTimecode(cue.end, 'srt');\n const text = cue.text ?? '';\n\n return `${id}\\n${startStr} --> ${endStr}\\n${text}`;\n });\n\n return blocks.join('\\n\\n') + '\\n';\n}\n","import type {\n SubtitleCue,\n LintDiagnostic,\n LintProfile,\n} from './types.js';\n\n/** Default quality profiles for subtitle verification */\nexport const DEFAULT_PROFILES: Record<string, LintProfile> = {\n standardLatin: {\n name: 'Standard Latin (Netflix/BBC Style Guide)',\n locale: 'en',\n maxCps: 20,\n maxCpl: 42,\n minDurationMs: 800,\n maxDurationMs: 7000,\n includeSpaces: true,\n },\n indicKannada: {\n name: 'Indic - Kannada Locale',\n locale: 'kn',\n maxCps: 15,\n maxCpl: 35,\n minDurationMs: 800,\n maxDurationMs: 7000,\n includeSpaces: true,\n },\n};\n\n/**\n * Strips HTML formatting tags from subtitle text before measuring character limits.\n * Handles <i>, <b>, <u>, <font color=\"...\">, etc.\n */\nexport function stripFormattingTags(text: string): string {\n return text.replace(/<[^>]*>/g, '');\n}\n\n/**\n * Counts user-perceived grapheme clusters using native ECMAScript Intl.Segmenter.\n * Accurately measures multi-byte conjuncts and vowel modifiers in Indic scripts (Kannada, Hindi),\n * as well as emojis and complex Unicode characters without UTF-16 code unit inflation.\n */\nexport function countGraphemes(text: string, locale: string = 'en'): number {\n if (!text) return 0;\n\n if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {\n const segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });\n let count = 0;\n // Iterate over segments efficiently without allocating full array\n for (const _ of segmenter.segment(text)) {\n count++;\n }\n return count;\n }\n\n // Fallback for environments lacking Intl.Segmenter\n return [...text].length;\n}\n\n/**\n * Inspects an array of SubtitleCues against timing, structural, and reading speed rules.\n *\n * Rules verified:\n * - out-of-order: Cues not in ascending chronological order\n * - zero-or-negative-duration: Cues where end <= start\n * - overlap: Cues overlapping chronologically with preceding cues\n * - min-duration: Cues flashing too fast for comprehension (< 800ms)\n * - max-duration: Cues remaining on screen too long (> 7000ms)\n * - max-cpl: Any line exceeding characters-per-line limit\n * - max-cps: Reading speed exceeding characters-per-second limit\n *\n * @param cues - Array of SubtitleCue objects to inspect\n * @param customProfile - Optional custom thresholds or language profile\n * @returns Array of LintDiagnostic reports\n */\nexport function lintCues(\n cues: readonly SubtitleCue[],\n customProfile?: Partial<LintProfile>\n): LintDiagnostic[] {\n const diagnostics: LintDiagnostic[] = [];\n if (!cues || cues.length === 0) return diagnostics;\n\n const profile: LintProfile = {\n ...DEFAULT_PROFILES['standardLatin']!,\n ...customProfile,\n };\n\n const {\n locale = 'en',\n maxCps,\n maxCpl,\n minDurationMs,\n maxDurationMs,\n includeSpaces = true,\n } = profile;\n\n for (let i = 0; i < cues.length; i++) {\n const cue = cues[i];\n if (!cue) continue;\n\n const prevCue = i > 0 ? cues[i - 1] : undefined;\n const duration = cue.end - cue.start;\n\n // --- TIMING RULES ---\n\n // 1. Out-of-order check\n if (prevCue && cue.start < prevCue.start) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'out-of-order',\n severity: 'error',\n message: `Cue starts at ${cue.start}ms before previous cue start of ${prevCue.start}ms.`,\n expected: `start >= ${prevCue.start}`,\n actual: `start = ${cue.start}`,\n });\n }\n\n // 2. Zero or negative duration check\n if (cue.end <= cue.start) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'zero-or-negative-duration',\n severity: 'error',\n message: `Cue has invalid duration (${duration}ms). End timestamp must be greater than start timestamp.`,\n expected: `end > ${cue.start}`,\n actual: `end = ${cue.end}`,\n });\n }\n\n // 3. Overlap check (only if not out-of-order)\n if (prevCue && cue.start < prevCue.end && cue.start >= prevCue.start) {\n const overlapMs = prevCue.end - cue.start;\n diagnostics.push({\n cueId: cue.id,\n rule: 'overlap',\n severity: 'error',\n message: `Cue overlaps with previous cue by ${overlapMs}ms.`,\n expected: `start >= ${prevCue.end}`,\n actual: `start = ${cue.start}`,\n });\n }\n\n // 4. Min duration check (only if duration is positive)\n if (duration > 0 && duration < minDurationMs) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'min-duration',\n severity: 'warning',\n message: `Cue duration of ${duration}ms is below the minimum threshold of ${minDurationMs}ms.`,\n expected: minDurationMs,\n actual: duration,\n });\n }\n\n // 5. Max duration check\n if (duration > maxDurationMs) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-duration',\n severity: 'warning',\n message: `Cue duration of ${duration}ms exceeds the maximum threshold of ${maxDurationMs}ms.`,\n expected: maxDurationMs,\n actual: duration,\n });\n }\n\n // --- TEXT & PERCEPTION RULES ---\n\n const rawText = cue.text || '';\n const cleanText = stripFormattingTags(rawText);\n\n // 6. Characters Per Line (CPL) check\n const lines = cleanText.split('\\n');\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex];\n if (!line) continue;\n\n const cpl = countGraphemes(line, locale);\n if (cpl > maxCpl) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-cpl',\n severity: 'warning',\n message: `Line ${lineIndex + 1} has ${cpl} characters, exceeding the maximum limit of ${maxCpl} CPL.`,\n expected: maxCpl,\n actual: cpl,\n });\n }\n }\n\n // 7. Characters Per Second (CPS) reading speed check\n if (duration > 0) {\n const textToMeasure = includeSpaces\n ? cleanText\n : cleanText.replace(/\\s+/g, '');\n\n const totalGraphemes = countGraphemes(textToMeasure, locale);\n const cps = Number(((totalGraphemes / duration) * 1000).toFixed(1));\n\n if (cps > maxCps) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-cps',\n severity: 'warning',\n message: `Reading speed of ${cps} CPS exceeds maximum limit of ${maxCps} CPS.`,\n expected: maxCps,\n actual: cps,\n });\n }\n }\n }\n\n return diagnostics;\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/types.ts","../src/timecode.ts","../src/parser.ts","../src/serializer.ts","../src/vtt.ts","../src/linter.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Core normalized representation of a single subtitle cue.\n * Timestamps are strictly stored in integer milliseconds.\n */\nexport interface SubtitleCue {\n /** Unique sequential identifier (e.g. \"1\", \"2\") */\n id: string;\n /** Start time in integer milliseconds */\n start: number;\n /** End time in integer milliseconds */\n end: number;\n /** Raw subtitle text content (may contain newlines) */\n text: string;\n /** Optional WebVTT cue placement/styling settings (e.g., \"align:middle line:90%\") */\n settings?: string;\n}\n\n/**\n * Calculates the duration of a cue in milliseconds.\n * Derived helper function to keep the core data model minimal and immutable.\n */\nexport const getCueDuration = (cue: SubtitleCue): number => cue.end - cue.start;\n\n/** Supported subtitle file formats */\nexport type TimecodeFormat = 'srt' | 'vtt';\n\n/**\n * Detailed error reported when a cue block fails parsing syntax rules.\n */\nexport interface ParseError {\n index?: number;\n rawBlock: string;\n message: string;\n}\n\n/**\n * Resilient parser result returning successfully parsed cues\n * alongside any syntax errors encountered without throwing.\n */\nexport interface ParseResult {\n cues: SubtitleCue[];\n errors: ParseError[];\n}\n\n/** Severity levels for lint diagnostics */\nexport type LintSeverity = 'error' | 'warning';\n\n/** Quality rules enforced by the linter */\nexport type LintRule =\n | 'out-of-order'\n | 'overlap'\n | 'zero-or-negative-duration'\n | 'min-duration'\n | 'max-duration'\n | 'max-cpl'\n | 'max-cps';\n\n/**\n * Diagnostic report produced when a cue violates a quality rule.\n */\nexport interface LintDiagnostic {\n cueId: string;\n rule: LintRule;\n severity: LintSeverity;\n message: string;\n expected?: number | string;\n actual?: number | string;\n}\n\n/**\n * Configuration options for locale-aware subtitle validation.\n */\nexport interface LintProfile {\n name: string;\n locale?: string;\n maxCps: number;\n maxCpl: number;\n minDurationMs: number;\n maxDurationMs: number;\n includeSpaces?: boolean;\n}\n","import type { SubtitleCue, TimecodeFormat } from './types.js';\n\n/**\n * Regular expression to match both SRT (comma) and WebVTT (dot) timecodes.\n * Supports:\n * - HH:MM:SS,mmm or HH:MM:SS.mmm\n * - MM:SS.mmm (WebVTT short format)\n */\nconst TIMECODE_REGEX = /^(?:(?:(\\d{1,2}):)?(\\d{2}):)?(\\d{2})[,.](\\d{3})$/;\n\n/**\n * Converts a formatted timecode string into integer milliseconds.\n *\n * Supported formats:\n * - \"01:23:45,678\" (SRT standard)\n * - \"01:23:45.678\" (WebVTT standard)\n * - \"23:45.678\" (WebVTT short form without hours)\n *\n * @throws {Error} if timecode format is invalid\n */\nexport function timecodeToMs(timecode: string): number {\n const trimmed = timecode.trim();\n const match = trimmed.match(TIMECODE_REGEX);\n\n if (!match) {\n throw new Error(\n `Invalid timecode format: \"${timecode}\". Expected \"HH:MM:SS,mmm\" or \"HH:MM:SS.mmm\".`\n );\n }\n\n const hours = match[1] ? parseInt(match[1], 10) : 0;\n const minutes = match[2] ? parseInt(match[2], 10) : 0;\n const seconds = parseInt(match[3] ?? '0', 10);\n const milliseconds = parseInt(match[4] ?? '0', 10);\n\n return (\n hours * 3_600_000 +\n minutes * 60_000 +\n seconds * 1_000 +\n milliseconds\n );\n}\n\n/**\n * Converts integer milliseconds into a standard formatted timecode string.\n *\n * @param ms - Non-negative integer milliseconds (negative values clamped to 0)\n * @param format - 'srt' (uses comma delimiter) or 'vtt' (uses dot delimiter)\n *\n * @example\n * msToTimecode(83456, 'srt') // \"00:01:23,456\"\n * msToTimecode(83456, 'vtt') // \"00:01:23.456\"\n */\nexport function msToTimecode(ms: number, format: TimecodeFormat = 'srt'): string {\n const safeMs = Math.max(0, Math.floor(ms));\n\n const hours = Math.floor(safeMs / 3_600_000);\n const minutes = Math.floor((safeMs % 3_600_000) / 60_000);\n const seconds = Math.floor((safeMs % 60_000) / 1_000);\n const remainderMs = safeMs % 1_000;\n\n const hh = String(hours).padStart(2, '0');\n const mm = String(minutes).padStart(2, '0');\n const ss = String(seconds).padStart(2, '0');\n const mmm = String(remainderMs).padStart(3, '0');\n\n const delimiter = format === 'vtt' ? '.' : ',';\n return `${hh}:${mm}:${ss}${delimiter}${mmm}`;\n}\n\n/**\n * Linearly shifts timecodes by a constant millisecond offset.\n * Used to correct audio-sync delay or advance.\n * Clamps negative results to 0.\n *\n * @param cues - Array of SubtitleCue objects\n * @param offsetMs - Offset in milliseconds (positive advances, negative delays)\n * @returns A new immutable array of shifted cues\n */\nexport function shiftTimecodes(\n cues: readonly SubtitleCue[],\n offsetMs: number\n): SubtitleCue[] {\n return cues.map((cue) => ({\n ...cue,\n start: Math.max(0, cue.start + offsetMs),\n end: Math.max(0, cue.end + offsetMs),\n }));\n}\n\n/**\n * Scales all cue timecodes by a multiplicative ratio.\n * Used for frame-rate drift compensation between video standards.\n * All results are strictly rounded to integer milliseconds with Math.round.\n *\n * Direction definition:\n * targetTimestamp = Math.round(sourceTimestamp * ratio)\n *\n * @example\n * // 24 fps to 25 fps (PAL speed-up): video is ~4% faster, timecodes compress\n * const palCues = retime(filmCues, 24 / 25); // ratio = 0.96\n *\n * @example\n * // 25 fps to 24 fps (Slow-down): video is ~4% slower, timecodes stretch\n * const filmCues = retime(palCues, 25 / 24); // ratio = 1.04167\n *\n * @param cues - Array of SubtitleCue objects\n * @param ratio - Positive scaling multiplier (e.g., 24/25 or 25/24)\n * @returns A new immutable array of retimed cues\n */\nexport function retime(\n cues: readonly SubtitleCue[],\n ratio: number\n): SubtitleCue[] {\n if (ratio <= 0) {\n throw new Error(`Retime ratio must be greater than 0. Received: ${ratio}`);\n }\n\n return cues.map((cue) => ({\n ...cue,\n start: Math.round(cue.start * ratio),\n end: Math.round(cue.end * ratio),\n }));\n}\n","import { timecodeToMs } from './timecode.js';\nimport type { SubtitleCue, ParseError, ParseResult } from './types.js';\n\n/** Matches the standard SRT timestamp line: 00:00:00,000 --> 00:00:00,000 */\nconst ARROW_REGEX =\n /((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})\\s*-->\\s*((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})/;\n\n/**\n * Parses raw SRT subtitle text into structured SubtitleCue objects.\n * Resilient parser that collects syntax errors without throwing unhandled exceptions.\n *\n * Handles:\n * - UTF-8 Byte Order Marks (BOM `\\uFEFF`)\n * - Windows (CRLF), Unix (LF), and legacy Mac (CR) line endings\n * - Optional or missing cue numeric index headers\n * - Multi-line subtitle texts\n * - Trailing and irregular blank lines\n *\n * @param rawText - Raw SRT file content as a string\n * @returns {ParseResult} Object containing successfully parsed cues and any block errors\n */\nexport function parseSRT(rawText: string): ParseResult {\n const cues: SubtitleCue[] = [];\n const errors: ParseError[] = [];\n\n if (!rawText || !rawText.trim()) {\n return { cues, errors };\n }\n\n // 1. Strip UTF-8 Byte Order Mark (BOM)\n const cleanText = rawText.replace(/^\\uFEFF/, '');\n\n // 2. Normalize CRLF and CR to standard Unix LF\n const normalized = cleanText.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n // 3. Split by 2 or more consecutive newlines into cue blocks\n const rawBlocks = normalized.trim().split(/\\n\\s*\\n+/);\n\n let fallbackIndex = 1;\n\n for (let bIndex = 0; bIndex < rawBlocks.length; bIndex++) {\n const rawBlock = rawBlocks[bIndex]?.trim();\n if (!rawBlock) continue;\n\n const lines = rawBlock.split('\\n');\n\n let id: string;\n let timecodeLine: string;\n let textLines: string[];\n\n // Check if line 0 is the timecode arrow (missing numeric index)\n if (lines[0] && ARROW_REGEX.test(lines[0])) {\n id = String(fallbackIndex);\n timecodeLine = lines[0];\n textLines = lines.slice(1);\n } else if (lines[1] && ARROW_REGEX.test(lines[1])) {\n // Standard SRT: line 0 is ID, line 1 is timecode\n id = lines[0]?.trim() || String(fallbackIndex);\n timecodeLine = lines[1];\n textLines = lines.slice(2);\n } else {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message:\n 'Invalid block format. Expected timecode arrow \"-->\" on line 1 or 2.',\n });\n continue;\n }\n\n const match = timecodeLine.match(ARROW_REGEX);\n if (!match || !match[1] || !match[2]) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: `Failed to extract start and end timestamps from: \"${timecodeLine}\".`,\n });\n continue;\n }\n\n try {\n const start = timecodeToMs(match[1]);\n const end = timecodeToMs(match[2]);\n const text = textLines.join('\\n').trim();\n\n cues.push({\n id,\n start,\n end,\n text,\n });\n\n fallbackIndex++;\n } catch (err) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { cues, errors };\n}\n","import { msToTimecode } from './timecode.js';\nimport type { SubtitleCue } from './types.js';\n\n/**\n * Serializes an array of SubtitleCue objects into standard SRT file format.\n *\n * Output format:\n * 1\n * 00:00:20,000 --> 00:00:24,000\n * Subtitle text\n *\n * @param cues - Array of SubtitleCue objects to format\n * @returns Standard formatted SRT string\n */\nexport function stringifySRT(cues: readonly SubtitleCue[]): string {\n if (!cues || cues.length === 0) {\n return '';\n }\n\n const blocks = cues.map((cue, idx) => {\n const id = cue.id ? String(cue.id) : String(idx + 1);\n const startStr = msToTimecode(cue.start, 'srt');\n const endStr = msToTimecode(cue.end, 'srt');\n const text = cue.text ?? '';\n\n return `${id}\\n${startStr} --> ${endStr}\\n${text}`;\n });\n\n return blocks.join('\\n\\n') + '\\n';\n}\n","import { timecodeToMs, msToTimecode } from './timecode.js';\nimport { parseSRT } from './parser.js';\nimport { stringifySRT } from './serializer.js';\nimport type { SubtitleCue, ParseError, ParseResult } from './types.js';\n\n/**\n * Matches the WebVTT timestamp line with optional cue settings:\n * 00:01:20.500 --> 00:01:23.000 align:middle line:90%\n */\nconst VTT_ARROW_REGEX =\n /((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})\\s*-->\\s*((?:\\d{1,2}:)?\\d{2}:\\d{2}[,.]\\d{3})(?:[ \\t]+([^\\r\\n]+))?/;\n\nexport interface StringifyVTTOptions {\n /** Optional custom header line after WEBVTT (e.g. \"WEBVTT - Commentary Track\") */\n header?: string;\n /** Whether to output cue numeric ID headers (default: false for standard clean WebVTT) */\n includeIds?: boolean;\n}\n\n/**\n * Parses raw WebVTT (.vtt) subtitle text into structured SubtitleCue objects.\n *\n * Handles:\n * - UTF-8 Byte Order Marks (BOM \\uFEFF)\n * - WEBVTT header and metadata blocks\n * - NOTE comment blocks (skipped automatically)\n * - Dot-separated milliseconds (00:01:20.500) and comma-separated fallback\n * - Short timecodes without hours (MM:SS.mmm)\n * - Cue placement/styling settings (e.g. align:middle line:90%)\n * - Optional cue identifiers\n *\n * @param rawText - Raw WebVTT file content as a string\n * @returns {ParseResult} Successfully parsed cues and any block errors\n */\nexport function parseVTT(rawText: string): ParseResult {\n const cues: SubtitleCue[] = [];\n const errors: ParseError[] = [];\n\n if (!rawText || !rawText.trim()) {\n return { cues, errors };\n }\n\n // 1. Strip BOM & normalize newlines\n const clean = rawText.replace(/^\\uFEFF/, '');\n const normalized = clean.replace(/\\r\\n/g, '\\n').replace(/\\r/g, '\\n');\n\n // 2. Validate and strip WEBVTT header\n if (!normalized.startsWith('WEBVTT')) {\n errors.push({\n rawBlock: normalized.slice(0, 50),\n message: 'Missing standard \"WEBVTT\" signature at the beginning of the file.',\n });\n }\n\n // Split into blocks by double newlines\n const rawBlocks = normalized.trim().split(/\\n\\s*\\n+/);\n let fallbackIndex = 1;\n\n for (let bIndex = 0; bIndex < rawBlocks.length; bIndex++) {\n const rawBlock = rawBlocks[bIndex]?.trim();\n if (!rawBlock) continue;\n\n // Skip the very first block if it is just the WEBVTT header line\n if (bIndex === 0 && rawBlock.startsWith('WEBVTT')) {\n continue;\n }\n\n // Skip NOTE comment blocks\n if (rawBlock.startsWith('NOTE')) {\n continue;\n }\n\n // Skip STYLE or REGION metadata blocks if encountered\n if (rawBlock.startsWith('STYLE') || rawBlock.startsWith('REGION')) {\n continue;\n }\n\n const lines = rawBlock.split('\\n');\n let id: string;\n let timecodeLine: string;\n let textLines: string[];\n\n // Case 1: First line is the timestamp arrow (no cue ID header)\n if (lines[0] && VTT_ARROW_REGEX.test(lines[0])) {\n id = String(fallbackIndex);\n timecodeLine = lines[0];\n textLines = lines.slice(1);\n } else if (lines[1] && VTT_ARROW_REGEX.test(lines[1])) {\n // Case 2: Line 0 is ID header, line 1 is timestamp\n id = lines[0]?.trim() || String(fallbackIndex);\n timecodeLine = lines[1];\n textLines = lines.slice(2);\n } else {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: 'Invalid WebVTT cue block. Missing timecode arrow \"-->\".',\n });\n continue;\n }\n\n const match = timecodeLine.match(VTT_ARROW_REGEX);\n if (!match || !match[1] || !match[2]) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: `Failed to parse timestamps from WebVTT cue: \"${timecodeLine}\".`,\n });\n continue;\n }\n\n try {\n const start = timecodeToMs(match[1]);\n const end = timecodeToMs(match[2]);\n const settings = match[3]?.trim();\n const text = textLines.join('\\n').trim();\n\n const cue: SubtitleCue = {\n id,\n start,\n end,\n text,\n };\n\n if (settings) {\n cue.settings = settings;\n }\n\n cues.push(cue);\n fallbackIndex++;\n } catch (err) {\n errors.push({\n index: fallbackIndex,\n rawBlock,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n\n return { cues, errors };\n}\n\n/**\n * Serializes an array of SubtitleCue objects into standard WebVTT (.vtt) file format.\n *\n * @param cues - Array of SubtitleCue objects to format\n * @param options - Formatting options (header title, inclusion of cue IDs)\n * @returns Formatted WebVTT string\n */\nexport function stringifyVTT(\n cues: readonly SubtitleCue[],\n options?: StringifyVTTOptions\n): string {\n const header = options?.header ? `WEBVTT - ${options.header}` : 'WEBVTT';\n\n if (!cues || cues.length === 0) {\n return `${header}\\n`;\n }\n\n const includeIds = options?.includeIds ?? false;\n\n const blocks = cues.map((cue, idx) => {\n const id = cue.id ? String(cue.id) : String(idx + 1);\n const startStr = msToTimecode(cue.start, 'vtt');\n const endStr = msToTimecode(cue.end, 'vtt');\n const settingsStr = cue.settings ? ` ${cue.settings}` : '';\n const timeLine = `${startStr} --> ${endStr}${settingsStr}`;\n const text = cue.text ?? '';\n\n if (includeIds) {\n return `${id}\\n${timeLine}\\n${text}`;\n }\n return `${timeLine}\\n${text}`;\n });\n\n return `${header}\\n\\n${blocks.join('\\n\\n')}\\n`;\n}\n\n/**\n * Converts raw SRT subtitle text directly into a valid WebVTT (.vtt) string.\n *\n * @param srtText - Raw SubRip (.srt) text content\n * @param options - WebVTT formatting options\n * @returns Formatted WebVTT string\n */\nexport function srtToVTT(\n srtText: string,\n options?: StringifyVTTOptions\n): string {\n const { cues } = parseSRT(srtText);\n return stringifyVTT(cues, options);\n}\n\n/**\n * Converts raw WebVTT subtitle text directly into a standard SRT (.srt) string.\n *\n * @param vttText - Raw WebVTT (.vtt) text content\n * @returns Standard SubRip (.srt) string\n */\nexport function vttToSRT(vttText: string): string {\n const { cues } = parseVTT(vttText);\n return stringifySRT(cues);\n}\n","import type {\n SubtitleCue,\n LintDiagnostic,\n LintProfile,\n} from './types.js';\n\n/** Default quality profiles for subtitle verification */\nexport const DEFAULT_PROFILES: Record<string, LintProfile> = {\n standardLatin: {\n name: 'Standard Latin (Netflix/BBC Style Guide)',\n locale: 'en',\n maxCps: 20,\n maxCpl: 42,\n minDurationMs: 800,\n maxDurationMs: 7000,\n includeSpaces: true,\n },\n indicKannada: {\n name: 'Indic - Kannada Locale',\n locale: 'kn',\n maxCps: 15,\n maxCpl: 35,\n minDurationMs: 800,\n maxDurationMs: 7000,\n includeSpaces: true,\n },\n};\n\n/**\n * Strips HTML formatting tags from subtitle text before measuring character limits.\n * Handles <i>, <b>, <u>, <font color=\"...\">, etc.\n */\nexport function stripFormattingTags(text: string): string {\n return text.replace(/<[^>]*>/g, '');\n}\n\n/**\n * Counts user-perceived grapheme clusters using native ECMAScript Intl.Segmenter.\n * Accurately measures multi-byte conjuncts and vowel modifiers in Indic scripts (Kannada, Hindi),\n * as well as emojis and complex Unicode characters without UTF-16 code unit inflation.\n */\nexport function countGraphemes(text: string, locale: string = 'en'): number {\n if (!text) return 0;\n\n if (typeof Intl !== 'undefined' && 'Segmenter' in Intl) {\n const segmenter = new Intl.Segmenter(locale, { granularity: 'grapheme' });\n let count = 0;\n // Iterate over segments efficiently without allocating full array\n for (const _ of segmenter.segment(text)) {\n count++;\n }\n return count;\n }\n\n // Fallback for environments lacking Intl.Segmenter\n return [...text].length;\n}\n\n/**\n * Inspects an array of SubtitleCues against timing, structural, and reading speed rules.\n *\n * Rules verified:\n * - out-of-order: Cues not in ascending chronological order\n * - zero-or-negative-duration: Cues where end <= start\n * - overlap: Cues overlapping chronologically with preceding cues\n * - min-duration: Cues flashing too fast for comprehension (< 800ms)\n * - max-duration: Cues remaining on screen too long (> 7000ms)\n * - max-cpl: Any line exceeding characters-per-line limit\n * - max-cps: Reading speed exceeding characters-per-second limit\n *\n * @param cues - Array of SubtitleCue objects to inspect\n * @param customProfile - Optional custom thresholds or language profile\n * @returns Array of LintDiagnostic reports\n */\nexport function lintCues(\n cues: readonly SubtitleCue[],\n customProfile?: Partial<LintProfile>\n): LintDiagnostic[] {\n const diagnostics: LintDiagnostic[] = [];\n if (!cues || cues.length === 0) return diagnostics;\n\n const profile: LintProfile = {\n ...DEFAULT_PROFILES['standardLatin']!,\n ...customProfile,\n };\n\n const {\n locale = 'en',\n maxCps,\n maxCpl,\n minDurationMs,\n maxDurationMs,\n includeSpaces = true,\n } = profile;\n\n for (let i = 0; i < cues.length; i++) {\n const cue = cues[i];\n if (!cue) continue;\n\n const prevCue = i > 0 ? cues[i - 1] : undefined;\n const duration = cue.end - cue.start;\n\n // --- TIMING RULES ---\n\n // 1. Out-of-order check\n if (prevCue && cue.start < prevCue.start) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'out-of-order',\n severity: 'error',\n message: `Cue starts at ${cue.start}ms before previous cue start of ${prevCue.start}ms.`,\n expected: `start >= ${prevCue.start}`,\n actual: `start = ${cue.start}`,\n });\n }\n\n // 2. Zero or negative duration check\n if (cue.end <= cue.start) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'zero-or-negative-duration',\n severity: 'error',\n message: `Cue has invalid duration (${duration}ms). End timestamp must be greater than start timestamp.`,\n expected: `end > ${cue.start}`,\n actual: `end = ${cue.end}`,\n });\n }\n\n // 3. Overlap check (only if not out-of-order)\n if (prevCue && cue.start < prevCue.end && cue.start >= prevCue.start) {\n const overlapMs = prevCue.end - cue.start;\n diagnostics.push({\n cueId: cue.id,\n rule: 'overlap',\n severity: 'error',\n message: `Cue overlaps with previous cue by ${overlapMs}ms.`,\n expected: `start >= ${prevCue.end}`,\n actual: `start = ${cue.start}`,\n });\n }\n\n // 4. Min duration check (only if duration is positive)\n if (duration > 0 && duration < minDurationMs) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'min-duration',\n severity: 'warning',\n message: `Cue duration of ${duration}ms is below the minimum threshold of ${minDurationMs}ms.`,\n expected: minDurationMs,\n actual: duration,\n });\n }\n\n // 5. Max duration check\n if (duration > maxDurationMs) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-duration',\n severity: 'warning',\n message: `Cue duration of ${duration}ms exceeds the maximum threshold of ${maxDurationMs}ms.`,\n expected: maxDurationMs,\n actual: duration,\n });\n }\n\n // --- TEXT & PERCEPTION RULES ---\n\n const rawText = cue.text || '';\n const cleanText = stripFormattingTags(rawText);\n\n // 6. Characters Per Line (CPL) check\n const lines = cleanText.split('\\n');\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex];\n if (!line) continue;\n\n const cpl = countGraphemes(line, locale);\n if (cpl > maxCpl) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-cpl',\n severity: 'warning',\n message: `Line ${lineIndex + 1} has ${cpl} characters, exceeding the maximum limit of ${maxCpl} CPL.`,\n expected: maxCpl,\n actual: cpl,\n });\n }\n }\n\n // 7. Characters Per Second (CPS) reading speed check\n if (duration > 0) {\n const textToMeasure = includeSpaces\n ? cleanText\n : cleanText.replace(/\\s+/g, '');\n\n const totalGraphemes = countGraphemes(textToMeasure, locale);\n const cps = Number(((totalGraphemes / duration) * 1000).toFixed(1));\n\n if (cps > maxCps) {\n diagnostics.push({\n cueId: cue.id,\n rule: 'max-cps',\n severity: 'warning',\n message: `Reading speed of ${cps} CPS exceeds maximum limit of ${maxCps} CPS.`,\n expected: maxCps,\n actual: cps,\n });\n }\n }\n }\n\n return diagnostics;\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "subcraft",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Zero-dependency isomorphic TypeScript subtitle engine for parsing, retiming, and Unicode grapheme-cluster quality linting (powered by Intl.Segmenter).",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Zero-dependency isomorphic TypeScript subtitle engine for parsing, retiming, WebVTT/SRT conversion, and Unicode grapheme-cluster quality linting (powered by Intl.Segmenter).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|