@gentbajko/slopify 2.0.1 → 2.0.2
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/dist/adapters/alignment/cache.js +17 -1
- package/dist/adapters/alignment/prefetch.js +35 -0
- package/dist/edge/cli.js +4 -1
- package/dist/kernel/errors.js +12 -0
- package/dist/kernel/paths.js +5 -0
- package/dist/kernel/runner/index.js +16 -6
- package/dist/kernel/runner/progress.js +56 -0
- package/dist/main.js +18 -2
- package/dist/slices/rebuild/runtime-store.js +45 -15
- package/dist/slices/rebuild/runtime-subtitles.js +2 -2
- package/dist/slices/subtitles/prepare.js +2 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { createReadStream, createWriteStream } from "node:fs";
|
|
3
|
-
import { mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises";
|
|
3
|
+
import { chmod, copyFile, mkdir, mkdtemp, rename, rm, stat } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { Readable } from "node:stream";
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
@@ -18,6 +18,8 @@ export async function prepareModel(input, deps = { model: alignmentModel, fetch:
|
|
|
18
18
|
const target = join(input.cacheDir, deps.model.filename);
|
|
19
19
|
if (await verified(target, deps.model, input.signal))
|
|
20
20
|
return target;
|
|
21
|
+
if (input.seed !== undefined && (await verified(input.seed, deps.model, input.signal)))
|
|
22
|
+
return seedModel(input, input.seed, target);
|
|
21
23
|
let lastFailure;
|
|
22
24
|
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
23
25
|
try {
|
|
@@ -127,6 +129,20 @@ async function downloadModel(input, deps, target) {
|
|
|
127
129
|
await rm(staging, { recursive: true, force: true });
|
|
128
130
|
}
|
|
129
131
|
}
|
|
132
|
+
async function seedModel(input, seed, target) {
|
|
133
|
+
const staging = await mkdtemp(join(input.cacheDir, ".alignment-seed-"));
|
|
134
|
+
try {
|
|
135
|
+
const part = join(staging, "model.part");
|
|
136
|
+
await copyFile(seed, part);
|
|
137
|
+
await chmod(part, 0o600);
|
|
138
|
+
input.signal.throwIfAborted();
|
|
139
|
+
await rename(part, target);
|
|
140
|
+
return target;
|
|
141
|
+
}
|
|
142
|
+
finally {
|
|
143
|
+
await rm(staging, { recursive: true, force: true });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
130
146
|
function interrupted(cause) {
|
|
131
147
|
const error = new Error("The subtitle model download was interrupted.", { cause });
|
|
132
148
|
error.name = "SubtitleModelDownloadInterrupted";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { prepareModel } from "./cache.js";
|
|
4
|
+
import { claimWorker } from "./lock.js";
|
|
5
|
+
// Readies the subtitle model when the app starts, so the first captioned render never waits
|
|
6
|
+
// on a 95 MB download in the middle of a run. It takes the worker lock, so a render that
|
|
7
|
+
// starts meanwhile waits for this copy rather than fetching a second one.
|
|
8
|
+
export async function prefetchModel(input, deps) {
|
|
9
|
+
await mkdir(input.cacheDir, { recursive: true, mode: 0o700 });
|
|
10
|
+
await clearOwnLock(input.cacheDir);
|
|
11
|
+
const release = await claimWorker(input.cacheDir, input.signal);
|
|
12
|
+
try {
|
|
13
|
+
return await prepareModel({ cacheDir: input.cacheDir, seed: input.seed, signal: input.signal }, deps);
|
|
14
|
+
}
|
|
15
|
+
finally {
|
|
16
|
+
await release();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
// The lock names its holder by pid. A lock that names this process when the app has only
|
|
20
|
+
// just started was left by an earlier run: in a container the app is always pid 1, so the
|
|
21
|
+
// liveness check in claimWorker would take it for a live holder and wait forever.
|
|
22
|
+
async function clearOwnLock(cacheDir) {
|
|
23
|
+
const path = join(cacheDir, "alignment-worker.lock");
|
|
24
|
+
let owner;
|
|
25
|
+
try {
|
|
26
|
+
owner = await readFile(path, "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
30
|
+
return;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
if (Number(owner) === process.pid)
|
|
34
|
+
await rm(path, { force: true });
|
|
35
|
+
}
|
package/dist/edge/cli.js
CHANGED
|
@@ -80,7 +80,10 @@ try {
|
|
|
80
80
|
const forwarded = await forwardManagedUpdate(config.dataDir, readVersion(), process.argv.slice(2));
|
|
81
81
|
if (forwarded !== undefined)
|
|
82
82
|
process.exit(forwarded);
|
|
83
|
-
const { paths, url, stop } = await boot(config
|
|
83
|
+
const { paths, url, stop } = await boot(config, {
|
|
84
|
+
prefetchSubtitleModel: ["", "0", "false"].includes((process.env.SLOPIFY_NO_MODEL_PREFETCH ?? "").trim().toLowerCase()),
|
|
85
|
+
subtitleModelSeed: process.env.SLOPIFY_SUBTITLE_MODEL_SEED?.trim() || undefined,
|
|
86
|
+
});
|
|
84
87
|
console.log(`Slopify is running at ${url}`);
|
|
85
88
|
console.log(`Slopify data directory: ${paths.dataDir}`);
|
|
86
89
|
console.log(`Database: ${paths.db}`);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// A failure's own sentence and every cause it wraps, for the log. The page shows only the
|
|
2
|
+
// sentence; the chain is the only place a "download interrupted" says the server closed the
|
|
3
|
+
// stream.
|
|
4
|
+
export function causedBy(error) {
|
|
5
|
+
const chain = [error instanceof Error ? error.message : String(error)];
|
|
6
|
+
let cause = error instanceof Error ? error.cause : undefined;
|
|
7
|
+
while (cause !== undefined && cause !== null && chain.length < 6) {
|
|
8
|
+
chain.push(cause instanceof Error ? `${cause.name}: ${cause.message}` : String(cause));
|
|
9
|
+
cause = cause instanceof Error ? cause.cause : undefined;
|
|
10
|
+
}
|
|
11
|
+
return chain.join(" <- ");
|
|
12
|
+
}
|
package/dist/kernel/paths.js
CHANGED
|
@@ -19,3 +19,8 @@ export function ensureDirs(paths, options) {
|
|
|
19
19
|
chmodSync(dir, options.mode);
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
// The subtitle-timing model's cache. The app readies it at start and every captioned render
|
|
23
|
+
// reads it, so both name it through here.
|
|
24
|
+
export function subtitleModelDir(dataDir) {
|
|
25
|
+
return join(dataDir, "models", "english-subtitles");
|
|
26
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { causedBy } from "../errors.js";
|
|
1
2
|
import { derive, deps as graph, satisfied } from "./graph.js";
|
|
3
|
+
import { progressGate } from "./progress.js";
|
|
2
4
|
const pauseReason = new Error("paused by user");
|
|
3
5
|
export function createRunner(deps) {
|
|
4
6
|
const inflight = new Map();
|
|
@@ -60,6 +62,12 @@ export function createRunner(deps) {
|
|
|
60
62
|
emitStage(stage, state, reason);
|
|
61
63
|
};
|
|
62
64
|
async function execute(stage, controller) {
|
|
65
|
+
const tag = (event) => ({
|
|
66
|
+
...event,
|
|
67
|
+
revisionId: stage.work.revisionId,
|
|
68
|
+
workId: stage.work.workId,
|
|
69
|
+
});
|
|
70
|
+
const progress = progressGate((event) => deps.emit(stage.projectId, tag(event)));
|
|
63
71
|
try {
|
|
64
72
|
const run = deps.runs[stage.kind];
|
|
65
73
|
if (run === undefined) {
|
|
@@ -76,16 +84,18 @@ export function createRunner(deps) {
|
|
|
76
84
|
deps.stages.maySubmit(stage.work, pieceId),
|
|
77
85
|
signal: controller.signal,
|
|
78
86
|
emit: (event) => {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
});
|
|
87
|
+
if (event.type === "stage.progress")
|
|
88
|
+
progress.offer(event);
|
|
89
|
+
else
|
|
90
|
+
deps.emit(stage.projectId, tag(event));
|
|
84
91
|
},
|
|
85
92
|
});
|
|
93
|
+
progress.flush();
|
|
94
|
+
progress.close();
|
|
86
95
|
conclude(stage, result === "held" ? "pending" : "done", null);
|
|
87
96
|
}
|
|
88
97
|
catch (error) {
|
|
98
|
+
progress.close();
|
|
89
99
|
if (controller.signal.aborted) {
|
|
90
100
|
// A late rejection from the aborted call is how a stage learns it was canceled, so
|
|
91
101
|
// it is not logged as a fault.
|
|
@@ -96,7 +106,7 @@ export function createRunner(deps) {
|
|
|
96
106
|
deps.log.write("error", "stage.failed", {
|
|
97
107
|
projectId: stage.projectId,
|
|
98
108
|
stage: stage.kind,
|
|
99
|
-
detail:
|
|
109
|
+
detail: causedBy(error),
|
|
100
110
|
});
|
|
101
111
|
conclude(stage, "failed", reasonOf(error));
|
|
102
112
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// The most progress a single stage invocation reports per second. A stage may call emit on
|
|
2
|
+
// every chunk it reads (a 95 MB download is ~6,000 of them) and each report writes the
|
|
3
|
+
// database and reaches every open page; nobody can read a meter faster than this.
|
|
4
|
+
export const progressIntervalMs = 500;
|
|
5
|
+
// Repeats of the last value are dropped. The first report of a window goes out at once and
|
|
6
|
+
// the newest one offered inside it goes out when the window ends, so a meter never sits on
|
|
7
|
+
// a stale value longer than one interval.
|
|
8
|
+
export function progressGate(send, intervalMs = progressIntervalMs) {
|
|
9
|
+
let last;
|
|
10
|
+
let held;
|
|
11
|
+
let timer;
|
|
12
|
+
let closed = false;
|
|
13
|
+
const same = (a, b) => a !== undefined && a.current === b.current && a.total === b.total;
|
|
14
|
+
const deliver = (event) => {
|
|
15
|
+
last = event;
|
|
16
|
+
send(event);
|
|
17
|
+
timer = setTimeout(() => {
|
|
18
|
+
timer = undefined;
|
|
19
|
+
const next = held;
|
|
20
|
+
held = undefined;
|
|
21
|
+
if (next !== undefined && !closed && !same(last, next))
|
|
22
|
+
deliver(next);
|
|
23
|
+
}, intervalMs);
|
|
24
|
+
timer.unref?.();
|
|
25
|
+
};
|
|
26
|
+
return {
|
|
27
|
+
offer: (event) => {
|
|
28
|
+
if (closed)
|
|
29
|
+
return;
|
|
30
|
+
if (timer !== undefined) {
|
|
31
|
+
held = event;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (!same(last, event))
|
|
35
|
+
deliver(event);
|
|
36
|
+
},
|
|
37
|
+
flush: () => {
|
|
38
|
+
if (timer !== undefined)
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
timer = undefined;
|
|
41
|
+
const next = held;
|
|
42
|
+
held = undefined;
|
|
43
|
+
if (next !== undefined && !closed && !same(last, next)) {
|
|
44
|
+
last = next;
|
|
45
|
+
send(next);
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
close: () => {
|
|
49
|
+
closed = true;
|
|
50
|
+
held = undefined;
|
|
51
|
+
if (timer !== undefined)
|
|
52
|
+
clearTimeout(timer);
|
|
53
|
+
timer = undefined;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -5,6 +5,7 @@ import { serve } from "@hono/node-server";
|
|
|
5
5
|
import ffmpegStatic from "ffmpeg-static";
|
|
6
6
|
import { buildRegistry } from "./adapter-registry.js";
|
|
7
7
|
import { alignSubtitles } from "./adapters/alignment/index.js";
|
|
8
|
+
import { prefetchModel } from "./adapters/alignment/prefetch.js";
|
|
8
9
|
import { prepareFfmpeg } from "./adapters/ffmpeg.js";
|
|
9
10
|
import { createHostCliClient } from "./adapters/host-cli/index.js";
|
|
10
11
|
import { nodeRunCli } from "./adapters/llm/run-cli.js";
|
|
@@ -21,10 +22,11 @@ import { systemClock } from "./kernel/clock.js";
|
|
|
21
22
|
import { openDb } from "./kernel/db/index.js";
|
|
22
23
|
import { migrate } from "./kernel/db/migrate.js";
|
|
23
24
|
import { transact } from "./kernel/db/tx.js";
|
|
25
|
+
import { causedBy } from "./kernel/errors.js";
|
|
24
26
|
import { ulidIds } from "./kernel/ids.js";
|
|
25
27
|
import { acquireInstanceLock } from "./kernel/lock.js";
|
|
26
28
|
import { openLog } from "./kernel/log.js";
|
|
27
|
-
import { ensureDirs, layout } from "./kernel/paths.js";
|
|
29
|
+
import { ensureDirs, layout, subtitleModelDir } from "./kernel/paths.js";
|
|
28
30
|
import { sqliteAttempts } from "./kernel/runner/attempt-repo.js";
|
|
29
31
|
import { createCheckpointAuthority, } from "./kernel/runner/checkpoint-authority.js";
|
|
30
32
|
import { createRunner } from "./kernel/runner/index.js";
|
|
@@ -109,7 +111,7 @@ export function createScheduleTickLifecycle(deps) {
|
|
|
109
111
|
},
|
|
110
112
|
};
|
|
111
113
|
}
|
|
112
|
-
export async function boot(config) {
|
|
114
|
+
export async function boot(config, options = {}) {
|
|
113
115
|
const clock = systemClock;
|
|
114
116
|
const ids = ulidIds;
|
|
115
117
|
const paths = layout(config.dataDir);
|
|
@@ -364,6 +366,18 @@ export async function boot(config) {
|
|
|
364
366
|
// Whatever last run left queued goes out at start. Nothing waits for
|
|
365
367
|
// it, and an unreachable collector costs one refused socket.
|
|
366
368
|
flusher.soon();
|
|
369
|
+
const modelPrefetch = new AbortController();
|
|
370
|
+
const prefetching = options.prefetchSubtitleModel
|
|
371
|
+
? prefetchModel({
|
|
372
|
+
cacheDir: subtitleModelDir(paths.dataDir),
|
|
373
|
+
seed: options.subtitleModelSeed,
|
|
374
|
+
signal: modelPrefetch.signal,
|
|
375
|
+
}).then(() => log.write("info", "subtitle-model.ready"), (error) => {
|
|
376
|
+
// Not fatal: the first captioned render tries again and says why on its row.
|
|
377
|
+
if (!modelPrefetch.signal.aborted)
|
|
378
|
+
log.write("warn", "subtitle-model.prefetch", { detail: causedBy(error) });
|
|
379
|
+
})
|
|
380
|
+
: Promise.resolve();
|
|
367
381
|
const open = db;
|
|
368
382
|
let stopping;
|
|
369
383
|
let stopActivation = () => { };
|
|
@@ -376,6 +390,7 @@ export async function boot(config) {
|
|
|
376
390
|
const serverClose = beginServerClose(server);
|
|
377
391
|
stopActivation();
|
|
378
392
|
audioPreviews.close();
|
|
393
|
+
modelPrefetch.abort();
|
|
379
394
|
try {
|
|
380
395
|
// Admission closes before the listener. Requests already admitted keep the
|
|
381
396
|
// database until their response settles; anything racing shutdown receives 503.
|
|
@@ -394,6 +409,7 @@ export async function boot(config) {
|
|
|
394
409
|
// closes and fail to mark its batch delivered, which costs one re-send that
|
|
395
410
|
// the collector deduplicates by event id.
|
|
396
411
|
flusher.stop();
|
|
412
|
+
await prefetching;
|
|
397
413
|
log.write("info", "shutdown");
|
|
398
414
|
open.close();
|
|
399
415
|
lock.release();
|
|
@@ -94,12 +94,12 @@ export function invocationReady(deps, work) {
|
|
|
94
94
|
});
|
|
95
95
|
});
|
|
96
96
|
}
|
|
97
|
-
export function executionStandings(deps, projectId) {
|
|
97
|
+
export function executionStandings(deps, projectId, derived) {
|
|
98
98
|
const revisionId = currentRevisionId(deps.db, projectId);
|
|
99
99
|
const view = revisionId === undefined ? undefined : getRevisionView(deps, projectId, revisionId);
|
|
100
100
|
if (view === undefined)
|
|
101
101
|
return [];
|
|
102
|
-
const work = executionStages(deps, projectId);
|
|
102
|
+
const work = derived ?? executionStages(deps, projectId);
|
|
103
103
|
return stageKinds.map((kind) => {
|
|
104
104
|
const source = view.revision.config.sources[kind];
|
|
105
105
|
const group = work
|
|
@@ -136,20 +136,18 @@ export function executionStandings(deps, projectId) {
|
|
|
136
136
|
});
|
|
137
137
|
}
|
|
138
138
|
export function projectStandings(deps, projectId) {
|
|
139
|
-
|
|
139
|
+
// One derivation serves every stage: each builds the whole execution plan.
|
|
140
|
+
const work = executionStages(deps, projectId);
|
|
141
|
+
const standings = executionStandings(deps, projectId, work);
|
|
140
142
|
for (const stage of standings) {
|
|
141
|
-
const invocations =
|
|
142
|
-
const current = invocations.reduce((total,
|
|
143
|
-
if (
|
|
143
|
+
const invocations = work.filter((entry) => entry.kind === stage.kind);
|
|
144
|
+
const current = invocations.reduce((total, entry) => {
|
|
145
|
+
if (entry.state === "done")
|
|
144
146
|
return total + 1;
|
|
145
147
|
const progress = deps.db
|
|
146
148
|
.prepare("SELECT progress_current,progress_total FROM revision_work WHERE id=?")
|
|
147
|
-
.get(
|
|
148
|
-
|
|
149
|
-
typeof progress.progress_total !== "number" ||
|
|
150
|
-
progress.progress_total <= 0)
|
|
151
|
-
return total;
|
|
152
|
-
return total + Math.max(0, Math.min(1, progress.progress_current / progress.progress_total));
|
|
149
|
+
.get(entry.work.workId);
|
|
150
|
+
return total + fraction(progress?.progress_current, progress?.progress_total);
|
|
153
151
|
}, 0);
|
|
154
152
|
const failed = invocations.find((work) => work.state === "failed");
|
|
155
153
|
const failure = failed === undefined
|
|
@@ -163,6 +161,11 @@ export function projectStandings(deps, projectId) {
|
|
|
163
161
|
}
|
|
164
162
|
settleScheduleRunsForProject(deps.db, projectId, deps.clock.now().toISOString());
|
|
165
163
|
}
|
|
164
|
+
// Progress moves one number and nothing else. The stage row already counts this invocation's
|
|
165
|
+
// last fraction, from the claim or the previous event, so it takes the difference instead
|
|
166
|
+
// of re-deriving the project: that derivation rebuilds every stage's plan and ran to seconds
|
|
167
|
+
// on a long article, which froze the process once per downloaded chunk. State changes still
|
|
168
|
+
// go through projectStandings.
|
|
166
169
|
export function recordWorkProgress(deps, event) {
|
|
167
170
|
if (event.workId === undefined ||
|
|
168
171
|
event.revisionId === undefined ||
|
|
@@ -170,8 +173,35 @@ export function recordWorkProgress(deps, event) {
|
|
|
170
173
|
!Number.isFinite(event.total) ||
|
|
171
174
|
event.total <= 0)
|
|
172
175
|
return;
|
|
176
|
+
const before = deps.db
|
|
177
|
+
.prepare("SELECT progress_current,progress_total FROM revision_work WHERE id=? AND revision_id=? AND project_id=? AND kind=? AND state='running'")
|
|
178
|
+
.get(event.workId, event.revisionId, event.projectId, event.stage);
|
|
179
|
+
if (before === undefined)
|
|
180
|
+
return;
|
|
181
|
+
const current = Math.max(0, Math.min(event.current, event.total));
|
|
173
182
|
deps.db
|
|
174
|
-
.prepare("UPDATE revision_work SET progress_current=?,progress_total=? WHERE id=?
|
|
175
|
-
.run(
|
|
176
|
-
|
|
183
|
+
.prepare("UPDATE revision_work SET progress_current=?,progress_total=? WHERE id=?")
|
|
184
|
+
.run(current, event.total, event.workId);
|
|
185
|
+
// Work an edit retired keeps running until it notices, but the stage row belongs to the
|
|
186
|
+
// current revision's work and must not move with it.
|
|
187
|
+
const head = currentRevisionId(deps.db, event.projectId);
|
|
188
|
+
if (head === undefined ||
|
|
189
|
+
deps.db
|
|
190
|
+
.prepare("SELECT 1 FROM revision_work_reservations WHERE work_id=? AND revision_id=? LIMIT 1")
|
|
191
|
+
.get(event.workId, head) === undefined)
|
|
192
|
+
return;
|
|
193
|
+
const delta = current / event.total - fraction(before.progress_current, before.progress_total);
|
|
194
|
+
if (delta === 0)
|
|
195
|
+
return;
|
|
196
|
+
const moved = deps.db
|
|
197
|
+
.prepare("UPDATE stages SET progress_current=MIN(progress_total,MAX(0,progress_current+?)) WHERE project_id=? AND kind=? AND progress_current IS NOT NULL AND progress_total IS NOT NULL")
|
|
198
|
+
.run(delta, event.projectId, event.stage);
|
|
199
|
+
// A row no derivation has counted yet has nothing to adjust: derive it once.
|
|
200
|
+
if (moved.changes === 0)
|
|
201
|
+
projectStandings(deps, event.projectId);
|
|
202
|
+
}
|
|
203
|
+
function fraction(current, total) {
|
|
204
|
+
if (typeof current !== "number" || typeof total !== "number" || total <= 0)
|
|
205
|
+
return 0;
|
|
206
|
+
return Math.max(0, Math.min(1, current / total));
|
|
177
207
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
3
2
|
import { z } from "zod";
|
|
3
|
+
import { subtitleModelDir } from "../../kernel/paths.js";
|
|
4
4
|
import { resolveFont } from "../fonts/index.js";
|
|
5
5
|
import { validateCues } from "../revisions/rules.js";
|
|
6
6
|
import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
|
|
@@ -59,7 +59,7 @@ async function timing(deps, context, piece, snapshot) {
|
|
|
59
59
|
const aligned = await deps.alignSubtitles({
|
|
60
60
|
audioPath: segment.path,
|
|
61
61
|
text: revisionTranscript(deps, snapshot, segment.kind),
|
|
62
|
-
cacheDir:
|
|
62
|
+
cacheDir: subtitleModelDir(deps.paths.dataDir),
|
|
63
63
|
ffmpeg: deps.ffmpeg,
|
|
64
64
|
signal: context.signal,
|
|
65
65
|
onOmission: (value) => omissions.push({ ...value, start: value.start + offset }),
|
|
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { copyFileSync, createReadStream, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { join, relative, sep } from "node:path";
|
|
4
4
|
import { z } from "zod";
|
|
5
|
+
import { subtitleModelDir } from "../../kernel/paths.js";
|
|
5
6
|
import { projectById, setStageProgress } from "../admission/repo.js";
|
|
6
7
|
import { resolveFont } from "../fonts/index.js";
|
|
7
8
|
import { outputPath, projectDir } from "../storage/layout.js";
|
|
@@ -128,7 +129,7 @@ async function alignSegments(deps, context, segments, omissions) {
|
|
|
128
129
|
audioPath: segment.path,
|
|
129
130
|
onOmission: (omission) => omissions.push({ ...omission, start: omission.start + offset }),
|
|
130
131
|
text: segment.text,
|
|
131
|
-
cacheDir:
|
|
132
|
+
cacheDir: subtitleModelDir(deps.paths.dataDir),
|
|
132
133
|
ffmpeg: deps.ffmpeg,
|
|
133
134
|
signal: context.signal,
|
|
134
135
|
onProgress: (current, maximum) => {
|