srtsync 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Rodrigo Polo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,130 @@
1
+ # srtsync
2
+
3
+ Resync SRT subtitle files — shift timestamps by a fixed offset or linearly rescale them to match a different video duration.
4
+
5
+ Works as a **CLI tool** and as a **Node.js / browser library**. Zero runtime dependencies.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install srtsync
11
+ ```
12
+
13
+ ## CLI
14
+
15
+ ### Shift all timestamps by a fixed offset
16
+
17
+ ```bash
18
+ srtsync shift <offset> <file.srt> [-o output.srt]
19
+ ```
20
+
21
+ The offset format is `[+|-]HH:MM:SS.mmm`:
22
+
23
+ ```bash
24
+ # Shift subtitles forward by 1.5 seconds
25
+ srtsync shift +00:00:01.500 movie.srt > movie-fixed.srt
26
+
27
+ # Shift subtitles back by 3 seconds, write to file
28
+ srtsync shift -00:00:03.000 movie.srt -o movie-fixed.srt
29
+ ```
30
+
31
+ ### Linearly rescale timestamps
32
+
33
+ Use this when the video has been rate-stretched (e.g. PAL speed-up from 24fps to 25fps) and all timestamps need proportional adjustment.
34
+
35
+ ```bash
36
+ srtsync linear <newFirst> <newLast> <file.srt> [-o output.srt]
37
+ ```
38
+
39
+ You provide the correct start time for the **first** and **last** subtitle. Every timestamp in between is linearly interpolated:
40
+
41
+ ```bash
42
+ # The first subtitle should start at 00:00:21.278
43
+ # and the last subtitle should start at 01:18:48.956
44
+ srtsync linear 00:00:21.278 01:18:48.956 movie.srt > movie-fixed.srt
45
+ ```
46
+
47
+ Output goes to **stdout** by default (pipe-friendly), or use `-o` to write directly to a file.
48
+
49
+ ## Library
50
+
51
+ ### `timeShift(srtContent, offset)`
52
+
53
+ Parse an SRT string, shift all timestamps, and return the resynced SRT string.
54
+
55
+ ```js
56
+ import { timeShift } from "srtsync";
57
+ import { readFileSync } from "fs";
58
+
59
+ const srt = readFileSync("movie.srt", "utf-8");
60
+ const shifted = timeShift(srt, "+00:00:02.000");
61
+ ```
62
+
63
+ ### `linearCorrection(srtContent, newFirst, newLast)`
64
+
65
+ Parse an SRT string, linearly rescale all timestamps between two reference points, and return the resynced SRT string.
66
+
67
+ ```js
68
+ import { linearCorrection } from "srtsync";
69
+ import { readFileSync } from "fs";
70
+
71
+ const srt = readFileSync("movie.srt", "utf-8");
72
+ const corrected = linearCorrection(srt, "00:00:21.278", "01:18:48.956");
73
+ ```
74
+
75
+ ### Lower-level API
76
+
77
+ For more control, you can work with parsed subtitle objects directly:
78
+
79
+ ```js
80
+ import {
81
+ parseSrt,
82
+ formatSrt,
83
+ applyShift,
84
+ applyLinearCorrection,
85
+ parseTimestamp,
86
+ formatTimestamp,
87
+ } from "srtsync";
88
+
89
+ const subs = parseSrt(srtContent);
90
+ // => [{ index: 1, startMs: 20400, endMs: 21680, text: "Hello" }, ...]
91
+
92
+ // Shift by 5 seconds
93
+ const shifted = applyShift(subs, 5000);
94
+
95
+ // Or linearly rescale
96
+ const corrected = applyLinearCorrection(subs, 21278, 4728956);
97
+
98
+ // Serialize back to SRT
99
+ const output = formatSrt(corrected);
100
+ ```
101
+
102
+ The `Subtitle` type is also exported:
103
+
104
+ ```ts
105
+ import type { Subtitle } from "srtsync";
106
+ ```
107
+
108
+ ## How linear correction works
109
+
110
+ Given the start time of the first and last subtitles as reference points, every timestamp is rescaled with a linear mapping:
111
+
112
+ ```
113
+ scale = (newLast - newFirst) / (origLast - origFirst)
114
+ new_t = newFirst + (t - origFirst) * scale
115
+ ```
116
+
117
+ This corrects uniform drift caused by frame-rate conversion, player speed changes, or different video cuts.
118
+
119
+ ## Timestamp format
120
+
121
+ All timestamp arguments accept both SRT-standard comma separators and dot separators:
122
+
123
+ - `00:01:30,500` (SRT standard)
124
+ - `00:01:30.500` (also accepted)
125
+
126
+ Offsets accept an optional `+`/`-` sign prefix. Unsigned offsets are treated as positive.
127
+
128
+ ## License
129
+
130
+ MIT
package/dist/cli.mjs ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync, writeFileSync } from "fs";
5
+
6
+ // src/time.ts
7
+ function parseTimestamp(ts) {
8
+ const m = ts.trim().match(/^(\d{2}):(\d{2}):(\d{2})[,.](\d{3})$/);
9
+ if (!m) throw new Error(`Invalid timestamp: "${ts}"`);
10
+ const [, h, min, sec, ms] = m;
11
+ return Number(h) * 36e5 + Number(min) * 6e4 + Number(sec) * 1e3 + Number(ms);
12
+ }
13
+ function formatTimestamp(ms) {
14
+ if (ms < 0) ms = 0;
15
+ const h = Math.floor(ms / 36e5);
16
+ ms %= 36e5;
17
+ const min = Math.floor(ms / 6e4);
18
+ ms %= 6e4;
19
+ const sec = Math.floor(ms / 1e3);
20
+ const millis = ms % 1e3;
21
+ return String(h).padStart(2, "0") + ":" + String(min).padStart(2, "0") + ":" + String(sec).padStart(2, "0") + "," + String(millis).padStart(3, "0");
22
+ }
23
+ function parseOffset(offset) {
24
+ const m = offset.trim().match(/^([+-]?)(\d{2}):(\d{2}):(\d{2})[,.](\d{3})$/);
25
+ if (!m) throw new Error(`Invalid offset: "${offset}"`);
26
+ const [, sign, h, min, sec, ms] = m;
27
+ const value = Number(h) * 36e5 + Number(min) * 6e4 + Number(sec) * 1e3 + Number(ms);
28
+ return sign === "-" ? -value : value;
29
+ }
30
+
31
+ // src/parser.ts
32
+ function parseSrt(content) {
33
+ const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
34
+ const blocks = normalized.trim().split(/\n\n+/);
35
+ const subtitles = [];
36
+ for (const block of blocks) {
37
+ const lines = block.trim().split("\n");
38
+ if (lines.length < 3) continue;
39
+ const index = parseInt(lines[0], 10);
40
+ if (isNaN(index)) continue;
41
+ const timeLine = lines[1];
42
+ const timeMatch = timeLine.match(
43
+ /(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})/
44
+ );
45
+ if (!timeMatch) continue;
46
+ const startMs = parseTimestamp(timeMatch[1]);
47
+ const endMs = parseTimestamp(timeMatch[2]);
48
+ const text = lines.slice(2).join("\n");
49
+ subtitles.push({ index, startMs, endMs, text });
50
+ }
51
+ return subtitles;
52
+ }
53
+ function formatSrt(subtitles) {
54
+ return subtitles.map((sub, i) => {
55
+ const idx = i + 1;
56
+ const start = formatTimestamp(sub.startMs);
57
+ const end = formatTimestamp(sub.endMs);
58
+ return `${idx}
59
+ ${start} --> ${end}
60
+ ${sub.text}`;
61
+ }).join("\n\n") + "\n";
62
+ }
63
+
64
+ // src/shift.ts
65
+ function applyShift(subtitles, offsetMs) {
66
+ return subtitles.map((sub) => ({
67
+ ...sub,
68
+ startMs: Math.max(0, sub.startMs + offsetMs),
69
+ endMs: Math.max(0, sub.endMs + offsetMs)
70
+ }));
71
+ }
72
+
73
+ // src/linear.ts
74
+ function applyLinearCorrection(subtitles, newFirstMs, newLastMs) {
75
+ if (subtitles.length < 2) {
76
+ throw new Error("Need at least 2 subtitles for linear correction");
77
+ }
78
+ const origFirst = subtitles[0].startMs;
79
+ const origLast = subtitles[subtitles.length - 1].startMs;
80
+ const scale = (newLastMs - newFirstMs) / (origLast - origFirst);
81
+ return subtitles.map((sub) => ({
82
+ ...sub,
83
+ startMs: Math.max(0, Math.round(newFirstMs + (sub.startMs - origFirst) * scale)),
84
+ endMs: Math.max(0, Math.round(newFirstMs + (sub.endMs - origFirst) * scale))
85
+ }));
86
+ }
87
+
88
+ // src/index.ts
89
+ function timeShift(srtContent, offset) {
90
+ const subs = parseSrt(srtContent);
91
+ const offsetMs = parseOffset(offset);
92
+ return formatSrt(applyShift(subs, offsetMs));
93
+ }
94
+ function linearCorrection(srtContent, newFirst, newLast) {
95
+ const subs = parseSrt(srtContent);
96
+ const newFirstMs = parseTimestamp(newFirst);
97
+ const newLastMs = parseTimestamp(newLast);
98
+ return formatSrt(applyLinearCorrection(subs, newFirstMs, newLastMs));
99
+ }
100
+
101
+ // src/cli.ts
102
+ function usage() {
103
+ console.error(
104
+ `Usage:
105
+ srtsync shift <offset> <file.srt> [-o output.srt]
106
+ srtsync linear <newFirst> <newLast> <file.srt> [-o output.srt]
107
+
108
+ Examples:
109
+ srtsync shift +00:00:01.000 input.srt > output.srt
110
+ srtsync linear 00:00:21.278 01:18:48.956 input.srt -o output.srt`
111
+ );
112
+ process.exit(1);
113
+ }
114
+ function main() {
115
+ const args = process.argv.slice(2);
116
+ if (args.length < 1) usage();
117
+ const command = args[0];
118
+ let result;
119
+ if (command === "shift") {
120
+ if (args.length < 3) usage();
121
+ const offset = args[1];
122
+ const file = args[2];
123
+ const content = readFileSync(file, "utf-8");
124
+ result = timeShift(content, offset);
125
+ } else if (command === "linear") {
126
+ if (args.length < 4) usage();
127
+ const newFirst = args[1];
128
+ const newLast = args[2];
129
+ const file = args[3];
130
+ const content = readFileSync(file, "utf-8");
131
+ result = linearCorrection(content, newFirst, newLast);
132
+ } else {
133
+ usage();
134
+ }
135
+ const oIdx = args.indexOf("-o");
136
+ if (oIdx !== -1 && args[oIdx + 1]) {
137
+ writeFileSync(args[oIdx + 1], result, "utf-8");
138
+ } else {
139
+ process.stdout.write(result);
140
+ }
141
+ }
142
+ main();
package/dist/index.cjs ADDED
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ applyLinearCorrection: () => applyLinearCorrection,
24
+ applyShift: () => applyShift,
25
+ formatSrt: () => formatSrt,
26
+ formatTimestamp: () => formatTimestamp,
27
+ linearCorrection: () => linearCorrection,
28
+ parseSrt: () => parseSrt,
29
+ parseTimestamp: () => parseTimestamp,
30
+ timeShift: () => timeShift
31
+ });
32
+ module.exports = __toCommonJS(index_exports);
33
+
34
+ // src/time.ts
35
+ function parseTimestamp(ts) {
36
+ const m = ts.trim().match(/^(\d{2}):(\d{2}):(\d{2})[,.](\d{3})$/);
37
+ if (!m) throw new Error(`Invalid timestamp: "${ts}"`);
38
+ const [, h, min, sec, ms] = m;
39
+ return Number(h) * 36e5 + Number(min) * 6e4 + Number(sec) * 1e3 + Number(ms);
40
+ }
41
+ function formatTimestamp(ms) {
42
+ if (ms < 0) ms = 0;
43
+ const h = Math.floor(ms / 36e5);
44
+ ms %= 36e5;
45
+ const min = Math.floor(ms / 6e4);
46
+ ms %= 6e4;
47
+ const sec = Math.floor(ms / 1e3);
48
+ const millis = ms % 1e3;
49
+ return String(h).padStart(2, "0") + ":" + String(min).padStart(2, "0") + ":" + String(sec).padStart(2, "0") + "," + String(millis).padStart(3, "0");
50
+ }
51
+ function parseOffset(offset) {
52
+ const m = offset.trim().match(/^([+-]?)(\d{2}):(\d{2}):(\d{2})[,.](\d{3})$/);
53
+ if (!m) throw new Error(`Invalid offset: "${offset}"`);
54
+ const [, sign, h, min, sec, ms] = m;
55
+ const value = Number(h) * 36e5 + Number(min) * 6e4 + Number(sec) * 1e3 + Number(ms);
56
+ return sign === "-" ? -value : value;
57
+ }
58
+
59
+ // src/parser.ts
60
+ function parseSrt(content) {
61
+ const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
62
+ const blocks = normalized.trim().split(/\n\n+/);
63
+ const subtitles = [];
64
+ for (const block of blocks) {
65
+ const lines = block.trim().split("\n");
66
+ if (lines.length < 3) continue;
67
+ const index = parseInt(lines[0], 10);
68
+ if (isNaN(index)) continue;
69
+ const timeLine = lines[1];
70
+ const timeMatch = timeLine.match(
71
+ /(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})/
72
+ );
73
+ if (!timeMatch) continue;
74
+ const startMs = parseTimestamp(timeMatch[1]);
75
+ const endMs = parseTimestamp(timeMatch[2]);
76
+ const text = lines.slice(2).join("\n");
77
+ subtitles.push({ index, startMs, endMs, text });
78
+ }
79
+ return subtitles;
80
+ }
81
+ function formatSrt(subtitles) {
82
+ return subtitles.map((sub, i) => {
83
+ const idx = i + 1;
84
+ const start = formatTimestamp(sub.startMs);
85
+ const end = formatTimestamp(sub.endMs);
86
+ return `${idx}
87
+ ${start} --> ${end}
88
+ ${sub.text}`;
89
+ }).join("\n\n") + "\n";
90
+ }
91
+
92
+ // src/shift.ts
93
+ function applyShift(subtitles, offsetMs) {
94
+ return subtitles.map((sub) => ({
95
+ ...sub,
96
+ startMs: Math.max(0, sub.startMs + offsetMs),
97
+ endMs: Math.max(0, sub.endMs + offsetMs)
98
+ }));
99
+ }
100
+
101
+ // src/linear.ts
102
+ function applyLinearCorrection(subtitles, newFirstMs, newLastMs) {
103
+ if (subtitles.length < 2) {
104
+ throw new Error("Need at least 2 subtitles for linear correction");
105
+ }
106
+ const origFirst = subtitles[0].startMs;
107
+ const origLast = subtitles[subtitles.length - 1].startMs;
108
+ const scale = (newLastMs - newFirstMs) / (origLast - origFirst);
109
+ return subtitles.map((sub) => ({
110
+ ...sub,
111
+ startMs: Math.max(0, Math.round(newFirstMs + (sub.startMs - origFirst) * scale)),
112
+ endMs: Math.max(0, Math.round(newFirstMs + (sub.endMs - origFirst) * scale))
113
+ }));
114
+ }
115
+
116
+ // src/index.ts
117
+ function timeShift(srtContent, offset) {
118
+ const subs = parseSrt(srtContent);
119
+ const offsetMs = parseOffset(offset);
120
+ return formatSrt(applyShift(subs, offsetMs));
121
+ }
122
+ function linearCorrection(srtContent, newFirst, newLast) {
123
+ const subs = parseSrt(srtContent);
124
+ const newFirstMs = parseTimestamp(newFirst);
125
+ const newLastMs = parseTimestamp(newLast);
126
+ return formatSrt(applyLinearCorrection(subs, newFirstMs, newLastMs));
127
+ }
128
+ // Annotate the CommonJS export names for ESM import in node:
129
+ 0 && (module.exports = {
130
+ applyLinearCorrection,
131
+ applyShift,
132
+ formatSrt,
133
+ formatTimestamp,
134
+ linearCorrection,
135
+ parseSrt,
136
+ parseTimestamp,
137
+ timeShift
138
+ });
@@ -0,0 +1,58 @@
1
+ interface Subtitle {
2
+ index: number;
3
+ startMs: number;
4
+ endMs: number;
5
+ text: string;
6
+ }
7
+ /**
8
+ * Parse an SRT file into an array of Subtitle objects.
9
+ */
10
+ declare function parseSrt(content: string): Subtitle[];
11
+ /**
12
+ * Serialize an array of Subtitle objects back to SRT format.
13
+ * Re-indexes subtitles starting from 1.
14
+ */
15
+ declare function formatSrt(subtitles: Subtitle[]): string;
16
+
17
+ /**
18
+ * Parse an SRT timestamp (HH:MM:SS,mmm or HH:MM:SS.mmm) into milliseconds.
19
+ */
20
+ declare function parseTimestamp(ts: string): number;
21
+ /**
22
+ * Format milliseconds into SRT timestamp format (HH:MM:SS,mmm).
23
+ * Clamps negative values to 0.
24
+ */
25
+ declare function formatTimestamp(ms: number): string;
26
+
27
+ /**
28
+ * Add a fixed offset (in milliseconds) to every subtitle timestamp.
29
+ * Clamps results to 0.
30
+ */
31
+ declare function applyShift(subtitles: Subtitle[], offsetMs: number): Subtitle[];
32
+
33
+ /**
34
+ * Linearly rescale all timestamps so that the first subtitle's start time
35
+ * maps to `newFirstMs` and the last subtitle's start time maps to `newLastMs`.
36
+ */
37
+ declare function applyLinearCorrection(subtitles: Subtitle[], newFirstMs: number, newLastMs: number): Subtitle[];
38
+
39
+ /**
40
+ * Parse an SRT string, shift all timestamps by the given offset, and return
41
+ * the resynced SRT string.
42
+ *
43
+ * @param srtContent - Raw SRT file content
44
+ * @param offset - Signed offset like "+00:00:01.000" or "-00:00:02.500"
45
+ */
46
+ declare function timeShift(srtContent: string, offset: string): string;
47
+ /**
48
+ * Parse an SRT string, linearly rescale timestamps so that the first subtitle
49
+ * starts at `newFirst` and the last subtitle starts at `newLast`, and return
50
+ * the resynced SRT string.
51
+ *
52
+ * @param srtContent - Raw SRT file content
53
+ * @param newFirst - New start time for the first subtitle (e.g. "00:00:21.278")
54
+ * @param newLast - New start time for the last subtitle (e.g. "01:18:48.956")
55
+ */
56
+ declare function linearCorrection(srtContent: string, newFirst: string, newLast: string): string;
57
+
58
+ export { type Subtitle, applyLinearCorrection, applyShift, formatSrt, formatTimestamp, linearCorrection, parseSrt, parseTimestamp, timeShift };
@@ -0,0 +1,58 @@
1
+ interface Subtitle {
2
+ index: number;
3
+ startMs: number;
4
+ endMs: number;
5
+ text: string;
6
+ }
7
+ /**
8
+ * Parse an SRT file into an array of Subtitle objects.
9
+ */
10
+ declare function parseSrt(content: string): Subtitle[];
11
+ /**
12
+ * Serialize an array of Subtitle objects back to SRT format.
13
+ * Re-indexes subtitles starting from 1.
14
+ */
15
+ declare function formatSrt(subtitles: Subtitle[]): string;
16
+
17
+ /**
18
+ * Parse an SRT timestamp (HH:MM:SS,mmm or HH:MM:SS.mmm) into milliseconds.
19
+ */
20
+ declare function parseTimestamp(ts: string): number;
21
+ /**
22
+ * Format milliseconds into SRT timestamp format (HH:MM:SS,mmm).
23
+ * Clamps negative values to 0.
24
+ */
25
+ declare function formatTimestamp(ms: number): string;
26
+
27
+ /**
28
+ * Add a fixed offset (in milliseconds) to every subtitle timestamp.
29
+ * Clamps results to 0.
30
+ */
31
+ declare function applyShift(subtitles: Subtitle[], offsetMs: number): Subtitle[];
32
+
33
+ /**
34
+ * Linearly rescale all timestamps so that the first subtitle's start time
35
+ * maps to `newFirstMs` and the last subtitle's start time maps to `newLastMs`.
36
+ */
37
+ declare function applyLinearCorrection(subtitles: Subtitle[], newFirstMs: number, newLastMs: number): Subtitle[];
38
+
39
+ /**
40
+ * Parse an SRT string, shift all timestamps by the given offset, and return
41
+ * the resynced SRT string.
42
+ *
43
+ * @param srtContent - Raw SRT file content
44
+ * @param offset - Signed offset like "+00:00:01.000" or "-00:00:02.500"
45
+ */
46
+ declare function timeShift(srtContent: string, offset: string): string;
47
+ /**
48
+ * Parse an SRT string, linearly rescale timestamps so that the first subtitle
49
+ * starts at `newFirst` and the last subtitle starts at `newLast`, and return
50
+ * the resynced SRT string.
51
+ *
52
+ * @param srtContent - Raw SRT file content
53
+ * @param newFirst - New start time for the first subtitle (e.g. "00:00:21.278")
54
+ * @param newLast - New start time for the last subtitle (e.g. "01:18:48.956")
55
+ */
56
+ declare function linearCorrection(srtContent: string, newFirst: string, newLast: string): string;
57
+
58
+ export { type Subtitle, applyLinearCorrection, applyShift, formatSrt, formatTimestamp, linearCorrection, parseSrt, parseTimestamp, timeShift };
package/dist/index.js ADDED
@@ -0,0 +1,104 @@
1
+ // src/time.ts
2
+ function parseTimestamp(ts) {
3
+ const m = ts.trim().match(/^(\d{2}):(\d{2}):(\d{2})[,.](\d{3})$/);
4
+ if (!m) throw new Error(`Invalid timestamp: "${ts}"`);
5
+ const [, h, min, sec, ms] = m;
6
+ return Number(h) * 36e5 + Number(min) * 6e4 + Number(sec) * 1e3 + Number(ms);
7
+ }
8
+ function formatTimestamp(ms) {
9
+ if (ms < 0) ms = 0;
10
+ const h = Math.floor(ms / 36e5);
11
+ ms %= 36e5;
12
+ const min = Math.floor(ms / 6e4);
13
+ ms %= 6e4;
14
+ const sec = Math.floor(ms / 1e3);
15
+ const millis = ms % 1e3;
16
+ return String(h).padStart(2, "0") + ":" + String(min).padStart(2, "0") + ":" + String(sec).padStart(2, "0") + "," + String(millis).padStart(3, "0");
17
+ }
18
+ function parseOffset(offset) {
19
+ const m = offset.trim().match(/^([+-]?)(\d{2}):(\d{2}):(\d{2})[,.](\d{3})$/);
20
+ if (!m) throw new Error(`Invalid offset: "${offset}"`);
21
+ const [, sign, h, min, sec, ms] = m;
22
+ const value = Number(h) * 36e5 + Number(min) * 6e4 + Number(sec) * 1e3 + Number(ms);
23
+ return sign === "-" ? -value : value;
24
+ }
25
+
26
+ // src/parser.ts
27
+ function parseSrt(content) {
28
+ const normalized = content.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
29
+ const blocks = normalized.trim().split(/\n\n+/);
30
+ const subtitles = [];
31
+ for (const block of blocks) {
32
+ const lines = block.trim().split("\n");
33
+ if (lines.length < 3) continue;
34
+ const index = parseInt(lines[0], 10);
35
+ if (isNaN(index)) continue;
36
+ const timeLine = lines[1];
37
+ const timeMatch = timeLine.match(
38
+ /(\d{2}:\d{2}:\d{2}[,.]\d{3})\s*-->\s*(\d{2}:\d{2}:\d{2}[,.]\d{3})/
39
+ );
40
+ if (!timeMatch) continue;
41
+ const startMs = parseTimestamp(timeMatch[1]);
42
+ const endMs = parseTimestamp(timeMatch[2]);
43
+ const text = lines.slice(2).join("\n");
44
+ subtitles.push({ index, startMs, endMs, text });
45
+ }
46
+ return subtitles;
47
+ }
48
+ function formatSrt(subtitles) {
49
+ return subtitles.map((sub, i) => {
50
+ const idx = i + 1;
51
+ const start = formatTimestamp(sub.startMs);
52
+ const end = formatTimestamp(sub.endMs);
53
+ return `${idx}
54
+ ${start} --> ${end}
55
+ ${sub.text}`;
56
+ }).join("\n\n") + "\n";
57
+ }
58
+
59
+ // src/shift.ts
60
+ function applyShift(subtitles, offsetMs) {
61
+ return subtitles.map((sub) => ({
62
+ ...sub,
63
+ startMs: Math.max(0, sub.startMs + offsetMs),
64
+ endMs: Math.max(0, sub.endMs + offsetMs)
65
+ }));
66
+ }
67
+
68
+ // src/linear.ts
69
+ function applyLinearCorrection(subtitles, newFirstMs, newLastMs) {
70
+ if (subtitles.length < 2) {
71
+ throw new Error("Need at least 2 subtitles for linear correction");
72
+ }
73
+ const origFirst = subtitles[0].startMs;
74
+ const origLast = subtitles[subtitles.length - 1].startMs;
75
+ const scale = (newLastMs - newFirstMs) / (origLast - origFirst);
76
+ return subtitles.map((sub) => ({
77
+ ...sub,
78
+ startMs: Math.max(0, Math.round(newFirstMs + (sub.startMs - origFirst) * scale)),
79
+ endMs: Math.max(0, Math.round(newFirstMs + (sub.endMs - origFirst) * scale))
80
+ }));
81
+ }
82
+
83
+ // src/index.ts
84
+ function timeShift(srtContent, offset) {
85
+ const subs = parseSrt(srtContent);
86
+ const offsetMs = parseOffset(offset);
87
+ return formatSrt(applyShift(subs, offsetMs));
88
+ }
89
+ function linearCorrection(srtContent, newFirst, newLast) {
90
+ const subs = parseSrt(srtContent);
91
+ const newFirstMs = parseTimestamp(newFirst);
92
+ const newLastMs = parseTimestamp(newLast);
93
+ return formatSrt(applyLinearCorrection(subs, newFirstMs, newLastMs));
94
+ }
95
+ export {
96
+ applyLinearCorrection,
97
+ applyShift,
98
+ formatSrt,
99
+ formatTimestamp,
100
+ linearCorrection,
101
+ parseSrt,
102
+ parseTimestamp,
103
+ timeShift
104
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "srtsync",
3
+ "version": "1.0.0",
4
+ "description": "Resync SRT subtitle files — shift timestamps or linearly rescale them",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "bin": {
22
+ "srtsync": "./dist/cli.mjs"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "test": "vitest run"
30
+ },
31
+ "devDependencies": {
32
+ "tsup": "^8.4.0",
33
+ "typescript": "^5.7.0",
34
+ "vitest": "^3.0.0"
35
+ }
36
+ }