@gentbajko/slopify 2.0.2 → 2.1.0

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 (224) hide show
  1. package/SUBTITLES.md +1 -1
  2. package/dist/adapters/alignment/protocol.js +11 -1
  3. package/dist/adapters/alignment/runner.js +4 -1
  4. package/dist/adapters/alignment/threads.js +11 -0
  5. package/dist/adapters/alignment/worker.js +66 -9
  6. package/dist/adapters/explain.js +93 -0
  7. package/dist/adapters/ffmpeg.js +3 -3
  8. package/dist/adapters/host-cli/index.js +34 -3
  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/assets/document/SOURCE.txt +7 -0
  35. package/dist/assets/document/background.jpg +0 -0
  36. package/dist/assets/document/fonts/Cinzel-Black.ttf +0 -0
  37. package/dist/assets/document/fonts/Cinzel-Bold.ttf +0 -0
  38. package/dist/assets/document/fonts/Cinzel-Medium.ttf +0 -0
  39. package/dist/assets/document/fonts/Cinzel-Regular.ttf +0 -0
  40. package/dist/assets/document/fonts/OFL.txt +93 -0
  41. package/dist/assets/document/fonts/SOURCE.txt +11 -0
  42. package/dist/catalog/registry.js +13 -10
  43. package/dist/catalog/store.js +1 -1
  44. package/dist/catalog/validate.js +16 -6
  45. package/dist/edge/cli.js +33 -7
  46. package/dist/edge/docker-launch.js +6 -6
  47. package/dist/edge/docker-projects/engine.js +34 -13
  48. package/dist/edge/docker-projects/install.js +8 -8
  49. package/dist/edge/docker-projects/state.js +11 -7
  50. package/dist/edge/docker-projects/tree.js +6 -6
  51. package/dist/edge/docker.js +13 -13
  52. package/dist/edge/host-cli.js +14 -0
  53. package/dist/edge/http/actions.js +27 -25
  54. package/dist/edge/http/app.js +3 -3
  55. package/dist/edge/http/audio-preview.js +1 -1
  56. package/dist/edge/http/checkpoints.js +5 -1
  57. package/dist/edge/http/diagnostics.js +3 -1
  58. package/dist/edge/http/draft-files.js +2 -2
  59. package/dist/edge/http/draft-problem.js +11 -1
  60. package/dist/edge/http/files.js +4 -4
  61. package/dist/edge/http/folder-location-schema.js +5 -8
  62. package/dist/edge/http/folder-location.js +13 -8
  63. package/dist/edge/http/fonts.js +22 -10
  64. package/dist/edge/http/host-cli.js +61 -14
  65. package/dist/edge/http/multipart.js +7 -5
  66. package/dist/edge/http/open-folder.js +2 -2
  67. package/dist/edge/http/planning.js +4 -4
  68. package/dist/edge/http/problem.js +2 -2
  69. package/dist/edge/http/project-templates.js +7 -3
  70. package/dist/edge/http/projects.js +8 -8
  71. package/dist/edge/http/prompts.js +9 -4
  72. package/dist/edge/http/providers.js +15 -7
  73. package/dist/edge/http/revision-files.js +8 -4
  74. package/dist/edge/http/revisions.js +30 -4
  75. package/dist/edge/http/schedules.js +9 -9
  76. package/dist/edge/http/settings.js +15 -9
  77. package/dist/edge/http/staging.js +5 -5
  78. package/dist/edge/http/storage.js +4 -4
  79. package/dist/edge/http/subtitles.js +1 -1
  80. package/dist/edge/http/tutorial.js +2 -2
  81. package/dist/edge/http/update.js +3 -3
  82. package/dist/host-cli/install.js +28 -6
  83. package/dist/host-cli/open-folder.js +95 -0
  84. package/dist/host-cli/paths.js +6 -6
  85. package/dist/host-cli/runtime.js +2 -0
  86. package/dist/host-cli/server.js +1 -1
  87. package/dist/host-cli/service.js +11 -11
  88. package/dist/host-cli/status.js +7 -3
  89. package/dist/kernel/config/index.js +6 -4
  90. package/dist/kernel/db/migrate.js +27 -2
  91. package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
  92. package/dist/kernel/db/migrations/0014-document-stage.sql +44 -0
  93. package/dist/kernel/db/migrations/0015-description-prompts.sql +13 -0
  94. package/dist/kernel/lock.js +4 -3
  95. package/dist/kernel/pipeline.js +11 -1
  96. package/dist/kernel/ports/host-cli.js +14 -0
  97. package/dist/kernel/ports/subtitles.js +15 -1
  98. package/dist/kernel/runner/attempt.js +26 -2
  99. package/dist/kernel/runner/graph.js +6 -1
  100. package/dist/main.js +8 -4
  101. package/dist/slices/admission/model.js +7 -1
  102. package/dist/slices/admission/rules.js +98 -22
  103. package/dist/slices/admission/schema.js +16 -3
  104. package/dist/slices/admission/start.js +3 -2
  105. package/dist/slices/article/continuation.js +4 -2
  106. package/dist/slices/article/plain.js +2 -2
  107. package/dist/slices/article/provided-entries.js +3 -3
  108. package/dist/slices/article/run.js +2 -2
  109. package/dist/slices/article/segments.js +5 -3
  110. package/dist/slices/control/providers.js +20 -7
  111. package/dist/slices/document/blocks.js +129 -0
  112. package/dist/slices/document/flow.js +117 -0
  113. package/dist/slices/document/fonts.js +54 -0
  114. package/dist/slices/document/model.js +11 -0
  115. package/dist/slices/document/pages.js +198 -0
  116. package/dist/slices/document/render.js +242 -0
  117. package/dist/slices/document/sources.js +35 -0
  118. package/dist/slices/document/theme.js +158 -0
  119. package/dist/slices/document/writer.js +66 -0
  120. package/dist/slices/estimate/index.js +14 -2
  121. package/dist/slices/fonts/catalog.js +1 -1
  122. package/dist/slices/fonts/upload.js +1 -1
  123. package/dist/slices/images/run.js +5 -5
  124. package/dist/slices/library/lint.js +13 -7
  125. package/dist/slices/library/model.js +1 -1
  126. package/dist/slices/library/slots.js +6 -3
  127. package/dist/slices/narration/concat.js +1 -1
  128. package/dist/slices/narration/pronunciation-chunks.js +3 -3
  129. package/dist/slices/narration/pronunciation.js +2 -2
  130. package/dist/slices/narration/run.js +7 -7
  131. package/dist/slices/narration/steering.js +3 -3
  132. package/dist/slices/play-drafts/convert.js +35 -5
  133. package/dist/slices/play-drafts/readiness.js +15 -8
  134. package/dist/slices/play-drafts/review-inputs.js +7 -4
  135. package/dist/slices/play-drafts/review.js +3 -3
  136. package/dist/slices/play-drafts/schema.js +22 -5
  137. package/dist/slices/play-drafts/service.js +3 -1
  138. package/dist/slices/play-drafts/uploads.js +1 -1
  139. package/dist/slices/project-templates/from-project.js +10 -0
  140. package/dist/slices/project-templates/setup.js +1 -0
  141. package/dist/slices/rebuild/admission-repo.js +2 -2
  142. package/dist/slices/rebuild/preview-details.js +11 -1
  143. package/dist/slices/rebuild/recipe-audio.js +3 -0
  144. package/dist/slices/rebuild/recipe-build.js +6 -1
  145. package/dist/slices/rebuild/recipe-document.js +36 -0
  146. package/dist/slices/rebuild/recipe-exports.js +14 -2
  147. package/dist/slices/rebuild/recipe-fixture.js +4 -0
  148. package/dist/slices/rebuild/recipe-legacy.js +6 -1
  149. package/dist/slices/rebuild/recipe-model.js +2 -0
  150. package/dist/slices/rebuild/recipe-narration-text.js +1 -1
  151. package/dist/slices/rebuild/recipe-provider-choice.js +3 -0
  152. package/dist/slices/rebuild/recipe-save.js +15 -3
  153. package/dist/slices/rebuild/recipe-validation.js +15 -2
  154. package/dist/slices/rebuild/recipe-visual.js +11 -2
  155. package/dist/slices/rebuild/recipe-work.js +15 -0
  156. package/dist/slices/rebuild/recipe-youtube.js +33 -0
  157. package/dist/slices/rebuild/recovery-selection.js +2 -0
  158. package/dist/slices/rebuild/recovery.js +12 -7
  159. package/dist/slices/rebuild/repo.js +3 -3
  160. package/dist/slices/rebuild/runtime-actions.js +2 -0
  161. package/dist/slices/rebuild/runtime-admission.js +4 -4
  162. package/dist/slices/rebuild/runtime-article.js +5 -5
  163. package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
  164. package/dist/slices/rebuild/runtime-document.js +85 -0
  165. package/dist/slices/rebuild/runtime-export-inputs.js +7 -6
  166. package/dist/slices/rebuild/runtime-export.js +39 -23
  167. package/dist/slices/rebuild/runtime-local.js +6 -6
  168. package/dist/slices/rebuild/runtime-narration-text.js +46 -7
  169. package/dist/slices/rebuild/runtime-piece-label.js +66 -0
  170. package/dist/slices/rebuild/runtime-provider.js +6 -4
  171. package/dist/slices/rebuild/runtime-publication.js +4 -3
  172. package/dist/slices/rebuild/runtime-run.js +22 -4
  173. package/dist/slices/rebuild/runtime-store.js +7 -2
  174. package/dist/slices/rebuild/runtime-subtitles.js +82 -16
  175. package/dist/slices/rebuild/runtime-youtube.js +84 -0
  176. package/dist/slices/rebuild/transition-repo.js +8 -6
  177. package/dist/slices/reruns/cascade.js +6 -4
  178. package/dist/slices/reruns/index.js +1 -1
  179. package/dist/slices/research/run.js +9 -7
  180. package/dist/slices/research/synthesis.js +4 -2
  181. package/dist/slices/revisions/mutation-assets.js +26 -8
  182. package/dist/slices/revisions/mutation-cues.js +4 -2
  183. package/dist/slices/revisions/projection.js +2 -1
  184. package/dist/slices/revisions/rules.js +18 -9
  185. package/dist/slices/revisions/schema.js +1 -1
  186. package/dist/slices/schedules/repo.js +55 -11
  187. package/dist/slices/schedules/scheduler.js +25 -8
  188. package/dist/slices/schedules/schema.js +19 -4
  189. package/dist/slices/schedules/service.js +2 -0
  190. package/dist/slices/settings/cli-paths.js +7 -4
  191. package/dist/slices/settings/models.js +4 -4
  192. package/dist/slices/settings/playback.js +2 -2
  193. package/dist/slices/storage/assets.js +5 -5
  194. package/dist/slices/storage/downloads.js +1 -0
  195. package/dist/slices/storage/layout.js +7 -1
  196. package/dist/slices/storage/model.js +3 -0
  197. package/dist/slices/subtitles/captions.js +1 -1
  198. package/dist/slices/subtitles/prepare.js +7 -6
  199. package/dist/slices/subtitles/transcript.js +1 -1
  200. package/dist/slices/telemetry/model.js +1 -1
  201. package/dist/slices/thumbnail/run.js +8 -8
  202. package/dist/slices/video/audio-export.js +11 -3
  203. package/dist/slices/video/audio-inputs.js +2 -2
  204. package/dist/slices/video/edit-list.js +73 -0
  205. package/dist/slices/video/ffmpeg.js +129 -61
  206. package/dist/slices/video/motion.js +51 -0
  207. package/dist/slices/video/plan.js +62 -28
  208. package/dist/slices/video/reuse-audio.js +2 -0
  209. package/dist/slices/video/run.js +23 -12
  210. package/dist/slices/video/slideshow.js +48 -0
  211. package/dist/slices/video/write-export.js +17 -25
  212. package/dist/slices/youtube/answer.js +199 -0
  213. package/dist/slices/youtube/model.js +27 -0
  214. package/dist/slices/youtube/timestamps.js +24 -0
  215. package/dist/slices/youtube/transcript.js +47 -0
  216. package/dist/updater/plan.js +1 -1
  217. package/dist/updater/service.js +4 -3
  218. package/dist/updater/worker.js +6 -4
  219. package/dist/web/assets/index-B6NbhpjD.css +1 -0
  220. package/dist/web/assets/index-ComF5gwP.js +142 -0
  221. package/dist/web/index.html +2 -2
  222. package/package.json +4 -2
  223. package/dist/web/assets/index-CZNiHm8P.css +0 -1
  224. package/dist/web/assets/index-DPgGLX5m.js +0 -137
@@ -5,7 +5,7 @@ import { stream } from "hono/streaming";
5
5
  import { z } from "zod";
6
6
  import { sniffImage } from "../../adapters/image/bytes.js";
7
7
  import { redact } from "../../kernel/log.js";
8
- import { bridgeLimits, hostCliIds, hostCliProtocol, hostFrameSchema, hostImageSchema, hostLlmIds, hostLlmSchema, hostModelsSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
8
+ import { bridgeLimits, HostFolderRefused, hostCliIds, hostCliProtocol, hostFrameSchema, hostImageSchema, hostLlmIds, hostLlmSchema, hostModelsSchema, hostOpenFolderSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
9
9
  import { isProviderError, providerError } from "../../kernel/ports/model.js";
10
10
  export function hostGate() {
11
11
  let accepting = true;
@@ -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
  });
@@ -78,13 +82,14 @@ export function hostCliRoutes(options) {
78
82
  accepting: gate.accepting,
79
83
  }));
80
84
  app.use("/v1/*", async (c, next) => {
81
- const kind = c.req.method === "POST" ? "generation" : "metadata";
85
+ // Opening a folder is quick and must not look like running work to the launcher's upgrade check.
86
+ const kind = c.req.method === "POST" && c.req.path !== "/v1/open-folder" ? "generation" : "metadata";
82
87
  const job = gate.acquire(kind);
83
88
  if (!job)
84
89
  return c.json({
85
90
  type: "error",
86
91
  kind: "unavailable",
87
- message: "The host helper is busy or restarting. Review the rebuild after it is ready.",
92
+ message: "The Slopify helper on your computer is busy or restarting. Wait a moment, then use Retry stage.",
88
93
  }, 503);
89
94
  const outgoing = c.env?.outgoing;
90
95
  let responseEnded = !outgoing;
@@ -128,7 +133,11 @@ export function hostCliRoutes(options) {
128
133
  });
129
134
  app.use("/v1/*", bodyLimit({
130
135
  maxSize: bridgeLimits.request,
131
- onError: (c) => c.json({ type: "error", kind: "unavailable", message: "Host request is too large." }, 413),
136
+ onError: (c) => c.json({
137
+ type: "error",
138
+ kind: "unavailable",
139
+ 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.",
140
+ }, 413),
132
141
  }));
133
142
  app.get("/v1/status/:provider", async (c) => {
134
143
  const id = z.enum(hostCliIds).safeParse(c.req.param("provider"));
@@ -137,7 +146,10 @@ export function hostCliRoutes(options) {
137
146
  const status = hostStatusSchema.parse(await ports.status(id.data));
138
147
  const text = JSON.stringify(status);
139
148
  if (Buffer.byteLength(text) > bridgeLimits.status)
140
- throw providerError({ kind: "unavailable", message: "Host status exceeded its limit." });
149
+ throw providerError({
150
+ kind: "unavailable",
151
+ 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.",
152
+ });
141
153
  return c.json(status);
142
154
  });
143
155
  app.get("/v1/models/:provider", async (c) => {
@@ -148,7 +160,10 @@ export function hostCliRoutes(options) {
148
160
  models: await (id.data === "codex-image" ? ports.image : ports.llm(id.data)).models(),
149
161
  });
150
162
  if (Buffer.byteLength(JSON.stringify(result)) > bridgeLimits.models)
151
- throw providerError({ kind: "unavailable", message: "Host models exceeded their limit." });
163
+ throw providerError({
164
+ kind: "unavailable",
165
+ 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.",
166
+ });
152
167
  return c.json(result);
153
168
  });
154
169
  app.post("/v1/llm/:provider", async (c) => {
@@ -177,11 +192,11 @@ export function hostCliRoutes(options) {
177
192
  timer = setTimeout(() => controller.abort(), 120_000);
178
193
  const parsed = hostFrameSchema.parse(event);
179
194
  if (done || parsed.type === "error")
180
- throw new Error("Invalid provider stream.");
195
+ 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
196
  const frame = `${JSON.stringify(parsed)}\n`;
182
197
  total += Buffer.byteLength(frame);
183
198
  if (Buffer.byteLength(frame) > bridgeLimits.frame || total > bridgeLimits.stream)
184
- throw new Error("Host stream exceeded its limit.");
199
+ 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
200
  await output.write(frame);
186
201
  signal.throwIfAborted();
187
202
  done = parsed.type === "done";
@@ -217,7 +232,7 @@ export function hostCliRoutes(options) {
217
232
  if (signal.aborted)
218
233
  throw providerError({ kind: "unavailable", message: unavailable });
219
234
  if (result.bytes.byteLength > bridgeLimits.image || sniffImage(result.bytes) !== result.mime)
220
- throw new Error("Invalid host image.");
235
+ 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
236
  c.header("Content-Type", result.mime);
222
237
  c.header("Content-Length", String(result.bytes.byteLength));
223
238
  return c.body(new Uint8Array(result.bytes));
@@ -230,8 +245,36 @@ export function hostCliRoutes(options) {
230
245
  controller.abort();
231
246
  }
232
247
  });
248
+ app.post("/v1/open-folder", async (c) => {
249
+ const open = ports.openFolder;
250
+ if (!open)
251
+ return c.notFound();
252
+ const body = hostOpenFolderSchema.safeParse(await json(c.req.raw));
253
+ if (!body.success)
254
+ return invalid(c);
255
+ try {
256
+ await open(body.data.path);
257
+ }
258
+ catch (error) {
259
+ const refused = error instanceof HostFolderRefused;
260
+ return c.json({
261
+ type: "error",
262
+ kind: "unavailable",
263
+ message: (error instanceof Error
264
+ ? error.message
265
+ : "The Slopify host helper could not open the folder. Copy the path into your file manager instead.")
266
+ .replaceAll(token, "[redacted]")
267
+ .slice(0, 4096),
268
+ }, refused ? 403 : 503);
269
+ }
270
+ return c.json({ opened: true });
271
+ });
233
272
  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));
273
+ app.notFound((c) => c.json({
274
+ type: "error",
275
+ kind: "unavailable",
276
+ message: "The Slopify helper on your computer does not match this version of Slopify. Run the Slopify Docker launcher again to update it.",
277
+ }, 404));
235
278
  return app;
236
279
  }
237
280
  async function json(request) {
@@ -243,5 +286,9 @@ async function json(request) {
243
286
  }
244
287
  }
245
288
  function invalid(c) {
246
- return c.json({ type: "error", kind: "unavailable", message: "Invalid host provider request." }, 400);
289
+ return c.json({
290
+ type: "error",
291
+ kind: "unavailable",
292
+ 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.",
293
+ }, 400);
247
294
  }
@@ -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
  }
@@ -33,10 +33,14 @@ export function revisionFileRoutes(deps) {
33
33
  if (!result.ok)
34
34
  return unavailable(c, result.reason);
35
35
  const value = result.download;
36
+ // `?inline=1` lets the project page open a PDF in a browser tab instead of saving
37
+ // it. Only PDFs: anything else the browser might render stays a download.
38
+ const inline = c.req.query("inline") === "1" && value.contentType === "application/pdf";
36
39
  return c.body(Readable.toWeb(createReadStream(value.path)), 200, {
37
40
  "content-type": value.contentType,
38
41
  "content-length": String(value.bytes),
39
- "content-disposition": `attachment; filename="${value.filename}"`,
42
+ "content-disposition": `${inline ? "inline" : "attachment"}; filename="${value.filename}"`,
43
+ "x-content-type-options": "nosniff",
40
44
  });
41
45
  });
42
46
  }
@@ -47,7 +51,7 @@ export function revisionFolderRoutes(deps) {
47
51
  return problem(c, {
48
52
  status: 403,
49
53
  title: titleOf(403),
50
- detail: "Open folders from Slopify itself.",
54
+ detail: "For your safety, folders can only be opened from the Slopify page itself. Open Slopify and try again there.",
51
55
  });
52
56
  const { id: projectId, revisionId, recordId } = c.req.valid("param");
53
57
  const result = findRevisionDownload(deps, projectId, revisionId, recordId);
@@ -61,8 +65,8 @@ function unavailable(c, reason) {
61
65
  status: 404,
62
66
  title: titleOf(404),
63
67
  detail: reason === "missing-file"
64
- ? "This retained file is no longer on disk. Review the affected rebuild to recreate it."
65
- : "The retained file is unavailable.",
68
+ ? "This file was deleted or moved from the project folder. In the Edit tab, use Rebuild affected outputs to make it again."
69
+ : "This file is no longer part of the project. Reload the page to see its current files.",
66
70
  extensions: { reason },
67
71
  });
68
72
  }
@@ -3,6 +3,7 @@ import { Hono } from "hono";
3
3
  import { z } from "zod";
4
4
  import { projectExists } from "../../slices/admission/repo.js";
5
5
  import { previewRebuildSchema, startRebuildSchema } from "../../slices/rebuild/model.js";
6
+ import { narrationChunkOrder } from "../../slices/rebuild/runtime-narration-text.js";
6
7
  import { previewRebuild, startRebuild } from "../../slices/rebuild/service.js";
7
8
  import { ensureBaseline, restoreRevision, saveRevision } from "../../slices/revisions/index.js";
8
9
  import { currentRevisionId, listRevisionHistory } from "../../slices/revisions/repo.js";
@@ -30,12 +31,24 @@ const statuses = {
30
31
  "cost-ack-required": 409,
31
32
  readiness: 409,
32
33
  };
34
+ const details = {
35
+ "no-project": "This project no longer exists. Go back to Projects to pick another.",
36
+ "no-revision": "This saved version of the project no longer exists. Reload the page, then try again.",
37
+ conflict: "This project changed while you were editing. Reload the page to see the latest, then make your change again.",
38
+ "idempotency-conflict": "This change was already sent with different details. Reload the page, then try again.",
39
+ "invalid-edit": "Some of your changes are not valid. Fix the highlighted fields and save again.",
40
+ "stale-preview": "What needs rebuilding changed since you opened this review. Cancel it and open Rebuild affected outputs again to see the latest.",
41
+ "invalid-selection": "Some of the outputs you picked can no longer be rebuilt. Reload the page and choose again.",
42
+ "review-required": 'This rebuild touches content you supplied yourself. Tick "Keep the provided content" for each item in the review, then choose Start rebuild.',
43
+ "cost-ack-required": "Some costs for this rebuild are unknown. Tick the box confirming you understand, then choose Start rebuild.",
44
+ readiness: "Slopify cannot run this rebuild yet. Fix the problems listed, then try again.",
45
+ };
33
46
  function refused(c, deps, projectId, value) {
34
47
  const status = statuses[value.reason];
35
48
  return problem(c, {
36
49
  status,
37
50
  title: titleOf(status),
38
- detail: value.reason.replaceAll("-", " "),
51
+ detail: details[value.reason],
39
52
  extensions: {
40
53
  reason: value.reason,
41
54
  currentRevisionId: currentRevisionId(deps.db, projectId) ?? null,
@@ -47,11 +60,11 @@ function unavailable(c) {
47
60
  return problem(c, {
48
61
  status: 503,
49
62
  title: titleOf(503),
50
- detail: "Rebuild services are unavailable.",
63
+ detail: "Rebuilding is not ready yet because Slopify is still starting. Wait a moment, reload the page and try again.",
51
64
  });
52
65
  }
53
66
  export function revisionRoutes(deps) {
54
- return new Hono()
67
+ return (new Hono()
55
68
  .post("/:id/revisions/prepare", zValidator("param", projectParam, onInvalid), async (c) => {
56
69
  const projectId = c.req.valid("param").id;
57
70
  const result = await ensureBaseline(deps, projectId);
@@ -69,6 +82,19 @@ export function revisionRoutes(deps) {
69
82
  return view === undefined
70
83
  ? refused(c, deps, projectId, { reason: "no-revision" })
71
84
  : c.json({ view });
85
+ })
86
+ // Chunk keys in spoken order, so the Narration editor numbers chunks the way a subtitle
87
+ // error names them. Building the plan is the costly part, so it has its own request.
88
+ .get("/:id/revisions/:revisionId/narration-chunks", zValidator("param", revisionParam, onInvalid), (c) => {
89
+ const { id: projectId, revisionId } = c.req.valid("param");
90
+ let chunks;
91
+ try {
92
+ chunks = narrationChunkOrder(deps, projectId, revisionId);
93
+ }
94
+ catch {
95
+ chunks = undefined;
96
+ }
97
+ return c.json({ chunks: chunks ?? null });
72
98
  })
73
99
  .post("/:id/revisions", zValidator("param", projectParam, onInvalid), zValidator("json", saveBody, onInvalid), async (c) => {
74
100
  const projectId = c.req.valid("param").id;
@@ -99,5 +125,5 @@ export function revisionRoutes(deps) {
99
125
  const projectId = c.req.valid("param").id;
100
126
  const result = await startRebuild(deps.rebuild, { projectId, ...c.req.valid("json") });
101
127
  return result.ok ? c.json(result, 202) : refused(c, deps, projectId, result);
102
- });
128
+ }));
103
129
  }
@@ -13,15 +13,15 @@ function refused(c, result) {
13
13
  ? 409
14
14
  : 400;
15
15
  const detail = {
16
- "invalid-input": "Check the schedule fields and try again.",
17
- "not-found": "The schedule was not found.",
18
- conflict: "The schedule changed elsewhere. Reload it before trying again.",
19
- "cancel-required": "Cancel this schedule before deleting it. Completed schedules can be deleted directly.",
20
- "missing-template": "The selected template version is no longer available.",
21
- "unsupported-media": "This template contains provided media and cannot run unattended.",
22
- "not-due": "Choose a future one-off time.",
23
- "spend-limit": "The estimate exceeds the schedule spend limit or contains unknown pricing.",
24
- readiness: "Provider or local readiness checks refused this scheduled run.",
16
+ "invalid-input": "Some schedule settings are not valid. Check each field and save again.",
17
+ "not-found": "This schedule no longer exists; it may have been deleted. Reload the page to see your schedules.",
18
+ conflict: "This schedule changed while you were editing, possibly because a run just started. Reload the page to see the latest, then make your change again.",
19
+ "cancel-required": "This schedule is still active. Cancel it first, then delete it. Completed schedules can be deleted straight away.",
20
+ "missing-template": "The template you picked was deleted or changed. Choose a template again, then save.",
21
+ "unsupported-media": "This template uses audio, images or a thumbnail you supplied, so it cannot run on a schedule. Pick a template that generates these instead.",
22
+ "not-due": "This one-off time has already passed. Choose a time in the future.",
23
+ "spend-limit": "The estimated cost is above this schedule's spend limit, or some prices are unknown. Raise the spend limit or choose models with known prices.",
24
+ readiness: "A provider this template needs is not ready, for example a missing API key, model or voice. Check Settings → Providers, then try again.",
25
25
  };
26
26
  return problem(c, {
27
27
  status,