@gentbajko/slopify 0.8.4 → 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/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/catalog/registry.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { providerError } from "../kernel/ports/model.js";
|
|
2
|
-
import { splitText } from "../kernel/ports/text.js";
|
|
3
2
|
export function curateRegistry(registry, catalogue) {
|
|
4
3
|
const requireModel = (provider, family, id) => {
|
|
5
4
|
const models = catalogue.models(provider, family);
|
|
@@ -40,7 +39,10 @@ export function curateRegistry(registry, catalogue) {
|
|
|
40
39
|
kind: "unsupported",
|
|
41
40
|
message: "This model does not support the selected thinking setting. Choose an available setting.",
|
|
42
41
|
});
|
|
43
|
-
yield* port.complete({
|
|
42
|
+
yield* port.complete({
|
|
43
|
+
...request,
|
|
44
|
+
...(request.thinkingConfig === undefined && thinkingConfig ? { thinkingConfig } : {}),
|
|
45
|
+
});
|
|
44
46
|
},
|
|
45
47
|
};
|
|
46
48
|
},
|
|
@@ -66,82 +68,20 @@ export function curateRegistry(registry, catalogue) {
|
|
|
66
68
|
...port,
|
|
67
69
|
models: async () => catalogue.models(id, "tts"),
|
|
68
70
|
synthesize: async (request) => {
|
|
71
|
+
// Retrieval resumes an already accepted job even if new submissions are disabled.
|
|
72
|
+
if (request.continuation?.read() !== undefined)
|
|
73
|
+
return port.synthesize(request);
|
|
69
74
|
const model = requireModel(id, "tts", request.model);
|
|
70
75
|
if (!("tts" in model))
|
|
71
76
|
throw new Error("Invalid TTS catalogue entry");
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
return {
|
|
79
|
-
container: "mp3",
|
|
80
|
-
audio: new ReadableStream({
|
|
81
|
-
async pull(controller) {
|
|
82
|
-
try {
|
|
83
|
-
const next = await iterator.next();
|
|
84
|
-
if (next.done)
|
|
85
|
-
controller.close();
|
|
86
|
-
else
|
|
87
|
-
controller.enqueue(next.value);
|
|
88
|
-
}
|
|
89
|
-
catch (error) {
|
|
90
|
-
controller.error(error);
|
|
91
|
-
}
|
|
92
|
-
},
|
|
93
|
-
async cancel() {
|
|
94
|
-
abort.abort();
|
|
95
|
-
await iterator.return();
|
|
96
|
-
},
|
|
97
|
-
}, { highWaterMark: 0 }),
|
|
98
|
-
};
|
|
77
|
+
if (request.text.length > model.tts.maxCharacters)
|
|
78
|
+
throw providerError({
|
|
79
|
+
kind: "unsupported",
|
|
80
|
+
message: `${id}: this physical narration request exceeds the current ${model.tts.maxCharacters}-character limit. Rebuild narration to split it using the new limit.`,
|
|
81
|
+
});
|
|
82
|
+
return port.synthesize({ ...request, model: model.id });
|
|
99
83
|
},
|
|
100
84
|
};
|
|
101
85
|
},
|
|
102
86
|
};
|
|
103
87
|
}
|
|
104
|
-
async function* audioParts(parts, request, synthesize) {
|
|
105
|
-
let tokens = {};
|
|
106
|
-
const saved = request.continuation?.read();
|
|
107
|
-
if (saved) {
|
|
108
|
-
try {
|
|
109
|
-
const parsed = JSON.parse(saved);
|
|
110
|
-
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
111
|
-
tokens = Object.fromEntries(Object.entries(parsed).filter((entry) => typeof entry[1] === "string"));
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
/* A legacy single-job token cannot identify several split jobs. */
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
for (const [index, text] of parts.entries()) {
|
|
118
|
-
request.signal.throwIfAborted();
|
|
119
|
-
// Body chunks are persisted individually by narration. Long intro/outro text
|
|
120
|
-
// is bounded here as well; each split has its own remote continuation.
|
|
121
|
-
const response = await synthesize({
|
|
122
|
-
...request,
|
|
123
|
-
text,
|
|
124
|
-
continuation: {
|
|
125
|
-
read: () => tokens[String(index)],
|
|
126
|
-
write: (value) => {
|
|
127
|
-
tokens[String(index)] = value;
|
|
128
|
-
request.continuation?.write(JSON.stringify(tokens));
|
|
129
|
-
},
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
const reader = response.audio.getReader();
|
|
133
|
-
try {
|
|
134
|
-
for (;;) {
|
|
135
|
-
const next = await reader.read();
|
|
136
|
-
request.signal.throwIfAborted();
|
|
137
|
-
if (next.done)
|
|
138
|
-
break;
|
|
139
|
-
yield next.value;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
finally {
|
|
143
|
-
await reader.cancel().catch(() => { });
|
|
144
|
-
reader.releaseLock();
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
package/dist/edge/cli.js
CHANGED
|
@@ -5,6 +5,7 @@ import { readVersion } from "../kernel/version.js";
|
|
|
5
5
|
import { boot } from "../main.js";
|
|
6
6
|
import { forwardManagedUpdate } from "../updater/forward.js";
|
|
7
7
|
import { openBrowser } from "./open-browser.js";
|
|
8
|
+
import { installSignalShutdown } from "./signal-shutdown.js";
|
|
8
9
|
const { values } = parseArgs({
|
|
9
10
|
options: {
|
|
10
11
|
port: { type: "string" },
|
|
@@ -31,8 +32,10 @@ try {
|
|
|
31
32
|
console.warn(message);
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
installSignalShutdown({
|
|
36
|
+
on: (signal, listener) => process.on(signal, listener),
|
|
37
|
+
stop,
|
|
38
|
+
exit: (code) => process.exit(code),
|
|
36
39
|
});
|
|
37
40
|
}
|
|
38
41
|
catch (error) {
|
package/dist/edge/events/hub.js
CHANGED
|
@@ -52,7 +52,8 @@ export function createHub(deps) {
|
|
|
52
52
|
});
|
|
53
53
|
if (set.has(subscriber))
|
|
54
54
|
for (const event of previews.snapshot(projectId))
|
|
55
|
-
|
|
55
|
+
if (deps.acceptEvent?.(event) !== false)
|
|
56
|
+
send(subscriber, event);
|
|
56
57
|
return subscriber.done;
|
|
57
58
|
},
|
|
58
59
|
subscribeGlobal: (stream, signal) => {
|
|
@@ -63,7 +64,11 @@ export function createHub(deps) {
|
|
|
63
64
|
return subscriber.done;
|
|
64
65
|
},
|
|
65
66
|
emit: (projectId, event) => {
|
|
66
|
-
|
|
67
|
+
const accepted = deps.acceptEvent?.(event) !== false;
|
|
68
|
+
if (accepted || event.type === "stage.state")
|
|
69
|
+
previews.observe(event);
|
|
70
|
+
if (!accepted)
|
|
71
|
+
return;
|
|
67
72
|
for (const subscriber of projects.get(projectId) ?? []) {
|
|
68
73
|
send(subscriber, event);
|
|
69
74
|
}
|
|
@@ -8,7 +8,9 @@ export function createPreviewCache() {
|
|
|
8
8
|
const calls = projects.get(event.projectId);
|
|
9
9
|
if (calls) {
|
|
10
10
|
for (const [id, call] of calls)
|
|
11
|
-
if (call.stage === event.stage
|
|
11
|
+
if (call.stage === event.stage &&
|
|
12
|
+
call.revisionId === event.revisionId &&
|
|
13
|
+
call.workId === event.workId)
|
|
12
14
|
calls.delete(id);
|
|
13
15
|
if (calls.size === 0)
|
|
14
16
|
projects.delete(event.projectId);
|
|
@@ -18,10 +20,11 @@ export function createPreviewCache() {
|
|
|
18
20
|
if (event.type !== "llm.preview")
|
|
19
21
|
return;
|
|
20
22
|
const calls = projects.get(event.projectId) ?? new Map();
|
|
21
|
-
const
|
|
23
|
+
const identity = JSON.stringify([event.revisionId, event.workId, event.callId]);
|
|
24
|
+
const prior = calls.get(identity);
|
|
22
25
|
const text = (event.reset ? event.text : `${prior?.text ?? ""}${event.text}`).slice(-64 * 1024);
|
|
23
|
-
calls.delete(
|
|
24
|
-
calls.set(
|
|
26
|
+
calls.delete(identity);
|
|
27
|
+
calls.set(identity, { ...event, text, reset: true });
|
|
25
28
|
while (calls.size > 8) {
|
|
26
29
|
const id = calls.keys().next().value;
|
|
27
30
|
if (id !== undefined)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { currentRevisionId } from "../../slices/revisions/repo.js";
|
|
2
|
+
export function currentWorkOrigin(db, projectId, origin) {
|
|
3
|
+
if (currentRevisionId(db, projectId) === undefined)
|
|
4
|
+
return origin.revisionId === undefined && origin.workId === undefined;
|
|
5
|
+
if (origin.revisionId === undefined || origin.workId === undefined)
|
|
6
|
+
return false;
|
|
7
|
+
return (db
|
|
8
|
+
.prepare(`
|
|
9
|
+
SELECT 1 FROM revision_work w
|
|
10
|
+
JOIN revision_work_reservations r ON r.work_id=w.id AND r.project_id=w.project_id
|
|
11
|
+
JOIN project_heads h ON h.project_id=r.project_id AND h.revision_id=r.revision_id
|
|
12
|
+
JOIN project_revisions v ON v.id=r.revision_id
|
|
13
|
+
JOIN json_each(v.fingerprints) f ON f.key=COALESCE(r.logical_key,r.work_key)
|
|
14
|
+
AND f.value=COALESCE(r.desired_fingerprint,r.fingerprint)
|
|
15
|
+
LEFT JOIN revision_work_pieces p ON p.id=r.piece_id AND p.work_id=w.id
|
|
16
|
+
WHERE w.project_id=? AND w.revision_id=? AND w.id=?
|
|
17
|
+
AND (? IS NULL OR r.piece_id=?)
|
|
18
|
+
AND (r.piece_id IS NULL OR (p.work_key=r.work_key AND p.fingerprint=r.fingerprint))
|
|
19
|
+
`)
|
|
20
|
+
.get(projectId, origin.revisionId, origin.workId, origin.workPieceId ?? null, origin.workPieceId ?? null) !== undefined);
|
|
21
|
+
}
|
|
22
|
+
export function currentProjectEvent(db, event) {
|
|
23
|
+
if (event.type === "project.updated")
|
|
24
|
+
return true;
|
|
25
|
+
if (event.type === "project.state" &&
|
|
26
|
+
event.revisionId === undefined &&
|
|
27
|
+
event.workId === undefined)
|
|
28
|
+
return true;
|
|
29
|
+
const head = currentRevisionId(db, event.projectId);
|
|
30
|
+
if (head !== event.revisionId)
|
|
31
|
+
return false;
|
|
32
|
+
return currentWorkOrigin(db, event.projectId, event);
|
|
33
|
+
}
|
|
@@ -5,17 +5,15 @@ import { stageKinds } from "../../kernel/pipeline.js";
|
|
|
5
5
|
import { derive } from "../../kernel/runner/graph.js";
|
|
6
6
|
import { projectById, projectPaused, stagesOf } from "../../slices/admission/repo.js";
|
|
7
7
|
import { cancelProject } from "../../slices/cancel/index.js";
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { settleReleasedCheckpoints } from "../../slices/checkpoints/recovery.js";
|
|
9
|
+
import { pauseProject } from "../../slices/control/index.js";
|
|
10
|
+
import { revisionControlSchema, } from "../../slices/control/revision-control-schema.js";
|
|
11
|
+
import { adoptBaseline } from "../../slices/revisions/adopt.js";
|
|
12
|
+
import { currentRevisionId } from "../../slices/revisions/repo.js";
|
|
12
13
|
import { allowsCustomModel } from "../../slices/settings/models.js";
|
|
13
14
|
import { providerStatuses } from "../../slices/settings/readiness.js";
|
|
14
15
|
import { outputsOf } from "../../slices/storage/repo.js";
|
|
15
16
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
16
|
-
// The actions on the project header and on each stage: Cancel, Retry, Re-run, Save &
|
|
17
|
-
// re-run, Regenerate one image, Delete one image. Every one changes rows and files through
|
|
18
|
-
// a slice and then ticks the runner; the route itself decides nothing.
|
|
19
17
|
const idParam = z.object({
|
|
20
18
|
id: z
|
|
21
19
|
.string()
|
|
@@ -31,35 +29,11 @@ const imageParam = idParam.extend({
|
|
|
31
29
|
.max(64)
|
|
32
30
|
.regex(/^[0-9A-Za-z_-]+$/),
|
|
33
31
|
});
|
|
34
|
-
// ceiling: half a megabyte of markdown, which is far past any narratable article and
|
|
35
|
-
// keeps a paste from filling the process's memory. A longer one needs a streamed edit.
|
|
36
|
-
const articleBody = z.object({ markdown: z.string().min(1).max(500_000) });
|
|
37
|
-
const status = {
|
|
38
|
-
"no-project": 404,
|
|
39
|
-
"unknown-image": 404,
|
|
40
|
-
"empty-article": 400,
|
|
41
|
-
running: 409,
|
|
42
|
-
"not-rerunnable": 409,
|
|
43
|
-
"not-retryable": 409,
|
|
44
|
-
"no-article": 409,
|
|
45
|
-
"last-image": 409,
|
|
46
|
-
};
|
|
47
|
-
const details = {
|
|
48
|
-
"no-project": "No project has that id.",
|
|
49
|
-
"unknown-image": "This project has no image with that id.",
|
|
50
|
-
"empty-article": "An article cannot be saved empty.",
|
|
51
|
-
// The precondition on every re-run: no stage of the project is `running`.
|
|
52
|
-
running: "This project is still running. Cancel it or wait for it to finish.",
|
|
53
|
-
"not-rerunnable": "Only a stage that has finished, failed, or been canceled can be re-run.",
|
|
54
|
-
"not-retryable": "Only a failed or canceled stage can be retried.",
|
|
55
|
-
"no-article": "This project has no article to edit yet.",
|
|
56
|
-
// At least one image always remains.
|
|
57
|
-
"last-image": "At least one image must remain, so the last one cannot be deleted.",
|
|
58
|
-
};
|
|
59
32
|
// The return type is inferred so Hono keeps the route types the SPA's client is
|
|
60
33
|
// generated from; see stagingRoutes.
|
|
61
34
|
export function actionRoutes(deps) {
|
|
62
35
|
const reruns = {
|
|
36
|
+
measureAudio: deps.measureAudio,
|
|
63
37
|
db: deps.db,
|
|
64
38
|
paths: deps.paths,
|
|
65
39
|
ids: deps.ids,
|
|
@@ -71,11 +45,14 @@ export function actionRoutes(deps) {
|
|
|
71
45
|
clock: deps.clock,
|
|
72
46
|
log: deps.log,
|
|
73
47
|
abort: (projectId) => deps.runner.abortProject(projectId),
|
|
48
|
+
hasInflight: deps.runner.hasInflight,
|
|
49
|
+
settleCheckpoints: (projectId) => settleReleasedCheckpoints(deps, projectId),
|
|
74
50
|
emit: (projectId, event) => {
|
|
75
51
|
deps.hub.emit(projectId, event);
|
|
76
52
|
},
|
|
77
53
|
};
|
|
78
54
|
const control = {
|
|
55
|
+
catalogue: deps.catalogue,
|
|
79
56
|
...reruns,
|
|
80
57
|
runner: deps.runner,
|
|
81
58
|
emit: (projectId, event) => deps.hub.emit(projectId, event),
|
|
@@ -86,6 +63,7 @@ export function actionRoutes(deps) {
|
|
|
86
63
|
const view = (projectId) => {
|
|
87
64
|
const stages = stagesOf(deps.db, projectId);
|
|
88
65
|
return {
|
|
66
|
+
revisionId: currentRevisionId(deps.db, projectId) ?? null,
|
|
89
67
|
project: {
|
|
90
68
|
...projectById(deps.db, projectId),
|
|
91
69
|
status: derive(stages, projectPaused(deps.db, projectId)),
|
|
@@ -94,21 +72,6 @@ export function actionRoutes(deps) {
|
|
|
94
72
|
outputs: outputsOf(deps.db, projectId),
|
|
95
73
|
};
|
|
96
74
|
};
|
|
97
|
-
// Every re-run action ends the same way: the rows are written, then the runner is asked
|
|
98
|
-
// to look at the project. The cascade does the rest by itself.
|
|
99
|
-
const started = (c, projectId, result) => {
|
|
100
|
-
if (!result.ok) {
|
|
101
|
-
return problem(c, {
|
|
102
|
-
status: status[result.reason],
|
|
103
|
-
title: titleOf(status[result.reason]),
|
|
104
|
-
detail: details[result.reason],
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
deps.hub.emit(projectId, { type: "project.updated", projectId });
|
|
108
|
-
if (!projectPaused(deps.db, projectId))
|
|
109
|
-
deps.runner.tick(projectId);
|
|
110
|
-
return c.json({ ...view(projectId), redone: result.redone });
|
|
111
|
-
};
|
|
112
75
|
const controlled = (c, id, result) => {
|
|
113
76
|
if (result.ok)
|
|
114
77
|
return c.json(view(id));
|
|
@@ -118,6 +81,10 @@ export function actionRoutes(deps) {
|
|
|
118
81
|
"not-editable": 409,
|
|
119
82
|
"invalid-providers": 400,
|
|
120
83
|
"catalog-unavailable": 503,
|
|
84
|
+
"revision-required": 409,
|
|
85
|
+
conflict: 409,
|
|
86
|
+
"idempotency-conflict": 409,
|
|
87
|
+
"rebuild-required": 409,
|
|
121
88
|
};
|
|
122
89
|
const messages = {
|
|
123
90
|
"no-project": "No project has that id.",
|
|
@@ -125,56 +92,84 @@ export function actionRoutes(deps) {
|
|
|
125
92
|
"not-editable": "Providers can be changed only while a project is paused or failed.",
|
|
126
93
|
"invalid-providers": "The provider choices need attention.",
|
|
127
94
|
"catalog-unavailable": "The provider model catalog could not be loaded. Try again.",
|
|
95
|
+
"revision-required": "Reload the project before using this control. A current revision and request ID are required.",
|
|
96
|
+
conflict: "The project changed. Reload it before using this control.",
|
|
97
|
+
"idempotency-conflict": "This request ID has already been used for a different action.",
|
|
98
|
+
"rebuild-required": "Review the affected outputs and cost before rebuilding this revision.",
|
|
128
99
|
};
|
|
129
100
|
return problem(c, {
|
|
130
101
|
status: codes[result.reason],
|
|
131
102
|
title: titleOf(codes[result.reason]),
|
|
132
103
|
detail: messages[result.reason],
|
|
133
|
-
|
|
104
|
+
extensions: {
|
|
105
|
+
reason: result.reason,
|
|
106
|
+
currentRevisionId: currentRevisionId(deps.db, id) ?? null,
|
|
107
|
+
fields: result.fields ?? [],
|
|
108
|
+
},
|
|
134
109
|
});
|
|
135
110
|
};
|
|
111
|
+
const controlInput = async (c, id) => {
|
|
112
|
+
if (deps.catalogue !== undefined)
|
|
113
|
+
adoptBaseline(deps, id);
|
|
114
|
+
if (currentRevisionId(deps.db, id) === undefined)
|
|
115
|
+
return undefined;
|
|
116
|
+
let body;
|
|
117
|
+
try {
|
|
118
|
+
body = await c.req.json();
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
if (!(error instanceof SyntaxError))
|
|
122
|
+
throw error;
|
|
123
|
+
return controlled(c, id, { ok: false, reason: "revision-required" });
|
|
124
|
+
}
|
|
125
|
+
const parsed = revisionControlSchema.safeParse(body);
|
|
126
|
+
return parsed.success
|
|
127
|
+
? parsed.data
|
|
128
|
+
: controlled(c, id, { ok: false, reason: "revision-required" });
|
|
129
|
+
};
|
|
136
130
|
return new Hono()
|
|
137
131
|
.post("/:id/pause", zValidator("param", idParam, onInvalid), async (c) => {
|
|
138
132
|
const { id } = c.req.valid("param");
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
return controlled(c, id, await resumeProject(control, id));
|
|
133
|
+
const input = await controlInput(c, id);
|
|
134
|
+
return input instanceof Response
|
|
135
|
+
? input
|
|
136
|
+
: controlled(c, id, await pauseProject(control, id, input));
|
|
144
137
|
})
|
|
145
|
-
.
|
|
138
|
+
.post("/:id/resume", zValidator("param", idParam, onInvalid), (c) => {
|
|
146
139
|
const { id } = c.req.valid("param");
|
|
147
|
-
return controlled(c, id,
|
|
140
|
+
return controlled(c, id, {
|
|
141
|
+
ok: false,
|
|
142
|
+
reason: projectById(deps.db, id) === undefined ? "no-project" : "rebuild-required",
|
|
143
|
+
});
|
|
148
144
|
})
|
|
145
|
+
.patch("/:id/providers", zValidator("param", idParam, onInvalid), revisionRequired)
|
|
149
146
|
.post("/:id/cancel", zValidator("param", idParam, onInvalid), async (c) => {
|
|
150
147
|
const { id } = c.req.valid("param");
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
});
|
|
148
|
+
const input = await controlInput(c, id);
|
|
149
|
+
if (input instanceof Response)
|
|
150
|
+
return input;
|
|
151
|
+
const result = await cancelProject(cancel, id, input);
|
|
152
|
+
if (!result.ok)
|
|
153
|
+
return controlled(c, id, result);
|
|
154
|
+
return c.json({ ...view(id), canceled: result.canceled });
|
|
159
155
|
})
|
|
160
156
|
.post("/:id/stages/:kind/retry", zValidator("param", stageParam, onInvalid), (c) => {
|
|
161
|
-
const { id, kind } = c.req.valid("param");
|
|
162
|
-
return withProjectControl(deps.db, id, () => started(c, id, retryStage(reruns, id, kind)));
|
|
163
|
-
})
|
|
164
|
-
.post("/:id/stages/:kind/rerun", zValidator("param", stageParam, onInvalid), (c) => {
|
|
165
|
-
const { id, kind } = c.req.valid("param");
|
|
166
|
-
return withProjectControl(deps.db, id, () => started(c, id, rerunStage(reruns, id, kind)));
|
|
167
|
-
})
|
|
168
|
-
.put("/:id/article", zValidator("param", idParam, onInvalid), zValidator("json", articleBody, onInvalid), (c) => {
|
|
169
157
|
const { id } = c.req.valid("param");
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
return withProjectControl(deps.db, id, () => started(c, id, deleteImage(reruns, id, outputId)));
|
|
158
|
+
return controlled(c, id, {
|
|
159
|
+
ok: false,
|
|
160
|
+
reason: projectById(deps.db, id) === undefined ? "no-project" : "rebuild-required",
|
|
161
|
+
});
|
|
175
162
|
})
|
|
176
|
-
.post("/:id/
|
|
177
|
-
|
|
178
|
-
|
|
163
|
+
.post("/:id/stages/:kind/rerun", zValidator("param", stageParam, onInvalid), revisionRequired)
|
|
164
|
+
.put("/:id/article", zValidator("param", idParam, onInvalid), revisionRequired)
|
|
165
|
+
.delete("/:id/images/:outputId", zValidator("param", imageParam, onInvalid), revisionRequired)
|
|
166
|
+
.post("/:id/images/:outputId/regenerate", zValidator("param", imageParam, onInvalid), revisionRequired);
|
|
167
|
+
}
|
|
168
|
+
function revisionRequired(c) {
|
|
169
|
+
return problem(c, {
|
|
170
|
+
status: 409,
|
|
171
|
+
title: titleOf(409),
|
|
172
|
+
detail: "Open Edit project to save changes, then review the affected rebuild.",
|
|
173
|
+
extensions: { reason: "revision-required" },
|
|
179
174
|
});
|
|
180
175
|
}
|
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);
|