@gentbajko/slopify 3.20.0 → 3.21.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/SUBTITLES.md +10 -2
- package/dist/adapters/alignment/cache.js +36 -9
- package/dist/adapters/alignment/index.js +16 -4
- package/dist/adapters/alignment/prefetch.js +11 -6
- package/dist/edge/http/settings.js +6 -1
- package/dist/edge/mcp/tools-setup.js +2 -2
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/kernel/ports/subtitles.js +6 -0
- package/dist/main.js +6 -2
- package/dist/patch-notes/3.21.0.md +11 -0
- package/dist/patch-notes/index.json +6 -0
- package/dist/slices/settings/model.js +2 -0
- package/dist/slices/settings/playback.js +11 -0
- package/dist/tutorials/FAQ.md +1 -1
- package/dist/tutorials/Install.md +3 -3
- package/dist/tutorials/Other-Languages.md +1 -1
- package/dist/tutorials/Play-Video-and-Style.md +1 -1
- package/dist/tutorials/Settings-Reference.md +9 -1
- package/dist/tutorials/Troubleshooting.md +1 -1
- package/dist/tutorials/Video-Editing.md +1 -1
- package/dist/web/assets/{index-DNPct8pl.js → index-wwRT6LH3.js} +136 -136
- package/dist/web/assets/{pdf-tyJWWiKA.js → pdf-DH2f4NNx.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
package/SUBTITLES.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
1. In Play, enable narration or provide matching English audio and article text.
|
|
4
4
|
2. In Subtitles, choose **Subtitle files (.srt + .vtt)** or **Burn into video + files**.
|
|
5
5
|
3. For burned captions, select a bundled or system font, or upload a `.ttf` or `.otf` file (up to 32 MiB). Choose a size from 16 to 120 and a position: top, upper-middle, center, lower-middle or bottom. The side preview shows the selected font, size and position in a 16:9 or 9:16 frame.
|
|
6
|
-
4. Start the run. Slopify times the article against the actual narration on your computer. The first use downloads
|
|
6
|
+
4. Start the run. Slopify times the article against the actual narration on your computer. The first use downloads the English speech model chosen in Settings → Production defaults → **Subtitle timing model** (378 MB unless changed); later runs work offline with the cached model. Allow roughly 1.2 GB of available memory during alignment (0.7 GB with Smallest). No extra API key, Python, or compiler is needed.
|
|
7
7
|
5. Download SRT/VTT beside the final export. Files mode adds an optional native caption track to the in-app video preview. Burned captions remain visible in the downloaded MP4.
|
|
8
8
|
|
|
9
9
|
For a completed project, open its final Video or Audio export section, set subtitles, and click **Save subtitles**. This rebuilds only the local export from saved narration and images. Changing font, size or position reuses word timing when the audio and spoken text are unchanged. A paused run saves these choices until Resume; pause an active run before editing. Failed alignment or rendering keeps the previous finished export.
|
|
@@ -18,6 +18,14 @@ Fonts are copied into the completed project's caption assets, so an existing exp
|
|
|
18
18
|
|
|
19
19
|
Speech inference uses MIT-licensed `onnxruntime-node@1.30.0` (native CPU) in a separate process, with `min(8, CPUs − 1)` threads; set `SLOPIFY_SUBTITLE_THREADS` to override. The npm package bundles CPU builds for Linux x64/arm64, Windows x64/arm64 and Apple silicon macOS (about 220 MB unpacked); Intel Macs have no build, so subtitle timing falls back to the slower single-threaded `onnxruntime-web` there. Its postinstall script only fetches optional CUDA files on Linux x64 and is not needed (the Docker image installs with `--ignore-scripts`); model weights are downloaded lazily into `<data-dir>/models/english-subtitles/`.
|
|
20
20
|
|
|
21
|
-
Model: [Xenova/wav2vec2-base-960h](https://huggingface.co/Xenova/wav2vec2-base-960h/tree/a19f851b3d42865797e410752b4c570c871e4825), an ONNX conversion of [facebook/wav2vec2-base-960h](https://huggingface.co/facebook/wav2vec2-base-960h), licensed Apache-2.0. Pinned revision: `a19f851b3d42865797e410752b4c570c871e4825`.
|
|
21
|
+
Model: [Xenova/wav2vec2-base-960h](https://huggingface.co/Xenova/wav2vec2-base-960h/tree/a19f851b3d42865797e410752b4c570c871e4825), an ONNX conversion of [facebook/wav2vec2-base-960h](https://huggingface.co/facebook/wav2vec2-base-960h), licensed Apache-2.0. Pinned revision: `a19f851b3d42865797e410752b4c570c871e4825`. Three precisions of that revision can be chosen per computer, each verified before use and kept under its own file name so they can sit side by side:
|
|
22
|
+
|
|
23
|
+
| Setting | File | Bytes | SHA256 |
|
|
24
|
+
|---|---|---|---|
|
|
25
|
+
| Most accurate (default) | `onnx/model.onnx` (fp32) | 377,887,594 | `e46614273f03ff4b87923a965e417fa72004825522cb007c9c25633b8475490d` |
|
|
26
|
+
| Balanced | `onnx/model_fp16.onnx` | 189,192,204 | `378348ee38b739cc77e77e3fb8502f0f40ed53da0dbf7401b24c25c8fafe03de` |
|
|
27
|
+
| Smallest | `onnx/model_quantized.onnx` (int8) | 95,286,046 | `cd5040c147381580ed73258143dd8e0c28e800a09e74ee42ee2b3e8cb4d760a3` |
|
|
28
|
+
|
|
29
|
+
On a 2 h 28 min narration fp32 aligned in 2 min 40 s (1.1 GB peak) and fp16 in 3 min 16 s (1.2 GB), both hearing every line; int8 took 5 min 35 s (0.7 GB) and misheard two lines, stopping the run with a false "Subtitles stopped matching the audio".
|
|
22
30
|
|
|
23
31
|
The bundled Barlow font uses the SIL Open Font License; its license and source record ship in `dist/assets/fonts/`. Uploaded/system font licensing remains with its author.
|
|
@@ -5,13 +5,36 @@ import { join } from "node:path";
|
|
|
5
5
|
import { Readable } from "node:stream";
|
|
6
6
|
import { pipeline } from "node:stream/promises";
|
|
7
7
|
import { setTimeout as delay } from "node:timers/promises";
|
|
8
|
+
import { defaultEnglishSubtitleModel, } from "../../kernel/ports/subtitles.js";
|
|
9
|
+
// Xenova/wav2vec2-base-960h at one pinned revision, which carries the same model at three
|
|
10
|
+
// precisions. Each keeps its own file name so the cache can hold any of them at once; int8
|
|
11
|
+
// keeps the name every install used before the choice existed.
|
|
8
12
|
const revision = "a19f851b3d42865797e410752b4c570c871e4825";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
13
|
+
const repo = `https://huggingface.co/Xenova/wav2vec2-base-960h/resolve/${revision}/onnx`;
|
|
14
|
+
export const englishModels = {
|
|
15
|
+
fp32: {
|
|
16
|
+
filename: "wav2vec2-base-960h-fp32-e4661427.onnx",
|
|
17
|
+
url: `${repo}/model.onnx`,
|
|
18
|
+
bytes: 377_887_594,
|
|
19
|
+
sha256: "e46614273f03ff4b87923a965e417fa72004825522cb007c9c25633b8475490d",
|
|
20
|
+
timeoutMs: 1_800_000,
|
|
21
|
+
},
|
|
22
|
+
fp16: {
|
|
23
|
+
filename: "wav2vec2-base-960h-fp16-378348ee.onnx",
|
|
24
|
+
url: `${repo}/model_fp16.onnx`,
|
|
25
|
+
bytes: 189_192_204,
|
|
26
|
+
sha256: "378348ee38b739cc77e77e3fb8502f0f40ed53da0dbf7401b24c25c8fafe03de",
|
|
27
|
+
timeoutMs: 1_200_000,
|
|
28
|
+
},
|
|
29
|
+
int8: {
|
|
30
|
+
filename: "wav2vec2-base-960h-cd5040c1.onnx",
|
|
31
|
+
url: `${repo}/model_quantized.onnx`,
|
|
32
|
+
bytes: 95_286_046,
|
|
33
|
+
sha256: "cd5040c147381580ed73258143dd8e0c28e800a09e74ee42ee2b3e8cb4d760a3",
|
|
34
|
+
},
|
|
14
35
|
};
|
|
36
|
+
// The default English model, and the one the Docker image ships as its seed.
|
|
37
|
+
export const alignmentModel = englishModels[defaultEnglishSubtitleModel];
|
|
15
38
|
export async function prepareModel(input, deps = { model: alignmentModel, fetch: globalThis.fetch }) {
|
|
16
39
|
input.signal.throwIfAborted();
|
|
17
40
|
await mkdir(input.cacheDir, { recursive: true, mode: 0o700 });
|
|
@@ -38,7 +61,7 @@ export async function prepareModel(input, deps = { model: alignmentModel, fetch:
|
|
|
38
61
|
}
|
|
39
62
|
}
|
|
40
63
|
}
|
|
41
|
-
throw new Error(
|
|
64
|
+
throw new Error(`The free subtitle model download (${megabytes(deps.model.bytes)}) was interrupted after 3 attempts, so the captions could not be timed. Check your internet connection, then Continue the run.${deps.model === alignmentModel || deps.model === englishModels.fp16 ? smaller : ""}`, { cause: lastFailure });
|
|
42
65
|
}
|
|
43
66
|
async function downloadModel(input, deps, target) {
|
|
44
67
|
input.signal.throwIfAborted();
|
|
@@ -58,7 +81,7 @@ async function downloadModel(input, deps, target) {
|
|
|
58
81
|
throw interrupted(error);
|
|
59
82
|
}
|
|
60
83
|
if (!response.ok || response.body === null) {
|
|
61
|
-
const error = new Error(`The free subtitle model could not be downloaded (HTTP ${response.status}). Check your connection
|
|
84
|
+
const error = new Error(`The free subtitle model could not be downloaded (HTTP ${response.status}) from Hugging Face, so the captions could not be timed. Check your internet connection, then Continue the run.`);
|
|
62
85
|
let failure = error;
|
|
63
86
|
try {
|
|
64
87
|
if (response.body !== null)
|
|
@@ -91,7 +114,7 @@ async function downloadModel(input, deps, target) {
|
|
|
91
114
|
break;
|
|
92
115
|
current += result.value.length;
|
|
93
116
|
if (current > deps.model.bytes)
|
|
94
|
-
throw new Error("The subtitle model failed download size verification.");
|
|
117
|
+
throw new Error("The subtitle model failed download size verification: more arrived than the file Slopify expects, so it was thrown away. Continue the run to download it again.");
|
|
95
118
|
hash.update(result.value);
|
|
96
119
|
input.onProgress?.(current, deps.model.bytes);
|
|
97
120
|
yield result.value;
|
|
@@ -112,7 +135,7 @@ async function downloadModel(input, deps, target) {
|
|
|
112
135
|
});
|
|
113
136
|
input.signal.throwIfAborted();
|
|
114
137
|
if (current !== deps.model.bytes || hash.digest("hex") !== deps.model.sha256)
|
|
115
|
-
throw new Error("The subtitle model failed download verification.
|
|
138
|
+
throw new Error("The subtitle model failed download verification: the file that arrived is not the one Slopify expects, so it was thrown away. Continue the run to download it again.");
|
|
116
139
|
// Each caller writes in its own directory. On Windows a completed concurrent download
|
|
117
140
|
// may already own the final filename; reuse it only after the same verification.
|
|
118
141
|
try {
|
|
@@ -146,6 +169,10 @@ async function seedModel(input, seed, target) {
|
|
|
146
169
|
await rm(staging, { recursive: true, force: true });
|
|
147
170
|
}
|
|
148
171
|
}
|
|
172
|
+
const smaller = " To download less, pick a smaller model in Settings → Production defaults → Subtitle timing model.";
|
|
173
|
+
function megabytes(bytes) {
|
|
174
|
+
return `${String(Math.round(bytes / 1_000_000))} MB`;
|
|
175
|
+
}
|
|
149
176
|
function interrupted(cause) {
|
|
150
177
|
const error = new Error("The subtitle model download was interrupted.", { cause });
|
|
151
178
|
error.name = "SubtitleModelDownloadInterrupted";
|
|
@@ -1,13 +1,25 @@
|
|
|
1
1
|
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { languageInfo } from "../../kernel/ports/languages.js";
|
|
4
|
+
import { defaultEnglishSubtitleModel, } from "../../kernel/ports/subtitles.js";
|
|
4
5
|
import { decodeAudio } from "./audio.js";
|
|
5
|
-
import {
|
|
6
|
+
import { englishModels, prepareModel } from "./cache.js";
|
|
6
7
|
import { claimWorker } from "./lock.js";
|
|
7
8
|
import { alignmentSpecFor, multilingualModel } from "./multilingual.js";
|
|
8
9
|
import { runAlignmentWorker } from "./runner.js";
|
|
9
10
|
import { sentenceTiming, speechShape } from "./sentences.js";
|
|
10
|
-
|
|
11
|
+
// The model file that times a language: English with the model chosen in Settings, every other
|
|
12
|
+
// word-timed language with the multilingual one.
|
|
13
|
+
export function modelFor(language, english) {
|
|
14
|
+
return language === undefined || language === "en" ? englishModels[english] : multilingualModel;
|
|
15
|
+
}
|
|
16
|
+
// `chosen` is asked once per alignment, when it starts: an alignment already running keeps the
|
|
17
|
+
// model it opened, and the next one uses whatever Settings says then.
|
|
18
|
+
export function alignSubtitlesWith(chosen) {
|
|
19
|
+
return (request) => align(request, chosen);
|
|
20
|
+
}
|
|
21
|
+
export const alignSubtitles = alignSubtitlesWith(() => defaultEnglishSubtitleModel);
|
|
22
|
+
async function align(request, chosen) {
|
|
11
23
|
request.signal.throwIfAborted();
|
|
12
24
|
if (languageInfo(request.language).timing === "sentences")
|
|
13
25
|
return timeSentences(request);
|
|
@@ -20,7 +32,7 @@ export const alignSubtitles = async (request) => {
|
|
|
20
32
|
cacheDir: request.cacheDir,
|
|
21
33
|
signal: request.signal,
|
|
22
34
|
onProgress: (current, total) => request.onProgress?.(Math.round((current / total) * 20), 100),
|
|
23
|
-
}, { model:
|
|
35
|
+
}, { model: modelFor(request.language, chosen()), fetch: globalThis.fetch });
|
|
24
36
|
request.onProgress?.(20, 100);
|
|
25
37
|
const working = await mkdtemp(join(request.cacheDir, ".alignment-audio-"));
|
|
26
38
|
try {
|
|
@@ -42,7 +54,7 @@ export const alignSubtitles = async (request) => {
|
|
|
42
54
|
finally {
|
|
43
55
|
await release();
|
|
44
56
|
}
|
|
45
|
-
}
|
|
57
|
+
}
|
|
46
58
|
// No model and no download: the narration's loudness says where it pauses, and the sentences
|
|
47
59
|
// are laid over it (`sentences.ts`).
|
|
48
60
|
async function timeSentences(request) {
|
|
@@ -1,16 +1,21 @@
|
|
|
1
1
|
import { mkdir, readFile, rm } from "node:fs/promises";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { defaultEnglishSubtitleModel, } from "../../kernel/ports/subtitles.js";
|
|
4
|
+
import { englishModels, prepareModel } from "./cache.js";
|
|
4
5
|
import { claimWorker } from "./lock.js";
|
|
5
|
-
// Readies the subtitle model when the app starts, so the first captioned render
|
|
6
|
-
// on a
|
|
7
|
-
// starts meanwhile waits for this copy rather than fetching a second one.
|
|
8
|
-
export async function prefetchModel(input, deps) {
|
|
6
|
+
// Readies the chosen English subtitle model when the app starts, so the first captioned render
|
|
7
|
+
// never waits on a download of up to 378 MB in the middle of a run. It takes the worker lock,
|
|
8
|
+
// so a render that starts meanwhile waits for this copy rather than fetching a second one.
|
|
9
|
+
export async function prefetchModel(input, deps = {}) {
|
|
9
10
|
await mkdir(input.cacheDir, { recursive: true, mode: 0o700 });
|
|
10
11
|
await clearOwnLock(input.cacheDir);
|
|
11
12
|
const release = await claimWorker(input.cacheDir, input.signal);
|
|
12
13
|
try {
|
|
13
|
-
return await prepareModel({ cacheDir: input.cacheDir, seed: input.seed, signal: input.signal },
|
|
14
|
+
return await prepareModel({ cacheDir: input.cacheDir, seed: input.seed, signal: input.signal }, {
|
|
15
|
+
...deps,
|
|
16
|
+
model: deps.model ?? englishModels[input.choice ?? defaultEnglishSubtitleModel],
|
|
17
|
+
fetch: deps.fetch ?? globalThis.fetch,
|
|
18
|
+
});
|
|
14
19
|
}
|
|
15
20
|
finally {
|
|
16
21
|
await release();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { zValidator } from "@hono/zod-validator";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { englishSubtitleModels } from "../../kernel/ports/subtitles.js";
|
|
4
5
|
import { lastAgentConnection } from "../../slices/agents/connection.js";
|
|
5
6
|
import { recentAgentActions } from "../../slices/agents/repo.js";
|
|
6
7
|
import { notificationUrlMax, notificationUrlProblem, testNotice, webhookBody, } from "../../slices/notifications/address.js";
|
|
@@ -42,6 +43,8 @@ const playbackBody = z.object({
|
|
|
42
43
|
loudness: z
|
|
43
44
|
.object({ enabled: z.boolean(), videoLufs: z.number(), audioFilesLufs: z.number() })
|
|
44
45
|
.optional(),
|
|
46
|
+
// Absent keeps what is saved, as with loudness.
|
|
47
|
+
subtitleModel: z.enum(englishSubtitleModels).optional(),
|
|
45
48
|
});
|
|
46
49
|
// The return type is inferred so Hono keeps the route types the SPA's client is
|
|
47
50
|
// generated from; see stagingRoutes.
|
|
@@ -53,9 +56,11 @@ export function settingsRoutes(deps) {
|
|
|
53
56
|
.get("/", (c) => c.json(readSettings(playback)))
|
|
54
57
|
.put("/", zValidator("json", playbackBody, onInvalid), (c) => {
|
|
55
58
|
const body = c.req.valid("json");
|
|
59
|
+
const saved = readSettings(playback);
|
|
56
60
|
const result = saveSettings(playback, {
|
|
57
61
|
...body,
|
|
58
|
-
loudness: body.loudness ??
|
|
62
|
+
loudness: body.loudness ?? saved.loudness,
|
|
63
|
+
subtitleModel: body.subtitleModel ?? saved.subtitleModel,
|
|
59
64
|
});
|
|
60
65
|
if (!result.ok) {
|
|
61
66
|
return problem(c, {
|
|
@@ -52,13 +52,13 @@ const settings = [
|
|
|
52
52
|
name: "get_settings",
|
|
53
53
|
route: "GET /api/settings",
|
|
54
54
|
title: "Read production defaults",
|
|
55
|
-
description: "Settings → Production defaults: silence gap, appearance and
|
|
55
|
+
description: "Settings → Production defaults: silence gap, appearance, loudness and the English subtitle timing model.",
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
name: "update_settings",
|
|
59
59
|
route: "PUT /api/settings",
|
|
60
60
|
title: "Save production defaults",
|
|
61
|
-
description: "Body: {silenceGapSeconds, appearance: system | light | dark, loudness?: {enabled, videoLufs, audioFilesLufs}}.",
|
|
61
|
+
description: "Body: {silenceGapSeconds, appearance: system | light | dark, loudness?: {enabled, videoLufs, audioFilesLufs}, subtitleModel?: fp32 | fp16 | int8}. subtitleModel is this computer's English caption-timing model (fp32 378 MB most accurate, the default; fp16 189 MB; int8 95 MB, less accurate); it never marks subtitles outdated.",
|
|
62
62
|
},
|
|
63
63
|
{
|
|
64
64
|
name: "get_default_channel_links",
|
|
Binary file
|
|
Binary file
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
// The English timing model, chosen per computer in Settings → Production defaults: the same
|
|
2
|
+
// pinned wav2vec2 conversion at three precisions. Bigger hears more accurately and, on most
|
|
3
|
+
// computers, also runs faster. The choice never enters a fingerprint: every precision times the
|
|
4
|
+
// same words, so switching must not mark finished subtitles outdated.
|
|
5
|
+
export const englishSubtitleModels = ["fp32", "fp16", "int8"];
|
|
6
|
+
export const defaultEnglishSubtitleModel = "fp32";
|
|
1
7
|
// Where the audio stopped matching its transcript: seconds into the aligned file, the
|
|
2
8
|
// transcript words it expected there and what the model heard instead (either may be empty).
|
|
3
9
|
// The aligner throws it so the caller can say which part of the narration to redo.
|
package/dist/main.js
CHANGED
|
@@ -5,7 +5,7 @@ import { fileURLToPath } from "node:url";
|
|
|
5
5
|
import { serve } from "@hono/node-server";
|
|
6
6
|
import ffmpegStatic from "ffmpeg-static";
|
|
7
7
|
import { buildRegistry } from "./adapter-registry.js";
|
|
8
|
-
import {
|
|
8
|
+
import { alignSubtitlesWith } from "./adapters/alignment/index.js";
|
|
9
9
|
import { prefetchModel } from "./adapters/alignment/prefetch.js";
|
|
10
10
|
import { prepareFfmpeg } from "./adapters/ffmpeg.js";
|
|
11
11
|
import { createHostCliClient } from "./adapters/host-cli/index.js";
|
|
@@ -72,6 +72,7 @@ import { settleTerminalScheduleRuns } from "./slices/schedules/repo.js";
|
|
|
72
72
|
import { createScheduleRunner } from "./slices/schedules/scheduler.js";
|
|
73
73
|
import { nodeCliProbe } from "./slices/settings/cli-status.js";
|
|
74
74
|
import { isLocalCliProvider, localCliConcurrency } from "./slices/settings/model.js";
|
|
75
|
+
import { readSettings } from "./slices/settings/playback.js";
|
|
75
76
|
import { providerStatuses } from "./slices/settings/readiness.js";
|
|
76
77
|
import { readSetting } from "./slices/settings/repo.js";
|
|
77
78
|
import { arrangeChanged } from "./slices/storage/arrange.js";
|
|
@@ -692,6 +693,7 @@ export async function boot(config, options = {}) {
|
|
|
692
693
|
? prefetchModel({
|
|
693
694
|
cacheDir: subtitleModelDir(paths.dataDir),
|
|
694
695
|
seed: options.subtitleModelSeed,
|
|
696
|
+
choice: readSettings({ db, log }).subtitleModel,
|
|
695
697
|
signal: modelPrefetch.signal,
|
|
696
698
|
}).then(() => log.write("info", "subtitle-model.ready"), (error) => {
|
|
697
699
|
// Not fatal: the first captioned render tries again and says why on its row.
|
|
@@ -762,7 +764,9 @@ export async function boot(config, options = {}) {
|
|
|
762
764
|
throw error;
|
|
763
765
|
}
|
|
764
766
|
}
|
|
765
|
-
export function wireRunner({ onFinished, audioPreviews, db, paths, clock, ids, log, hub, telemetry, flusher, registry, catalogue, ffmpeg,
|
|
767
|
+
export function wireRunner({ onFinished, audioPreviews, db, paths, clock, ids, log, hub, telemetry, flusher, registry, catalogue, ffmpeg,
|
|
768
|
+
// Settings → Production defaults → Subtitle timing model, read as each alignment starts.
|
|
769
|
+
alignSubtitles: aligner = alignSubtitlesWith(() => readSettings({ db, log }).subtitleModel), }) {
|
|
766
770
|
// A stage counts what it did and the queue is flushed after each new event. `record`
|
|
767
771
|
// swallows its own failures, so this can neither fail a stage nor widen what leaves the
|
|
768
772
|
// machine - the payload allow-list is checked inside it.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Slopify 3.21.0
|
|
2
|
+
|
|
3
|
+
Released 13 October 2026.
|
|
4
|
+
|
|
5
|
+
- **Captions are timed with a more accurate speech model by default.** The old 95 MB model sometimes misheard a line of narration, stopped the run with "Subtitles stopped matching the audio" and recorded a good narration chunk again at a cost. The full-precision version of the same model hears those lines correctly. On a 2½-hour narration it was also faster: 2 min 40 s instead of 5 min 35 s.
|
|
6
|
+
- **Choose the subtitle timing model** in Settings → Production defaults: Most accurate (378 MB, the default), Balanced (189 MB) or Smallest (95 MB, less accurate). The choice belongs to this computer and downloads once, the first time captions are timed.
|
|
7
|
+
|
|
8
|
+
## Upgrading from 3.20.0
|
|
9
|
+
|
|
10
|
+
- The first video you make downloads the 378 MB model once. To keep the smaller model, choose Smallest in Settings → Production defaults.
|
|
11
|
+
- Nothing becomes outdated: the model choice doesn't change any word timings already made.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// The provider catalogue and the settings domain types.
|
|
2
|
+
import { defaultEnglishSubtitleModel, } from "../../kernel/ports/subtitles.js";
|
|
2
3
|
import { defaultLoudness } from "../loudness/model.js";
|
|
3
4
|
// The three families are the three ports, so the set is named beside them.
|
|
4
5
|
export { providerFamilies } from "../../kernel/ports/model.js";
|
|
@@ -135,4 +136,5 @@ export const defaultSettings = {
|
|
|
135
136
|
silenceGapSeconds: 3,
|
|
136
137
|
appearance: "system",
|
|
137
138
|
loudness: defaultLoudness,
|
|
139
|
+
subtitleModel: defaultEnglishSubtitleModel,
|
|
138
140
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { englishSubtitleModels } from "../../kernel/ports/subtitles.js";
|
|
2
3
|
import { silenceGapSecondsMax } from "../admission/rules.js";
|
|
3
4
|
import { loudnessFields } from "../loudness/model.js";
|
|
4
5
|
import { appearances, defaultSettings } from "./model.js";
|
|
@@ -8,8 +9,10 @@ import { readSetting, writeSetting } from "./repo.js";
|
|
|
8
9
|
const silenceGapKey = "silenceGapSeconds";
|
|
9
10
|
const appearanceKey = "appearance";
|
|
10
11
|
const loudnessKey = "loudness";
|
|
12
|
+
const subtitleModelKey = "subtitleModel";
|
|
11
13
|
const silenceGapValue = z.number().int().min(0).max(silenceGapSecondsMax);
|
|
12
14
|
const appearanceValue = z.enum(appearances);
|
|
15
|
+
const subtitleModelValue = z.enum(englishSubtitleModels);
|
|
13
16
|
const loudnessValue = z.object({
|
|
14
17
|
enabled: z.boolean(),
|
|
15
18
|
videoLufs: z.number(),
|
|
@@ -20,6 +23,7 @@ export function readSettings(deps) {
|
|
|
20
23
|
silenceGapSeconds: parsed(deps, silenceGapKey, silenceGapValue, defaultSettings.silenceGapSeconds),
|
|
21
24
|
appearance: parsed(deps, appearanceKey, appearanceValue, defaultSettings.appearance),
|
|
22
25
|
loudness: parsed(deps, loudnessKey, loudnessValue, defaultSettings.loudness),
|
|
26
|
+
subtitleModel: parsed(deps, subtitleModelKey, subtitleModelValue, defaultSettings.subtitleModel),
|
|
23
27
|
};
|
|
24
28
|
}
|
|
25
29
|
export function saveSettings(deps, settings) {
|
|
@@ -39,12 +43,19 @@ export function saveSettings(deps, settings) {
|
|
|
39
43
|
else {
|
|
40
44
|
fields.push(...loudnessFields(settings.loudness));
|
|
41
45
|
}
|
|
46
|
+
if (!subtitleModelValue.safeParse(settings.subtitleModel).success) {
|
|
47
|
+
fields.push({
|
|
48
|
+
field: "subtitleModel",
|
|
49
|
+
message: "Choose Most accurate, Balanced or Smallest in Settings → Production defaults → Subtitle timing model.",
|
|
50
|
+
});
|
|
51
|
+
}
|
|
42
52
|
if (fields.length > 0) {
|
|
43
53
|
return { ok: false, fields };
|
|
44
54
|
}
|
|
45
55
|
writeSetting(deps.db, silenceGapKey, JSON.stringify(settings.silenceGapSeconds));
|
|
46
56
|
writeSetting(deps.db, appearanceKey, JSON.stringify(settings.appearance));
|
|
47
57
|
writeSetting(deps.db, loudnessKey, JSON.stringify(settings.loudness));
|
|
58
|
+
writeSetting(deps.db, subtitleModelKey, JSON.stringify(settings.subtitleModel));
|
|
48
59
|
return { ok: true, settings };
|
|
49
60
|
}
|
|
50
61
|
// A stored value is JSON, and a hand-edited database is the only way it can be anything
|
package/dist/tutorials/FAQ.md
CHANGED
|
@@ -48,7 +48,7 @@ What you write is sent only to the AI providers you pick, because they make the
|
|
|
48
48
|
|
|
49
49
|
### Does it work offline?
|
|
50
50
|
|
|
51
|
-
Partly. The AI steps (article, research, narration, images, thumbnails, descriptions) call providers over the internet, or CLIs that do. Rendering the video, levelling the volume, captions, the style preview and the System voice run on your computer. Captions need a speech model of about
|
|
51
|
+
Partly. The AI steps (article, research, narration, images, thumbnails, descriptions) call providers over the internet, or CLIs that do. Rendering the video, levelling the volume, captions, the style preview and the System voice run on your computer. Captions need a speech model of about 378 MB (smaller ones can be chosen in Settings → Production defaults) that is downloaded once, on first use. Patch notes ship with the app and open offline.
|
|
52
52
|
|
|
53
53
|
## Making and editing videos
|
|
54
54
|
|
|
@@ -11,7 +11,7 @@ Slopify is a local web app you start from a terminal. It runs on your own comput
|
|
|
11
11
|
| Node.js | Version 26 or newer, with npm (it comes with Node). Get it from nodejs.org. |
|
|
12
12
|
| Operating system | Windows, macOS or Linux for the normal install. The managed Docker install is Linux only (see [Docker](Docker)). |
|
|
13
13
|
| ffmpeg | Nothing to install: Slopify brings its own copy. See [ffmpeg](#ffmpeg) below. |
|
|
14
|
-
| Disk space | Enough for your videos. Every project keeps its video, images, narration and working files. The caption model is a 95 MB
|
|
14
|
+
| Disk space | Enough for your videos. Every project keeps its video, images, narration and working files. The caption model is a 378 MB download (95 MB if you pick Smallest in Settings → Production defaults). |
|
|
15
15
|
| Keys or CLIs | Text and images can come from Claude Code, Codex or Gemini CLI, or from API keys. Narration can use a voice provider key, or your computer's own speech for free (the System voice; on Linux, install `espeak-ng`). See [Providers and Keys](Providers-and-Keys) and [AI CLIs](AI-CLIs). |
|
|
16
16
|
|
|
17
17
|
## Choose how to run it
|
|
@@ -71,7 +71,7 @@ A flag beats the environment variable, and the variable beats the default.
|
|
|
71
71
|
| `--no-open` | `SLOPIFY_NO_OPEN` | opens a browser | Doesn't open a browser tab. For the variable, any value except empty, `0` or `false` counts as on. |
|
|
72
72
|
| `--autostart` / `--no-autostart` | | asks once | Turns [Start at Login](Start-at-Login) on or off without asking. Useful in scripts. |
|
|
73
73
|
| | `SLOPIFY_FFMPEG` | the bundled ffmpeg | The full path of another ffmpeg to use. |
|
|
74
|
-
| | `SLOPIFY_NO_MODEL_PREFETCH` | downloads at start | Set to `1` to skip fetching the
|
|
74
|
+
| | `SLOPIFY_NO_MODEL_PREFETCH` | downloads at start | Set to `1` to skip fetching the caption model (the one chosen in Settings → Production defaults, 378 MB by default) when Slopify starts. It is then fetched the first time a video needs captions. |
|
|
75
75
|
|
|
76
76
|
Options that only work with `--docker`: `--projects-dir <folder>`, `--host-cli=off` and `--accept-host-cli`. See [Docker](Docker). With `--docker`, `--host` and `--data-dir` are refused, because the Docker version always listens on localhost and keeps its data in a Docker volume.
|
|
77
77
|
|
|
@@ -112,7 +112,7 @@ The bundled ffmpeg is a separate program under the GPL-3.0-or-later licence. Slo
|
|
|
112
112
|
|
|
113
113
|
## The caption model
|
|
114
114
|
|
|
115
|
-
Captions are timed against the narration by a speech model that runs on your computer, at no cost. The English model
|
|
115
|
+
Captions are timed against the narration by a speech model that runs on your computer, at no cost. The English model is downloaded when Slopify starts, in the background, so a run never waits for it later: the one chosen in Settings → Production defaults → **Subtitle timing model**, Most accurate (378 MB) unless changed. The Docker image already carries it. It is kept in `<data folder>/models/`. Set `SLOPIFY_NO_MODEL_PREFETCH=1` to skip the download at start. Other languages use a separate model that is downloaded the first time a project in that language needs it.
|
|
116
116
|
|
|
117
117
|
## Anonymous usage stats
|
|
118
118
|
|
|
@@ -32,7 +32,7 @@ This applies with the brand kit off too, because language is not styling. Projec
|
|
|
32
32
|
| | English | Spanish, German, French, Italian, Portuguese, Dutch, Catalan, Polish, Czech | All other languages |
|
|
33
33
|
|---|---|---|---|
|
|
34
34
|
| Article, narration preparation, YouTube description, chapters, tags, shorts titles and hashtags, multi-voice scripts | English | Written in the language | Written in the language |
|
|
35
|
-
| Caption timing | Word by word, English model (95 MB) | Word by word, multilingual model (248 MB, downloaded once, on first use) | Sentence by sentence, no model |
|
|
35
|
+
| Caption timing | Word by word, English model (378 MB; 189 MB or 95 MB in Settings → Production defaults) | Word by word, multilingual model (248 MB, downloaded once, on first use) | Sentence by sentence, no model |
|
|
36
36
|
| Word-by-word Shorts captions | On | On | Off: shorts show whole caption groups |
|
|
37
37
|
| Cuts that follow the narration | Available | Available | Off: cuts fall every N seconds |
|
|
38
38
|
| Pronunciation Glossary | Standard-English IPA | Full IPA (ç, ʁ, ø, y, ɲ, x and so on) | Full IPA |
|
|
@@ -84,7 +84,7 @@ Below the ambient sound are **Pause between sentences**, **Pause between paragra
|
|
|
84
84
|
|
|
85
85
|
## Captions
|
|
86
86
|
|
|
87
|
-
Captions are timed from your narration on this computer, with no paid API. The first use downloads a speech model of about
|
|
87
|
+
Captions are timed from your narration on this computer, with no paid API. The first use downloads a speech model of about 378 MB (Settings → Production defaults → **Subtitle timing model** offers smaller ones). They need narration: with narration Off, the section says "Turn Audio on to add subtitles."
|
|
88
88
|
|
|
89
89
|
| Option | What it does | Default / range |
|
|
90
90
|
| --- | --- | --- |
|
|
@@ -116,7 +116,7 @@ There is nothing to turn on: the address works whenever Slopify runs. See [Agent
|
|
|
116
116
|
|
|
117
117
|
## Production defaults
|
|
118
118
|
|
|
119
|
-
"How new runs pace the narration and level the volume."
|
|
119
|
+
"How new runs pace the narration and level the volume, and which model times English captions."
|
|
120
120
|
|
|
121
121
|
### Silence between segments
|
|
122
122
|
|
|
@@ -136,6 +136,14 @@ Videos already started keep their own gap. A value that is not a whole number fr
|
|
|
136
136
|
|
|
137
137
|
The volume fields show only while the switch is on. A project keeps what it started with; change it there in **Edit project**. Levelling runs on this computer and makes no API calls. Changes save as you make them.
|
|
138
138
|
|
|
139
|
+
### Subtitle timing model
|
|
140
|
+
|
|
141
|
+
| Control | What it does | Default / range |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| **Subtitle timing model** | The speech model that times English captions on this computer. The same model in three sizes: **Most accurate (378 MB, recommended)**, **Balanced (189 MB)** and **Smallest (95 MB, less accurate)**. Bigger hears the narration more accurately and, on most computers, also runs faster. | Most accurate |
|
|
144
|
+
|
|
145
|
+
The chosen model downloads once, the first time captions are timed (or when Slopify starts, unless `SLOPIFY_NO_MODEL_PREFETCH` is set), and is kept in `<data folder>/models/english-subtitles/`. A model already downloaded stays there when you switch, so switching back costs nothing. Smallest can mishear a line and stop a run with "Subtitles stopped matching the audio" where the narration is fine. Changing the model never marks finished subtitles outdated, and a timing that is already running finishes with the model it started with; the next one uses the new choice. It is a setting of this computer, not of a project. Other languages use their own model and are not affected. Changes save as you make them.
|
|
146
|
+
|
|
139
147
|
## Notifications
|
|
140
148
|
|
|
141
149
|
"When a run finishes, fails, or waits for you."
|
|
@@ -67,7 +67,7 @@ Slopify ships its own ffmpeg; you do not need to install one. In Docker it is in
|
|
|
67
67
|
| "The audio/video export failed (ffmpeg exited with code ...)" | Open **Error details**: it quotes what ffmpeg could not do, for example an input file it could not open. Replace that file (an uploaded image or clip in **Edit project → Images**), or try again. |
|
|
68
68
|
| "Slopify couldn't measure the loudness of the narration" | Try again. If it happens again, turn off **Level the volume** in **Edit project → Inputs → Pauses and volume**, then download diagnostics and report it. |
|
|
69
69
|
| "The style preview couldn't render ... Press Render again." | Press **Render again**. If it fails again, download diagnostics and report it. |
|
|
70
|
-
| "The free subtitle model could not be downloaded" / "download was interrupted" / "failed download verification" | The
|
|
70
|
+
| "The free subtitle model could not be downloaded" / "download was interrupted" / "failed download verification" | The caption model (378 MB by default, the size chosen in Settings → Production defaults → **Subtitle timing model**) downloads on first use. Check your connection, then **Continue** the run, or pick a smaller model there. `SLOPIFY_NO_MODEL_PREFETCH` only skips the download at start; captions still need it. |
|
|
71
71
|
| "Captions can't be edited until the current narration has finished and its subtitle timing is ready." | Let the narration and subtitle timing finish, then edit captions. |
|
|
72
72
|
| "The saved subtitle timing doesn't match the current narration." | Do what the message says: make the subtitle timing again (More → make it again), then edit the captions. |
|
|
73
73
|
| "Subtitles recovered after missing narration (N)" under the video | Some transcript passages could not be matched to the audio and were left out of the captions. The audio is fine. Review those passages before sharing. |
|
|
@@ -90,7 +90,7 @@ With **Level the volume** on, the bed is mixed and ducked first, then the whole
|
|
|
90
90
|
| **Subtitle font size** | How big burned-in captions are. Larger is easier to read on phones but covers more of the picture. | 48; 16 to 120 |
|
|
91
91
|
| **Subtitle position** | Where burned-in captions sit: **Top**, **Upper-middle**, **Center**, **Lower-middle** or **Bottom**. Move them up when pictures have detail near the bottom. | Bottom |
|
|
92
92
|
|
|
93
|
-
Captions are timed from your narration on your computer, with no paid API. The first use downloads a speech model of about
|
|
93
|
+
Captions are timed from your narration on your computer, with no paid API. The first use downloads a speech model of about 378 MB (Settings → Production defaults → **Subtitle timing model** offers smaller ones). Subtitle files carry no font, size or position, because players style them. Changing the font, size or position remakes only the video.
|
|
94
94
|
|
|
95
95
|
Subtitles need narration: with Audio off the section says "Turn Audio on to add subtitles." With Video off (an audio export), only subtitle files are offered.
|
|
96
96
|
|