@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
@@ -80,14 +80,14 @@ export function createApp(deps) {
80
80
  return problem(c, {
81
81
  status: 503,
82
82
  title: titleOf(503),
83
- detail: "Docker installation is being verified. Wait for the launcher to finish.",
83
+ detail: "Slopify is still finishing its Docker setup. Wait for the launcher to finish, then try again.",
84
84
  });
85
85
  const releaseRequest = deps.mutations?.begin();
86
86
  if (deps.mutations !== undefined && releaseRequest === undefined)
87
87
  return problem(c, {
88
88
  status: 503,
89
89
  title: titleOf(503),
90
- detail: "Slopify is shutting down. Wait for it to restart before making changes.",
90
+ detail: "Slopify is shutting down, so changes cannot be saved right now. Start it again, reload the page and try again.",
91
91
  });
92
92
  const updateRoute = [
93
93
  "/api/update",
@@ -101,7 +101,7 @@ export function createApp(deps) {
101
101
  return problem(c, {
102
102
  status: 409,
103
103
  title: titleOf(409),
104
- detail: "Slopify is updating. Wait for it to restart before making changes.",
104
+ detail: "Slopify is updating. Wait for it to restart, reload the page and try again.",
105
105
  });
106
106
  }
107
107
  try {
@@ -49,6 +49,6 @@ function missing(c) {
49
49
  return problem(c, {
50
50
  status: 404,
51
51
  title: titleOf(404),
52
- detail: "This live audio preview is no longer available. Finished narration is available in the audio outputs.",
52
+ detail: "This live audio preview has ended. Play the finished narration from the project's audio outputs instead.",
53
53
  });
54
54
  }
@@ -20,7 +20,11 @@ function refused(c, reason) {
20
20
  return problem(c, {
21
21
  status,
22
22
  title: titleOf(status),
23
- detail: "Reload the current checkpoint status before approving or changing this gate.",
23
+ detail: reason === "not-found"
24
+ ? "This checkpoint no longer exists; the project may have moved on or changed. Reload the page to see where it is now."
25
+ : reason === "invalid-input"
26
+ ? "This checkpoint answer could not be read. Reload the page and try again."
27
+ : "This checkpoint changed since the page loaded, for example it was already approved. Reload the page to see its current state, then try again.",
24
28
  extensions: { reason },
25
29
  });
26
30
  }
@@ -29,7 +29,9 @@ export function diagnosticsRoutes(deps) {
29
29
  ...(provider.cliPath === undefined ? {} : { cliPath: provider.cliPath }),
30
30
  })),
31
31
  projects: { total: typeof projectCount?.count === "number" ? projectCount.count : 0 },
32
- catalogue: deps.catalogue?.status() ?? { warning: "Catalogue unavailable." },
32
+ catalogue: deps.catalogue?.status() ?? {
33
+ warning: "The model list (models.yaml) is not loaded yet. Wait a moment and reload the page; if it stays like this, restart Slopify.",
34
+ },
33
35
  });
34
36
  });
35
37
  }
@@ -32,7 +32,7 @@ export function draftFileRoutes(service) {
32
32
  return problem(c, {
33
33
  status: 400,
34
34
  title: titleOf(400),
35
- detail: "The request carried no file part.",
35
+ detail: "No file arrived with the upload. Choose the file again and retry.",
36
36
  });
37
37
  return result.ok
38
38
  ? c.json(draftAttachmentSchema.parse(result.value))
@@ -57,7 +57,7 @@ export function draftFileRoutes(service) {
57
57
  return problem(c, {
58
58
  status: 404,
59
59
  title: titleOf(404),
60
- detail: "This draft attachment has no preview bytes.",
60
+ detail: "This attached file has not finished uploading, so it cannot be previewed yet. Wait a moment or attach it again.",
61
61
  });
62
62
  return c.body(Readable.toWeb(createReadStream(stagingPath(deps.paths, staged.path))), 200, {
63
63
  "content-type": contentType,
@@ -6,10 +6,20 @@ export function draftProblem(c, result) {
6
6
  ? 400
7
7
  : 409;
8
8
  const { ok: _ok, ...extensions } = result;
9
+ const details = {
10
+ "not-found": "This draft no longer exists; it may have been started or deleted. Reload the page to start a new one.",
11
+ "invalid-draft": "This saved draft could not be read. Reload the page and set the video up again in Play.",
12
+ conflict: "This draft changed in another tab or window. Reload the page to see the latest, then make your change again.",
13
+ "invalid-edit": "Some settings need fixing. Fix the highlighted fields, then try again.",
14
+ "pending-start": "This video is already starting. Wait a moment, then find it in Projects.",
15
+ "already-started": "This draft was already started. Find the video in Projects.",
16
+ "stale-review": "Something changed since you reviewed this video, such as a setting or a price. Choose Review and start again to see the latest.",
17
+ readiness: "Slopify is not ready to start this video yet. Fix the highlighted problems, then try again.",
18
+ };
9
19
  return problem(c, {
10
20
  status,
11
21
  title: titleOf(status),
12
- detail: "Review the saved draft and the listed fields.",
22
+ detail: details[result.reason],
13
23
  extensions,
14
24
  });
15
25
  }
@@ -58,10 +58,10 @@ function disposition(filename) {
58
58
  }
59
59
  function missing(c, reason) {
60
60
  const details = {
61
- "unknown-project": "No project has that id.",
62
- "unknown-asset": "This project has no such file.",
63
- "missing-file": "This file is recorded but is no longer on disk; re-run the stage.",
64
- "no-images": "This project has no images or thumbnail yet.",
61
+ "unknown-project": "This project no longer exists. Go back to Projects to pick another.",
62
+ "unknown-asset": "This project has no file by that name. Reload the page to see its current files.",
63
+ "missing-file": "This file was deleted or moved from the project folder. Use Re-run section on the project page to make it again.",
64
+ "no-images": "This project has no images or thumbnail yet. Wait for the Images step to finish, then try again.",
65
65
  };
66
66
  return problem(c, { status: 404, title: titleOf(404), detail: details[reason] });
67
67
  }
@@ -10,7 +10,7 @@ export async function replyForFolder(c, deps, projectId, file) {
10
10
  return problem(c, {
11
11
  status: 503,
12
12
  title: titleOf(503),
13
- detail: "This Docker installation has no verified host project folder. Downloads remain available. Rerun the managed Docker launcher to set up host files.",
13
+ detail: "Slopify cannot show where this file is on your computer because its Docker project folder was not set up. You can still use the download buttons, or run the Slopify Docker launcher again to set up the folder.",
14
14
  });
15
15
  try {
16
16
  const r = relative(deps.paths.projects, file);
@@ -41,7 +41,7 @@ export async function replyForFolder(c, deps, projectId, file) {
41
41
  return problem(c, {
42
42
  status: 404,
43
43
  title: titleOf(404),
44
- detail: "This saved output folder is no longer available.",
44
+ detail: "This output's folder was deleted or moved. Use Re-run section on the project page to make the file again.",
45
45
  });
46
46
  }
47
47
  }
@@ -59,7 +59,7 @@ export async function replyForFolder(c, deps, projectId, file) {
59
59
  return problem(c, {
60
60
  status: 503,
61
61
  title: titleOf(503),
62
- detail: "Could not open the file manager on the machine running Slopify. Make sure a desktop session is available.",
62
+ detail: "Slopify could not open a file manager window on the computer it runs on. Make sure you are signed in to that computer's desktop, or use the download buttons instead.",
63
63
  });
64
64
  }
65
65
  }
@@ -17,14 +17,14 @@ export function fontsRoutes(deps) {
17
17
  return problem(c, {
18
18
  status: 415,
19
19
  title: titleOf(415),
20
- detail: "Upload one .ttf or .otf file as multipart/form-data.",
20
+ detail: "The font was not sent as a file. Reload the page and choose a .ttf or .otf file again.",
21
21
  });
22
22
  const body = c.req.raw.body;
23
23
  if (body === null)
24
24
  return problem(c, {
25
25
  status: 400,
26
26
  title: titleOf(400),
27
- detail: "The request carried no font file.",
27
+ detail: "No font file arrived. Choose a .ttf or .otf file and try again.",
28
28
  });
29
29
  const part = await readUpload(contentType, body);
30
30
  if (!part.ok) {
@@ -33,8 +33,8 @@ export function fontsRoutes(deps) {
33
33
  status: tooLarge ? 413 : 400,
34
34
  title: tooLarge ? "Content Too Large" : titleOf(400),
35
35
  detail: tooLarge
36
- ? "A font file must be no larger than 32 MiB."
37
- : "Upload exactly one complete font file in the file part.",
36
+ ? "This font file is larger than 32 MB. Choose a smaller .ttf or .otf file."
37
+ : "The font upload did not arrive complete. Choose one .ttf or .otf file and try again.",
38
38
  });
39
39
  }
40
40
  const result = await uploadFont(deps.paths, part.upload);
@@ -44,7 +44,11 @@ export function fontsRoutes(deps) {
44
44
  })
45
45
  .get("/:id/file", zValidator("param", idParam, (result, c) => {
46
46
  if (!result.success)
47
- return problem(c, { status: 404, title: titleOf(404), detail: "No font has that id." });
47
+ return problem(c, {
48
+ status: 404,
49
+ title: titleOf(404),
50
+ detail: "This font no longer exists; it may have been deleted. Reload the page to see your current fonts.",
51
+ });
48
52
  return undefined;
49
53
  }), async (c) => {
50
54
  try {
@@ -58,7 +62,11 @@ export function fontsRoutes(deps) {
58
62
  catch (error) {
59
63
  if (!isMissingFont(error))
60
64
  throw error;
61
- return problem(c, { status: 404, title: titleOf(404), detail: "No font has that id." });
65
+ return problem(c, {
66
+ status: 404,
67
+ title: titleOf(404),
68
+ detail: "This font no longer exists; it may have been deleted. Reload the page to see your current fonts.",
69
+ });
62
70
  }
63
71
  });
64
72
  }
@@ -127,21 +135,25 @@ function failure(reason) {
127
135
  return {
128
136
  status: 413,
129
137
  title: "Content Too Large",
130
- detail: "A font file must be no larger than 32 MiB.",
138
+ detail: "This font file is larger than 32 MB. Choose a smaller .ttf or .otf file.",
131
139
  };
132
140
  case "unsupported-format":
133
- return { status: 415, title: titleOf(415), detail: "Choose a .ttf or .otf font file." };
141
+ return {
142
+ status: 415,
143
+ title: titleOf(415),
144
+ detail: "Slopify only accepts .ttf or .otf font files. Choose a file with one of those endings.",
145
+ };
134
146
  case "unsafe-filename":
135
147
  return {
136
148
  status: 400,
137
149
  title: titleOf(400),
138
- detail: "The font filename cannot contain a path or control character.",
150
+ detail: "The font's file name contains characters Slopify cannot use. Rename the file on your computer, then upload it again.",
139
151
  };
140
152
  case "invalid-font":
141
153
  return {
142
154
  status: 400,
143
155
  title: titleOf(400),
144
- detail: "This file is not a supported, valid TrueType or OpenType font.",
156
+ detail: "This file is not a valid TrueType (.ttf) or OpenType (.otf) font; it may be damaged. Download the font again or choose another.",
145
157
  };
146
158
  }
147
159
  }
@@ -48,14 +48,14 @@ export function hostGate() {
48
48
  },
49
49
  };
50
50
  }
51
- const unavailable = "Host CLI operation did not finish reliably; its result may be uncertain. Review the affected rebuild before retrying.";
51
+ const unavailable = "The AI command-line tool on your computer stopped before it finished, so this result is uncertain. Check the section's outputs on the project page, then use Retry stage.";
52
52
  function fault(error, token) {
53
53
  return {
54
54
  type: "error",
55
55
  kind: isProviderError(error) ? error.fault.kind : "unavailable",
56
56
  message: redact(isProviderError(error)
57
57
  ? error.message
58
- : "Host helper operation failed. Check the host CLI and rerun the Docker launcher.")
58
+ : "The Slopify helper on your computer could not run the AI command-line tool. Check the tool is installed and signed in, then run the Slopify Docker launcher again.")
59
59
  .replaceAll(token, "[redacted]")
60
60
  .slice(0, 4096),
61
61
  };
@@ -67,7 +67,11 @@ export function hostCliRoutes(options) {
67
67
  const supplied = Buffer.from(c.req.header("authorization") ?? "");
68
68
  const expected = Buffer.from(`Bearer ${token}`);
69
69
  if (supplied.length !== expected.length || !timingSafeEqual(supplied, expected))
70
- return c.json({ type: "error", kind: "unavailable", message: "Host helper authorization failed." }, 401);
70
+ return c.json({
71
+ type: "error",
72
+ kind: "unavailable",
73
+ message: "The Slopify helper on your computer refused the connection. Run the Slopify Docker launcher again to reconnect it.",
74
+ }, 401);
71
75
  c.header("Cache-Control", "no-store");
72
76
  await next();
73
77
  });
@@ -84,7 +88,7 @@ export function hostCliRoutes(options) {
84
88
  return c.json({
85
89
  type: "error",
86
90
  kind: "unavailable",
87
- message: "The host helper is busy or restarting. Review the rebuild after it is ready.",
91
+ message: "The Slopify helper on your computer is busy or restarting. Wait a moment, then use Retry stage.",
88
92
  }, 503);
89
93
  const outgoing = c.env?.outgoing;
90
94
  let responseEnded = !outgoing;
@@ -128,7 +132,11 @@ export function hostCliRoutes(options) {
128
132
  });
129
133
  app.use("/v1/*", bodyLimit({
130
134
  maxSize: bridgeLimits.request,
131
- onError: (c) => c.json({ type: "error", kind: "unavailable", message: "Host request is too large." }, 413),
135
+ onError: (c) => c.json({
136
+ type: "error",
137
+ kind: "unavailable",
138
+ message: "Slopify hit an internal error (a request to the helper on your computer was too large). Try again; if it keeps happening, use Download diagnostics in Settings and report it.",
139
+ }, 413),
132
140
  }));
133
141
  app.get("/v1/status/:provider", async (c) => {
134
142
  const id = z.enum(hostCliIds).safeParse(c.req.param("provider"));
@@ -137,7 +145,10 @@ export function hostCliRoutes(options) {
137
145
  const status = hostStatusSchema.parse(await ports.status(id.data));
138
146
  const text = JSON.stringify(status);
139
147
  if (Buffer.byteLength(text) > bridgeLimits.status)
140
- throw providerError({ kind: "unavailable", message: "Host status exceeded its limit." });
148
+ throw providerError({
149
+ kind: "unavailable",
150
+ message: "Slopify hit an internal error (the helper on your computer sent a status report that was too large). Try again; if it keeps happening, use Download diagnostics in Settings and report it.",
151
+ });
141
152
  return c.json(status);
142
153
  });
143
154
  app.get("/v1/models/:provider", async (c) => {
@@ -148,7 +159,10 @@ export function hostCliRoutes(options) {
148
159
  models: await (id.data === "codex-image" ? ports.image : ports.llm(id.data)).models(),
149
160
  });
150
161
  if (Buffer.byteLength(JSON.stringify(result)) > bridgeLimits.models)
151
- throw providerError({ kind: "unavailable", message: "Host models exceeded their limit." });
162
+ throw providerError({
163
+ kind: "unavailable",
164
+ message: "Slopify hit an internal error (the helper on your computer sent a model list that was too large). Try again; if it keeps happening, use Download diagnostics in Settings and report it.",
165
+ });
152
166
  return c.json(result);
153
167
  });
154
168
  app.post("/v1/llm/:provider", async (c) => {
@@ -177,11 +191,11 @@ export function hostCliRoutes(options) {
177
191
  timer = setTimeout(() => controller.abort(), 120_000);
178
192
  const parsed = hostFrameSchema.parse(event);
179
193
  if (done || parsed.type === "error")
180
- throw new Error("Invalid provider stream.");
194
+ throw new Error("Slopify hit an internal error (the AI tool on your computer sent a reply Slopify could not read). Try again; if it keeps happening, use Download diagnostics in Settings and report it.");
181
195
  const frame = `${JSON.stringify(parsed)}\n`;
182
196
  total += Buffer.byteLength(frame);
183
197
  if (Buffer.byteLength(frame) > bridgeLimits.frame || total > bridgeLimits.stream)
184
- throw new Error("Host stream exceeded its limit.");
198
+ throw new Error("Slopify hit an internal error (the AI tool on your computer sent a reply that was too large). Try again; if it keeps happening, use Download diagnostics in Settings and report it.");
185
199
  await output.write(frame);
186
200
  signal.throwIfAborted();
187
201
  done = parsed.type === "done";
@@ -217,7 +231,7 @@ export function hostCliRoutes(options) {
217
231
  if (signal.aborted)
218
232
  throw providerError({ kind: "unavailable", message: unavailable });
219
233
  if (result.bytes.byteLength > bridgeLimits.image || sniffImage(result.bytes) !== result.mime)
220
- throw new Error("Invalid host image.");
234
+ throw new Error("Slopify hit an internal error (the image made on your computer was too large or not a valid image). Try again; if it keeps happening, use Download diagnostics in Settings and report it.");
221
235
  c.header("Content-Type", result.mime);
222
236
  c.header("Content-Length", String(result.bytes.byteLength));
223
237
  return c.body(new Uint8Array(result.bytes));
@@ -231,7 +245,11 @@ export function hostCliRoutes(options) {
231
245
  }
232
246
  });
233
247
  app.onError((error, c) => c.json(fault(error, token), 503));
234
- app.notFound((c) => c.json({ type: "error", kind: "unavailable", message: "Unknown host helper operation or protocol." }, 404));
248
+ app.notFound((c) => c.json({
249
+ type: "error",
250
+ kind: "unavailable",
251
+ message: "The Slopify helper on your computer does not match this version of Slopify. Run the Slopify Docker launcher again to update it.",
252
+ }, 404));
235
253
  return app;
236
254
  }
237
255
  async function json(request) {
@@ -243,5 +261,9 @@ async function json(request) {
243
261
  }
244
262
  }
245
263
  function invalid(c) {
246
- return c.json({ type: "error", kind: "unavailable", message: "Invalid host provider request." }, 400);
264
+ return c.json({
265
+ type: "error",
266
+ kind: "unavailable",
267
+ message: "Slopify hit an internal error (the helper on your computer received a request it could not read). Try again; if it keeps happening, use Download diagnostics in Settings and report it.",
268
+ }, 400);
247
269
  }
@@ -6,10 +6,12 @@ export async function readMultipart(request, consume) {
6
6
  const contentType = request.headers.get("content-type");
7
7
  if (contentType === null || !contentType.startsWith("multipart/form-data"))
8
8
  throw new HTTPException(415, {
9
- message: "An upload is sent as multipart/form-data with the file in one part.",
9
+ message: "The upload was not sent as a file. Reload the page and choose the file again.",
10
10
  });
11
11
  if (request.body === null)
12
- throw new HTTPException(400, { message: "The request carried no body." });
12
+ throw new HTTPException(400, {
13
+ message: "The upload arrived empty. Choose the file again and retry.",
14
+ });
13
15
  let parser;
14
16
  try {
15
17
  parser = new Busboy({
@@ -20,7 +22,7 @@ export async function readMultipart(request, consume) {
20
22
  }
21
23
  catch (error) {
22
24
  throw new HTTPException(400, {
23
- message: "The multipart boundary is missing or invalid.",
25
+ message: "The upload did not arrive complete. Choose the file again and retry.",
24
26
  cause: error,
25
27
  });
26
28
  }
@@ -43,7 +45,7 @@ export async function readMultipart(request, consume) {
43
45
  };
44
46
  source.on("error", fail);
45
47
  parser.on("error", (error) => fail(new HTTPException(400, {
46
- message: "The multipart request is malformed or incomplete.",
48
+ message: "The upload did not arrive complete. Choose the file again and retry.",
47
49
  cause: error,
48
50
  })));
49
51
  parser.on("finish", () => complete());
@@ -62,7 +64,7 @@ export async function readMultipart(request, consume) {
62
64
  fail(error instanceof Error ? error : new Error("Upload failed", { cause: error }));
63
65
  });
64
66
  });
65
- const abort = () => fail(new Error("Upload request was aborted."));
67
+ const abort = () => fail(new Error("The upload was interrupted before it finished. Choose the file again and retry."));
66
68
  request.signal.addEventListener("abort", abort, { once: true });
67
69
  if (request.signal.aborted)
68
70
  abort();
@@ -23,7 +23,7 @@ export function openFolderRoutes(deps) {
23
23
  return problem(c, {
24
24
  status: 403,
25
25
  title: titleOf(403),
26
- detail: "Open folders from Slopify itself.",
26
+ detail: "For your safety, folders can only be opened from the Slopify page itself. Open Slopify and try again there.",
27
27
  });
28
28
  }
29
29
  const { id } = c.req.valid("param");
@@ -41,7 +41,7 @@ export function openFolderRoutes(deps) {
41
41
  return problem(c, {
42
42
  status: 404,
43
43
  title: titleOf(404),
44
- detail: "No saved file was found for this output. Re-run the stage if the file was removed.",
44
+ detail: "This output's file was deleted or moved. Use Re-run section on the project page to make it again.",
45
45
  });
46
46
  }
47
47
  return replyForFolder(c, deps, id, download.download.path);
@@ -66,7 +66,7 @@ export function planningRoutes(deps) {
66
66
  return problem(c, {
67
67
  status: 400,
68
68
  title: titleOf(400),
69
- detail: "Review the run setup.",
69
+ detail: "Some settings need fixing before Slopify can estimate the cost. Fix the highlighted fields.",
70
70
  extensions: { fields },
71
71
  });
72
72
  return c.json({
@@ -79,7 +79,7 @@ export function planningRoutes(deps) {
79
79
  return problem(c, {
80
80
  status: 409,
81
81
  title: titleOf(409),
82
- detail: "Queue checkpoint-enabled projects through Play Review.",
82
+ detail: "Videos with checkpoints turned on cannot be queued. Start them one at a time from Play with Review and start.",
83
83
  });
84
84
  if (batchExists(deps.db, input.requestId))
85
85
  return c.json({ queue: queueEntries(deps.db, input.requestId) });
@@ -92,7 +92,7 @@ export function planningRoutes(deps) {
92
92
  return problem(c, {
93
93
  status: 400,
94
94
  title: titleOf(400),
95
- detail: "Choose an available subtitle font before starting.",
95
+ detail: "The subtitle font you picked is no longer available. Choose another font before starting.",
96
96
  });
97
97
  }
98
98
  }
@@ -102,7 +102,7 @@ export function planningRoutes(deps) {
102
102
  return problem(c, {
103
103
  status: 400,
104
104
  title: titleOf(400),
105
- detail: "No videos were queued. Review the listed fields.",
105
+ detail: "No videos were queued. Fix the highlighted fields, then try again.",
106
106
  extensions: { fields },
107
107
  });
108
108
  const queue = enqueueBatch({ ...deps, emit: (event) => deps.hub.emitGlobal(event) }, input.requestId, runs);
@@ -38,7 +38,7 @@ export function problemFromError(c, error, deps) {
38
38
  return problem(c, {
39
39
  status: 500,
40
40
  title: titleOf(500),
41
- detail: `The server failed to handle this request. Correlation id ${correlationId} is in the log.`,
41
+ detail: `Slopify hit an internal error while handling this request. Try again; if it keeps happening, use Download diagnostics in Settings and report it (reference ${correlationId}).`,
42
42
  extensions: { correlationId },
43
43
  });
44
44
  }
@@ -54,7 +54,7 @@ export function onInvalid(result, c) {
54
54
  return problem(c, {
55
55
  status: 400,
56
56
  title: titleOf(400),
57
- detail: "The request does not match this endpoint's schema.",
57
+ detail: "Slopify could not read this request, which usually means the page is out of date. Reload the page and try again.",
58
58
  extensions: {
59
59
  errors: result.error.issues.map((issue) => ({
60
60
  path: issue.path.join("."),
@@ -17,10 +17,14 @@ function refused(c, result) {
17
17
  status,
18
18
  title: titleOf(status),
19
19
  detail: result.reason === "referenced-by-schedule"
20
- ? "Cancel and delete the schedules using this template before deleting it. Their run history will be retained."
20
+ ? "Some schedules still use this template. In Library > Schedules, cancel and delete them first; their run history is kept."
21
21
  : result.reason === "missing-prompt"
22
- ? "A selected prompt or entry is unavailable. Choose another before saving the template."
23
- : "Reload the template and check its setup before trying again.",
22
+ ? "A prompt or intro/outro entry this template uses was deleted. Choose another in the template, then save again."
23
+ : result.reason === "not-found"
24
+ ? "This template no longer exists; it may have been deleted. Go to Library > Templates to see your templates."
25
+ : result.reason === "conflict"
26
+ ? "This template or its project changed while you were editing. Reload the page to see the latest, then make your change again."
27
+ : "Some template settings are not valid, for example an image you supplied yourself. Check the template's settings and try again.",
24
28
  extensions: { reason: result.reason },
25
29
  });
26
30
  }
@@ -60,7 +60,7 @@ export function projectRoutes(deps) {
60
60
  return problem(c, {
61
61
  status: 409,
62
62
  title: titleOf(409),
63
- detail: "Start checkpoint-enabled projects through Play Review.",
63
+ detail: "This project has checkpoints turned on, so it must be started from Play with Review and start.",
64
64
  });
65
65
  const requestedSubtitles = c.req.valid("json").subtitles;
66
66
  if (requestedSubtitles !== undefined && requestedSubtitles.mode !== "off") {
@@ -71,12 +71,12 @@ export function projectRoutes(deps) {
71
71
  return problem(c, {
72
72
  status: 400,
73
73
  title: titleOf(400),
74
- detail: "Choose an available subtitle font before starting.",
74
+ detail: "The subtitle font you picked is no longer available. Choose another font before starting.",
75
75
  extensions: {
76
76
  fields: [
77
77
  {
78
78
  field: "subtitles.fontId",
79
- message: "This font is no longer available; choose another or upload it again.",
79
+ message: "This font is no longer available. Choose another or upload it again in Settings.",
80
80
  },
81
81
  ],
82
82
  },
@@ -96,7 +96,7 @@ export function projectRoutes(deps) {
96
96
  return problem(c, {
97
97
  status: 400,
98
98
  title: titleOf(400),
99
- detail: "This run cannot start yet; the listed fields need attention.",
99
+ detail: "This video cannot start yet. Fix the highlighted fields, then try again.",
100
100
  extensions: {
101
101
  fields: [...picked.missing, ...modelErrors, ...(admitted.ok ? [] : admitted.fields)],
102
102
  },
@@ -136,7 +136,7 @@ export function projectRoutes(deps) {
136
136
  return problem(c, {
137
137
  status: 404,
138
138
  title: titleOf(404),
139
- detail: "No project has that id.",
139
+ detail: "This project no longer exists. Go back to Projects to pick another.",
140
140
  });
141
141
  }
142
142
  if (deps.catalogue !== undefined)
@@ -171,8 +171,8 @@ const deleteStatus = {
171
171
  files: 500,
172
172
  };
173
173
  const deleteDetails = {
174
- "no-project": "No project has that id.",
174
+ "no-project": "This project no longer exists. Go back to Projects to pick another.",
175
175
  // The run has to be stopped before its files can go.
176
- running: "This project is still running. Cancel the run first.",
177
- files: "Some of this project's files could not be removed.",
176
+ running: "This project is still running. Use Cancel run on the project page first, then delete it.",
177
+ files: "Some of this project's files could not be removed. Close any program using files in the project folder, then try deleting again.",
178
178
  };
@@ -50,7 +50,7 @@ export function refused(c, failure, noun) {
50
50
  return problem(c, {
51
51
  status: 400,
52
52
  title: titleOf(400),
53
- detail: `This ${noun} cannot be saved; the listed fields need attention.`,
53
+ detail: `This ${noun} cannot be saved yet. Fix the highlighted fields and try again.`,
54
54
  extensions: { fields: failure.fields },
55
55
  });
56
56
  // The name is refused against a row that exists, and the form marks the field.
@@ -58,16 +58,21 @@ export function refused(c, failure, noun) {
58
58
  return problem(c, {
59
59
  status: 409,
60
60
  title: titleOf(409),
61
- detail: `Another ${noun} already has this name.`,
61
+ detail: `Another ${noun} already has this name. Choose a different name.`,
62
62
  extensions: {
63
- fields: [{ field: "name", message: `Another ${noun} already has this name.` }],
63
+ fields: [
64
+ {
65
+ field: "name",
66
+ message: `Another ${noun} already has this name. Choose a different name.`,
67
+ },
68
+ ],
64
69
  },
65
70
  });
66
71
  case "not-found":
67
72
  return problem(c, {
68
73
  status: 404,
69
74
  title: titleOf(404),
70
- detail: `No ${noun} has that id.`,
75
+ detail: `This ${noun} no longer exists; it may have been deleted. Go back to the Library to pick another.`,
71
76
  });
72
77
  }
73
78
  }
@@ -29,10 +29,16 @@ export function providerRoutes(deps) {
29
29
  report: (provider) => deps.log.write("warn", "model-catalog", { detail: `Could not load models for ${provider}` }),
30
30
  });
31
31
  return (new Hono()
32
- .get("/catalogue", (c) => c.json(deps.catalogue?.status() ?? { warning: "Catalogue unavailable." }))
32
+ .get("/catalogue", (c) => c.json(deps.catalogue?.status() ?? {
33
+ warning: "The model list (models.yaml) is not loaded yet. Wait a moment and reload the page; if it stays like this, restart Slopify.",
34
+ }))
33
35
  .post("/catalogue/refresh", async (c) => {
34
36
  if (!deps.catalogue)
35
- return problem(c, { status: 503, title: titleOf(503), detail: "Catalogue unavailable." });
37
+ return problem(c, {
38
+ status: 503,
39
+ title: titleOf(503),
40
+ detail: "The model list (models.yaml) is not loaded yet. Wait a moment and reload the page; if it stays like this, restart Slopify.",
41
+ });
36
42
  try {
37
43
  await deps.catalogue.refresh();
38
44
  }
@@ -40,7 +46,7 @@ export function providerRoutes(deps) {
40
46
  return problem(c, {
41
47
  status: 502,
42
48
  title: titleOf(502),
43
- detail: "Could not update the catalogue. The last valid version remains in use.",
49
+ detail: "Slopify could not download the latest model list. Your current list is still in use; check your internet connection and try Refresh again.",
44
50
  });
45
51
  }
46
52
  return c.json(deps.catalogue.status());
@@ -104,20 +110,22 @@ function refusal(c, id, reason) {
104
110
  return problem(c, {
105
111
  status: 400,
106
112
  title: titleOf(400),
107
- detail: `${name} signs in through its own CLI, so there is no key to store here.`,
113
+ detail: `${name} signs in through its own command-line tool, so it has no API key to save here. Sign in with that tool on your computer instead.`,
108
114
  });
109
115
  }
110
116
  if (reason === "absent") {
111
117
  return problem(c, {
112
118
  status: 404,
113
119
  title: titleOf(404),
114
- detail: `No key is stored for ${name}.`,
120
+ detail: `There is no saved API key for ${name}, so there is nothing to remove.`,
115
121
  });
116
122
  }
117
123
  return problem(c, {
118
124
  status: 400,
119
125
  title: titleOf(400),
120
- detail: "This key cannot be saved; the listed fields need attention.",
121
- extensions: { fields: [{ field: "key", message: "An API key is required." }] },
126
+ detail: "This API key cannot be saved yet. Paste the key and try again.",
127
+ extensions: {
128
+ fields: [{ field: "key", message: "Paste the API key from your provider's account page." }],
129
+ },
122
130
  });
123
131
  }