@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
@@ -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(`project ${projectId} has no row`);
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("the run has no image provider or model");
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("the run ticked no image prompt");
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(`the run has no rendered text for the image prompt ${picked.name}`);
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(`image ${piece.id} has no payload`);
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: "A name is required." });
16
+ fields.push({ field: "name", message: "Enter a name." });
17
17
  }
18
18
  else if (trimmed.length > nameMax) {
19
- fields.push({ field: "name", message: `A name is at most ${String(nameMax)} characters.` });
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: "A body is required." });
25
+ fields.push({ field: "body", message: "Enter the text." });
23
26
  return fields;
24
27
  }
25
28
  if (body.length > bodyMax) {
26
- fields.push({ field: "body", message: `A body is at most ${String(bodyMax)} characters.` });
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 slot at ${where} has no name.`;
48
+ return `The keyword at ${where} has no name. Write a name between \`{{\` and \`}}\`.`;
43
49
  case "nested":
44
- return `The slot at ${where} holds a brace; slots do not nest.`;
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 no longer exists; pick another.` });
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 no longer exists; pick another.`,
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 are no audio chunks to join");
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("Narration chunk is missing from its source.");
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("Narration chunk disappeared.");
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("Narration group has no first chunk.");
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("Matched glossary term has no mapping.");
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("Validated glossary term has no IPA word.");
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
  }
@@ -14,7 +14,7 @@ import { planLegacyNarration } from "./legacy-plan.js";
14
14
  import { observeNarration } from "./live.js";
15
15
  // An empty narration source fails the stage immediately with no retries. Thrown from the
16
16
  // slice rather than from a provider call, so the attempt wrapper never sees it.
17
- export const nothingToNarrate = "nothing to narrate";
17
+ export const nothingToNarrate = "There is nothing to narrate because the article is empty. Add article text in Edit project → Article, then Retry stage.";
18
18
  // What a chunk piece carries between runs: the text that was sent, and the file it came
19
19
  // back as once the provider answered. That is the whole of the resume.
20
20
  const chunkPayload = z.object({ text: z.string(), file: z.string().optional() });
@@ -35,13 +35,13 @@ export async function runNarration(deps, context, providers) {
35
35
  deps.audioPreviews?.clear(projectId);
36
36
  const project = projectById(deps.db, projectId);
37
37
  if (project === undefined) {
38
- throw new Error(`project ${projectId} has no row`);
38
+ throw new Error("This project no longer exists; it may have been deleted while it was running.");
39
39
  }
40
40
  const choice = project.config.audio;
41
41
  if (choice === undefined) {
42
42
  // Admission refuses a run whose audio is Generate without a provider and a voice, so
43
43
  // reaching here is a bug in admission rather than something the user did.
44
- throw new Error("the run has no TTS provider or voice");
44
+ throw new Error("No voice is set for the narration. Choose a voice in Edit project → Providers, then Retry stage.");
45
45
  }
46
46
  // Read once, before anything is written: the segment steps below ask it what a previous
47
47
  // run already stored, and the body step is the only thing that adds to it.
@@ -168,7 +168,7 @@ async function speakSegments(deps, context, providers, choice, outputs) {
168
168
  if (text === "") {
169
169
  // The same rule, applied to a segment: there is nothing to say, and silently
170
170
  // dropping an intro the user picked would lose it without telling anyone.
171
- throw new Error(`the ${segment.category} segment has ${nothingToNarrate}`);
171
+ throw new Error(`The ${segment.category} is empty, so there is nothing to narrate for it. Write the ${segment.category} or remove it in Edit project, then Retry stage.`);
172
172
  }
173
173
  const spoken = await providers.forPiece(piece.id).tts({
174
174
  provider: choice.provider,
@@ -200,7 +200,7 @@ function segmentPieces(deps, projectId) {
200
200
  if (article === undefined) {
201
201
  // Admission writes all six stage rows with the project, so a project without an article
202
202
  // stage is a bug rather than a run the user configured.
203
- throw new Error(`project ${projectId} has no article stage`);
203
+ 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.");
204
204
  }
205
205
  return piecesOf(deps.db, article.id, "segment");
206
206
  }
@@ -257,13 +257,13 @@ function store(deps, projectId, role, path, durationMs, choice) {
257
257
  }
258
258
  function chunkOf(piece) {
259
259
  if (piece.payload === null) {
260
- throw new Error(`chunk ${piece.id} has no payload`);
260
+ throw new Error("Slopify hit an internal error (a saved narration chunk is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
261
261
  }
262
262
  return chunkPayload.parse(JSON.parse(piece.payload));
263
263
  }
264
264
  function segmentOf(piece) {
265
265
  if (piece.payload === null) {
266
- throw new Error(`segment ${piece.id} has no payload`);
266
+ throw new Error("Slopify hit an internal error (a saved intro or outro is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
267
267
  }
268
268
  return segmentPayload.parse(JSON.parse(piece.payload));
269
269
  }
@@ -9,14 +9,14 @@ function atoms(source, from, to, spans) {
9
9
  const span = spans.get(at);
10
10
  if (span !== undefined) {
11
11
  if (span.end > to || span.end <= at)
12
- throw new Error("Pronunciation span crosses a word.");
12
+ throw new Error("Slopify hit an internal error (a pronunciation fix spans more than one word). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
13
13
  result.push({ text: span.text, spokenText: source.slice(at, span.end) });
14
14
  at = span.end;
15
15
  }
16
16
  else {
17
17
  const point = source.codePointAt(at);
18
18
  if (point === undefined)
19
- throw new Error("Narration offset is outside the source.");
19
+ throw new Error("Slopify hit an internal error (a narration position is outside the article text). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
20
20
  const text = String.fromCodePoint(point);
21
21
  result.push({ text, spokenText: text });
22
22
  at += text.length;
@@ -34,7 +34,7 @@ function checkedSpans(source, pronunciation) {
34
34
  span.end <= span.start ||
35
35
  span.end > source.length ||
36
36
  !/^[^\s\uD800-\uDFFF]+$/u.test(source.slice(span.start, span.end)))
37
- throw new Error("Pronunciation spans must cover disjoint whole source characters in a word.");
37
+ throw new Error("Slopify hit an internal error (pronunciation fixes overlap or split a word). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
38
38
  spans.set(span.start, span);
39
39
  end = span.end;
40
40
  }
@@ -31,7 +31,7 @@ export function toAdmissionDraft(input) {
31
31
  return { name: saved.name, mode: saved.mode };
32
32
  fields.push({
33
33
  field: category,
34
- message: `That ${category} entry no longer exists; pick another.`,
34
+ message: `That ${category} entry was deleted. Choose another.`,
35
35
  });
36
36
  return undefined;
37
37
  };
@@ -42,8 +42,8 @@ export function toAdmissionDraft(input) {
42
42
  fields.push({
43
43
  field,
44
44
  message: attachment?.state === "copying" || attachment?.state === "pending"
45
- ? "This upload is still copying."
46
- : "That upload is no longer available; pick the file again.",
45
+ ? "This file is still uploading. Wait for it to finish."
46
+ : "That upload is no longer available. Choose the file again.",
47
47
  });
48
48
  return undefined;
49
49
  };
@@ -117,7 +117,8 @@ export function toAdmissionDraft(input) {
117
117
  if (!parsed.success)
118
118
  fields.push(...parsed.error.issues.map((issue) => ({
119
119
  field: issue.path.join("."),
120
- message: issue.message,
120
+ // The schema carries no sentences of its own; its defaults read like code.
121
+ message: "This setting is not valid. Reload the page and choose it again.",
121
122
  })));
122
123
  return fields.length || !parsed.success
123
124
  ? { ok: false, fields }
@@ -48,7 +48,7 @@ export async function checkDraftReadiness(deps, runs) {
48
48
  field: c.field,
49
49
  message: provider?.readiness.kind === "cli" && provider.readiness.issue
50
50
  ? provider.readiness.issue
51
- : "Configure this provider before starting.",
51
+ : "Set up this provider in Settings → Providers before starting.",
52
52
  });
53
53
  continue;
54
54
  }
@@ -56,12 +56,12 @@ export async function checkDraftReadiness(deps, runs) {
56
56
  if (result === "missing")
57
57
  fields.push({
58
58
  field: `${c.field}.model`,
59
- message: "Choose an available model before starting.",
59
+ message: "This model is no longer available. Choose another model.",
60
60
  });
61
61
  else if (result === "thinking")
62
62
  fields.push({
63
63
  field: `${c.field}.thinking`,
64
- message: "Choose a supported thinking setting.",
64
+ message: "This model does not support that thinking setting. Choose another.",
65
65
  });
66
66
  }
67
67
  return { fields: [...fields, ...localDraftReadiness(deps, runs, providers)], providers };
@@ -73,15 +73,21 @@ export function localDraftReadiness(deps, runs, providers) {
73
73
  if (cliPathChanged(deps.db, provider))
74
74
  fields.push({
75
75
  field: `${c.field}.cliPath`,
76
- message: "The configured CLI command changed. Check readiness again.",
76
+ message: "This provider's command-line tool changed in Settings → Providers. Choose Review and start again.",
77
77
  });
78
78
  if (provider?.readiness.kind === "keyed" && !hasKey(deps.db, provider.id))
79
- fields.push({ field: c.field, message: "Save the provider key before starting." });
79
+ fields.push({
80
+ field: c.field,
81
+ message: "Add this provider's API key in Settings → Providers before starting.",
82
+ });
80
83
  }
81
84
  for (const { draft } of runs)
82
85
  if (draft.sources.audio === "generate" &&
83
86
  draft.audio &&
84
87
  !listVoices(deps.db).some((v) => v.provider === draft.audio?.provider && v.voiceId === draft.audio.voice))
85
- fields.push({ field: "audio.voice", message: "Choose a saved voice before starting." });
88
+ fields.push({
89
+ field: "audio.voice",
90
+ message: "Choose one of your saved voices. Add voices in Settings → Voices.",
91
+ });
86
92
  return fields;
87
93
  }
@@ -47,7 +47,10 @@ export function resolveReviewInputs(deps, view, font) {
47
47
  if (!words.success)
48
48
  fields.push({ field: "expectedWords", message: "Enter a whole number between 1 and 100000." });
49
49
  if (document.variants.length > 49)
50
- fields.push({ field: "variants", message: "Review at most 50 runs including the base run." });
50
+ fields.push({
51
+ field: "variants",
52
+ message: "You can review at most 50 videos at once, counting the first one. Remove some variations.",
53
+ });
51
54
  const converted = toAdmissionDraft({
52
55
  document,
53
56
  attachments: fresh.value.attachments,
@@ -96,7 +99,7 @@ export function resolveReviewInputs(deps, view, font) {
96
99
  field: index > 0 && (issue.field === "title" || issue.field.startsWith("values."))
97
100
  ? `variants.${index - 1}.${issue.field}`
98
101
  : issue.field,
99
- message: `${index > 0 ? `Run ${index + 1}: ` : ""}${issue.message}`,
102
+ message: `${index > 0 ? `Video ${index + 1}: ` : ""}${issue.message}`,
100
103
  })));
101
104
  runs.push({
102
105
  draft: accepted,
@@ -118,7 +121,7 @@ export function resolveReviewInputs(deps, view, font) {
118
121
  if (!stat?.isFile() || font.id !== converted.draft.subtitles?.fontId)
119
122
  fields.push({
120
123
  field: "subtitles.fontId",
121
- message: "The selected font is no longer available. Choose another font.",
124
+ message: "This font was deleted. Choose another font.",
122
125
  });
123
126
  else
124
127
  fontHash = createHash("sha256").update(readFileSync(font.path)).digest("hex");
@@ -133,7 +136,7 @@ export function resolveReviewInputs(deps, view, font) {
133
136
  : run.draft.sources[stage] !== "generate")
134
137
  fields.push({
135
138
  field: `checkpoints.${stage}`,
136
- message: "Choose a checkpoint only before an enabled generated stage or export.",
139
+ message: "Checkpoints can only go before a step Slopify generates, or before export. Remove this checkpoint or turn that step on.",
137
140
  });
138
141
  if (fields.length)
139
142
  return reviewRefusal(view, fields);
@@ -27,7 +27,7 @@ export async function resolvePlayReview(deps, view) {
27
27
  return reviewRefusal(view, [
28
28
  {
29
29
  field: "subtitles.fontId",
30
- message: "The selected font is no longer available. Choose another font.",
30
+ message: "This font was deleted. Choose another font.",
31
31
  },
32
32
  ]);
33
33
  }
@@ -41,12 +41,12 @@ export async function resolvePlayReview(deps, view) {
41
41
  if (result === "missing")
42
42
  fields.push({
43
43
  field: `${choice.field}.model`,
44
- message: "Choose an available model before reviewing.",
44
+ message: "This model is no longer available. Choose another model.",
45
45
  });
46
46
  else if (result === "thinking")
47
47
  fields.push({
48
48
  field: `${choice.field}.thinking`,
49
- message: "Choose a supported thinking setting.",
49
+ message: "This model does not support that thinking setting. Choose another.",
50
50
  });
51
51
  }
52
52
  if (fields.length)
@@ -68,7 +68,9 @@ function attachmentsOf(deps, id) {
68
68
  state,
69
69
  stagedFileId: invalid ? null : row.staged_file_id,
70
70
  bytes: invalid ? 0 : (staged?.bytes ?? 0),
71
- error: invalid ? "Upload is missing or incomplete. Reattach the file." : row.error,
71
+ error: invalid
72
+ ? "This file is missing or did not finish uploading. Attach it again."
73
+ : row.error,
72
74
  };
73
75
  });
74
76
  }
@@ -43,7 +43,7 @@ export async function uploadDraftAttachment(deps, input) {
43
43
  if (!result.ok) {
44
44
  deps.db
45
45
  .prepare("UPDATE play_draft_attachments SET status='reattach',error=? WHERE id=? AND draft_id=? AND staged_file_id IS NULL")
46
- .run("Upload failed. Reattach the file.", input.attachmentId, input.draftId);
46
+ .run("The upload failed. Attach the file again.", input.attachmentId, input.draftId);
47
47
  return refuse("invalid-edit");
48
48
  }
49
49
  const updated = deps.db
@@ -92,14 +92,14 @@ export function admitPreview(deps, input) {
92
92
  continue;
93
93
  }
94
94
  if (existing?.state === "running")
95
- throw new Error("Selected work has a mismatched running reservation.");
95
+ throw new Error("Slopify hit an internal error (a step is already running for a different version of the project). Try again; if it happens again, use Download diagnostics in Settings and report it.");
96
96
  deps.db
97
97
  .prepare("DELETE FROM revision_work_reservations WHERE revision_id=? AND work_key=?")
98
98
  .run(view.revision.id, recipe.key);
99
99
  const key = reservationKey(recipe, view);
100
100
  const fingerprint = snapshot.anchors[key];
101
101
  if (fingerprint === undefined)
102
- throw new Error("Selected work has no reviewed desired anchor.");
102
+ throw new Error("Slopify hit an internal error (a step has no saved starting point). Try again; if it happens again, use Download diagnostics in Settings and report it.");
103
103
  const work = insertInvocation(deps, view, recipe, catalogue, { key, fingerprint }, false, view.revision.id, admissionId);
104
104
  workIds.push(work.workId);
105
105
  }
@@ -117,7 +117,7 @@ export function legacyImageOutput(piece, outputs) {
117
117
  row.meta.promptName === undefined ||
118
118
  row.meta.promptName === promptName)));
119
119
  if (matches.length > 1)
120
- throw new Error(`Legacy image piece ${piece.id} matches multiple outputs.`);
120
+ throw new Error(`Slopify hit an internal error (an image from an older Slopify version, ${piece.id}, matches multiple outputs). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
121
121
  return matches[0];
122
122
  }
123
123
  const legacyImageMetadata = z
@@ -26,7 +26,7 @@ export function narrationFileRecipe(context, segment, parts) {
26
26
  ];
27
27
  if (part.input.kind === "provided")
28
28
  return [resourceIdentity(context, part), part.input.semantic, null];
29
- throw new Error("Narration files require exact audio parts.");
29
+ throw new Error("Slopify hit an internal error (a narration chunk has an unexpected source). Try again; if it happens again, use Download diagnostics in Settings and report it.");
30
30
  }),
31
31
  },
32
32
  }, dependencies, { unresolved: parts.length === 0 });
@@ -7,7 +7,7 @@ export function visualRecipes(config, content, audioFingerprint, captionFingerpr
7
7
  for (const key of imageKeys) {
8
8
  const image = content.imageDefinitions[key];
9
9
  if (image === undefined)
10
- throw new Error("Saved image definition is missing");
10
+ throw new Error("Slopify hit an internal error (one of the project's images has no saved settings). Try again; if it happens again, use Download diagnostics in Settings and report it.");
11
11
  const raw = image.templateKey === undefined || image.templateKey === null
12
12
  ? undefined
13
13
  : content.promptTemplates[image.templateKey];
@@ -78,7 +78,7 @@ export async function recoverProject(deps, projectId, raw) {
78
78
  const prior = retainedPreviewPlan(deps, base, deps.catalogue.read());
79
79
  const action = input.action;
80
80
  if (action.kind !== "rerun")
81
- throw new Error("Recovery edit has no rerun action");
81
+ throw new Error("Slopify hit an internal error (a recovery edit has nothing to re-run). Try again; if it happens again, use Download diagnostics in Settings and report it.");
82
82
  const affected = dependentClosure(prior.recipes, sectionRoots(base, prior, action.stage));
83
83
  const savedIntent = deps.db
84
84
  .prepare("SELECT result_revision_id FROM revision_mutations WHERE project_id=? AND idempotency_key=?")
@@ -176,7 +176,7 @@ export async function recoverProject(deps, projectId, raw) {
176
176
  storePreview(deps, preview.value.preview, preview.value.execution);
177
177
  const stored = previewById(deps.db, projectId, preview.value.preview.id);
178
178
  if (stored === undefined)
179
- throw new Error("Stored recovery preview is missing.");
179
+ throw new Error("Slopify hit an internal error (the saved recovery preview is missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
180
180
  return {
181
181
  plan: { preview: stored, execution: preview.value.execution },
182
182
  view,
@@ -7,7 +7,7 @@ export { transitionRevisionWork } from "./transition-repo.js";
7
7
  export function publicationTargets(db, publication, workKey, workFingerprint) {
8
8
  const { work, pieceId, publicationId } = publication;
9
9
  if (publicationId !== (pieceId ?? work.workId))
10
- throw new Error("Publication identity does not match its durable work.");
10
+ throw new Error("Slopify hit an internal error (a finished result is filed under the wrong step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
11
11
  if (!workExists(db, work))
12
12
  return [];
13
13
  if (pieceId !== null) {
@@ -15,9 +15,9 @@ export function publicationTargets(db, publication, workKey, workFingerprint) {
15
15
  .prepare("SELECT work_key,fingerprint FROM revision_work_pieces WHERE id=? AND work_id=?")
16
16
  .get(pieceId, work.workId);
17
17
  if (piece === undefined)
18
- throw new Error("Publication piece does not belong to its work.");
18
+ throw new Error("Slopify hit an internal error (a finished result belongs to a different step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
19
19
  if (piece.work_key !== workKey || piece.fingerprint !== workFingerprint)
20
- throw new Error("Publication authority does not match its durable piece.");
20
+ throw new Error("Slopify hit an internal error (a finished result doesn't match its saved step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
21
21
  }
22
22
  const currentId = currentRevisionId(db, work.projectId);
23
23
  const owns = (revisionId) => {
@@ -18,11 +18,11 @@ export function reservationKey(recipe, view) {
18
18
  export function admitInitialRevision(deps, view, catalogue) {
19
19
  transact(deps.db, () => {
20
20
  if (currentRevisionId(deps.db, view.revision.projectId) !== view.revision.id)
21
- throw new Error("Initial admission requires the selected revision.");
21
+ throw new Error("The project was changed while this run was starting. Try again.");
22
22
  if (deps.db
23
23
  .prepare("SELECT 1 FROM revision_work WHERE project_id=? LIMIT 1")
24
24
  .get(view.revision.projectId) !== undefined)
25
- throw new Error("An existing project requires explicit rebuild admission.");
25
+ throw new Error("Slopify hit an internal error (this project was started twice). Try again; if it happens again, use Download diagnostics in Settings and report it.");
26
26
  const snapshot = executionCatalogue(catalogue, view.revision.config);
27
27
  const plan = executionPlan(deps, view, snapshot);
28
28
  for (const recipe of plan.recipes) {
@@ -32,7 +32,7 @@ export function admitInitialRevision(deps, view, catalogue) {
32
32
  const logicalKey = recipe.input.kind === "tts" ? `${recipe.input.logicalKey}:1` : recipe.key;
33
33
  const anchor = view.revision.fingerprints[logicalKey] ?? desired;
34
34
  if (anchor === undefined)
35
- throw new Error(`Initial work ${recipe.key} has no desired anchor.`);
35
+ throw new Error(`Slopify hit an internal error (step ${recipe.key} has no saved starting point). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
36
36
  const reuse = plan.work.find((row) => row.key === recipe.key)?.disposition === "reuse";
37
37
  if (reuse)
38
38
  bindNarrationReuse(deps, view, recipe, narrationOrdinal(plan.recipes, recipe.key));
@@ -46,7 +46,7 @@ export function insertInvocation(deps, view, recipe, catalogue, anchor, complete
46
46
  .prepare("SELECT id FROM stages WHERE project_id=? AND kind=?")
47
47
  .get(projectId, recipe.stage);
48
48
  if (typeof stage?.id !== "string")
49
- throw new Error("Revision work has no owned stage.");
49
+ throw new Error("Slopify hit an internal error (a step has no matching stage). Try again; if it happens again, use Download diagnostics in Settings and report it.");
50
50
  const work = {
51
51
  projectId,
52
52
  revisionId,
@@ -12,14 +12,14 @@ const answerSchema = z.object({
12
12
  });
13
13
  export async function executeArticleRequests(deps, context, providers, initial) {
14
14
  if (initial.input.kind !== "llm")
15
- throw new Error("Article generation needs a pinned LLM request.");
15
+ throw new Error("Slopify hit an internal error (the article step has no AI request). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
16
16
  let piece = initial;
17
17
  let text = "";
18
18
  let inputTokens = 0;
19
19
  let outputTokens = 0;
20
20
  for (let part = 0; part <= continuationLimit; part += 1) {
21
21
  if (piece.input.kind !== "llm")
22
- throw new Error("An article continuation must contain an exact LLM request.");
22
+ throw new Error("Slopify hit an internal error (an article continuation has no AI request). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
23
23
  const saved = deps.db
24
24
  .prepare("SELECT result_json FROM revision_work_pieces WHERE id=? AND work_id=?")
25
25
  .get(piece.id, context.work.workId);
@@ -38,9 +38,9 @@ export async function executeArticleRequests(deps, context, providers, initial)
38
38
  webSearch: input.webSearch,
39
39
  previewLabel: part === 0 ? "Article" : `Continuation ${part}`,
40
40
  check: (value) => value.text.trim() === ""
41
- ? "The article answered with nothing."
41
+ ? "The AI model returned an empty article. Retry stage; if it keeps happening, choose a different model in Edit project → Providers."
42
42
  : part === continuationLimit && value.finishReason === "length"
43
- ? `The article was still unfinished after ${continuationLimit} continuations.`
43
+ ? `The article was still unfinished after ${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.`
44
44
  : undefined,
45
45
  }, (event) => {
46
46
  if (event.type === "delta")
@@ -72,7 +72,7 @@ export async function executeArticleRequests(deps, context, providers, initial)
72
72
  return { ok: false, reason: "held" };
73
73
  piece = next;
74
74
  }
75
- throw new Error("The article exceeded its continuation limit.");
75
+ throw new Error(`The article was still unfinished after ${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.`);
76
76
  }
77
77
  function continuationPiece(deps, context, initial, part, text) {
78
78
  return transact(deps.db, () => {
@@ -11,14 +11,14 @@ export function admitReviewedCheckpoints(deps, projectIds, reviewed, catalogue)
11
11
  const revisionId = currentRevisionId(deps.db, projectId);
12
12
  const view = revisionId === undefined ? undefined : getRevisionView(deps, projectId, revisionId);
13
13
  if (!view)
14
- throw new Error("Checkpoint admission has no retained revision");
14
+ throw new Error("Slopify hit an internal error (the project version to review is missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
15
15
  const plan = executionPlan(deps, view, executionCatalogue(catalogue, view.revision.config));
16
16
  const checkpoints = gates.map((gate) => {
17
17
  const work = deps.db
18
18
  .prepare("SELECT id FROM revision_work WHERE project_id=? AND revision_id=? AND kind=? ORDER BY id LIMIT 1")
19
19
  .get(projectId, view.revision.id, gate.stage);
20
20
  if (typeof work?.id !== "string")
21
- throw new Error("Checkpoint admission has no stage work");
21
+ throw new Error("Slopify hit an internal error (a review checkpoint has no matching stage). Try again; if it happens again, use Download diagnostics in Settings and report it.");
22
22
  return {
23
23
  checkpointId: gate.checkpointId,
24
24
  stage: gate.stage,
@@ -34,11 +34,11 @@ export function admitReviewedCheckpoints(deps, projectIds, reviewed, catalogue)
34
34
  createdAt: deps.clock.now().toISOString(),
35
35
  });
36
36
  if (!saved.ok)
37
- throw new Error("Checkpoint admission could not persist reviewed gates");
37
+ throw new Error("Slopify hit an internal error (the review checkpoints could not be saved). Try again; if it happens again, use Download diagnostics in Settings and report it.");
38
38
  return saved.value.map((row) => {
39
39
  const gate = gates.find((gate) => gate.checkpointId === row.checkpointId);
40
40
  if (!gate)
41
- throw new Error("Checkpoint admission lost its reviewed identity");
41
+ throw new Error("Slopify hit an internal error (a saved review checkpoint could not be matched). Try again; if it happens again, use Download diagnostics in Settings and report it.");
42
42
  return { ...row, reviewedFingerprint: gate.fingerprint };
43
43
  });
44
44
  });