@gentbajko/slopify 0.8.3 → 1.0.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.
- package/README.md +28 -6
- package/SUBTITLES.md +2 -0
- package/dist/adapters/alignment/window.js +46 -5
- package/dist/adapters/alignment/worker.js +2 -2
- package/dist/adapters/llm/claude-code.js +6 -3
- package/dist/adapters/llm/codex.js +102 -12
- package/dist/adapters/llm/gemini.js +11 -8
- package/dist/adapters/llm/run-cli.js +61 -6
- package/dist/adapters/tts/inworld-async.js +4 -4
- package/dist/adapters/tts/inworld.js +73 -68
- package/dist/catalog/registry.js +13 -73
- package/dist/edge/cli.js +5 -2
- package/dist/edge/events/hub.js +7 -2
- package/dist/edge/events/preview-cache.js +7 -4
- package/dist/edge/events/visibility.js +33 -0
- package/dist/edge/http/actions.js +74 -79
- package/dist/edge/http/app.js +39 -6
- package/dist/edge/http/audio-preview.js +9 -1
- package/dist/edge/http/checkpoints.js +95 -0
- package/dist/edge/http/diagnostics.js +31 -0
- package/dist/edge/http/draft-files.js +69 -0
- package/dist/edge/http/draft-problem.js +15 -0
- package/dist/edge/http/drafts.js +95 -0
- package/dist/edge/http/multipart.js +86 -0
- package/dist/edge/http/mutations.js +49 -0
- package/dist/edge/http/planning.js +12 -1
- package/dist/edge/http/project-templates.js +74 -0
- package/dist/edge/http/projects.js +19 -2
- package/dist/edge/http/revision-files.js +84 -0
- package/dist/edge/http/revisions.js +103 -0
- package/dist/edge/http/schedules.js +100 -0
- package/dist/edge/http/staging.js +11 -52
- package/dist/edge/http/storage.js +110 -0
- package/dist/edge/http/subtitles.js +9 -79
- package/dist/edge/http/tutorial.js +26 -0
- package/dist/edge/signal-shutdown.js +23 -0
- package/dist/kernel/audio-preview.js +11 -5
- package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
- package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
- package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
- package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
- package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
- package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
- package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
- package/dist/kernel/ports/model.js +5 -0
- package/dist/kernel/runner/attempt-repo.js +15 -0
- package/dist/kernel/runner/attempt.js +14 -2
- package/dist/kernel/runner/checkpoint-authority.js +23 -0
- package/dist/kernel/runner/index.js +35 -11
- package/dist/kernel/runner/providers.js +18 -8
- package/dist/kernel/runner/work-attempt.js +47 -0
- package/dist/kernel/runner/work-authority.js +37 -0
- package/dist/kernel/runner/work.js +23 -0
- package/dist/main.js +227 -69
- package/dist/slices/admission/repo.js +5 -56
- package/dist/slices/admission/schema.js +64 -0
- package/dist/slices/admission/start.js +13 -4
- package/dist/slices/article/continuation.js +9 -3
- package/dist/slices/article/provided-entries.js +7 -3
- package/dist/slices/article/run.js +20 -7
- package/dist/slices/article/segments.js +13 -4
- package/dist/slices/batch/index.js +8 -11
- package/dist/slices/cancel/index.js +78 -34
- package/dist/slices/checkpoints/change.js +250 -0
- package/dist/slices/checkpoints/fingerprint.js +33 -0
- package/dist/slices/checkpoints/index.js +2 -0
- package/dist/slices/checkpoints/model.js +1 -0
- package/dist/slices/checkpoints/recovery.js +116 -0
- package/dist/slices/checkpoints/repo.js +132 -0
- package/dist/slices/checkpoints/rules.js +30 -0
- package/dist/slices/checkpoints/schema.js +59 -0
- package/dist/slices/control/index.js +88 -22
- package/dist/slices/control/providers.js +3 -2
- package/dist/slices/control/revision-control-schema.js +7 -0
- package/dist/slices/control/revision-control.js +59 -0
- package/dist/slices/estimate/index.js +77 -59
- package/dist/slices/estimate/requests.js +102 -0
- package/dist/slices/images/run.js +14 -3
- package/dist/slices/library/slots.js +11 -11
- package/dist/slices/library/snapshot.js +33 -0
- package/dist/slices/narration/legacy-plan.js +58 -0
- package/dist/slices/narration/live.js +2 -2
- package/dist/slices/narration/plan.js +48 -54
- package/dist/slices/narration/run.js +21 -8
- package/dist/slices/play-drafts/convert.js +120 -0
- package/dist/slices/play-drafts/model.js +1 -0
- package/dist/slices/play-drafts/readiness.js +80 -0
- package/dist/slices/play-drafts/repo.js +76 -0
- package/dist/slices/play-drafts/review-inputs.js +150 -0
- package/dist/slices/play-drafts/review.js +109 -0
- package/dist/slices/play-drafts/schema.js +218 -0
- package/dist/slices/play-drafts/service.js +242 -0
- package/dist/slices/play-drafts/start-repo.js +113 -0
- package/dist/slices/play-drafts/start.js +148 -0
- package/dist/slices/play-drafts/uploads.js +67 -0
- package/dist/slices/project-templates/from-project.js +159 -0
- package/dist/slices/project-templates/model.js +1 -0
- package/dist/slices/project-templates/repo.js +41 -0
- package/dist/slices/project-templates/schema.js +37 -0
- package/dist/slices/project-templates/service.js +136 -0
- package/dist/slices/project-templates/setup.js +55 -0
- package/dist/slices/rebuild/admission-repo.js +120 -0
- package/dist/slices/rebuild/dependencies.js +33 -0
- package/dist/slices/rebuild/model.js +94 -0
- package/dist/slices/rebuild/narration-history.js +43 -0
- package/dist/slices/rebuild/narration-reuse.js +20 -0
- package/dist/slices/rebuild/preview-details.js +144 -0
- package/dist/slices/rebuild/preview-plan.js +223 -0
- package/dist/slices/rebuild/preview-retained.js +128 -0
- package/dist/slices/rebuild/provided-review.js +32 -0
- package/dist/slices/rebuild/recipe-audio.js +172 -0
- package/dist/slices/rebuild/recipe-build.js +17 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
- package/dist/slices/rebuild/recipe-exports.js +65 -0
- package/dist/slices/rebuild/recipe-fixture.js +150 -0
- package/dist/slices/rebuild/recipe-input-schema.js +82 -0
- package/dist/slices/rebuild/recipe-legacy.js +135 -0
- package/dist/slices/rebuild/recipe-model.js +77 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
- package/dist/slices/rebuild/recipe-save.js +221 -0
- package/dist/slices/rebuild/recipe-text.js +170 -0
- package/dist/slices/rebuild/recipe-validation.js +86 -0
- package/dist/slices/rebuild/recipe-visual.js +111 -0
- package/dist/slices/rebuild/recipe-work.js +212 -0
- package/dist/slices/rebuild/recipes.js +4 -0
- package/dist/slices/rebuild/repo.js +64 -0
- package/dist/slices/rebuild/runtime-actions.js +76 -0
- package/dist/slices/rebuild/runtime-admission.js +71 -0
- package/dist/slices/rebuild/runtime-article.js +117 -0
- package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
- package/dist/slices/rebuild/runtime-export.js +139 -0
- package/dist/slices/rebuild/runtime-instructions.js +51 -0
- package/dist/slices/rebuild/runtime-local.js +137 -0
- package/dist/slices/rebuild/runtime-materialize.js +105 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
- package/dist/slices/rebuild/runtime-plan.js +103 -0
- package/dist/slices/rebuild/runtime-provider.js +184 -0
- package/dist/slices/rebuild/runtime-publication.js +172 -0
- package/dist/slices/rebuild/runtime-run.js +35 -0
- package/dist/slices/rebuild/runtime-store.js +166 -0
- package/dist/slices/rebuild/runtime-subtitles.js +202 -0
- package/dist/slices/rebuild/service-readiness.js +110 -0
- package/dist/slices/rebuild/service.js +149 -0
- package/dist/slices/rebuild/transition-repo.js +108 -0
- package/dist/slices/rebuild/work-records.js +44 -0
- package/dist/slices/reruns/index.js +5 -0
- package/dist/slices/research/run.js +33 -12
- package/dist/slices/revisions/adopt-assets.js +31 -0
- package/dist/slices/revisions/adopt-content.js +112 -0
- package/dist/slices/revisions/adopt.js +100 -0
- package/dist/slices/revisions/downloads.js +80 -0
- package/dist/slices/revisions/index.js +5 -0
- package/dist/slices/revisions/manifest-repo.js +125 -0
- package/dist/slices/revisions/model.js +1 -0
- package/dist/slices/revisions/mutation-assets.js +143 -0
- package/dist/slices/revisions/mutation-cues.js +95 -0
- package/dist/slices/revisions/mutation-prepare.js +182 -0
- package/dist/slices/revisions/mutation-request.js +58 -0
- package/dist/slices/revisions/mutation-work.js +47 -0
- package/dist/slices/revisions/mutations.js +194 -0
- package/dist/slices/revisions/projection.js +82 -0
- package/dist/slices/revisions/publication-model.js +1 -0
- package/dist/slices/revisions/publication-rules.js +166 -0
- package/dist/slices/revisions/publish.js +115 -0
- package/dist/slices/revisions/repo.js +83 -0
- package/dist/slices/revisions/restore.js +60 -0
- package/dist/slices/revisions/rules.js +99 -0
- package/dist/slices/revisions/schema.js +160 -0
- package/dist/slices/revisions/view.js +44 -0
- package/dist/slices/schedules/calendar.js +47 -0
- package/dist/slices/schedules/model.js +1 -0
- package/dist/slices/schedules/repo.js +281 -0
- package/dist/slices/schedules/scheduler.js +144 -0
- package/dist/slices/schedules/schema.js +80 -0
- package/dist/slices/schedules/service.js +178 -0
- package/dist/slices/settings/cli-paths.js +1 -1
- package/dist/slices/settings/cli-status.js +45 -5
- package/dist/slices/settings/tutorial-schema.js +39 -0
- package/dist/slices/settings/tutorial.js +66 -0
- package/dist/slices/storage/assets.js +108 -0
- package/dist/slices/storage/delete-project.js +2 -1
- package/dist/slices/storage/downloads.js +4 -1
- package/dist/slices/storage/portable.js +871 -0
- package/dist/slices/storage/prepare.js +67 -0
- package/dist/slices/storage/reconcile.js +25 -5
- package/dist/slices/storage/repo.js +2 -15
- package/dist/slices/storage/schema.js +35 -0
- package/dist/slices/storage/staging-refs.js +26 -0
- package/dist/slices/storage/staging.js +16 -11
- package/dist/slices/thumbnail/run.js +26 -9
- package/dist/slices/video/audio-export-args.js +33 -0
- package/dist/slices/video/audio-export.js +13 -36
- package/dist/slices/video/audio-inputs.js +4 -3
- package/dist/slices/video/ffmpeg.js +6 -0
- package/dist/slices/video/reuse-audio.js +11 -4
- package/dist/updater/install-flow.js +27 -2
- package/dist/updater/plan.js +28 -2
- package/dist/updater/service.js +27 -2
- package/dist/updater/worker.js +2 -1
- package/dist/web/assets/index-CO7DnVX9.js +137 -0
- package/dist/web/assets/index-DX1_chKs.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/adapters/tts/inworld-text.js +0 -32
- package/dist/web/assets/index-Ce2HApus.css +0 -1
- package/dist/web/assets/index-xbD0MZOY.js +0 -130
package/dist/edge/http/app.js
CHANGED
|
@@ -4,19 +4,28 @@ import { Hono } from "hono";
|
|
|
4
4
|
import { streamSSE } from "hono/streaming";
|
|
5
5
|
import { actionRoutes } from "./actions.js";
|
|
6
6
|
import { audioPreviewRoutes } from "./audio-preview.js";
|
|
7
|
+
import { checkpointRoutes } from "./checkpoints.js";
|
|
8
|
+
import { diagnosticsRoutes } from "./diagnostics.js";
|
|
9
|
+
import { draftRoutes } from "./drafts.js";
|
|
7
10
|
import { entryRoutes } from "./entries.js";
|
|
8
11
|
import { fileRoutes } from "./files.js";
|
|
9
12
|
import { fontsRoutes } from "./fonts.js";
|
|
10
13
|
import { openFolderRoutes } from "./open-folder.js";
|
|
11
14
|
import { planningRoutes } from "./planning.js";
|
|
12
15
|
import { problem, problemFromError, titleOf } from "./problem.js";
|
|
16
|
+
import { projectTemplateRoutes } from "./project-templates.js";
|
|
13
17
|
import { projectRoutes } from "./projects.js";
|
|
14
18
|
import { promptRoutes } from "./prompts.js";
|
|
15
19
|
import { providerRoutes } from "./providers.js";
|
|
20
|
+
import { revisionFileRoutes, revisionFolderRoutes } from "./revision-files.js";
|
|
21
|
+
import { revisionRoutes } from "./revisions.js";
|
|
22
|
+
import { scheduleRoutes } from "./schedules.js";
|
|
16
23
|
import { settingsRoutes } from "./settings.js";
|
|
17
24
|
import { stagingRoutes } from "./staging.js";
|
|
25
|
+
import { storageRoutes } from "./storage.js";
|
|
18
26
|
import { subtitleRoutes } from "./subtitles.js";
|
|
19
27
|
import { telemetryRoutes } from "./telemetry.js";
|
|
28
|
+
import { tutorialRoutes } from "./tutorial.js";
|
|
20
29
|
import { updateRoutes } from "./update.js";
|
|
21
30
|
import { usageRoutes } from "./usage.js";
|
|
22
31
|
const notBuilt = "The Slopify UI is not built yet. The API is running: try /api/health.\n" +
|
|
@@ -29,8 +38,16 @@ function apiRoutes(deps, startedAt) {
|
|
|
29
38
|
uptimeMs: deps.clock.now().getTime() - startedAt,
|
|
30
39
|
}))
|
|
31
40
|
.route("/staging", stagingRoutes(deps))
|
|
41
|
+
.route("/storage", storageRoutes(deps))
|
|
42
|
+
.route("/drafts", draftRoutes(deps.drafts))
|
|
43
|
+
.route("/diagnostics", diagnosticsRoutes(deps))
|
|
44
|
+
.route("/project-templates", projectTemplateRoutes(deps.drafts))
|
|
45
|
+
.route("/schedules", scheduleRoutes(deps.schedules))
|
|
32
46
|
.route("/projects", planningRoutes(deps))
|
|
33
47
|
.route("/projects", projectRoutes(deps))
|
|
48
|
+
.route("/projects", checkpointRoutes(deps))
|
|
49
|
+
.route("/projects", revisionRoutes(deps))
|
|
50
|
+
.route("/projects", revisionFolderRoutes(deps))
|
|
34
51
|
.route("/projects", openFolderRoutes(deps))
|
|
35
52
|
.route("/projects", audioPreviewRoutes(deps))
|
|
36
53
|
.route("/update", updateRoutes(deps))
|
|
@@ -44,6 +61,7 @@ function apiRoutes(deps, startedAt) {
|
|
|
44
61
|
.route("/telemetry", telemetryRoutes(deps))
|
|
45
62
|
.route("/usage", usageRoutes(deps))
|
|
46
63
|
.route("/settings", settingsRoutes(deps))
|
|
64
|
+
.route("/tutorial", tutorialRoutes(deps))
|
|
47
65
|
.route("/providers", providerRoutes(deps)));
|
|
48
66
|
}
|
|
49
67
|
export function createApp(deps) {
|
|
@@ -56,22 +74,36 @@ export function createApp(deps) {
|
|
|
56
74
|
await next();
|
|
57
75
|
})
|
|
58
76
|
.use("/api/*", async (c, next) => {
|
|
59
|
-
if (
|
|
60
|
-
["GET", "HEAD", "OPTIONS"].includes(c.req.method) ||
|
|
61
|
-
["/api/update", "/api/update/", "/api/update/activate", "/api/update/activate/"].includes(c.req.path))
|
|
77
|
+
if (["GET", "HEAD", "OPTIONS"].includes(c.req.method))
|
|
62
78
|
return next();
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
79
|
+
const releaseRequest = deps.mutations?.begin();
|
|
80
|
+
if (deps.mutations !== undefined && releaseRequest === undefined)
|
|
81
|
+
return problem(c, {
|
|
82
|
+
status: 503,
|
|
83
|
+
title: titleOf(503),
|
|
84
|
+
detail: "Slopify is shutting down. Wait for it to restart before making changes.",
|
|
85
|
+
});
|
|
86
|
+
const updateRoute = [
|
|
87
|
+
"/api/update",
|
|
88
|
+
"/api/update/",
|
|
89
|
+
"/api/update/activate",
|
|
90
|
+
"/api/update/activate/",
|
|
91
|
+
].includes(c.req.path);
|
|
92
|
+
const releaseUpdate = deps.updater === undefined || updateRoute ? undefined : deps.updater.beginMutation();
|
|
93
|
+
if (deps.updater !== undefined && !updateRoute && releaseUpdate === undefined) {
|
|
94
|
+
releaseRequest?.();
|
|
65
95
|
return problem(c, {
|
|
66
96
|
status: 409,
|
|
67
97
|
title: titleOf(409),
|
|
68
98
|
detail: "Slopify is updating. Wait for it to restart before making changes.",
|
|
69
99
|
});
|
|
100
|
+
}
|
|
70
101
|
try {
|
|
71
102
|
await next();
|
|
72
103
|
}
|
|
73
104
|
finally {
|
|
74
|
-
|
|
105
|
+
releaseUpdate?.();
|
|
106
|
+
releaseRequest?.();
|
|
75
107
|
}
|
|
76
108
|
})
|
|
77
109
|
.onError((error, c) => problemFromError(c, error, deps))
|
|
@@ -81,6 +113,7 @@ export function createApp(deps) {
|
|
|
81
113
|
.get("/api/events/projects/:id", (c) => streamSSE(c, (stream) => deps.hub.subscribe(c.req.param("id"), stream, c.req.raw.signal)))
|
|
82
114
|
// Files are served by URL, not through the API.
|
|
83
115
|
.route("/", fileRoutes(deps))
|
|
116
|
+
.route("/", revisionFileRoutes(deps))
|
|
84
117
|
// The API answers for its whole prefix, so an unknown endpoint is a problem+json 404
|
|
85
118
|
// rather than the SPA's index.html with a 200.
|
|
86
119
|
.all("/api/*", missing);
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { zValidator } from "@hono/zod-validator";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { currentRevisionId } from "../../slices/revisions/repo.js";
|
|
4
5
|
import { projectTitle } from "../../slices/storage/repo.js";
|
|
6
|
+
import { currentWorkOrigin } from "../events/visibility.js";
|
|
5
7
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
6
8
|
const projectParam = z.object({
|
|
7
9
|
projectId: z
|
|
@@ -18,12 +20,18 @@ export function audioPreviewRoutes(deps) {
|
|
|
18
20
|
if (projectTitle(deps.db, projectId) === undefined)
|
|
19
21
|
return missing(c);
|
|
20
22
|
c.header("Cache-Control", "no-store");
|
|
21
|
-
return c.json({
|
|
23
|
+
return c.json({
|
|
24
|
+
revisionId: currentRevisionId(deps.db, projectId) ?? null,
|
|
25
|
+
previews: (deps.audioPreviews?.list(projectId) ?? []).filter((preview) => currentWorkOrigin(deps.db, projectId, preview)),
|
|
26
|
+
});
|
|
22
27
|
})
|
|
23
28
|
.get("/:projectId/audio-preview/:previewId", zValidator("param", previewParam, onInvalid), (c) => {
|
|
24
29
|
const { projectId, previewId } = c.req.valid("param");
|
|
25
30
|
if (projectTitle(deps.db, projectId) === undefined)
|
|
26
31
|
return missing(c);
|
|
32
|
+
const preview = deps.audioPreviews?.list(projectId).find((item) => item.id === previewId);
|
|
33
|
+
if (preview === undefined || !currentWorkOrigin(deps.db, projectId, preview))
|
|
34
|
+
return missing(c);
|
|
27
35
|
const stream = deps.audioPreviews?.stream(projectId, previewId, c.req.raw.signal);
|
|
28
36
|
if (stream === undefined)
|
|
29
37
|
return missing(c);
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { changeCheckpoints, checkpointChangeSchema, readCheckpointStatus, replayCheckpointApproval, validateCheckpointApproval, } from "../../slices/checkpoints/change.js";
|
|
5
|
+
import { checkpointApprovalSchema, checkpointRowSchema } from "../../slices/checkpoints/schema.js";
|
|
6
|
+
import { withProjectControl } from "../../slices/control/lock.js";
|
|
7
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
8
|
+
const id = z
|
|
9
|
+
.string()
|
|
10
|
+
.min(1)
|
|
11
|
+
.max(64)
|
|
12
|
+
.regex(/^[0-9A-Za-z_-]+$/);
|
|
13
|
+
const projectParam = z.object({ id });
|
|
14
|
+
const approvalParam = projectParam.extend({ checkpointId: id });
|
|
15
|
+
const approvalBody = checkpointApprovalSchema
|
|
16
|
+
.unwrap()
|
|
17
|
+
.omit({ projectId: true, checkpointId: true, approvedAt: true });
|
|
18
|
+
function refused(c, reason) {
|
|
19
|
+
const status = reason === "not-found" ? 404 : reason === "invalid-input" ? 400 : 409;
|
|
20
|
+
return problem(c, {
|
|
21
|
+
status,
|
|
22
|
+
title: titleOf(status),
|
|
23
|
+
detail: "Reload the current checkpoint status before approving or changing this gate.",
|
|
24
|
+
extensions: { reason },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function checkpointRoutes(deps) {
|
|
28
|
+
return buildCheckpointRoutes(deps);
|
|
29
|
+
}
|
|
30
|
+
function buildCheckpointRoutes(deps) {
|
|
31
|
+
const notify = (projectId, revisionId, wake) => {
|
|
32
|
+
try {
|
|
33
|
+
deps.hub.emit(projectId, { type: "project.updated", projectId, revisionId });
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
deps.log.write("error", "checkpoint.notify", {
|
|
37
|
+
projectId,
|
|
38
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (wake) {
|
|
42
|
+
try {
|
|
43
|
+
deps.runner.tick(projectId);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
deps.log.write("error", "checkpoint.wake", {
|
|
47
|
+
projectId,
|
|
48
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
return new Hono()
|
|
54
|
+
.get("/:id/checkpoints", zValidator("param", projectParam, onInvalid), (c) => {
|
|
55
|
+
const result = readCheckpointStatus(deps, c.req.valid("param").id);
|
|
56
|
+
return result.ok ? c.json(result.value) : refused(c, result.reason);
|
|
57
|
+
})
|
|
58
|
+
.post("/:id/checkpoints/:checkpointId/approve", zValidator("param", approvalParam, onInvalid), zValidator("json", approvalBody, onInvalid), async (c) => {
|
|
59
|
+
const { id: projectId, checkpointId } = c.req.valid("param");
|
|
60
|
+
const identity = c.req.valid("json");
|
|
61
|
+
return withProjectControl(deps.db, projectId, () => {
|
|
62
|
+
const replay = replayCheckpointApproval(deps, { ...identity, projectId, checkpointId });
|
|
63
|
+
if (replay)
|
|
64
|
+
return replay.ok
|
|
65
|
+
? c.json({ checkpoint: replay.value, replayed: true })
|
|
66
|
+
: refused(c, replay.reason);
|
|
67
|
+
const valid = validateCheckpointApproval(deps, { ...identity, projectId, checkpointId });
|
|
68
|
+
if (!valid.ok)
|
|
69
|
+
return refused(c, valid.reason);
|
|
70
|
+
const authority = deps.runner.checkpoints;
|
|
71
|
+
if (!authority)
|
|
72
|
+
return refused(c, "conflict");
|
|
73
|
+
const result = authority.release(projectId, checkpointId, identity);
|
|
74
|
+
if (!result.ok && (result.reason !== "duplicate" || result.value === undefined))
|
|
75
|
+
return refused(c, result.reason);
|
|
76
|
+
const checkpoint = checkpointRowSchema.parse(result.value);
|
|
77
|
+
if (result.ok)
|
|
78
|
+
notify(projectId, identity.revisionId, false);
|
|
79
|
+
return c.json({ checkpoint, replayed: !result.ok });
|
|
80
|
+
});
|
|
81
|
+
})
|
|
82
|
+
.patch("/:id/checkpoints", zValidator("param", projectParam, onInvalid), zValidator("json", checkpointChangeSchema, onInvalid), async (c) => {
|
|
83
|
+
const projectId = c.req.valid("param").id;
|
|
84
|
+
const body = c.req.valid("json");
|
|
85
|
+
return withProjectControl(deps.db, projectId, () => {
|
|
86
|
+
const result = changeCheckpoints(deps, { ...body, projectId });
|
|
87
|
+
if (!result.ok)
|
|
88
|
+
return refused(c, result.reason);
|
|
89
|
+
if (result.value.changed)
|
|
90
|
+
notify(projectId, body.revisionId, result.value.released);
|
|
91
|
+
const status = readCheckpointStatus(deps, projectId);
|
|
92
|
+
return status.ok ? c.json(status.value) : refused(c, status.reason);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { providerStatuses } from "../../slices/settings/readiness.js";
|
|
3
|
+
/**
|
|
4
|
+
* A deliberately boring, secret-free support bundle. It is JSON rather than a
|
|
5
|
+
* filesystem archive so it can be copied from a remote/local browser and is
|
|
6
|
+
* still useful when a project output is unavailable.
|
|
7
|
+
*/
|
|
8
|
+
export function diagnosticsRoutes(deps) {
|
|
9
|
+
return new Hono().get("/", async (c) => {
|
|
10
|
+
const providers = await providerStatuses({ db: deps.db, probe: deps.probe });
|
|
11
|
+
const projectCount = deps.db.prepare("SELECT count(*) AS count FROM projects").get();
|
|
12
|
+
const schema = deps.db.prepare("SELECT max(version) AS version FROM schema_migrations").get();
|
|
13
|
+
c.header("Cache-Control", "no-store");
|
|
14
|
+
c.header("Content-Disposition", 'attachment; filename="slopify-diagnostics.json"');
|
|
15
|
+
return c.json({
|
|
16
|
+
appVersion: deps.version,
|
|
17
|
+
schemaVersion: typeof schema?.version === "number" ? schema.version : null,
|
|
18
|
+
platform: process.platform,
|
|
19
|
+
nodeMajor: Number.parseInt(process.versions.node.split(".")[0] ?? "0", 10),
|
|
20
|
+
providers: providers.map((provider) => ({
|
|
21
|
+
id: provider.id,
|
|
22
|
+
family: provider.family,
|
|
23
|
+
displayName: provider.displayName,
|
|
24
|
+
readiness: provider.readiness,
|
|
25
|
+
...(provider.cliPath === undefined ? {} : { cliPath: provider.cliPath }),
|
|
26
|
+
})),
|
|
27
|
+
projects: { total: typeof projectCount?.count === "number" ? projectCount.count : 0 },
|
|
28
|
+
catalogue: deps.catalogue?.status() ?? { warning: "Catalogue unavailable." },
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createReadStream } from "node:fs";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import { Readable } from "node:stream";
|
|
4
|
+
import { zValidator } from "@hono/zod-validator";
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { draftAttachmentSchema } from "../../slices/play-drafts/schema.js";
|
|
8
|
+
import { readDraft } from "../../slices/play-drafts/service.js";
|
|
9
|
+
import { uploadDraftAttachment } from "../../slices/play-drafts/uploads.js";
|
|
10
|
+
import { stagingPath } from "../../slices/storage/layout.js";
|
|
11
|
+
import { stagedFileById } from "../../slices/storage/repo.js";
|
|
12
|
+
import { draftProblem } from "./draft-problem.js";
|
|
13
|
+
import { readMultipart } from "./multipart.js";
|
|
14
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
15
|
+
const params = z.object({ id: z.uuid(), attachmentId: z.uuid() });
|
|
16
|
+
const imageTypes = {
|
|
17
|
+
".png": "image/png",
|
|
18
|
+
".jpg": "image/jpeg",
|
|
19
|
+
".jpeg": "image/jpeg",
|
|
20
|
+
".webp": "image/webp",
|
|
21
|
+
".gif": "image/gif",
|
|
22
|
+
".avif": "image/avif",
|
|
23
|
+
};
|
|
24
|
+
// Inference preserves the route contracts consumed by the SPA client.
|
|
25
|
+
export function draftFileRoutes(service) {
|
|
26
|
+
return new Hono()
|
|
27
|
+
.put("/:id/attachments/:attachmentId/file", zValidator("param", params, onInvalid), async (c) => {
|
|
28
|
+
const { id, attachmentId } = c.req.valid("param");
|
|
29
|
+
const deps = service();
|
|
30
|
+
const result = await readMultipart(c.req.raw, (content) => uploadDraftAttachment(deps, { draftId: id, attachmentId, content }));
|
|
31
|
+
if (result === undefined)
|
|
32
|
+
return problem(c, {
|
|
33
|
+
status: 400,
|
|
34
|
+
title: titleOf(400),
|
|
35
|
+
detail: "The request carried no file part.",
|
|
36
|
+
});
|
|
37
|
+
return result.ok
|
|
38
|
+
? c.json(draftAttachmentSchema.parse(result.value))
|
|
39
|
+
: draftProblem(c, result);
|
|
40
|
+
})
|
|
41
|
+
.get("/:id/attachments/:attachmentId/file", zValidator("param", params, onInvalid), (c) => {
|
|
42
|
+
const deps = service();
|
|
43
|
+
const { id, attachmentId } = c.req.valid("param");
|
|
44
|
+
const view = readDraft(deps, id);
|
|
45
|
+
if (!view.ok)
|
|
46
|
+
return draftProblem(c, view);
|
|
47
|
+
const attachment = view.value.attachments.find((a) => a.id === attachmentId);
|
|
48
|
+
const staged = attachment?.stagedFileId === null || attachment?.stagedFileId === undefined
|
|
49
|
+
? undefined
|
|
50
|
+
: stagedFileById(deps.db, attachment.stagedFileId);
|
|
51
|
+
const contentType = attachment === undefined ? undefined : imageTypes[extname(attachment.name).toLowerCase()];
|
|
52
|
+
if (attachment?.state !== "ready" ||
|
|
53
|
+
attachment.kind === "audio" ||
|
|
54
|
+
staged?.state !== "staged" ||
|
|
55
|
+
staged.stageKind !== attachment.kind ||
|
|
56
|
+
contentType === undefined)
|
|
57
|
+
return problem(c, {
|
|
58
|
+
status: 404,
|
|
59
|
+
title: titleOf(404),
|
|
60
|
+
detail: "This draft attachment has no preview bytes.",
|
|
61
|
+
});
|
|
62
|
+
return c.body(Readable.toWeb(createReadStream(stagingPath(deps.paths, staged.path))), 200, {
|
|
63
|
+
"content-type": contentType,
|
|
64
|
+
"content-length": String(staged.bytes),
|
|
65
|
+
"x-content-type-options": "nosniff",
|
|
66
|
+
"cache-control": "no-store",
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { problem, titleOf } from "./problem.js";
|
|
2
|
+
export function draftProblem(c, result) {
|
|
3
|
+
const status = result.reason === "not-found"
|
|
4
|
+
? 404
|
|
5
|
+
: ["invalid-edit", "readiness"].includes(result.reason)
|
|
6
|
+
? 400
|
|
7
|
+
: 409;
|
|
8
|
+
const { ok: _ok, ...extensions } = result;
|
|
9
|
+
return problem(c, {
|
|
10
|
+
status,
|
|
11
|
+
title: titleOf(status),
|
|
12
|
+
detail: "Review the saved draft and the listed fields.",
|
|
13
|
+
extensions,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { draftRow } from "../../slices/play-drafts/repo.js";
|
|
6
|
+
import { reviewDraft } from "../../slices/play-drafts/review.js";
|
|
7
|
+
import { createDraftInputSchema, discardDraftInputSchema, draftViewSchema, playReviewSchema, playStartResultSchema, saveDraftInputSchema, } from "../../slices/play-drafts/schema.js";
|
|
8
|
+
import { createDraft, discardDraft, forkDraft, listDrafts, readDraft, saveDraft, } from "../../slices/play-drafts/service.js";
|
|
9
|
+
import { startPlayDraft } from "../../slices/play-drafts/start.js";
|
|
10
|
+
import { draftFileRoutes } from "./draft-files.js";
|
|
11
|
+
import { draftProblem } from "./draft-problem.js";
|
|
12
|
+
import { onInvalid } from "./problem.js";
|
|
13
|
+
const idParam = z.object({ id: z.uuid() });
|
|
14
|
+
const versionBody = discardDraftInputSchema.unwrap().omit({ id: true });
|
|
15
|
+
const saveBody = saveDraftInputSchema.unwrap().omit({ id: true });
|
|
16
|
+
const startBody = versionBody.extend({ reviewId: z.uuid() }).strict();
|
|
17
|
+
const summariesSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
drafts: z.array(z
|
|
20
|
+
.object({
|
|
21
|
+
id: z.uuid(),
|
|
22
|
+
title: z.string(),
|
|
23
|
+
version: z.number().int().positive(),
|
|
24
|
+
updatedAt: z.string(),
|
|
25
|
+
readable: z.boolean(),
|
|
26
|
+
})
|
|
27
|
+
.strict()),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
// Inference preserves the route contracts consumed by the SPA client.
|
|
31
|
+
export function draftRoutes(deps) {
|
|
32
|
+
const service = () => {
|
|
33
|
+
if (deps === undefined)
|
|
34
|
+
throw new HTTPException(404);
|
|
35
|
+
return deps;
|
|
36
|
+
};
|
|
37
|
+
return new Hono()
|
|
38
|
+
.get("/", (c) => c.json(summariesSchema.parse({ drafts: listDrafts(service()) })))
|
|
39
|
+
.post("/", zValidator("json", createDraftInputSchema, onInvalid), (c) => {
|
|
40
|
+
const input = c.req.valid("json");
|
|
41
|
+
const existing = draftRow(service().db, input.id) !== undefined;
|
|
42
|
+
const result = createDraft(service(), input);
|
|
43
|
+
return result.ok
|
|
44
|
+
? c.json(draftViewSchema.parse(result.value), existing ? 200 : 201)
|
|
45
|
+
: draftProblem(c, result);
|
|
46
|
+
})
|
|
47
|
+
.get("/:id", zValidator("param", idParam, onInvalid), (c) => {
|
|
48
|
+
const result = readDraft(service(), c.req.valid("param").id);
|
|
49
|
+
return result.ok ? c.json(draftViewSchema.parse(result.value)) : draftProblem(c, result);
|
|
50
|
+
})
|
|
51
|
+
.put("/:id", zValidator("param", idParam, onInvalid), zValidator("json", saveBody, onInvalid), (c) => {
|
|
52
|
+
const result = saveDraft(service(), {
|
|
53
|
+
...c.req.valid("json"),
|
|
54
|
+
id: c.req.valid("param").id,
|
|
55
|
+
});
|
|
56
|
+
return result.ok ? c.json(draftViewSchema.parse(result.value)) : draftProblem(c, result);
|
|
57
|
+
})
|
|
58
|
+
.post("/:id/fork", zValidator("param", idParam, onInvalid), zValidator("json", createDraftInputSchema, onInvalid), (c) => {
|
|
59
|
+
const input = { ...c.req.valid("json"), sourceId: c.req.valid("param").id };
|
|
60
|
+
const existing = draftRow(service().db, input.id) !== undefined;
|
|
61
|
+
const result = forkDraft(service(), input);
|
|
62
|
+
return result.ok
|
|
63
|
+
? c.json(draftViewSchema.parse(result.value), existing ? 200 : 201)
|
|
64
|
+
: draftProblem(c, result);
|
|
65
|
+
})
|
|
66
|
+
.delete("/:id", zValidator("param", idParam, onInvalid), zValidator("json", versionBody, onInvalid), (c) => {
|
|
67
|
+
const result = discardDraft(service(), {
|
|
68
|
+
...c.req.valid("json"),
|
|
69
|
+
id: c.req.valid("param").id,
|
|
70
|
+
});
|
|
71
|
+
return result.ok
|
|
72
|
+
? c.json(z
|
|
73
|
+
.object({ discarded: z.literal(true) })
|
|
74
|
+
.strict()
|
|
75
|
+
.parse(result.value))
|
|
76
|
+
: draftProblem(c, result);
|
|
77
|
+
})
|
|
78
|
+
.post("/:id/review", zValidator("param", idParam, onInvalid), zValidator("json", versionBody, onInvalid), async (c) => {
|
|
79
|
+
const result = await reviewDraft(service(), {
|
|
80
|
+
...c.req.valid("json"),
|
|
81
|
+
id: c.req.valid("param").id,
|
|
82
|
+
});
|
|
83
|
+
return result.ok ? c.json(playReviewSchema.parse(result.value)) : draftProblem(c, result);
|
|
84
|
+
})
|
|
85
|
+
.post("/:id/start", zValidator("param", idParam, onInvalid), zValidator("json", startBody, onInvalid), async (c) => {
|
|
86
|
+
const result = await startPlayDraft(service(), {
|
|
87
|
+
...c.req.valid("json"),
|
|
88
|
+
draftId: c.req.valid("param").id,
|
|
89
|
+
});
|
|
90
|
+
return result.ok
|
|
91
|
+
? c.json(playStartResultSchema.parse(result.value), result.value.replayed ? 200 : 201)
|
|
92
|
+
: draftProblem(c, result);
|
|
93
|
+
})
|
|
94
|
+
.route("/", draftFileRoutes(service));
|
|
95
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { Busboy } from "@fastify/busboy";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
// The consumer cannot commit an upload until the entire multipart envelope is valid.
|
|
5
|
+
export async function readMultipart(request, consume) {
|
|
6
|
+
const contentType = request.headers.get("content-type");
|
|
7
|
+
if (contentType === null || !contentType.startsWith("multipart/form-data"))
|
|
8
|
+
throw new HTTPException(415, {
|
|
9
|
+
message: "An upload is sent as multipart/form-data with the file in one part.",
|
|
10
|
+
});
|
|
11
|
+
if (request.body === null)
|
|
12
|
+
throw new HTTPException(400, { message: "The request carried no body." });
|
|
13
|
+
let parser;
|
|
14
|
+
try {
|
|
15
|
+
parser = new Busboy({
|
|
16
|
+
headers: { "content-type": contentType },
|
|
17
|
+
limits: { files: 1, fields: 8 },
|
|
18
|
+
preservePath: true,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
throw new HTTPException(400, {
|
|
23
|
+
message: "The multipart boundary is missing or invalid.",
|
|
24
|
+
cause: error,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
const source = Readable.fromWeb(request.body);
|
|
28
|
+
let file;
|
|
29
|
+
let operation;
|
|
30
|
+
let complete = () => undefined;
|
|
31
|
+
let rejectEnvelope = () => undefined;
|
|
32
|
+
const completed = new Promise((resolve, reject) => {
|
|
33
|
+
complete = resolve;
|
|
34
|
+
rejectEnvelope = reject;
|
|
35
|
+
});
|
|
36
|
+
// Observe immediately: the consumer may reject before it reaches the envelope check.
|
|
37
|
+
const envelope = completed.then(() => ({ ok: true }), (error) => ({ ok: false, error }));
|
|
38
|
+
const fail = (error) => {
|
|
39
|
+
rejectEnvelope(error);
|
|
40
|
+
file?.destroy(error);
|
|
41
|
+
source.destroy();
|
|
42
|
+
parser.destroy();
|
|
43
|
+
};
|
|
44
|
+
source.on("error", fail);
|
|
45
|
+
parser.on("error", (error) => fail(new HTTPException(400, {
|
|
46
|
+
message: "The multipart request is malformed or incomplete.",
|
|
47
|
+
cause: error,
|
|
48
|
+
})));
|
|
49
|
+
parser.on("finish", () => complete());
|
|
50
|
+
parser.on("file", (_field, stream, filename) => {
|
|
51
|
+
file = stream;
|
|
52
|
+
stream.on("error", fail);
|
|
53
|
+
async function* content() {
|
|
54
|
+
for await (const chunk of stream)
|
|
55
|
+
yield chunk;
|
|
56
|
+
const result = await envelope;
|
|
57
|
+
if (!result.ok)
|
|
58
|
+
throw result.error;
|
|
59
|
+
}
|
|
60
|
+
operation = Promise.resolve().then(() => consume(content(), typeof filename === "string" ? filename : ""));
|
|
61
|
+
void operation.then(() => stream.resume(), (error) => {
|
|
62
|
+
fail(error instanceof Error ? error : new Error("Upload failed", { cause: error }));
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
const abort = () => fail(new Error("Upload request was aborted."));
|
|
66
|
+
request.signal.addEventListener("abort", abort, { once: true });
|
|
67
|
+
if (request.signal.aborted)
|
|
68
|
+
abort();
|
|
69
|
+
else
|
|
70
|
+
source.pipe(parser);
|
|
71
|
+
try {
|
|
72
|
+
const parsed = await envelope;
|
|
73
|
+
if (operation !== undefined) {
|
|
74
|
+
const result = await operation;
|
|
75
|
+
if (!parsed.ok)
|
|
76
|
+
throw parsed.error;
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
if (!parsed.ok)
|
|
80
|
+
throw parsed.error;
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
finally {
|
|
84
|
+
request.signal.removeEventListener("abort", abort);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Tracks admitted HTTP writes so shutdown can stop admission, then drain them. */
|
|
2
|
+
export function createMutationLifecycle() {
|
|
3
|
+
let accepting = true;
|
|
4
|
+
let active = 0;
|
|
5
|
+
let drained;
|
|
6
|
+
let resolveDrain;
|
|
7
|
+
return {
|
|
8
|
+
begin: () => {
|
|
9
|
+
if (!accepting)
|
|
10
|
+
return undefined;
|
|
11
|
+
active++;
|
|
12
|
+
let released = false;
|
|
13
|
+
return () => {
|
|
14
|
+
if (released)
|
|
15
|
+
return;
|
|
16
|
+
released = true;
|
|
17
|
+
active--;
|
|
18
|
+
if (!accepting && active === 0)
|
|
19
|
+
resolveDrain?.();
|
|
20
|
+
};
|
|
21
|
+
},
|
|
22
|
+
stop: () => {
|
|
23
|
+
accepting = false;
|
|
24
|
+
if (active === 0)
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
drained ??= new Promise((resolve) => {
|
|
27
|
+
resolveDrain = resolve;
|
|
28
|
+
});
|
|
29
|
+
return drained;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** Give admitted requests a grace period, then close their sockets and wait for their
|
|
34
|
+
* middleware `finally` blocks to release the database lease. */
|
|
35
|
+
export async function drainMutationsWithDeadline(drained, terminateConnections, timeoutMs) {
|
|
36
|
+
let timer;
|
|
37
|
+
const outcome = await Promise.race([
|
|
38
|
+
drained.then(() => "drained"),
|
|
39
|
+
new Promise((resolve) => {
|
|
40
|
+
timer = setTimeout(() => resolve("expired"), timeoutMs);
|
|
41
|
+
}),
|
|
42
|
+
]);
|
|
43
|
+
if (timer !== undefined)
|
|
44
|
+
clearTimeout(timer);
|
|
45
|
+
if (outcome === "drained")
|
|
46
|
+
return;
|
|
47
|
+
terminateConnections();
|
|
48
|
+
await drained;
|
|
49
|
+
}
|
|
@@ -48,7 +48,12 @@ export function planningRoutes(deps) {
|
|
|
48
48
|
...f,
|
|
49
49
|
message: `${drafts.length > 1 ? `Video ${index + 1}: ` : ""}${f.message}`,
|
|
50
50
|
}));
|
|
51
|
-
return {
|
|
51
|
+
return {
|
|
52
|
+
draft: accepted,
|
|
53
|
+
rendered: renderPicked(picked, accepted.values),
|
|
54
|
+
templates: Object.fromEntries(picked.bodies.map(({ key, body }) => [key, body])),
|
|
55
|
+
fields,
|
|
56
|
+
};
|
|
52
57
|
});
|
|
53
58
|
};
|
|
54
59
|
return new Hono()
|
|
@@ -70,6 +75,12 @@ export function planningRoutes(deps) {
|
|
|
70
75
|
})
|
|
71
76
|
.post("/batch", zValidator("json", batchBody, onInvalid), async (c) => {
|
|
72
77
|
const input = c.req.valid("json");
|
|
78
|
+
if (input.draft.checkpoints?.length)
|
|
79
|
+
return problem(c, {
|
|
80
|
+
status: 409,
|
|
81
|
+
title: titleOf(409),
|
|
82
|
+
detail: "Queue checkpoint-enabled projects through Play Review.",
|
|
83
|
+
});
|
|
73
84
|
if (batchExists(deps.db, input.requestId))
|
|
74
85
|
return c.json({ queue: queueEntries(deps.db, input.requestId) });
|
|
75
86
|
const subtitles = input.draft.subtitles;
|