@gentbajko/slopify 2.0.2 → 2.0.3
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/protocol.js +11 -1
- package/dist/adapters/alignment/runner.js +4 -1
- package/dist/adapters/alignment/worker.js +28 -5
- package/dist/adapters/explain.js +93 -0
- package/dist/adapters/ffmpeg.js +3 -3
- package/dist/adapters/host-cli/index.js +9 -2
- package/dist/adapters/host-cli/transport.js +4 -4
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +12 -12
- package/dist/adapters/image/codex.js +24 -8
- package/dist/adapters/image/fal.js +12 -6
- package/dist/adapters/image/google.js +18 -8
- package/dist/adapters/image/models.js +8 -2
- package/dist/adapters/image/openai.js +15 -6
- package/dist/adapters/image/replicate.js +16 -7
- package/dist/adapters/llm/claude-code-models.js +1 -1
- package/dist/adapters/llm/claude-code.js +16 -7
- package/dist/adapters/llm/cli-login-error.js +2 -1
- package/dist/adapters/llm/codex-models.js +1 -1
- package/dist/adapters/llm/codex.js +5 -6
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-models.js +1 -1
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +15 -11
- package/dist/adapters/llm/openrouter.js +21 -9
- package/dist/adapters/llm/run-cli.js +14 -11
- package/dist/adapters/tts/cartesia.js +10 -3
- package/dist/adapters/tts/elevenlabs.js +18 -6
- package/dist/adapters/tts/inworld-async.js +47 -16
- package/dist/adapters/tts/inworld.js +25 -15
- package/dist/adapters/tts/openai.js +18 -6
- package/dist/catalog/registry.js +13 -10
- package/dist/catalog/store.js +1 -1
- package/dist/catalog/validate.js +13 -4
- package/dist/edge/cli.js +33 -7
- package/dist/edge/docker-launch.js +6 -6
- package/dist/edge/docker-projects/engine.js +34 -13
- package/dist/edge/docker-projects/install.js +8 -8
- package/dist/edge/docker-projects/state.js +6 -6
- package/dist/edge/docker-projects/tree.js +6 -6
- package/dist/edge/docker.js +13 -13
- package/dist/edge/http/actions.js +27 -25
- package/dist/edge/http/app.js +3 -3
- package/dist/edge/http/audio-preview.js +1 -1
- package/dist/edge/http/checkpoints.js +5 -1
- package/dist/edge/http/diagnostics.js +3 -1
- package/dist/edge/http/draft-files.js +2 -2
- package/dist/edge/http/draft-problem.js +11 -1
- package/dist/edge/http/files.js +4 -4
- package/dist/edge/http/folder-location.js +3 -3
- package/dist/edge/http/fonts.js +22 -10
- package/dist/edge/http/host-cli.js +34 -12
- package/dist/edge/http/multipart.js +7 -5
- package/dist/edge/http/open-folder.js +2 -2
- package/dist/edge/http/planning.js +4 -4
- package/dist/edge/http/problem.js +2 -2
- package/dist/edge/http/project-templates.js +7 -3
- package/dist/edge/http/projects.js +8 -8
- package/dist/edge/http/prompts.js +9 -4
- package/dist/edge/http/providers.js +15 -7
- package/dist/edge/http/revision-files.js +3 -3
- package/dist/edge/http/revisions.js +30 -4
- package/dist/edge/http/schedules.js +9 -9
- package/dist/edge/http/settings.js +15 -9
- package/dist/edge/http/staging.js +5 -5
- package/dist/edge/http/storage.js +4 -4
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/tutorial.js +2 -2
- package/dist/edge/http/update.js +3 -3
- package/dist/host-cli/install.js +28 -6
- package/dist/host-cli/paths.js +6 -6
- package/dist/host-cli/server.js +1 -1
- package/dist/host-cli/service.js +11 -11
- package/dist/host-cli/status.js +7 -3
- package/dist/kernel/config/index.js +6 -4
- package/dist/kernel/db/migrate.js +2 -1
- package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
- package/dist/kernel/lock.js +4 -3
- package/dist/kernel/ports/subtitles.js +15 -1
- package/dist/kernel/runner/attempt.js +26 -2
- package/dist/main.js +2 -2
- package/dist/slices/admission/rules.js +32 -20
- package/dist/slices/article/continuation.js +4 -2
- package/dist/slices/article/plain.js +2 -2
- package/dist/slices/article/provided-entries.js +3 -3
- package/dist/slices/article/run.js +2 -2
- package/dist/slices/article/segments.js +5 -3
- package/dist/slices/control/providers.js +20 -7
- package/dist/slices/fonts/catalog.js +1 -1
- package/dist/slices/fonts/upload.js +1 -1
- package/dist/slices/images/run.js +5 -5
- package/dist/slices/library/lint.js +13 -7
- package/dist/slices/library/slots.js +2 -2
- package/dist/slices/narration/concat.js +1 -1
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +2 -2
- package/dist/slices/narration/run.js +7 -7
- package/dist/slices/narration/steering.js +3 -3
- package/dist/slices/play-drafts/convert.js +5 -4
- package/dist/slices/play-drafts/readiness.js +12 -6
- package/dist/slices/play-drafts/review-inputs.js +7 -4
- package/dist/slices/play-drafts/review.js +3 -3
- package/dist/slices/play-drafts/service.js +3 -1
- package/dist/slices/play-drafts/uploads.js +1 -1
- package/dist/slices/rebuild/admission-repo.js +2 -2
- package/dist/slices/rebuild/recipe-legacy.js +1 -1
- package/dist/slices/rebuild/recipe-narration-text.js +1 -1
- package/dist/slices/rebuild/recipe-visual.js +1 -1
- package/dist/slices/rebuild/recovery.js +2 -2
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/runtime-admission.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
- package/dist/slices/rebuild/runtime-export-inputs.js +6 -6
- package/dist/slices/rebuild/runtime-export.js +4 -4
- package/dist/slices/rebuild/runtime-local.js +6 -6
- package/dist/slices/rebuild/runtime-narration-text.js +46 -7
- package/dist/slices/rebuild/runtime-piece-label.js +62 -0
- package/dist/slices/rebuild/runtime-provider.js +6 -4
- package/dist/slices/rebuild/runtime-publication.js +3 -3
- package/dist/slices/rebuild/runtime-run.js +11 -1
- package/dist/slices/rebuild/runtime-subtitles.js +78 -15
- package/dist/slices/rebuild/transition-repo.js +6 -6
- package/dist/slices/reruns/index.js +1 -1
- package/dist/slices/research/run.js +9 -7
- package/dist/slices/research/synthesis.js +4 -2
- package/dist/slices/revisions/mutation-assets.js +26 -8
- package/dist/slices/revisions/mutation-cues.js +1 -1
- package/dist/slices/revisions/rules.js +18 -9
- package/dist/slices/revisions/schema.js +1 -1
- package/dist/slices/schedules/repo.js +55 -11
- package/dist/slices/schedules/scheduler.js +25 -8
- package/dist/slices/schedules/schema.js +19 -4
- package/dist/slices/schedules/service.js +2 -0
- package/dist/slices/settings/cli-paths.js +7 -4
- package/dist/slices/settings/models.js +4 -4
- package/dist/slices/settings/playback.js +2 -2
- package/dist/slices/storage/assets.js +5 -5
- package/dist/slices/storage/layout.js +1 -1
- package/dist/slices/subtitles/captions.js +1 -1
- package/dist/slices/subtitles/prepare.js +5 -5
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +8 -8
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/ffmpeg.js +6 -4
- package/dist/slices/video/plan.js +1 -1
- package/dist/slices/video/run.js +2 -2
- package/dist/updater/plan.js +1 -1
- package/dist/updater/service.js +4 -3
- package/dist/updater/worker.js +6 -4
- package/dist/web/assets/{index-CZNiHm8P.css → index-Bwtmu9ei.css} +1 -1
- package/dist/web/assets/index-DjhMJqMN.js +141 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DPgGLX5m.js +0 -137
|
@@ -100,8 +100,8 @@ function classify(error, expired, opts, limit) {
|
|
|
100
100
|
return providerError({
|
|
101
101
|
kind: "timeout",
|
|
102
102
|
message: opts.streaming === true
|
|
103
|
-
? `
|
|
104
|
-
: `
|
|
103
|
+
? `The ${callee[opts.kind]} sent nothing for ${seconds} seconds, so Slopify stopped waiting. It may be overloaded: wait a few minutes, then use Retry stage.`
|
|
104
|
+
: `The ${callee[opts.kind]} did not answer within ${seconds} seconds. It may be overloaded: wait a few minutes, then use Retry stage, or choose another model in the Providers section of Edit project.`,
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
if (isProviderError(error)) {
|
|
@@ -113,11 +113,35 @@ function classify(error, expired, opts, limit) {
|
|
|
113
113
|
...(error.fault.retryAfterMs === undefined ? {} : { retryAfterMs: error.fault.retryAfterMs }),
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
|
+
if (unreachable(error)) {
|
|
117
|
+
// `fetch` names only itself; the cause's code (ENOTFOUND, ECONNREFUSED) is the clue.
|
|
118
|
+
const code = causeCode(error);
|
|
119
|
+
return providerError({
|
|
120
|
+
kind: "other",
|
|
121
|
+
message: `Slopify could not reach the ${callee[opts.kind]} over the internet${code === undefined ? "" : ` (${code})`}. Check your internet connection, firewall or VPN, then use Retry stage.`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
116
124
|
return providerError({
|
|
117
125
|
kind: "other",
|
|
118
126
|
message: redact(error instanceof Error ? error.message : String(error)),
|
|
119
127
|
});
|
|
120
128
|
}
|
|
129
|
+
// What the user calls the other end of each kind of call.
|
|
130
|
+
const callee = {
|
|
131
|
+
llm: "AI model",
|
|
132
|
+
tts: "narration provider",
|
|
133
|
+
image: "image provider",
|
|
134
|
+
};
|
|
135
|
+
// Node's `fetch` rejects a request that never reached the server with this TypeError.
|
|
136
|
+
function unreachable(error) {
|
|
137
|
+
return error instanceof TypeError && error.message === "fetch failed";
|
|
138
|
+
}
|
|
139
|
+
function causeCode(error) {
|
|
140
|
+
const cause = error.cause;
|
|
141
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause))
|
|
142
|
+
return undefined;
|
|
143
|
+
return typeof cause.code === "string" && /^[A-Z_]+$/.test(cause.code) ? cause.code : undefined;
|
|
144
|
+
}
|
|
121
145
|
// The same milliseconds, measured two ways. `idle` restarts the count on every chunk, so a
|
|
122
146
|
// stalled stream is cut off and a long answer is not. Cancel still wins.
|
|
123
147
|
function deadline(clock, parent, ms, idle) {
|
package/dist/main.js
CHANGED
|
@@ -119,7 +119,7 @@ export async function boot(config, options = {}) {
|
|
|
119
119
|
if (dockerState !== undefined &&
|
|
120
120
|
(process.env.SLOPIFY_CONTAINER !== "1" ||
|
|
121
121
|
dockerState !== "/opt/slopify-install/activation.json"))
|
|
122
|
-
throw new Error("
|
|
122
|
+
throw new Error("This container was started with Slopify's Docker settings in the wrong place (SLOPIFY_DOCKER_INSTALL_STATE). Start it with the launcher instead: npx @gentbajko/slopify --docker");
|
|
123
123
|
const candidateToken = process.env.SLOPIFY_UPDATE_TOKEN ?? "";
|
|
124
124
|
const pendingActivation = isUpdateToken(candidateToken) && process.env.SLOPIFY_UPDATE_PENDING === "1";
|
|
125
125
|
ensureDirs(paths, { mode: 0o700 });
|
|
@@ -243,7 +243,7 @@ export async function boot(config, options = {}) {
|
|
|
243
243
|
: !existsSync(oldEntry) || !existsSync(workerEntry)
|
|
244
244
|
? "Run Slopify from its installed package to use in-app updates."
|
|
245
245
|
: npm === undefined
|
|
246
|
-
? "npm
|
|
246
|
+
? "npm was not found, so in-app updates are off. Install Node.js with npm (https://nodejs.org), or update from the terminal: npx @gentbajko/slopify@latest"
|
|
247
247
|
: undefined,
|
|
248
248
|
busy: () => updateDb.prepare("SELECT 1 FROM stages WHERE state = 'running' LIMIT 1").get() !==
|
|
249
249
|
undefined ||
|
|
@@ -24,16 +24,16 @@ export function admit(input) {
|
|
|
24
24
|
const draft = normaliseDraft(input.draft);
|
|
25
25
|
const { sources } = draft;
|
|
26
26
|
if (draft.title === "") {
|
|
27
|
-
fields.push({ field: "title", message: "
|
|
27
|
+
fields.push({ field: "title", message: "Enter a title for this video." });
|
|
28
28
|
}
|
|
29
29
|
else if (draft.title.length > titleMax) {
|
|
30
|
-
fields.push({ field: "title", message: `
|
|
30
|
+
fields.push({ field: "title", message: `Keep the title to ${titleMax} characters or fewer.` });
|
|
31
31
|
}
|
|
32
32
|
for (const kind of stageKinds) {
|
|
33
33
|
if (!allowedSources[kind].includes(sources[kind])) {
|
|
34
34
|
fields.push({
|
|
35
35
|
field: `sources.${kind}`,
|
|
36
|
-
message: `
|
|
36
|
+
message: `"${sources[kind]}" is not an option for ${kind}. Reload the page and choose again.`,
|
|
37
37
|
});
|
|
38
38
|
}
|
|
39
39
|
}
|
|
@@ -45,7 +45,7 @@ export function admit(input) {
|
|
|
45
45
|
draft.outro?.mode === "llm" ||
|
|
46
46
|
usesNarrationPreparation(draft);
|
|
47
47
|
if (needsLlm && !chosen(draft.llm)) {
|
|
48
|
-
fields.push({ field: "llm", message: "
|
|
48
|
+
fields.push({ field: "llm", message: "Choose a text (LLM) provider and model." });
|
|
49
49
|
}
|
|
50
50
|
if (sources.article === "generate" && blank(draft.articlePrompt)) {
|
|
51
51
|
fields.push({ field: "articlePrompt", message: "Pick an article prompt." });
|
|
@@ -54,10 +54,10 @@ export function admit(input) {
|
|
|
54
54
|
fields.push(...narrationPreparationFields(draft));
|
|
55
55
|
if (sources.audio === "generate") {
|
|
56
56
|
if (voiced === undefined || !chosen(voiced)) {
|
|
57
|
-
fields.push({ field: "audio", message: "
|
|
57
|
+
fields.push({ field: "audio", message: "Choose a narration provider and model." });
|
|
58
58
|
}
|
|
59
59
|
else if (voiced.voice.trim() === "") {
|
|
60
|
-
fields.push({ field: "audio.voice", message: "
|
|
60
|
+
fields.push({ field: "audio.voice", message: "Choose a voice for the narration." });
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
if (sources.images === "generate") {
|
|
@@ -67,14 +67,17 @@ export function admit(input) {
|
|
|
67
67
|
sources.thumbnail === "from_prompt" ||
|
|
68
68
|
sources.thumbnail === "prompt_by_llm") &&
|
|
69
69
|
!chosen(draft.images)) {
|
|
70
|
-
fields.push({ field: "images", message: "
|
|
70
|
+
fields.push({ field: "images", message: "Choose an image provider and model." });
|
|
71
71
|
}
|
|
72
72
|
if ((sources.thumbnail === "from_prompt" || sources.thumbnail === "prompt_by_llm") &&
|
|
73
73
|
blank(draft.thumbnailPrompt)) {
|
|
74
74
|
fields.push({ field: "thumbnailPrompt", message: "Pick a thumbnail prompt." });
|
|
75
75
|
}
|
|
76
76
|
if (sources.audio === "off" && draft.subtitles !== undefined && draft.subtitles.mode !== "off") {
|
|
77
|
-
fields.push({
|
|
77
|
+
fields.push({
|
|
78
|
+
field: "subtitles.mode",
|
|
79
|
+
message: "Subtitles need narration. Turn narration on, or turn subtitles off.",
|
|
80
|
+
});
|
|
78
81
|
}
|
|
79
82
|
checkProvided(draft, input.staged, fields);
|
|
80
83
|
checkValues(draft, input.requiredSlots, fields);
|
|
@@ -83,7 +86,7 @@ export function admit(input) {
|
|
|
83
86
|
draft.silenceGapSeconds > silenceGapSecondsMax) {
|
|
84
87
|
fields.push({
|
|
85
88
|
field: "silenceGapSeconds",
|
|
86
|
-
message: `
|
|
89
|
+
message: `Enter a silence gap between 0 and ${silenceGapSecondsMax} seconds.`,
|
|
87
90
|
});
|
|
88
91
|
}
|
|
89
92
|
return fields.length === 0 ? { ok: true, draft } : { ok: false, fields };
|
|
@@ -121,7 +124,10 @@ export function normaliseDraft(draft) {
|
|
|
121
124
|
function checkImagePrompts(draft, fields) {
|
|
122
125
|
if (draft.imagePrompts.length === 0) {
|
|
123
126
|
// A run always has an image source.
|
|
124
|
-
fields.push({
|
|
127
|
+
fields.push({
|
|
128
|
+
field: "imagePrompts",
|
|
129
|
+
message: "Tick at least one image prompt, or turn images off.",
|
|
130
|
+
});
|
|
125
131
|
return;
|
|
126
132
|
}
|
|
127
133
|
let total = 0;
|
|
@@ -135,14 +141,14 @@ function checkImagePrompts(draft, fields) {
|
|
|
135
141
|
prompt.number > numberPerPromptMax) {
|
|
136
142
|
fields.push({
|
|
137
143
|
field: `imagePrompts.${index}.number`,
|
|
138
|
-
message: `
|
|
144
|
+
message: `Enter a whole number between 1 and ${numberPerPromptMax}.`,
|
|
139
145
|
});
|
|
140
146
|
}
|
|
141
147
|
}
|
|
142
148
|
if (total > imagesPerRunMax) {
|
|
143
149
|
fields.push({
|
|
144
150
|
field: "imagePrompts",
|
|
145
|
-
message: `A
|
|
151
|
+
message: `A video can have at most ${imagesPerRunMax} images; these prompts ask for ${total}. Lower the numbers.`,
|
|
146
152
|
});
|
|
147
153
|
}
|
|
148
154
|
}
|
|
@@ -168,11 +174,14 @@ function checkProvided(draft, staged, fields) {
|
|
|
168
174
|
else if (ids.length > imagesPerRunMax) {
|
|
169
175
|
fields.push({
|
|
170
176
|
field: "provided.images",
|
|
171
|
-
message: `A
|
|
177
|
+
message: `A video can have at most ${imagesPerRunMax} images. Remove some.`,
|
|
172
178
|
});
|
|
173
179
|
}
|
|
174
180
|
if (new Set(ids).size !== ids.length) {
|
|
175
|
-
fields.push({
|
|
181
|
+
fields.push({
|
|
182
|
+
field: "provided.images",
|
|
183
|
+
message: "The same image is picked twice. Remove the duplicate.",
|
|
184
|
+
});
|
|
176
185
|
}
|
|
177
186
|
for (const [index, id] of ids.entries()) {
|
|
178
187
|
checkFile(staged, id, "images", `provided.images.${index}`, "Pick an image.", fields);
|
|
@@ -187,28 +196,31 @@ function checkFile(staged, id, kind, field, missing, fields) {
|
|
|
187
196
|
}
|
|
188
197
|
const file = staged.find((candidate) => candidate.id === id);
|
|
189
198
|
if (file === undefined || file.stageKind !== kind) {
|
|
190
|
-
fields.push({ field, message: "That upload is no longer available
|
|
199
|
+
fields.push({ field, message: "That upload is no longer available. Choose the file again." });
|
|
191
200
|
return;
|
|
192
201
|
}
|
|
193
202
|
if (file.state !== "staged") {
|
|
194
|
-
fields.push({ field, message: "This
|
|
203
|
+
fields.push({ field, message: "This file is still uploading. Wait for it to finish." });
|
|
195
204
|
}
|
|
196
205
|
}
|
|
197
206
|
function checkValues(draft, requiredSlots, fields) {
|
|
198
207
|
for (const name of requiredSlots) {
|
|
199
208
|
const value = Object.hasOwn(draft.values, name) ? draft.values[name] : undefined;
|
|
200
209
|
if (value === undefined || value === "") {
|
|
201
|
-
fields.push({ field: `values.${name}`, message: "
|
|
210
|
+
fields.push({ field: `values.${name}`, message: "Fill in this field." });
|
|
202
211
|
continue;
|
|
203
212
|
}
|
|
204
213
|
if (value.length > valueMax) {
|
|
205
214
|
fields.push({
|
|
206
215
|
field: `values.${name}`,
|
|
207
|
-
message: `
|
|
216
|
+
message: `Keep this to ${valueMax} characters or fewer.`,
|
|
208
217
|
});
|
|
209
218
|
}
|
|
210
219
|
if (/[\n\r]/.test(value)) {
|
|
211
|
-
fields.push({
|
|
220
|
+
fields.push({
|
|
221
|
+
field: `values.${name}`,
|
|
222
|
+
message: "Keep this to a single line, without line breaks.",
|
|
223
|
+
});
|
|
212
224
|
}
|
|
213
225
|
}
|
|
214
226
|
}
|
|
@@ -224,7 +236,7 @@ export function narrationPreparationFields(draft) {
|
|
|
224
236
|
? [
|
|
225
237
|
{
|
|
226
238
|
field: "narrationPrompt",
|
|
227
|
-
message: "Narration
|
|
239
|
+
message: "Narration preparation only works with the Inworld TTS-2 model. Choose that model under Narration, or turn preparation Off.",
|
|
228
240
|
},
|
|
229
241
|
]
|
|
230
242
|
: [];
|
|
@@ -81,7 +81,9 @@ function truncated(answer) {
|
|
|
81
81
|
}
|
|
82
82
|
// Empty response → failed attempt.
|
|
83
83
|
function written(answer) {
|
|
84
|
-
return answer.text.trim() === ""
|
|
84
|
+
return answer.text.trim() === ""
|
|
85
|
+
? "The AI model returned an empty article. Retry stage; if it keeps happening, choose a different model in Edit project → Providers."
|
|
86
|
+
: undefined;
|
|
85
87
|
}
|
|
86
88
|
function finished(answer) {
|
|
87
89
|
const empty = written(answer);
|
|
@@ -89,6 +91,6 @@ function finished(answer) {
|
|
|
89
91
|
return empty;
|
|
90
92
|
}
|
|
91
93
|
return truncated(answer)
|
|
92
|
-
? `
|
|
94
|
+
? `The article was still unfinished after ${String(continuationLimit)} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Retry stage.`
|
|
93
95
|
: undefined;
|
|
94
96
|
}
|
|
@@ -7,11 +7,11 @@ export function plainText(markdown) {
|
|
|
7
7
|
const paragraphs = tree.children
|
|
8
8
|
.map((node) => {
|
|
9
9
|
if (node.type !== "paragraph")
|
|
10
|
-
throw new Error("
|
|
10
|
+
throw new Error("Slopify hit an internal error (the article couldn't be turned into plain text). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
11
11
|
return node.children
|
|
12
12
|
.map((child) => {
|
|
13
13
|
if (child.type !== "text")
|
|
14
|
-
throw new Error("
|
|
14
|
+
throw new Error("Slopify hit an internal error (the article couldn't be turned into plain text). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
15
15
|
return child.value;
|
|
16
16
|
})
|
|
17
17
|
.join("");
|
|
@@ -11,7 +11,7 @@ export async function prepareProvidedArticleSegments(deps, audioContext, provide
|
|
|
11
11
|
const { projectId } = audioContext.stage;
|
|
12
12
|
const project = projectById(deps.db, projectId);
|
|
13
13
|
if (!project)
|
|
14
|
-
throw new Error(
|
|
14
|
+
throw new Error("This project no longer exists; it may have been deleted while it was running.");
|
|
15
15
|
const { config } = project;
|
|
16
16
|
if (config.sources.article !== "provide" || config.sources.audio !== "generate")
|
|
17
17
|
return "done";
|
|
@@ -20,11 +20,11 @@ export async function prepareProvidedArticleSegments(deps, audioContext, provide
|
|
|
20
20
|
audioContext.signal.throwIfAborted();
|
|
21
21
|
const article = stagesOf(deps.db, projectId).find((stage) => stage.kind === "article");
|
|
22
22
|
if (!article)
|
|
23
|
-
throw new Error(
|
|
23
|
+
throw new Error("Slopify hit an internal error (the project has no article stage). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
24
24
|
const outputs = outputsOf(deps.db, projectId);
|
|
25
25
|
const plain = outputs.find((output) => output.role === "article_txt");
|
|
26
26
|
if (!plain)
|
|
27
|
-
throw new Error("
|
|
27
|
+
throw new Error("The article text you provided is missing, so the intro and outro can't be written from it. Paste the article again in Edit project → Article, then Retry stage.");
|
|
28
28
|
const narration = readFileSync(outputPath(deps.paths, projectId, plain.path), "utf8");
|
|
29
29
|
const saved = outputs.find((output) => output.stageKind === "article" && output.role === "instructions");
|
|
30
30
|
let instructions = saved
|
|
@@ -13,14 +13,14 @@ export async function runArticle(deps, context, providers) {
|
|
|
13
13
|
const { projectId } = context.stage;
|
|
14
14
|
const project = projectById(deps.db, projectId);
|
|
15
15
|
if (project === undefined) {
|
|
16
|
-
throw new Error(
|
|
16
|
+
throw new Error("This project no longer exists; it may have been deleted while it was running.");
|
|
17
17
|
}
|
|
18
18
|
const choice = project.config.llm;
|
|
19
19
|
const articlePrompt = project.config.rendered.article;
|
|
20
20
|
if (choice === undefined || articlePrompt === undefined) {
|
|
21
21
|
// Admission refuses a run whose article is Generate without both, so
|
|
22
22
|
// reaching here is a bug in admission rather than something the user did.
|
|
23
|
-
throw new Error("
|
|
23
|
+
throw new Error("No AI model or article prompt is set for writing the article. Choose a model in Edit project → Providers and check Edit project → Prompts, then Retry stage.");
|
|
24
24
|
}
|
|
25
25
|
const notes = researchNotes(deps, projectId);
|
|
26
26
|
const brief = { articlePrompt, ...(notes === undefined ? {} : { notes }) };
|
|
@@ -7,12 +7,12 @@ export async function writeSegment(providers, choice, config, category, article,
|
|
|
7
7
|
return { ok: true, value: undefined };
|
|
8
8
|
const body = config.rendered[category];
|
|
9
9
|
if (body === undefined)
|
|
10
|
-
throw new Error(`
|
|
10
|
+
throw new Error(`Slopify hit an internal error (the ${category} text was never filled in). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
|
|
11
11
|
const common = { category, name: picked.name, mode: picked.mode };
|
|
12
12
|
if (picked.mode === "text")
|
|
13
13
|
return { ok: true, value: { ...common, text: body, tokens: noTokens } };
|
|
14
14
|
if (choice === undefined)
|
|
15
|
-
throw new Error(`the ${category}
|
|
15
|
+
throw new Error(`No AI model is set for writing the ${category}. Choose one in Edit project → Providers, then Retry stage.`);
|
|
16
16
|
const messages = segmentMessages(body, config, article);
|
|
17
17
|
sent.push({ label: category === "intro" ? "Intro" : "Outro", messages });
|
|
18
18
|
const answer = await providers.llm({
|
|
@@ -20,7 +20,9 @@ export async function writeSegment(providers, choice, config, category, article,
|
|
|
20
20
|
model: choice.model,
|
|
21
21
|
...(choice.thinking === undefined ? {} : { thinking: choice.thinking }),
|
|
22
22
|
messages,
|
|
23
|
-
check: (given) => given.text.trim() === ""
|
|
23
|
+
check: (given) => given.text.trim() === ""
|
|
24
|
+
? `The AI model returned an empty ${category}. Retry stage; if it keeps happening, choose a different model in Edit project → Providers.`
|
|
25
|
+
: undefined,
|
|
24
26
|
});
|
|
25
27
|
if (!answer.ok)
|
|
26
28
|
return answer;
|
|
@@ -25,7 +25,7 @@ export const providerChangesSchema = z
|
|
|
25
25
|
.optional(),
|
|
26
26
|
})
|
|
27
27
|
.strict()
|
|
28
|
-
.refine((value) => Object.keys(value).length > 0, "
|
|
28
|
+
.refine((value) => Object.keys(value).length > 0, "Change at least one provider before saving.");
|
|
29
29
|
export function validateLocalProviderChanges(changes, current) {
|
|
30
30
|
const fields = [];
|
|
31
31
|
const families = { llm: "llm", audio: "tts", images: "image" };
|
|
@@ -35,7 +35,10 @@ export function validateLocalProviderChanges(changes, current) {
|
|
|
35
35
|
continue;
|
|
36
36
|
const provider = current.providers.find((one) => one.id === picked.provider && one.family === families[key]);
|
|
37
37
|
if (provider === undefined) {
|
|
38
|
-
fields.push({
|
|
38
|
+
fields.push({
|
|
39
|
+
field: key,
|
|
40
|
+
message: "This provider cannot do this kind of work. Choose another provider.",
|
|
41
|
+
});
|
|
39
42
|
continue;
|
|
40
43
|
}
|
|
41
44
|
const usable = readinessIsUsable(provider.readiness);
|
|
@@ -43,14 +46,18 @@ export function validateLocalProviderChanges(changes, current) {
|
|
|
43
46
|
fields.push({
|
|
44
47
|
field: key,
|
|
45
48
|
message: provider.readiness.kind === "cli"
|
|
46
|
-
? (provider.readiness.issue ??
|
|
47
|
-
|
|
49
|
+
? (provider.readiness.issue ??
|
|
50
|
+
"Install this command-line tool on your computer and sign in to it, then try again.")
|
|
51
|
+
: "Add this provider's API key in Settings → Providers first.",
|
|
48
52
|
});
|
|
49
53
|
}
|
|
50
54
|
}
|
|
51
55
|
if (changes.audio !== undefined &&
|
|
52
56
|
!current.voices.some((voice) => voice.provider === changes.audio?.provider && voice.voiceId === changes.audio.voice)) {
|
|
53
|
-
fields.push({
|
|
57
|
+
fields.push({
|
|
58
|
+
field: "audio.voice",
|
|
59
|
+
message: "Choose one of your saved voices for this provider. Add voices in Settings → Voices.",
|
|
60
|
+
});
|
|
54
61
|
}
|
|
55
62
|
return fields;
|
|
56
63
|
}
|
|
@@ -67,10 +74,16 @@ export async function validateProviderChanges(changes, current) {
|
|
|
67
74
|
continue;
|
|
68
75
|
const result = await checkRuntimeModel(current.modelsFor, picked.provider, families[key], picked.model, picked.thinking);
|
|
69
76
|
if (result === "missing") {
|
|
70
|
-
fields.push({
|
|
77
|
+
fields.push({
|
|
78
|
+
field: `${key}.model`,
|
|
79
|
+
message: "This provider does not offer that model. Choose another model.",
|
|
80
|
+
});
|
|
71
81
|
}
|
|
72
82
|
else if (result === "thinking") {
|
|
73
|
-
fields.push({
|
|
83
|
+
fields.push({
|
|
84
|
+
field: `${key}.thinking`,
|
|
85
|
+
message: "This model does not support that thinking setting. Choose another.",
|
|
86
|
+
});
|
|
74
87
|
}
|
|
75
88
|
}
|
|
76
89
|
return fields;
|
|
@@ -31,7 +31,7 @@ export async function resolveFont(paths, id) {
|
|
|
31
31
|
return font;
|
|
32
32
|
}
|
|
33
33
|
export function missingFont() {
|
|
34
|
-
return new Error("The
|
|
34
|
+
return new Error("The chosen caption font is no longer available; it may have been uninstalled or deleted. Choose another font in Edit project → Subtitles, then try again.", {
|
|
35
35
|
cause: "font-not-found",
|
|
36
36
|
});
|
|
37
37
|
}
|
|
@@ -27,7 +27,7 @@ export async function uploadFont(paths, input) {
|
|
|
27
27
|
const root = join(paths.dataDir, "fonts");
|
|
28
28
|
await mkdir(root, { recursive: true, mode: 0o700 });
|
|
29
29
|
if (!(await lstat(root)).isDirectory())
|
|
30
|
-
throw new Error(
|
|
30
|
+
throw new Error(`Slopify can't save uploaded fonts because ${root} is not a folder. Move or rename that file, then upload the font again.`);
|
|
31
31
|
const temporary = join(root, `.upload-${randomUUID()}`);
|
|
32
32
|
try {
|
|
33
33
|
await writeFile(temporary, input.content, { mode: 0o600, flag: "wx" });
|
|
@@ -22,19 +22,19 @@ export async function runImages(deps, context, providers) {
|
|
|
22
22
|
const { projectId } = context.stage;
|
|
23
23
|
const project = projectById(deps.db, projectId);
|
|
24
24
|
if (project === undefined) {
|
|
25
|
-
throw new Error(
|
|
25
|
+
throw new Error("This project no longer exists; it may have been deleted while it was running.");
|
|
26
26
|
}
|
|
27
27
|
const choice = project.config.images;
|
|
28
28
|
if (choice === undefined) {
|
|
29
29
|
// Admission refuses a run whose images are Generate without a provider and a model, so
|
|
30
30
|
// reaching here is a bug in admission rather than the user's.
|
|
31
|
-
throw new Error("
|
|
31
|
+
throw new Error("No image model is set for this project. Choose one in Edit project → Providers, then Retry stage.");
|
|
32
32
|
}
|
|
33
33
|
const pieces = plan(deps, context, project.config);
|
|
34
34
|
if (pieces.length === 0) {
|
|
35
35
|
// Admission makes an image source mandatory and puts the Number at one or more, so an
|
|
36
36
|
// empty plan is a bug upstream rather than a run with no pictures.
|
|
37
|
-
throw new Error("
|
|
37
|
+
throw new Error("Slopify hit an internal error (no image prompt was selected). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
38
38
|
}
|
|
39
39
|
// The run's own frame. The adapter turns it into whatever its provider spells the
|
|
40
40
|
// closest supported size, and the render crops whatever is left over.
|
|
@@ -78,7 +78,7 @@ function plan(deps, context, config) {
|
|
|
78
78
|
// it, so the run carries the substituted text under this key.
|
|
79
79
|
const prompt = config.rendered[`imagePrompts.${String(at)}`];
|
|
80
80
|
if (prompt === undefined) {
|
|
81
|
-
throw new Error(`
|
|
81
|
+
throw new Error(`Slopify hit an internal error (the image prompt ${picked.name} was never filled in). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
|
|
82
82
|
}
|
|
83
83
|
for (let send = 1; send <= picked.number; send += 1) {
|
|
84
84
|
planned.push({
|
|
@@ -224,7 +224,7 @@ function report(deps, context, done, total) {
|
|
|
224
224
|
}
|
|
225
225
|
function payloadOf(piece) {
|
|
226
226
|
if (piece.payload === null) {
|
|
227
|
-
throw new Error(
|
|
227
|
+
throw new Error("Slopify hit an internal error (a saved image step is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
228
228
|
}
|
|
229
229
|
return imagePayload.parse(JSON.parse(piece.payload));
|
|
230
230
|
}
|
|
@@ -13,17 +13,23 @@ function lint(name, body) {
|
|
|
13
13
|
const fields = [];
|
|
14
14
|
const trimmed = name.trim();
|
|
15
15
|
if (trimmed === "") {
|
|
16
|
-
fields.push({ field: "name", message: "
|
|
16
|
+
fields.push({ field: "name", message: "Enter a name." });
|
|
17
17
|
}
|
|
18
18
|
else if (trimmed.length > nameMax) {
|
|
19
|
-
fields.push({
|
|
19
|
+
fields.push({
|
|
20
|
+
field: "name",
|
|
21
|
+
message: `Keep the name to ${String(nameMax)} characters or fewer.`,
|
|
22
|
+
});
|
|
20
23
|
}
|
|
21
24
|
if (body.trim() === "") {
|
|
22
|
-
fields.push({ field: "body", message: "
|
|
25
|
+
fields.push({ field: "body", message: "Enter the text." });
|
|
23
26
|
return fields;
|
|
24
27
|
}
|
|
25
28
|
if (body.length > bodyMax) {
|
|
26
|
-
fields.push({
|
|
29
|
+
fields.push({
|
|
30
|
+
field: "body",
|
|
31
|
+
message: `The text is too long. Keep it to ${String(bodyMax)} characters or fewer.`,
|
|
32
|
+
});
|
|
27
33
|
return fields;
|
|
28
34
|
}
|
|
29
35
|
for (const error of detectSlots(body).errors) {
|
|
@@ -37,11 +43,11 @@ function describe(error, body) {
|
|
|
37
43
|
const where = positionOf(body, error.at);
|
|
38
44
|
switch (error.kind) {
|
|
39
45
|
case "unclosed":
|
|
40
|
-
return `The \`{{\` at ${where} is never closed.`;
|
|
46
|
+
return `The \`{{\` at ${where} is never closed. Add \`}}\` after the keyword name.`;
|
|
41
47
|
case "empty":
|
|
42
|
-
return `The
|
|
48
|
+
return `The keyword at ${where} has no name. Write a name between \`{{\` and \`}}\`.`;
|
|
43
49
|
case "nested":
|
|
44
|
-
return `The
|
|
50
|
+
return `The keyword at ${where} contains a brace. Keywords cannot be placed inside other keywords.`;
|
|
45
51
|
}
|
|
46
52
|
}
|
|
47
53
|
function positionOf(body, at) {
|
|
@@ -58,7 +58,7 @@ function body(db, kind, name, field, missing, into, key, snapshot) {
|
|
|
58
58
|
}
|
|
59
59
|
const prompt = snapshotPrompt(db, snapshot, kind, name);
|
|
60
60
|
if (prompt === undefined) {
|
|
61
|
-
missing.push({ field, message: `That ${kind} prompt
|
|
61
|
+
missing.push({ field, message: `That ${kind} prompt was deleted. Choose another.` });
|
|
62
62
|
return;
|
|
63
63
|
}
|
|
64
64
|
into.push({ key, body: prompt.body });
|
|
@@ -71,7 +71,7 @@ function pickEntry(db, category, choice, missing, snapshot) {
|
|
|
71
71
|
if (entry === undefined) {
|
|
72
72
|
missing.push({
|
|
73
73
|
field: category,
|
|
74
|
-
message: `That ${category} entry
|
|
74
|
+
message: `That ${category} entry was deleted. Choose another.`,
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
77
|
return entry;
|
|
@@ -46,7 +46,7 @@ export async function joinNarration(deps, input) {
|
|
|
46
46
|
const first = input.files[0];
|
|
47
47
|
if (first === undefined) {
|
|
48
48
|
// The stage refuses an empty narration before it gets here, so this is a bug.
|
|
49
|
-
throw new Error("there
|
|
49
|
+
throw new Error("Slopify hit an internal error (there is no narration audio to join). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
50
50
|
}
|
|
51
51
|
mkdirSync(dirname(input.output), { recursive: true, mode: 0o700 });
|
|
52
52
|
if (input.files.length === 1) {
|
|
@@ -18,7 +18,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
|
|
|
18
18
|
const chunks = chunkNarration(source, chunking).map((text) => {
|
|
19
19
|
const start = source.indexOf(text, offset);
|
|
20
20
|
if (start < 0)
|
|
21
|
-
throw new Error("
|
|
21
|
+
throw new Error("Slopify hit an internal error (a narration chunk isn't in the article text). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
22
22
|
offset = start + text.length;
|
|
23
23
|
return { key: nextKey(text), text, start, end: offset };
|
|
24
24
|
});
|
|
@@ -78,7 +78,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
|
|
|
78
78
|
}
|
|
79
79
|
const chunk = chunks[index];
|
|
80
80
|
if (chunk === undefined)
|
|
81
|
-
throw new Error("
|
|
81
|
+
throw new Error("Slopify hit an internal error (a narration chunk went missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
82
82
|
while ((matches[matchIndex]?.end ?? Infinity) <= chunk.end)
|
|
83
83
|
matchIndex++;
|
|
84
84
|
const match = matches[matchIndex];
|
|
@@ -90,7 +90,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
|
|
|
90
90
|
continue;
|
|
91
91
|
const first = chunks[from];
|
|
92
92
|
if (first === undefined)
|
|
93
|
-
throw new Error("
|
|
93
|
+
throw new Error("Slopify hit an internal error (a group of narration chunks is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
94
94
|
const text = source.slice(first.start, chunk.end);
|
|
95
95
|
result.push(from === index
|
|
96
96
|
? { key: chunk.key, text }
|
|
@@ -105,7 +105,7 @@ export function pronunciationMatches(source, entries) {
|
|
|
105
105
|
continue;
|
|
106
106
|
const entry = ordered.find((_, index) => match[index + 1] !== undefined);
|
|
107
107
|
if (entry === undefined)
|
|
108
|
-
throw new Error("
|
|
108
|
+
throw new Error("Slopify hit an internal error (a pronunciation glossary term has no pronunciation). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
109
109
|
matches.push({ start: match.index, end, entry });
|
|
110
110
|
}
|
|
111
111
|
return matches;
|
|
@@ -117,7 +117,7 @@ export function pronunciationSpans(source, entries) {
|
|
|
117
117
|
for (const [index, word] of Array.from(words).entries()) {
|
|
118
118
|
const ipa = match.entry.ipa[index];
|
|
119
119
|
if (ipa === undefined)
|
|
120
|
-
throw new Error("
|
|
120
|
+
throw new Error("Slopify hit an internal error (a pronunciation glossary term is missing a word). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
121
121
|
const start = match.start + word.index;
|
|
122
122
|
spans.push({ start, end: start + word[0].length, text: `/${ipa}/` });
|
|
123
123
|
}
|