@gentbajko/slopify 2.0.1 → 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.
Files changed (162) hide show
  1. package/dist/adapters/alignment/cache.js +17 -1
  2. package/dist/adapters/alignment/prefetch.js +35 -0
  3. package/dist/adapters/alignment/protocol.js +11 -1
  4. package/dist/adapters/alignment/runner.js +4 -1
  5. package/dist/adapters/alignment/worker.js +28 -5
  6. package/dist/adapters/explain.js +93 -0
  7. package/dist/adapters/ffmpeg.js +3 -3
  8. package/dist/adapters/host-cli/index.js +9 -2
  9. package/dist/adapters/host-cli/transport.js +4 -4
  10. package/dist/adapters/image/bytes.js +2 -2
  11. package/dist/adapters/image/codex-output.js +12 -12
  12. package/dist/adapters/image/codex.js +24 -8
  13. package/dist/adapters/image/fal.js +12 -6
  14. package/dist/adapters/image/google.js +18 -8
  15. package/dist/adapters/image/models.js +8 -2
  16. package/dist/adapters/image/openai.js +15 -6
  17. package/dist/adapters/image/replicate.js +16 -7
  18. package/dist/adapters/llm/claude-code-models.js +1 -1
  19. package/dist/adapters/llm/claude-code.js +16 -7
  20. package/dist/adapters/llm/cli-login-error.js +2 -1
  21. package/dist/adapters/llm/codex-models.js +1 -1
  22. package/dist/adapters/llm/codex.js +5 -6
  23. package/dist/adapters/llm/document-workspace.js +2 -2
  24. package/dist/adapters/llm/gemini-models.js +1 -1
  25. package/dist/adapters/llm/gemini-workspace.js +1 -1
  26. package/dist/adapters/llm/gemini.js +15 -11
  27. package/dist/adapters/llm/openrouter.js +21 -9
  28. package/dist/adapters/llm/run-cli.js +14 -11
  29. package/dist/adapters/tts/cartesia.js +10 -3
  30. package/dist/adapters/tts/elevenlabs.js +18 -6
  31. package/dist/adapters/tts/inworld-async.js +47 -16
  32. package/dist/adapters/tts/inworld.js +25 -15
  33. package/dist/adapters/tts/openai.js +18 -6
  34. package/dist/catalog/registry.js +13 -10
  35. package/dist/catalog/store.js +1 -1
  36. package/dist/catalog/validate.js +13 -4
  37. package/dist/edge/cli.js +37 -8
  38. package/dist/edge/docker-launch.js +6 -6
  39. package/dist/edge/docker-projects/engine.js +34 -13
  40. package/dist/edge/docker-projects/install.js +8 -8
  41. package/dist/edge/docker-projects/state.js +6 -6
  42. package/dist/edge/docker-projects/tree.js +6 -6
  43. package/dist/edge/docker.js +13 -13
  44. package/dist/edge/http/actions.js +27 -25
  45. package/dist/edge/http/app.js +3 -3
  46. package/dist/edge/http/audio-preview.js +1 -1
  47. package/dist/edge/http/checkpoints.js +5 -1
  48. package/dist/edge/http/diagnostics.js +3 -1
  49. package/dist/edge/http/draft-files.js +2 -2
  50. package/dist/edge/http/draft-problem.js +11 -1
  51. package/dist/edge/http/files.js +4 -4
  52. package/dist/edge/http/folder-location.js +3 -3
  53. package/dist/edge/http/fonts.js +22 -10
  54. package/dist/edge/http/host-cli.js +34 -12
  55. package/dist/edge/http/multipart.js +7 -5
  56. package/dist/edge/http/open-folder.js +2 -2
  57. package/dist/edge/http/planning.js +4 -4
  58. package/dist/edge/http/problem.js +2 -2
  59. package/dist/edge/http/project-templates.js +7 -3
  60. package/dist/edge/http/projects.js +8 -8
  61. package/dist/edge/http/prompts.js +9 -4
  62. package/dist/edge/http/providers.js +15 -7
  63. package/dist/edge/http/revision-files.js +3 -3
  64. package/dist/edge/http/revisions.js +30 -4
  65. package/dist/edge/http/schedules.js +9 -9
  66. package/dist/edge/http/settings.js +15 -9
  67. package/dist/edge/http/staging.js +5 -5
  68. package/dist/edge/http/storage.js +4 -4
  69. package/dist/edge/http/subtitles.js +1 -1
  70. package/dist/edge/http/tutorial.js +2 -2
  71. package/dist/edge/http/update.js +3 -3
  72. package/dist/host-cli/install.js +28 -6
  73. package/dist/host-cli/paths.js +6 -6
  74. package/dist/host-cli/server.js +1 -1
  75. package/dist/host-cli/service.js +11 -11
  76. package/dist/host-cli/status.js +7 -3
  77. package/dist/kernel/config/index.js +6 -4
  78. package/dist/kernel/db/migrate.js +2 -1
  79. package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
  80. package/dist/kernel/errors.js +12 -0
  81. package/dist/kernel/lock.js +4 -3
  82. package/dist/kernel/paths.js +5 -0
  83. package/dist/kernel/ports/subtitles.js +15 -1
  84. package/dist/kernel/runner/attempt.js +26 -2
  85. package/dist/kernel/runner/index.js +16 -6
  86. package/dist/kernel/runner/progress.js +56 -0
  87. package/dist/main.js +20 -4
  88. package/dist/slices/admission/rules.js +32 -20
  89. package/dist/slices/article/continuation.js +4 -2
  90. package/dist/slices/article/plain.js +2 -2
  91. package/dist/slices/article/provided-entries.js +3 -3
  92. package/dist/slices/article/run.js +2 -2
  93. package/dist/slices/article/segments.js +5 -3
  94. package/dist/slices/control/providers.js +20 -7
  95. package/dist/slices/fonts/catalog.js +1 -1
  96. package/dist/slices/fonts/upload.js +1 -1
  97. package/dist/slices/images/run.js +5 -5
  98. package/dist/slices/library/lint.js +13 -7
  99. package/dist/slices/library/slots.js +2 -2
  100. package/dist/slices/narration/concat.js +1 -1
  101. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  102. package/dist/slices/narration/pronunciation.js +2 -2
  103. package/dist/slices/narration/run.js +7 -7
  104. package/dist/slices/narration/steering.js +3 -3
  105. package/dist/slices/play-drafts/convert.js +5 -4
  106. package/dist/slices/play-drafts/readiness.js +12 -6
  107. package/dist/slices/play-drafts/review-inputs.js +7 -4
  108. package/dist/slices/play-drafts/review.js +3 -3
  109. package/dist/slices/play-drafts/service.js +3 -1
  110. package/dist/slices/play-drafts/uploads.js +1 -1
  111. package/dist/slices/rebuild/admission-repo.js +2 -2
  112. package/dist/slices/rebuild/recipe-legacy.js +1 -1
  113. package/dist/slices/rebuild/recipe-narration-text.js +1 -1
  114. package/dist/slices/rebuild/recipe-visual.js +1 -1
  115. package/dist/slices/rebuild/recovery.js +2 -2
  116. package/dist/slices/rebuild/repo.js +3 -3
  117. package/dist/slices/rebuild/runtime-admission.js +4 -4
  118. package/dist/slices/rebuild/runtime-article.js +5 -5
  119. package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
  120. package/dist/slices/rebuild/runtime-export-inputs.js +6 -6
  121. package/dist/slices/rebuild/runtime-export.js +4 -4
  122. package/dist/slices/rebuild/runtime-local.js +6 -6
  123. package/dist/slices/rebuild/runtime-narration-text.js +46 -7
  124. package/dist/slices/rebuild/runtime-piece-label.js +62 -0
  125. package/dist/slices/rebuild/runtime-provider.js +6 -4
  126. package/dist/slices/rebuild/runtime-publication.js +3 -3
  127. package/dist/slices/rebuild/runtime-run.js +11 -1
  128. package/dist/slices/rebuild/runtime-store.js +45 -15
  129. package/dist/slices/rebuild/runtime-subtitles.js +80 -17
  130. package/dist/slices/rebuild/transition-repo.js +6 -6
  131. package/dist/slices/reruns/index.js +1 -1
  132. package/dist/slices/research/run.js +9 -7
  133. package/dist/slices/research/synthesis.js +4 -2
  134. package/dist/slices/revisions/mutation-assets.js +26 -8
  135. package/dist/slices/revisions/mutation-cues.js +1 -1
  136. package/dist/slices/revisions/rules.js +18 -9
  137. package/dist/slices/revisions/schema.js +1 -1
  138. package/dist/slices/schedules/repo.js +55 -11
  139. package/dist/slices/schedules/scheduler.js +25 -8
  140. package/dist/slices/schedules/schema.js +19 -4
  141. package/dist/slices/schedules/service.js +2 -0
  142. package/dist/slices/settings/cli-paths.js +7 -4
  143. package/dist/slices/settings/models.js +4 -4
  144. package/dist/slices/settings/playback.js +2 -2
  145. package/dist/slices/storage/assets.js +5 -5
  146. package/dist/slices/storage/layout.js +1 -1
  147. package/dist/slices/subtitles/captions.js +1 -1
  148. package/dist/slices/subtitles/prepare.js +7 -6
  149. package/dist/slices/subtitles/transcript.js +1 -1
  150. package/dist/slices/thumbnail/run.js +8 -8
  151. package/dist/slices/video/audio-inputs.js +2 -2
  152. package/dist/slices/video/ffmpeg.js +6 -4
  153. package/dist/slices/video/plan.js +1 -1
  154. package/dist/slices/video/run.js +2 -2
  155. package/dist/updater/plan.js +1 -1
  156. package/dist/updater/service.js +4 -3
  157. package/dist/updater/worker.js +6 -4
  158. package/dist/web/assets/{index-CZNiHm8P.css → index-Bwtmu9ei.css} +1 -1
  159. package/dist/web/assets/index-DjhMJqMN.js +141 -0
  160. package/dist/web/index.html +2 -2
  161. package/package.json +1 -1
  162. package/dist/web/assets/index-DPgGLX5m.js +0 -137
@@ -19,11 +19,11 @@ export function saveSettings(deps, settings) {
19
19
  if (!silenceGapValue.safeParse(settings.silenceGapSeconds).success) {
20
20
  fields.push({
21
21
  field: "silenceGapSeconds",
22
- message: `The silence gap is a whole number of seconds between 0 and ${String(silenceGapSecondsMax)}.`,
22
+ message: `Enter a whole number of seconds between 0 and ${String(silenceGapSecondsMax)}.`,
23
23
  });
24
24
  }
25
25
  if (!appearanceValue.safeParse(settings.appearance).success) {
26
- fields.push({ field: "appearance", message: "Pick System, Light or Dark." });
26
+ fields.push({ field: "appearance", message: "Choose System, Light or Dark." });
27
27
  }
28
28
  if (fields.length > 0) {
29
29
  return { ok: false, fields };
@@ -10,7 +10,7 @@ export function allocateAsset(deps, projectId, filename) {
10
10
  const absolutePath = outputPath(deps.paths, projectId, path);
11
11
  const directory = outputPath(deps.paths, projectId, `assets/${id}`);
12
12
  if (dirname(absolutePath) !== directory)
13
- throw new Error("Asset filenames must be basenames.");
13
+ throw new Error("Slopify hit an internal error (a file name to save is not a plain file name). Try again; if it happens again, use Download diagnostics in Settings and report it.");
14
14
  mkdirSync(outputPath(deps.paths, projectId, "assets"), { recursive: true, mode: 0o700 });
15
15
  mkdirSync(directory, { mode: 0o700 });
16
16
  return { id, projectId, path, absolutePath };
@@ -20,7 +20,7 @@ export function sealAsset(deps, pending) {
20
20
  try {
21
21
  const stats = fstatSync(file);
22
22
  if (!stats.isFile())
23
- throw new Error("An asset must be a regular file.");
23
+ throw new Error("Slopify hit an internal error (a saved file is not a regular file). Try again; if it happens again, use Download diagnostics in Settings and report it.");
24
24
  fchmodSync(file, 0o600);
25
25
  fsyncSync(file);
26
26
  return {
@@ -58,7 +58,7 @@ export function discardPreparedAssets(deps, assets) {
58
58
  const filename = basename(asset.path);
59
59
  requireBasename(filename);
60
60
  if (asset.path !== `assets/${asset.id}/${filename}`) {
61
- throw new Error("A prepared asset must belong to its allocated directory.");
61
+ throw new Error("Slopify hit an internal error (a prepared file is not in its allocated directory). Try again; if it happens again, use Download diagnostics in Settings and report it.");
62
62
  }
63
63
  const directory = outputPath(deps.paths, asset.projectId, `assets/${asset.id}`);
64
64
  const registered = deps.db
@@ -68,7 +68,7 @@ export function discardPreparedAssets(deps, assets) {
68
68
  if (row.id === asset.id)
69
69
  return true;
70
70
  if (typeof row.path !== "string")
71
- throw new Error("A registered asset must name a path.");
71
+ throw new Error("Slopify hit an internal error (a saved file has no path). Try again; if it happens again, use Download diagnostics in Settings and report it.");
72
72
  const inside = relative(directory, outputPath(deps.paths, asset.projectId, row.path));
73
73
  return (inside === "" ||
74
74
  (inside !== ".." && !inside.startsWith(`..${sep}`) && !isAbsolute(inside)));
@@ -92,7 +92,7 @@ function requireBasename(filename) {
92
92
  }) ||
93
93
  /[. ]$/.test(filename) ||
94
94
  /^(?:con|prn|aux|nul|conin\$|conout\$|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(filename))
95
- throw new Error("Asset filenames must be portable basenames.");
95
+ throw new Error("Slopify hit an internal error (a file name to save is not allowed on every system). Try again; if it happens again, use Download diagnostics in Settings and report it.");
96
96
  }
97
97
  function messageOf(error) {
98
98
  return error instanceof Error ? error.message : String(error);
@@ -14,7 +14,7 @@ function contained(root, path) {
14
14
  const target = resolve(root, path);
15
15
  const inside = relative(root, target);
16
16
  if (inside === "" || inside.startsWith("..") || isAbsolute(inside)) {
17
- throw new Error(`${path} resolves outside ${root}`);
17
+ throw new Error(`Slopify hit an internal error (the saved file path ${path} resolves outside ${root}). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
18
18
  }
19
19
  return target;
20
20
  }
@@ -21,7 +21,7 @@ export function captionCues(words) {
21
21
  !Number.isFinite(word.end) ||
22
22
  word.start < previousEnd - 0.001 ||
23
23
  word.end <= word.start) {
24
- throw new Error("The subtitle word timing is invalid; regenerate alignment.");
24
+ throw new Error("The saved caption timing is damaged. Use Re-run section on Video to time them again, then Retry stage.");
25
25
  }
26
26
  const first = pending[0];
27
27
  if (first !== undefined &&
@@ -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";
@@ -42,7 +43,7 @@ export async function prepareSubtitles(deps, context, audio, frame) {
42
43
  const { projectId } = context.stage;
43
44
  const project = projectById(deps.db, projectId);
44
45
  if (project === undefined)
45
- throw new Error("The project no longer exists.");
46
+ throw new Error("This project no longer exists; it may have been deleted while it was running.");
46
47
  const config = project.config.subtitles ?? defaultSubtitles;
47
48
  if (config.mode === "off")
48
49
  return undefined;
@@ -50,7 +51,7 @@ export async function prepareSubtitles(deps, context, audio, frame) {
50
51
  setStageProgress(deps.db, context.stage.id, 0, 100);
51
52
  context.emit({ type: "stage.progress", projectId, stage: "video", current: 0, total: 100 });
52
53
  if (audio.length === 0)
53
- throw new Error("Subtitles need narration audio.");
54
+ throw new Error("Captions need narration audio, but this project has none. Turn captions off in Edit project → Subtitles, or turn narration on, then Retry stage.");
54
55
  const outputs = outputsOf(deps.db, projectId);
55
56
  const segments = audio.map((segment) => ({
56
57
  ...segment,
@@ -67,7 +68,7 @@ export async function prepareSubtitles(deps, context, audio, frame) {
67
68
  context.signal.throwIfAborted();
68
69
  const cues = captionCues(words);
69
70
  if (cues.length === 0)
70
- throw new Error("No spoken words could be aligned. Check that the English article matches the audio.");
71
+ throw new Error("None of the narration could be matched to the article text, so captions can't be timed. Captions only work for English narration; if you uploaded your own audio, make sure it reads the article text, then Retry stage.");
71
72
  writeFileSync(join(directory, "subtitles.srt"), serializeSrt(cues), { mode: 0o600 });
72
73
  writeFileSync(join(directory, "subtitles.vtt"), serializeVtt(cues), { mode: 0o600 });
73
74
  writeFileSync(join(directory, "subtitles.ass"), serializeAss(cues, {
@@ -117,7 +118,7 @@ async function timingKey(segments, signal) {
117
118
  }
118
119
  async function alignSegments(deps, context, segments, omissions) {
119
120
  if (deps.alignSubtitles === undefined)
120
- throw new Error("Local subtitle alignment is unavailable in this build.");
121
+ throw new Error("Slopify hit an internal error (the caption timing tool is missing from this build). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
121
122
  const words = [];
122
123
  const total = segments.reduce((sum, segment) => sum + segment.seconds, 0);
123
124
  let offset = 0;
@@ -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: join(deps.paths.dataDir, "models", "english-subtitles"),
132
+ cacheDir: subtitleModelDir(deps.paths.dataDir),
132
133
  ffmpeg: deps.ffmpeg,
133
134
  signal: context.signal,
134
135
  onProgress: (current, maximum) => {
@@ -145,7 +146,7 @@ async function alignSegments(deps, context, segments, omissions) {
145
146
  });
146
147
  for (const word of aligned) {
147
148
  if (word.end > segment.seconds + 0.1)
148
- throw new Error("Subtitle timing exceeds the narration duration.");
149
+ throw new Error("Caption timing came out longer than the narration audio. Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
149
150
  words.push({
150
151
  ...word,
151
152
  start: word.start + offset,
@@ -27,7 +27,7 @@ export function spokenText(deps, projectId, kind, outputs) {
27
27
  return segment.text;
28
28
  }
29
29
  }
30
- throw new Error(`The saved ${kind} narration text is missing. Subtitles need the text spoken in the audio.`);
30
+ throw new Error(`The saved text of the ${kind === "body" ? "article" : kind} is missing, and captions need the text spoken in the narration. Use Re-run section on Article, then Retry stage.`);
31
31
  }
32
32
  function payload(raw) {
33
33
  return textPayload.parse(JSON.parse(raw ?? "null"));
@@ -16,19 +16,19 @@ export async function runThumbnail(deps, context, providers) {
16
16
  const { projectId } = context.stage;
17
17
  const project = projectById(deps.db, projectId);
18
18
  if (project === undefined) {
19
- throw new Error(`project ${projectId} has no row`);
19
+ throw new Error("This project no longer exists; it may have been deleted while it was running.");
20
20
  }
21
21
  const source = project.config.sources.thumbnail;
22
22
  if (source !== "from_prompt" && source !== "prompt_by_llm") {
23
23
  // Off is `skipped` and Provide is `provided` at project creation,
24
24
  // so the runner never starts this stage for either.
25
- throw new Error(`the thumbnail stage cannot run with its source set to ${source}`);
25
+ throw new Error(`Slopify hit an internal error (the thumbnail stage cannot run with its source set to ${source}). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
26
26
  }
27
27
  const choice = project.config.images;
28
28
  if (choice === undefined) {
29
29
  // Admission requires the image provider whenever the thumbnail is generated, so reaching
30
30
  // here is a bug in admission rather than the user's.
31
- throw new Error("the run has no image provider or model");
31
+ throw new Error("No image model is set for the thumbnail. Choose one in Edit project → Providers, then Retry stage.");
32
32
  }
33
33
  const result = source === "from_prompt" ? undefined : await byLlm(deps, context, providers, project);
34
34
  if (result !== undefined && !result.ok)
@@ -60,7 +60,7 @@ export async function runThumbnail(deps, context, providers) {
60
60
  function fromTemplate(project) {
61
61
  const rendered = project.config.rendered.thumbnailPrompt;
62
62
  if (rendered === undefined) {
63
- throw new Error("the run has no rendered thumbnail prompt");
63
+ throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
64
64
  }
65
65
  return rendered;
66
66
  }
@@ -83,7 +83,7 @@ async function byLlm(deps, context, providers, project) {
83
83
  }
84
84
  if (llm === undefined) {
85
85
  // Admission requires the LLM row when the thumbnail source is Prompt by LLM.
86
- throw new Error("the run has no LLM provider or model");
86
+ throw new Error("No AI model is set for writing the thumbnail prompt. Choose one in Edit project → Providers, then Retry stage.");
87
87
  }
88
88
  const messages = thumbnailMessages(brief(deps, project));
89
89
  const answer = await providers.llm({
@@ -117,7 +117,7 @@ function named(choice) {
117
117
  function brief(deps, project) {
118
118
  const instruction = project.config.rendered.thumbnailPrompt;
119
119
  if (instruction === undefined) {
120
- throw new Error("the run has no rendered thumbnail prompt");
120
+ throw new Error("Slopify hit an internal error (the thumbnail prompt was never filled in). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
121
121
  }
122
122
  return {
123
123
  instruction,
@@ -133,7 +133,7 @@ function brief(deps, project) {
133
133
  function articleText(deps, projectId) {
134
134
  const article = outputsOf(deps.db, projectId).find((output) => output.role === "article_txt");
135
135
  if (article === undefined) {
136
- throw new Error("the project has no article for the thumbnail prompt to be written from");
136
+ throw new Error("The thumbnail prompt is written from the article, but the article isn't ready. Let the Article stage finish, then Retry stage.");
137
137
  }
138
138
  return readFileSync(outputPath(deps.paths, projectId, article.path), "utf8");
139
139
  }
@@ -212,7 +212,7 @@ function instructionsText(messages) {
212
212
  }
213
213
  function payloadOf(piece) {
214
214
  if (piece.payload === null) {
215
- throw new Error(`the written thumbnail prompt ${piece.id} has no payload`);
215
+ throw new Error("Slopify hit an internal error (the saved thumbnail prompt is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
216
216
  }
217
217
  return writtenPayload.parse(JSON.parse(piece.payload));
218
218
  }
@@ -5,9 +5,9 @@ export async function audioInputs(deps, projectId, outputs, signal) {
5
5
  const dir = projectDir(deps.paths, projectId);
6
6
  const body = await audio(deps, outputs, "audio_body", dir, signal);
7
7
  if (!body)
8
- throw new Error("the project has no narration audio to render against");
8
+ throw new Error("The video has no narration audio to play. Let the Narration stage finish, then Retry stage.");
9
9
  if (body.seconds <= 0)
10
- throw new Error("the narration audio decodes to no sound");
10
+ throw new Error("The narration audio contains no sound; the file may be damaged or empty. Regenerate the narration (or upload a different audio file in Edit project → Narration), then Retry stage.");
11
11
  const intro = await audio(deps, outputs, "audio_intro", dir, signal);
12
12
  const outro = await audio(deps, outputs, "audio_outro", dir, signal);
13
13
  return { body, intro, outro };
@@ -24,8 +24,9 @@ export function resolveFfmpeg(env, bundled) {
24
24
  }
25
25
  // Never PATH: a Slopify that quietly rendered with whichever ffmpeg the machine
26
26
  // happens to carry would not be testing the binary it ships with.
27
- throw new Error("No ffmpeg binary is available. Slopify ships one through ffmpeg-static; if this " +
28
- "platform has no build, point SLOPIFY_FFMPEG at an ffmpeg executable.");
27
+ throw new Error("Slopify can't find ffmpeg, the tool it uses to make audio and video: its bundled copy is " +
28
+ "missing for this computer. Reinstall Slopify, or set the SLOPIFY_FFMPEG environment " +
29
+ "variable to the path of an ffmpeg program.");
29
30
  }
30
31
  export function renderArgs(plan, burnSubtitles = false) {
31
32
  const inputs = [];
@@ -180,7 +181,7 @@ export function runFfmpeg(run) {
180
181
  run.signal.addEventListener("abort", stop, { once: true });
181
182
  child.on("error", (error) => {
182
183
  run.signal.removeEventListener("abort", stop);
183
- reject(new Error(`ffmpeg could not be started: ${error.message}`));
184
+ reject(new Error(`Slopify could not start ffmpeg, the tool it uses to make audio and video (${error.message}). Reinstall Slopify, or if you set SLOPIFY_FFMPEG, check that it points to a working ffmpeg program.`));
184
185
  });
185
186
  child.on("close", (code, signal) => {
186
187
  run.signal.removeEventListener("abort", stop);
@@ -240,7 +241,8 @@ function messageOf(error) {
240
241
  }
241
242
  function failure(code, signal, errors) {
242
243
  const ended = code === null ? `killed by ${signal ?? "a signal"}` : `exited with code ${code}`;
243
- return errors.length === 0
244
+ const said = errors.length === 0
244
245
  ? `ffmpeg ${ended} and wrote nothing to its error stream`
245
246
  : `ffmpeg ${ended}: ${errors.join(" / ")}`;
247
+ return `The audio/video export failed (${said}). Retry stage; if it fails again, make sure your disk has free space (see Settings → Backup & storage), then use Download diagnostics in Settings and report it.`;
246
248
  }
@@ -13,7 +13,7 @@ export const zoomBy = 0.225;
13
13
  export function planRender(input) {
14
14
  if (input.images.length === 0) {
15
15
  // Admission permits Video only with an image source, so an empty set is a bug upstream.
16
- throw new Error("a render needs at least one image");
16
+ throw new Error("The video needs at least one image, but there are none. Check the Images section in Edit project, then Retry stage.");
17
17
  }
18
18
  const frame = frames[input.format];
19
19
  const audio = input.body === undefined ? [] : audioTimeline({ ...input, body: input.body });
@@ -12,10 +12,10 @@ export async function renderVideo(deps, context) {
12
12
  const { projectId } = context.stage;
13
13
  const project = projectById(deps.db, projectId);
14
14
  if (!project)
15
- throw new Error(`project ${projectId} has no row`);
15
+ throw new Error("This project no longer exists; it may have been deleted while it was running.");
16
16
  if (project.config.sources.video === "off") {
17
17
  if (project.config.sources.audio === "off")
18
- throw new Error("the project has no media export enabled");
18
+ throw new Error("There is nothing to export because both video and narration are turned off for this project. Turn one on in Edit project, then Retry stage.");
19
19
  await exportAudioWav(deps, context, project.config.silenceGapSeconds);
20
20
  return;
21
21
  }
@@ -147,7 +147,7 @@ export async function npmCommand() {
147
147
  /* PATH can include directories without npm. */
148
148
  }
149
149
  }
150
- throw new Error("npm is unavailable. Install Node.js with npm to use in-app updates.");
150
+ throw new Error("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");
151
151
  }
152
152
  export async function updateCommitted(dataDir, version, token) {
153
153
  try {
@@ -7,7 +7,7 @@ export function createUpdater(deps) {
7
7
  let status = deps.candidate?.pending === true ? "restarting" : "idle";
8
8
  let activated = deps.candidate?.pending !== true;
9
9
  let installError = deps.previousUpdateFailed
10
- ? "The update did not start. The previous version and database were restored. You can try again."
10
+ ? "The new version did not start, so Slopify put back the previous version and database. You can try again; the reason is in logs/updates.log inside your Slopify data folder."
11
11
  : undefined;
12
12
  let error = installError;
13
13
  let checking;
@@ -56,7 +56,8 @@ export function createUpdater(deps) {
56
56
  }
57
57
  catch {
58
58
  status = "error";
59
- error = "Could not check for updates. Check your connection and try again.";
59
+ error =
60
+ "Could not check for updates: the npm registry (registry.npmjs.org) did not answer properly. Check your internet connection and try again.";
60
61
  deps.report(error);
61
62
  }
62
63
  finally {
@@ -143,7 +144,7 @@ export function createUpdater(deps) {
143
144
  .catch(() => {
144
145
  status = "error";
145
146
  error =
146
- "The update could not be completed. Your current installation is still available. Try again or restart Slopify.";
147
+ "The update could not be installed, so Slopify kept your current version. Check your internet connection and try again; the reason is in logs/updates.log inside your Slopify data folder.";
147
148
  installError = error;
148
149
  deps.report(error);
149
150
  });
@@ -111,15 +111,17 @@ function installPackage(plan, directory) {
111
111
  timeout: 15 * 60_000,
112
112
  env: { ...process.env, npm_config_update_notifier: "false" },
113
113
  });
114
- child.once("error", () => reject(new Error("npm could not be started.")));
115
- child.once("close", (code) => code === 0 ? resolve() : reject(new Error("npm could not install the update.")));
114
+ child.once("error", () => reject(new Error(`npm (${plan.npm.file}) could not be started. Check that Node.js and npm are installed.`)));
115
+ child.once("close", (code) => code === 0
116
+ ? resolve()
117
+ : reject(new Error(`npm could not install Slopify ${plan.version} (exit code ${code}). Check your internet connection (npm must reach registry.npmjs.org) and try again.`)));
116
118
  });
117
119
  }
118
120
  async function waitForServer(plan, version, exited) {
119
121
  const until = Date.now() + 60_000;
120
122
  while (Date.now() < until) {
121
123
  if (exited())
122
- throw new Error("Slopify stopped during startup.");
124
+ throw new Error(`Slopify ${version} stopped while starting up, so the previous version was kept.`);
123
125
  try {
124
126
  if (await candidateReady(serverOrigin(plan), version, plan.token, exited, globalThis.fetch))
125
127
  return;
@@ -129,7 +131,7 @@ async function waitForServer(plan, version, exited) {
129
131
  }
130
132
  await delay(250);
131
133
  }
132
- throw new Error("Slopify did not restart in time.");
134
+ throw new Error(`Slopify ${version} did not answer within 60 seconds of starting.`);
133
135
  }
134
136
  function serverOrigin(plan) {
135
137
  const host = plan.host === "0.0.0.0" ? "127.0.0.1" : plan.host === "::" ? "::1" : plan.host;