@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.
Files changed (155) hide show
  1. package/dist/adapters/alignment/protocol.js +11 -1
  2. package/dist/adapters/alignment/runner.js +4 -1
  3. package/dist/adapters/alignment/worker.js +28 -5
  4. package/dist/adapters/explain.js +93 -0
  5. package/dist/adapters/ffmpeg.js +3 -3
  6. package/dist/adapters/host-cli/index.js +9 -2
  7. package/dist/adapters/host-cli/transport.js +4 -4
  8. package/dist/adapters/image/bytes.js +2 -2
  9. package/dist/adapters/image/codex-output.js +12 -12
  10. package/dist/adapters/image/codex.js +24 -8
  11. package/dist/adapters/image/fal.js +12 -6
  12. package/dist/adapters/image/google.js +18 -8
  13. package/dist/adapters/image/models.js +8 -2
  14. package/dist/adapters/image/openai.js +15 -6
  15. package/dist/adapters/image/replicate.js +16 -7
  16. package/dist/adapters/llm/claude-code-models.js +1 -1
  17. package/dist/adapters/llm/claude-code.js +16 -7
  18. package/dist/adapters/llm/cli-login-error.js +2 -1
  19. package/dist/adapters/llm/codex-models.js +1 -1
  20. package/dist/adapters/llm/codex.js +5 -6
  21. package/dist/adapters/llm/document-workspace.js +2 -2
  22. package/dist/adapters/llm/gemini-models.js +1 -1
  23. package/dist/adapters/llm/gemini-workspace.js +1 -1
  24. package/dist/adapters/llm/gemini.js +15 -11
  25. package/dist/adapters/llm/openrouter.js +21 -9
  26. package/dist/adapters/llm/run-cli.js +14 -11
  27. package/dist/adapters/tts/cartesia.js +10 -3
  28. package/dist/adapters/tts/elevenlabs.js +18 -6
  29. package/dist/adapters/tts/inworld-async.js +47 -16
  30. package/dist/adapters/tts/inworld.js +25 -15
  31. package/dist/adapters/tts/openai.js +18 -6
  32. package/dist/catalog/registry.js +13 -10
  33. package/dist/catalog/store.js +1 -1
  34. package/dist/catalog/validate.js +13 -4
  35. package/dist/edge/cli.js +33 -7
  36. package/dist/edge/docker-launch.js +6 -6
  37. package/dist/edge/docker-projects/engine.js +34 -13
  38. package/dist/edge/docker-projects/install.js +8 -8
  39. package/dist/edge/docker-projects/state.js +6 -6
  40. package/dist/edge/docker-projects/tree.js +6 -6
  41. package/dist/edge/docker.js +13 -13
  42. package/dist/edge/http/actions.js +27 -25
  43. package/dist/edge/http/app.js +3 -3
  44. package/dist/edge/http/audio-preview.js +1 -1
  45. package/dist/edge/http/checkpoints.js +5 -1
  46. package/dist/edge/http/diagnostics.js +3 -1
  47. package/dist/edge/http/draft-files.js +2 -2
  48. package/dist/edge/http/draft-problem.js +11 -1
  49. package/dist/edge/http/files.js +4 -4
  50. package/dist/edge/http/folder-location.js +3 -3
  51. package/dist/edge/http/fonts.js +22 -10
  52. package/dist/edge/http/host-cli.js +34 -12
  53. package/dist/edge/http/multipart.js +7 -5
  54. package/dist/edge/http/open-folder.js +2 -2
  55. package/dist/edge/http/planning.js +4 -4
  56. package/dist/edge/http/problem.js +2 -2
  57. package/dist/edge/http/project-templates.js +7 -3
  58. package/dist/edge/http/projects.js +8 -8
  59. package/dist/edge/http/prompts.js +9 -4
  60. package/dist/edge/http/providers.js +15 -7
  61. package/dist/edge/http/revision-files.js +3 -3
  62. package/dist/edge/http/revisions.js +30 -4
  63. package/dist/edge/http/schedules.js +9 -9
  64. package/dist/edge/http/settings.js +15 -9
  65. package/dist/edge/http/staging.js +5 -5
  66. package/dist/edge/http/storage.js +4 -4
  67. package/dist/edge/http/subtitles.js +1 -1
  68. package/dist/edge/http/tutorial.js +2 -2
  69. package/dist/edge/http/update.js +3 -3
  70. package/dist/host-cli/install.js +28 -6
  71. package/dist/host-cli/paths.js +6 -6
  72. package/dist/host-cli/server.js +1 -1
  73. package/dist/host-cli/service.js +11 -11
  74. package/dist/host-cli/status.js +7 -3
  75. package/dist/kernel/config/index.js +6 -4
  76. package/dist/kernel/db/migrate.js +2 -1
  77. package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
  78. package/dist/kernel/lock.js +4 -3
  79. package/dist/kernel/ports/subtitles.js +15 -1
  80. package/dist/kernel/runner/attempt.js +26 -2
  81. package/dist/main.js +2 -2
  82. package/dist/slices/admission/rules.js +32 -20
  83. package/dist/slices/article/continuation.js +4 -2
  84. package/dist/slices/article/plain.js +2 -2
  85. package/dist/slices/article/provided-entries.js +3 -3
  86. package/dist/slices/article/run.js +2 -2
  87. package/dist/slices/article/segments.js +5 -3
  88. package/dist/slices/control/providers.js +20 -7
  89. package/dist/slices/fonts/catalog.js +1 -1
  90. package/dist/slices/fonts/upload.js +1 -1
  91. package/dist/slices/images/run.js +5 -5
  92. package/dist/slices/library/lint.js +13 -7
  93. package/dist/slices/library/slots.js +2 -2
  94. package/dist/slices/narration/concat.js +1 -1
  95. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  96. package/dist/slices/narration/pronunciation.js +2 -2
  97. package/dist/slices/narration/run.js +7 -7
  98. package/dist/slices/narration/steering.js +3 -3
  99. package/dist/slices/play-drafts/convert.js +5 -4
  100. package/dist/slices/play-drafts/readiness.js +12 -6
  101. package/dist/slices/play-drafts/review-inputs.js +7 -4
  102. package/dist/slices/play-drafts/review.js +3 -3
  103. package/dist/slices/play-drafts/service.js +3 -1
  104. package/dist/slices/play-drafts/uploads.js +1 -1
  105. package/dist/slices/rebuild/admission-repo.js +2 -2
  106. package/dist/slices/rebuild/recipe-legacy.js +1 -1
  107. package/dist/slices/rebuild/recipe-narration-text.js +1 -1
  108. package/dist/slices/rebuild/recipe-visual.js +1 -1
  109. package/dist/slices/rebuild/recovery.js +2 -2
  110. package/dist/slices/rebuild/repo.js +3 -3
  111. package/dist/slices/rebuild/runtime-admission.js +4 -4
  112. package/dist/slices/rebuild/runtime-article.js +5 -5
  113. package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
  114. package/dist/slices/rebuild/runtime-export-inputs.js +6 -6
  115. package/dist/slices/rebuild/runtime-export.js +4 -4
  116. package/dist/slices/rebuild/runtime-local.js +6 -6
  117. package/dist/slices/rebuild/runtime-narration-text.js +46 -7
  118. package/dist/slices/rebuild/runtime-piece-label.js +62 -0
  119. package/dist/slices/rebuild/runtime-provider.js +6 -4
  120. package/dist/slices/rebuild/runtime-publication.js +3 -3
  121. package/dist/slices/rebuild/runtime-run.js +11 -1
  122. package/dist/slices/rebuild/runtime-subtitles.js +78 -15
  123. package/dist/slices/rebuild/transition-repo.js +6 -6
  124. package/dist/slices/reruns/index.js +1 -1
  125. package/dist/slices/research/run.js +9 -7
  126. package/dist/slices/research/synthesis.js +4 -2
  127. package/dist/slices/revisions/mutation-assets.js +26 -8
  128. package/dist/slices/revisions/mutation-cues.js +1 -1
  129. package/dist/slices/revisions/rules.js +18 -9
  130. package/dist/slices/revisions/schema.js +1 -1
  131. package/dist/slices/schedules/repo.js +55 -11
  132. package/dist/slices/schedules/scheduler.js +25 -8
  133. package/dist/slices/schedules/schema.js +19 -4
  134. package/dist/slices/schedules/service.js +2 -0
  135. package/dist/slices/settings/cli-paths.js +7 -4
  136. package/dist/slices/settings/models.js +4 -4
  137. package/dist/slices/settings/playback.js +2 -2
  138. package/dist/slices/storage/assets.js +5 -5
  139. package/dist/slices/storage/layout.js +1 -1
  140. package/dist/slices/subtitles/captions.js +1 -1
  141. package/dist/slices/subtitles/prepare.js +5 -5
  142. package/dist/slices/subtitles/transcript.js +1 -1
  143. package/dist/slices/thumbnail/run.js +8 -8
  144. package/dist/slices/video/audio-inputs.js +2 -2
  145. package/dist/slices/video/ffmpeg.js +6 -4
  146. package/dist/slices/video/plan.js +1 -1
  147. package/dist/slices/video/run.js +2 -2
  148. package/dist/updater/plan.js +1 -1
  149. package/dist/updater/service.js +4 -3
  150. package/dist/updater/worker.js +6 -4
  151. package/dist/web/assets/{index-CZNiHm8P.css → index-Bwtmu9ei.css} +1 -1
  152. package/dist/web/assets/index-DjhMJqMN.js +141 -0
  153. package/dist/web/index.html +2 -2
  154. package/package.json +1 -1
  155. package/dist/web/assets/index-DPgGLX5m.js +0 -137
@@ -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
  });
@@ -14,11 +14,11 @@ export function exportSnapshot(deps, context, piece) {
14
14
  .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
15
15
  .get(context.work.workId);
16
16
  if (view === undefined || row === undefined)
17
- throw new Error("The export revision no longer exists.");
17
+ throw new Error("Slopify hit an internal error (the project version being exported is missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
18
18
  const plan = executionPlan(deps, view, savedCatalogue(row.recipe_context));
19
19
  const recipe = plan.recipes.find((one) => one.key === piece.key && one.fingerprint === piece.fingerprint);
20
20
  if (recipe === undefined || recipe.deferred || recipe.unresolved)
21
- throw new Error("The export inputs do not match the admitted recipe.");
21
+ throw new Error("Slopify hit an internal error (the export no longer matches the project's saved inputs). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
22
22
  return { view, plan };
23
23
  }
24
24
  export async function revisionAudio(deps, context, view) {
@@ -34,12 +34,12 @@ export async function revisionAudio(deps, context, view) {
34
34
  (await (deps.measureAudio?.(path, context.signal) ??
35
35
  probeDurationMs(deps.ffmpeg, path, context.signal, deps.log)));
36
36
  if (!Number.isFinite(duration) || duration <= 0)
37
- throw new Error("The narration duration could not be measured.");
37
+ throw new Error("Slopify couldn't read the length of the narration audio, so the file may be damaged or empty. Regenerate the narration (or upload your audio file again in Edit project → Narration), then Retry stage.");
38
38
  return { path, seconds: duration / 1000 };
39
39
  };
40
40
  const body = await input("audio_body");
41
41
  if (body === undefined)
42
- throw new Error("The revision has no complete narration.");
42
+ throw new Error("The narration audio isn't finished yet. Let the Narration stage finish (Resume, or Retry stage on it), then retry this stage.");
43
43
  return audioTimeline({
44
44
  body,
45
45
  intro: config.sources.audio === "generate" && config.intro !== undefined
@@ -72,7 +72,7 @@ export function revisionTranscript(deps, snapshot, kind) {
72
72
  Array.isArray(recipe.input.semantic) &&
73
73
  typeof recipe.input.semantic[0] === "string")
74
74
  return recipe.input.semantic[0];
75
- throw new Error("The saved narration transcript is incomplete.");
75
+ throw new Error("Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Resume, or Retry stage on it), or regenerate the missing chunk in Edit project → Narration.");
76
76
  });
77
77
  if (texts.length > 0)
78
78
  return texts.join("\n");
@@ -84,7 +84,7 @@ export function revisionTranscript(deps, snapshot, kind) {
84
84
  if (view.articleMarkdown !== null)
85
85
  return plainText(splitEndMatter(view.articleMarkdown).body);
86
86
  }
87
- throw new Error(`The saved ${kind} narration transcript is missing.`);
87
+ throw new Error(`The saved text of the ${kind === "body" ? "article" : kind} is missing, so it can't be matched to the narration. Use Re-run section on Article, then Retry stage.`);
88
88
  }
89
89
  export function retainedOutput(deps, view, row) {
90
90
  const asset = z
@@ -20,9 +20,9 @@ export async function executeExportRecipe(deps, context, piece) {
20
20
  const audio = await revisionAudio(deps, context, view);
21
21
  const wav = piece.key === "export:wav";
22
22
  if (!wav && piece.key !== "export:video")
23
- throw new Error("Unknown media export recipe.");
23
+ throw new Error("Slopify hit an internal error (unknown kind of export). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
24
24
  if (wav && audio.length === 0)
25
- throw new Error("Audio export needs complete narration.");
25
+ throw new Error("The audio export has nothing to export because narration is turned off for this project. Turn narration on in Edit project, then Retry stage.");
26
26
  const filename = wav ? "audio.wav" : "video.mp4";
27
27
  const pending = allocateAsset(deps, context.work.projectId, filename);
28
28
  const prepared = [];
@@ -48,7 +48,7 @@ export async function executeExportRecipe(deps, context, piece) {
48
48
  const images = (wav ? [] : view.revision.content.imageOrder).map((key) => {
49
49
  const row = view.outputs.find((one) => one.workKey === `image:${key}` && one.selected && one.available && one.state === "ready");
50
50
  if (row === undefined)
51
- throw new Error("The revision is missing an image selected for its slideshow.");
51
+ throw new Error("The video export can't find one of the slideshow images. Use Re-run section on Images (or regenerate that image in Edit project → Images), then Retry stage.");
52
52
  return outputPath(deps.paths, context.work.projectId, row.output.path);
53
53
  });
54
54
  const plan = wav
@@ -124,7 +124,7 @@ function captionDirectory(deps, context, snapshot) {
124
124
  const ass = find("subtitle_ass");
125
125
  const font = find("subtitle_font");
126
126
  if (ass === undefined || font === undefined)
127
- throw new Error("Burned subtitles need prepared captions and their saved font.");
127
+ throw new Error("Burned-in captions are missing their caption file or font. Use Re-run section on Video (or choose the caption font again in Edit project → Subtitles), then Retry stage.");
128
128
  const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "render-"));
129
129
  try {
130
130
  mkdirSync(join(directory, "fonts"), { mode: 0o700 });
@@ -19,7 +19,7 @@ export async function executeLocalRecipe(deps, context, piece) {
19
19
  const input = piece.input;
20
20
  if (input.kind === "provided") {
21
21
  if (input.assetId === null)
22
- throw new Error("The provided recipe has no retained asset.");
22
+ throw new Error("A file you uploaded for this project is missing. Upload it again in Edit project, then Retry stage.");
23
23
  const row = z
24
24
  .object({ id: z.string(), project_id: z.string(), path: z.string(), created_at: z.string() })
25
25
  .parse(deps.db
@@ -43,7 +43,7 @@ export async function executeLocalRecipe(deps, context, piece) {
43
43
  return "done";
44
44
  }
45
45
  if (input.kind !== "local")
46
- throw new Error("Expected an exact local recipe.");
46
+ throw new Error("Slopify hit an internal error (a local step was set up wrongly). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
47
47
  if (input.operation === "narration-files-v1") {
48
48
  await publishNarrationText(deps, context, piece);
49
49
  return "done";
@@ -75,19 +75,19 @@ export async function executeLocalRecipe(deps, context, piece) {
75
75
  });
76
76
  return "done";
77
77
  }
78
- throw new Error(`No local executor for ${input.operation}.`);
78
+ throw new Error(`Slopify hit an internal error (no handler for the step "${input.operation}"). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
79
79
  }
80
80
  async function concatenate(deps, context, piece) {
81
81
  const view = executionView(deps, context.work.projectId, context.work.revisionId);
82
82
  if (view === undefined)
83
- throw new Error("The pinned revision is missing.");
83
+ throw new Error("Slopify hit an internal error (the project version being built is missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
84
84
  const row = deps.db
85
85
  .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
86
86
  .get(context.work.workId);
87
87
  const plan = executionPlan(deps, view, savedCatalogue(row?.recipe_context));
88
88
  const recipe = plan.recipes.find((candidate) => candidate.key === piece.key && candidate.fingerprint === piece.fingerprint);
89
89
  if (recipe === undefined)
90
- throw new Error("The pinned concatenation no longer matches its inputs.");
90
+ throw new Error("Slopify hit an internal error (the narration chunks to join no longer match the project's saved inputs). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
91
91
  const files = recipe.dependsOn.map((key) => {
92
92
  const input = view.pieces.find((candidate) => candidate.key === key &&
93
93
  candidate.selected &&
@@ -95,7 +95,7 @@ async function concatenate(deps, context, piece) {
95
95
  candidate.piece.state === "done" &&
96
96
  candidate.fingerprint === plan.recipes.find((value) => value.key === key)?.fingerprint);
97
97
  if (input?.assetId === null || input === undefined)
98
- throw new Error("A narration part has no retained audio.");
98
+ throw new Error("One narration chunk has no saved audio, so the narration can't be joined together. Regenerate the missing chunk in Edit project → Narration, then Retry stage.");
99
99
  const asset = deps.db
100
100
  .prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
101
101
  .get(input.assetId, context.work.projectId);