@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
|
@@ -1,12 +1,20 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { transact } from "../../kernel/db/tx.js";
|
|
2
3
|
import { derive, satisfied } from "../../kernel/runner/graph.js";
|
|
3
4
|
import { finishStage, projectById, resetStage, setProjectPaused, stagesOf, updateProjectConfig, } from "../admission/repo.js";
|
|
5
|
+
import { listCheckpoints } from "../checkpoints/repo.js";
|
|
4
6
|
import { sameChunking } from "../narration/chunk.js";
|
|
7
|
+
import { projectStandings } from "../rebuild/runtime-store.js";
|
|
5
8
|
import { clearUnfinishedAudio } from "../reruns/index.js";
|
|
9
|
+
import { adoptBaseline } from "../revisions/adopt.js";
|
|
10
|
+
import { saveRevision } from "../revisions/mutations.js";
|
|
11
|
+
import { currentRevisionId } from "../revisions/repo.js";
|
|
12
|
+
import { getRevisionView } from "../revisions/view.js";
|
|
6
13
|
import { providers as providerCatalog } from "../settings/model.js";
|
|
7
14
|
import { hasKey, listVoices } from "../settings/repo.js";
|
|
8
15
|
import { withProjectControl } from "./lock.js";
|
|
9
16
|
import { validateLocalProviderChanges, validateProviderChanges } from "./providers.js";
|
|
17
|
+
import { checkRevisionControl, rememberRevisionControl, } from "./revision-control.js";
|
|
10
18
|
export const providerCheckTimeoutMs = 15_000;
|
|
11
19
|
async function providerFields(deps, changes) {
|
|
12
20
|
let timeout;
|
|
@@ -38,33 +46,63 @@ function changed(deps, id, wake = false) {
|
|
|
38
46
|
state: derive(stagesOf(deps.db, id), projectById(deps.db, id)?.paused),
|
|
39
47
|
});
|
|
40
48
|
}
|
|
41
|
-
export function pauseProject(deps, id) {
|
|
49
|
+
export function pauseProject(deps, id, input) {
|
|
42
50
|
return withProjectControl(deps.db, id, async () => {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await deps.runner.abortProject(id, "pause");
|
|
55
|
-
// Covers a failed final row write and runners that report interruption as canceled.
|
|
56
|
-
for (const stage of stagesOf(deps.db, id)) {
|
|
57
|
-
if (running.includes(stage.id) && (stage.state === "running" || stage.state === "canceled")) {
|
|
58
|
-
finishStage(deps.db, stage.id, "pending", null, deps.clock.now().toISOString());
|
|
59
|
-
deps.emit(id, { type: "stage.state", projectId: id, stage: stage.kind, state: "pending" });
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
changed(deps, id);
|
|
63
|
-
return { ok: true };
|
|
51
|
+
if (deps.catalogue !== undefined)
|
|
52
|
+
adoptBaseline(deps, id);
|
|
53
|
+
const checked = checkRevisionControl(deps, id, "pause", input, z.object({ ok: z.literal(true) }));
|
|
54
|
+
if (!checked.ok)
|
|
55
|
+
return checked;
|
|
56
|
+
if (checked.response !== undefined)
|
|
57
|
+
return checked.response;
|
|
58
|
+
const result = await pause(deps, id);
|
|
59
|
+
if (result.ok)
|
|
60
|
+
rememberRevisionControl(deps, checked.identity, result);
|
|
61
|
+
return result;
|
|
64
62
|
});
|
|
65
63
|
}
|
|
64
|
+
async function pause(deps, id) {
|
|
65
|
+
const project = projectById(deps.db, id);
|
|
66
|
+
if (project === undefined)
|
|
67
|
+
return { ok: false, reason: "no-project" };
|
|
68
|
+
const before = stagesOf(deps.db, id);
|
|
69
|
+
if (before.every((stage) => satisfied(stage.state)))
|
|
70
|
+
return { ok: true };
|
|
71
|
+
const running = before.filter((stage) => stage.state === "running").map((stage) => stage.id);
|
|
72
|
+
if (project.paused === true && running.length === 0 && !deps.runner.hasInflight?.(id))
|
|
73
|
+
return { ok: true };
|
|
74
|
+
transact(deps.db, () => setProjectPaused(deps.db, id, true, deps.clock.now().toISOString()));
|
|
75
|
+
deps.emit(id, { type: "project.updated", projectId: id });
|
|
76
|
+
await deps.runner.abortProject(id, "pause");
|
|
77
|
+
// Covers a failed final row write and runners that report interruption as canceled.
|
|
78
|
+
for (const stage of stagesOf(deps.db, id)) {
|
|
79
|
+
if (running.includes(stage.id) && (stage.state === "running" || stage.state === "canceled")) {
|
|
80
|
+
finishStage(deps.db, stage.id, "pending", null, deps.clock.now().toISOString());
|
|
81
|
+
deps.emit(id, { type: "stage.state", projectId: id, stage: stage.kind, state: "pending" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
changed(deps, id);
|
|
85
|
+
return { ok: true };
|
|
86
|
+
}
|
|
66
87
|
export function resumeProject(deps, id) {
|
|
67
88
|
return withProjectControl(deps.db, id, () => {
|
|
89
|
+
if (deps.catalogue !== undefined)
|
|
90
|
+
adoptBaseline(deps, id);
|
|
91
|
+
const revisionId = currentRevisionId(deps.db, id);
|
|
92
|
+
if (revisionId !== undefined) {
|
|
93
|
+
const admitted = projectById(deps.db, id)?.paused === true &&
|
|
94
|
+
deps.db
|
|
95
|
+
.prepare(`SELECT 1 FROM revision_work w JOIN revision_work_reservations r ON r.work_id=w.id
|
|
96
|
+
WHERE r.project_id=? AND r.revision_id=? AND w.state='pending' AND w.dispatch_state='allowed' LIMIT 1`)
|
|
97
|
+
.get(id, revisionId) !== undefined;
|
|
98
|
+
if (listCheckpoints(deps.db, id, revisionId).length === 0 && !admitted)
|
|
99
|
+
return { ok: false, reason: "rebuild-required" };
|
|
100
|
+
if (stagesOf(deps.db, id).some((stage) => stage.state === "canceled"))
|
|
101
|
+
return { ok: false, reason: "rebuild-required" };
|
|
102
|
+
setProjectPaused(deps.db, id, false, deps.clock.now().toISOString());
|
|
103
|
+
changed(deps, id, true);
|
|
104
|
+
return { ok: true };
|
|
105
|
+
}
|
|
68
106
|
const project = projectById(deps.db, id);
|
|
69
107
|
if (project === undefined)
|
|
70
108
|
return { ok: false, reason: "no-project" };
|
|
@@ -134,6 +172,34 @@ export function changeProviders(deps, id, changes) {
|
|
|
134
172
|
});
|
|
135
173
|
if (localFields.length > 0)
|
|
136
174
|
return { ok: false, reason: "invalid-providers", fields: localFields };
|
|
175
|
+
if (deps.catalogue !== undefined)
|
|
176
|
+
adoptBaseline(deps, id);
|
|
177
|
+
const revisionId = currentRevisionId(deps.db, id);
|
|
178
|
+
if (revisionId !== undefined) {
|
|
179
|
+
const view = getRevisionView(deps, id, revisionId);
|
|
180
|
+
if (view === undefined)
|
|
181
|
+
return { ok: false, reason: "no-project" };
|
|
182
|
+
const saved = await saveRevision(deps, {
|
|
183
|
+
projectId: id,
|
|
184
|
+
baseRevisionId: revisionId,
|
|
185
|
+
idempotencyKey: deps.ids.next(),
|
|
186
|
+
edit: {
|
|
187
|
+
config: {
|
|
188
|
+
...view.revision.config,
|
|
189
|
+
...(changes.llm === undefined ? {} : { llm: changes.llm }),
|
|
190
|
+
...(changes.audio === undefined ? {} : { audio: changes.audio }),
|
|
191
|
+
...(changes.images === undefined ? {} : { images: changes.images }),
|
|
192
|
+
...(changes.chunking === undefined ? {} : { chunking: changes.chunking }),
|
|
193
|
+
},
|
|
194
|
+
content: view.revision.content,
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
if (!saved.ok)
|
|
198
|
+
return { ok: false, reason: "not-editable" };
|
|
199
|
+
projectStandings(deps, id);
|
|
200
|
+
deps.emit(id, { type: "project.updated", projectId: id });
|
|
201
|
+
return { ok: true };
|
|
202
|
+
}
|
|
137
203
|
const audioChanged = changes.audio !== undefined &&
|
|
138
204
|
(changes.audio.provider !== project.config.audio?.provider ||
|
|
139
205
|
changes.audio.model !== project.config.audio?.model ||
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { thinkingModes } from "../../kernel/ports/llm.js";
|
|
3
|
+
import { readinessIsUsable, } from "../../kernel/ports/model.js";
|
|
3
4
|
import { chunkModes } from "../narration/chunk.js";
|
|
4
5
|
const choice = z
|
|
5
6
|
.object({
|
|
@@ -36,12 +37,12 @@ export function validateLocalProviderChanges(changes, current) {
|
|
|
36
37
|
fields.push({ field: key, message: "Choose a provider for this kind of generation." });
|
|
37
38
|
continue;
|
|
38
39
|
}
|
|
39
|
-
const usable = provider.readiness
|
|
40
|
+
const usable = readinessIsUsable(provider.readiness);
|
|
40
41
|
if (!usable) {
|
|
41
42
|
fields.push({
|
|
42
43
|
field: key,
|
|
43
44
|
message: provider.readiness.kind === "cli"
|
|
44
|
-
? "Install and sign in to this CLI first."
|
|
45
|
+
? (provider.readiness.issue ?? "Install and sign in to this CLI first.")
|
|
45
46
|
: "Save this provider's API key in Settings first.",
|
|
46
47
|
});
|
|
47
48
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { projectExists } from "../admission/repo.js";
|
|
4
|
+
import { requestHash } from "../revisions/mutation-request.js";
|
|
5
|
+
import { currentRevisionId } from "../revisions/repo.js";
|
|
6
|
+
export { revisionControlSchema } from "./revision-control-schema.js";
|
|
7
|
+
import { revisionControlSchema } from "./revision-control-schema.js";
|
|
8
|
+
export function checkRevisionControl(deps, projectId, operation, input, responseSchema) {
|
|
9
|
+
const { db } = deps;
|
|
10
|
+
return transact(db, () => {
|
|
11
|
+
if (!projectExists(db, projectId))
|
|
12
|
+
return { ok: false, reason: "no-project" };
|
|
13
|
+
const head = currentRevisionId(db, projectId);
|
|
14
|
+
if (head === undefined)
|
|
15
|
+
return { ok: true, identity: null };
|
|
16
|
+
const parsed = revisionControlSchema.safeParse(input);
|
|
17
|
+
if (!parsed.success)
|
|
18
|
+
return { ok: false, reason: "revision-required" };
|
|
19
|
+
const identity = {
|
|
20
|
+
...parsed.data,
|
|
21
|
+
projectId,
|
|
22
|
+
operation,
|
|
23
|
+
hash: requestHash(operation, parsed.data.baseRevisionId, parsed.data),
|
|
24
|
+
};
|
|
25
|
+
const row = db
|
|
26
|
+
.prepare("SELECT operation,request_hash,response_json FROM project_control_receipts WHERE project_id=? AND idempotency_key=?")
|
|
27
|
+
.get(projectId, identity.idempotencyKey);
|
|
28
|
+
if (row !== undefined) {
|
|
29
|
+
if (row.operation !== operation || row.request_hash !== identity.hash)
|
|
30
|
+
return { ok: false, reason: "idempotency-conflict" };
|
|
31
|
+
const response = JSON.parse(String(row.response_json));
|
|
32
|
+
if (z
|
|
33
|
+
.object({ pending: z.literal(true) })
|
|
34
|
+
.strict()
|
|
35
|
+
.safeParse(response).success)
|
|
36
|
+
return head === identity.baseRevisionId
|
|
37
|
+
? { ok: true, identity }
|
|
38
|
+
: { ok: false, reason: "conflict" };
|
|
39
|
+
return { ok: true, identity: null, response: responseSchema.parse(response) };
|
|
40
|
+
}
|
|
41
|
+
if (db
|
|
42
|
+
.prepare("SELECT 1 FROM revision_mutations WHERE project_id=? AND idempotency_key=? UNION ALL SELECT 1 FROM rebuild_admissions WHERE project_id=? AND idempotency_key=?")
|
|
43
|
+
.get(projectId, identity.idempotencyKey, projectId, identity.idempotencyKey) !== undefined)
|
|
44
|
+
return { ok: false, reason: "idempotency-conflict" };
|
|
45
|
+
if (head !== identity.baseRevisionId)
|
|
46
|
+
return { ok: false, reason: "conflict" };
|
|
47
|
+
db.prepare("INSERT INTO project_control_receipts(project_id,idempotency_key,operation,request_hash,base_revision_id,response_json,created_at) VALUES (?,?,?,?,?,?,?)").run(identity.projectId, identity.idempotencyKey, identity.operation, identity.hash, identity.baseRevisionId, JSON.stringify({ pending: true }), deps.clock.now().toISOString());
|
|
48
|
+
return { ok: true, identity };
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
export function rememberRevisionControl(deps, identity, response) {
|
|
52
|
+
if (identity === null)
|
|
53
|
+
return;
|
|
54
|
+
const changed = deps.db
|
|
55
|
+
.prepare("UPDATE project_control_receipts SET response_json=? WHERE project_id=? AND idempotency_key=? AND operation=? AND request_hash=?")
|
|
56
|
+
.run(JSON.stringify(response), identity.projectId, identity.idempotencyKey, identity.operation, identity.hash);
|
|
57
|
+
if (changed.changes !== 1)
|
|
58
|
+
throw new Error("The control request lost its receipt.");
|
|
59
|
+
}
|
|
@@ -1,81 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { estimateRequests, groupEstimateRows } from "./requests.js";
|
|
3
|
+
export { estimateRequests } from "./requests.js";
|
|
1
4
|
export function estimateRun(draft, rendered, expectedWords, catalogue) {
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
z.number().finite().nonnegative().parse(expectedWords);
|
|
6
|
+
const data = catalogue?.read() ?? {
|
|
7
|
+
schemaVersion: 1,
|
|
8
|
+
updatedAt: "",
|
|
9
|
+
providers: {},
|
|
10
|
+
llm: [],
|
|
11
|
+
tts: [],
|
|
12
|
+
image: [],
|
|
13
|
+
};
|
|
14
|
+
const requests = [];
|
|
15
|
+
const llm = draft.llm ?? { provider: "", model: "" };
|
|
16
|
+
const tts = draft.audio ?? { provider: "", model: "" };
|
|
17
|
+
const image = draft.images ?? { provider: "", model: "" };
|
|
18
|
+
const textNote = data.llm.find((model) => model.provider === llm.provider &&
|
|
19
|
+
model.id === llm.model &&
|
|
20
|
+
model.enabled &&
|
|
21
|
+
!model.deprecated)?.pricing.note ??
|
|
22
|
+
"CLI subscription or model pricing is unavailable; usage may be billed by your account.";
|
|
23
|
+
const imageNote = data.image.find((model) => model.provider === image.provider &&
|
|
24
|
+
model.id === image.model &&
|
|
25
|
+
model.enabled &&
|
|
26
|
+
!model.deprecated)?.pricing.note ?? "Model or account pricing is unknown.";
|
|
9
27
|
const generatedArticle = draft.sources.article === "generate";
|
|
10
28
|
const articleChars = generatedArticle ? expectedWords * 6 : (draft.provided.article?.length ?? 0);
|
|
11
29
|
const promptChars = Object.values(rendered).reduce((sum, value) => sum + value.length, 0);
|
|
12
|
-
const
|
|
13
|
-
|
|
30
|
+
const local = (stage, detail) => {
|
|
31
|
+
requests.push({ kind: "local", stage, detail });
|
|
32
|
+
};
|
|
33
|
+
const text = (stage, inputCharacters, outputCharacters, detail) => {
|
|
34
|
+
requests.push({
|
|
35
|
+
kind: "llm",
|
|
14
36
|
stage,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
provider: llm.provider,
|
|
38
|
+
model: llm.model,
|
|
39
|
+
inputCharacters,
|
|
40
|
+
outputCharacters,
|
|
41
|
+
detail: detail === undefined ? textNote : `${detail} ${textNote}`,
|
|
18
42
|
});
|
|
19
43
|
};
|
|
20
|
-
const textCost = (input, output) => {
|
|
21
|
-
const p = llm?.pricing;
|
|
22
|
-
return p?.inputPerMillionTokens === undefined || p.outputPerMillionTokens === undefined
|
|
23
|
-
? undefined
|
|
24
|
-
: ((input / 4) * p.inputPerMillionTokens + (output / 4) * p.outputPerMillionTokens) / 1000000;
|
|
25
|
-
};
|
|
26
|
-
const textNote = llm?.pricing.note ??
|
|
27
|
-
"CLI subscription or model pricing is unavailable; usage may be billed by your account.";
|
|
28
44
|
if (draft.sources.research === "generate")
|
|
29
|
-
|
|
45
|
+
text("Research", promptChars + 6000, 12000, "Assumes about 2,000 words of notes; web search fees are excluded.");
|
|
30
46
|
else
|
|
31
|
-
|
|
47
|
+
local("Research", "Provided or off; no generation charge.");
|
|
32
48
|
if (generatedArticle)
|
|
33
|
-
|
|
49
|
+
text("Article", promptChars + (draft.sources.research === "generate" ? 12000 : 0), articleChars, `${expectedWords.toLocaleString()} expected words.`);
|
|
34
50
|
else
|
|
35
|
-
|
|
51
|
+
local("Article", "Provided article; no generation charge.");
|
|
36
52
|
if (draft.intro?.mode === "llm" || draft.outro?.mode === "llm")
|
|
37
|
-
|
|
53
|
+
text("Intro / outro text", promptChars + articleChars, 2400);
|
|
38
54
|
if (draft.sources.audio === "generate") {
|
|
39
55
|
const extras = ["intro", "outro"].reduce((n, key) => n + (rendered[key]?.length ?? 0), 0);
|
|
40
56
|
const llmExtras = (draft.intro?.mode === "llm" ? 1200 : 0) + (draft.outro?.mode === "llm" ? 1200 : 0);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
const choice = { stage: "Narration", provider: tts.provider, model: tts.model };
|
|
58
|
+
requests.push(generatedArticle || llmExtras > 0
|
|
59
|
+
? { ...choice, kind: "tts-estimate", characters: articleChars + extras + llmExtras }
|
|
60
|
+
: {
|
|
61
|
+
...choice,
|
|
62
|
+
kind: "tts",
|
|
63
|
+
text: (draft.provided.article ?? "") + (rendered.intro ?? "") + (rendered.outro ?? ""),
|
|
64
|
+
});
|
|
49
65
|
}
|
|
50
66
|
else
|
|
51
|
-
|
|
52
|
-
const images = draft.sources.images === "generate"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
local("Narration", "Provided or off; no generation charge.");
|
|
68
|
+
const images = draft.sources.images === "generate"
|
|
69
|
+
? draft.imagePrompts.reduce((n, prompt) => n + prompt.number, 0)
|
|
70
|
+
: 0;
|
|
71
|
+
z.number().int().nonnegative().parse(images);
|
|
72
|
+
for (let index = 0; index < images; index++)
|
|
73
|
+
requests.push({
|
|
74
|
+
kind: "image",
|
|
75
|
+
stage: "Images",
|
|
76
|
+
provider: image.provider,
|
|
77
|
+
model: image.model,
|
|
78
|
+
detail: `${images} images. ${imageNote}`,
|
|
79
|
+
});
|
|
80
|
+
if (images === 0)
|
|
81
|
+
local("Images", "Provided or off.");
|
|
82
|
+
if (["from_prompt", "prompt_by_llm"].includes(draft.sources.thumbnail))
|
|
83
|
+
requests.push({
|
|
84
|
+
kind: "image",
|
|
85
|
+
stage: "Thumbnail",
|
|
86
|
+
provider: image.provider,
|
|
87
|
+
model: image.model,
|
|
88
|
+
});
|
|
89
|
+
else
|
|
90
|
+
local("Thumbnail", "Provided or off.");
|
|
64
91
|
if (draft.sources.thumbnail === "prompt_by_llm")
|
|
65
|
-
|
|
66
|
-
|
|
92
|
+
text("Thumbnail prompt", promptChars + articleChars, 1200);
|
|
93
|
+
local("Export / subtitles", "Local processing; no API fee.");
|
|
67
94
|
return {
|
|
68
|
-
|
|
69
|
-
rows,
|
|
95
|
+
...groupEstimateRows(estimateRequests(requests, data)),
|
|
70
96
|
expectedWords,
|
|
71
|
-
catalogueDate: catalogue
|
|
72
|
-
low: rows.reduce((n, r) => n + (r.low ?? 0), 0),
|
|
73
|
-
high: rows.reduce((n, r) => n + (r.high ?? 0), 0),
|
|
74
|
-
unknown: rows.filter((r) => r.low === null).length,
|
|
75
|
-
assumptions: [
|
|
76
|
-
"Planning estimate, not a spending limit. Generated lengths use a ±50% range; actual output can exceed it.",
|
|
77
|
-
"Assumes roughly 6 characters per word, 4 characters per token, and 150 spoken words per minute.",
|
|
78
|
-
"Retries, extra reasoning tokens, web tools, taxes, discounts and included credits are excluded. Unknown charges are not counted as zero.",
|
|
79
|
-
],
|
|
97
|
+
catalogueDate: catalogue === undefined ? null : data.updatedAt,
|
|
80
98
|
};
|
|
81
99
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const quantity = z.number().finite().nonnegative();
|
|
3
|
+
const common = z.object({
|
|
4
|
+
stage: z.string(),
|
|
5
|
+
detail: z.string().optional(),
|
|
6
|
+
variable: z.boolean().optional(),
|
|
7
|
+
expectedWords: quantity.optional(),
|
|
8
|
+
});
|
|
9
|
+
const provider = common.extend({ provider: z.string(), model: z.string() });
|
|
10
|
+
const requestSchema = z.discriminatedUnion("kind", [
|
|
11
|
+
provider
|
|
12
|
+
.extend({ kind: z.literal("llm"), inputCharacters: quantity, outputCharacters: quantity })
|
|
13
|
+
.strict(),
|
|
14
|
+
provider.extend({ kind: z.literal("tts"), text: z.string() }).strict(),
|
|
15
|
+
provider.extend({ kind: z.literal("tts-estimate"), characters: quantity }).strict(),
|
|
16
|
+
provider.extend({ kind: z.literal("image") }).strict(),
|
|
17
|
+
common.extend({ kind: z.literal("local") }).strict(),
|
|
18
|
+
]);
|
|
19
|
+
export function estimateRequests(requests, catalogue) {
|
|
20
|
+
const parsed = z.array(requestSchema).parse(requests);
|
|
21
|
+
const rows = parsed.map((request) => price(request, catalogue));
|
|
22
|
+
return {
|
|
23
|
+
currency: "USD",
|
|
24
|
+
rows,
|
|
25
|
+
low: rows.reduce((total, row) => total + (row.low ?? 0), 0),
|
|
26
|
+
high: rows.reduce((total, row) => total + (row.high ?? 0), 0),
|
|
27
|
+
unknown: rows.filter((row) => row.low === null).length,
|
|
28
|
+
expectedWords: parsed.reduce((total, request) => total + (request.expectedWords ?? 0), 0),
|
|
29
|
+
catalogueDate: catalogue.updatedAt,
|
|
30
|
+
assumptions: [
|
|
31
|
+
"Planning estimate, not a spending limit. Generated lengths use a ±50% range; actual output can exceed it.",
|
|
32
|
+
"Assumes roughly 6 characters per word, 4 characters per token, and 150 spoken words per minute.",
|
|
33
|
+
"Retries, extra reasoning tokens, web tools, taxes, discounts and included credits are excluded. Unknown charges are not counted as zero.",
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function groupEstimateRows(estimate) {
|
|
38
|
+
const grouped = new Map();
|
|
39
|
+
for (const row of estimate.rows) {
|
|
40
|
+
const previous = grouped.get(row.stage);
|
|
41
|
+
grouped.set(row.stage, previous === undefined
|
|
42
|
+
? row
|
|
43
|
+
: {
|
|
44
|
+
...previous,
|
|
45
|
+
low: previous.low === null || row.low === null ? null : previous.low + row.low,
|
|
46
|
+
high: previous.high === null || row.high === null ? null : previous.high + row.high,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const rows = [...grouped.values()];
|
|
50
|
+
return { ...estimate, rows, unknown: rows.filter((row) => row.low === null).length };
|
|
51
|
+
}
|
|
52
|
+
function price(request, catalogue) {
|
|
53
|
+
if (request.kind === "local")
|
|
54
|
+
return {
|
|
55
|
+
stage: request.stage,
|
|
56
|
+
low: 0,
|
|
57
|
+
high: 0,
|
|
58
|
+
detail: request.detail ?? "Local processing or retained output; no API fee.",
|
|
59
|
+
};
|
|
60
|
+
const family = request.kind === "tts-estimate" ? "tts" : request.kind;
|
|
61
|
+
const model = catalogue[family].find((entry) => entry.provider === request.provider &&
|
|
62
|
+
entry.id === request.model &&
|
|
63
|
+
entry.enabled &&
|
|
64
|
+
!entry.deprecated);
|
|
65
|
+
const rates = model?.pricing;
|
|
66
|
+
let amount;
|
|
67
|
+
let variable = request.variable === true;
|
|
68
|
+
let detail = rates?.note ?? "Model or account pricing is unknown.";
|
|
69
|
+
switch (request.kind) {
|
|
70
|
+
case "llm":
|
|
71
|
+
if (rates?.inputPerMillionTokens !== undefined && rates.outputPerMillionTokens !== undefined)
|
|
72
|
+
amount =
|
|
73
|
+
((request.inputCharacters / 4) * rates.inputPerMillionTokens +
|
|
74
|
+
(request.outputCharacters / 4) * rates.outputPerMillionTokens) /
|
|
75
|
+
1000000;
|
|
76
|
+
variable = true;
|
|
77
|
+
break;
|
|
78
|
+
case "tts":
|
|
79
|
+
case "tts-estimate": {
|
|
80
|
+
const characters = request.kind === "tts" ? request.text.length : request.characters;
|
|
81
|
+
const perMinute = rates?.perMillionCharacters === undefined && rates?.perMinute !== undefined;
|
|
82
|
+
if (rates?.perMillionCharacters !== undefined)
|
|
83
|
+
amount = (characters / 1000000) * rates.perMillionCharacters;
|
|
84
|
+
else if (rates?.perMinute !== undefined)
|
|
85
|
+
amount = (characters / 6 / 150) * rates.perMinute;
|
|
86
|
+
variable ||= request.kind === "tts-estimate" || perMinute;
|
|
87
|
+
detail = `About ${characters.toLocaleString()} characters. ${detail}`;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "image":
|
|
91
|
+
amount = rates?.perImage;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
if (amount !== undefined)
|
|
95
|
+
quantity.parse(amount);
|
|
96
|
+
return {
|
|
97
|
+
stage: request.stage,
|
|
98
|
+
low: amount === undefined ? null : amount * (variable ? 0.5 : 1),
|
|
99
|
+
high: amount === undefined ? null : amount * (variable ? 1.5 : 1),
|
|
100
|
+
detail: request.detail ?? detail,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -38,7 +38,10 @@ export async function runImages(deps, context, providers) {
|
|
|
38
38
|
}
|
|
39
39
|
// The run's own frame. The adapter turns it into whatever its provider spells the
|
|
40
40
|
// closest supported size, and the render crops whatever is left over.
|
|
41
|
-
const
|
|
41
|
+
const result = await sendAll(deps, context, providers, choice, pieces, project.format);
|
|
42
|
+
if (!result.ok)
|
|
43
|
+
return "held";
|
|
44
|
+
const made = result.value;
|
|
42
45
|
// Images made means images stored, and only the ones this run stored: a resume does not
|
|
43
46
|
// count an image the previous run made, and a regenerated image counts again.
|
|
44
47
|
//
|
|
@@ -56,6 +59,7 @@ export async function runImages(deps, context, providers) {
|
|
|
56
59
|
stage: "images",
|
|
57
60
|
detail: `${String(pieces.length)} images from ${String(project.config.imagePrompts.length)} prompts`,
|
|
58
61
|
});
|
|
62
|
+
return "done";
|
|
59
63
|
}
|
|
60
64
|
// For each ticked image prompt, its rendered text is sent Number times as independent
|
|
61
65
|
// parallel calls. The plan is made once and kept, so a retry finds the rows the first run
|
|
@@ -117,12 +121,17 @@ async function sendAll(deps, context, providers, choice, pieces, format) {
|
|
|
117
121
|
}
|
|
118
122
|
setPiece(deps.db, piece.id, "running", piece.payload);
|
|
119
123
|
try {
|
|
120
|
-
const
|
|
124
|
+
const result = await providers.forPiece(piece.id).image({
|
|
121
125
|
provider: choice.provider,
|
|
122
126
|
model: choice.model,
|
|
123
127
|
prompt: asked.prompt,
|
|
124
128
|
aspect: format,
|
|
125
129
|
});
|
|
130
|
+
if (!result.ok) {
|
|
131
|
+
setPiece(deps.db, piece.id, "pending", piece.payload);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
const made = result.value;
|
|
126
135
|
const file = keep(deps, projectId, piece.idx, made);
|
|
127
136
|
// The prompt text, the prompt name, the index within the prompt, the
|
|
128
137
|
// provider and the model travel with the image.
|
|
@@ -146,11 +155,13 @@ async function sendAll(deps, context, providers, choice, pieces, format) {
|
|
|
146
155
|
let made = 0;
|
|
147
156
|
for (const outcome of outcomes) {
|
|
148
157
|
if (!outcome.ok) {
|
|
158
|
+
if ("reason" in outcome)
|
|
159
|
+
return outcome;
|
|
149
160
|
throw outcome.error;
|
|
150
161
|
}
|
|
151
162
|
made += outcome.made ? 1 : 0;
|
|
152
163
|
}
|
|
153
|
-
return made;
|
|
164
|
+
return { ok: true, value: made };
|
|
154
165
|
}
|
|
155
166
|
// An image counts as landed only when its row and its file are both still there: they are
|
|
156
167
|
// written one after the other, the boot reconcile can remove a file whose row survived,
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// is the one that runs.
|
|
5
5
|
import { normaliseDraft } from "../admission/rules.js";
|
|
6
6
|
import { collectFields, render } from "../admission/substitute.js";
|
|
7
|
-
import {
|
|
8
|
-
export function pickTemplates(db, input) {
|
|
7
|
+
import { snapshotEntry, snapshotPrompt } from "./snapshot.js";
|
|
8
|
+
export function pickTemplates(db, input, snapshot) {
|
|
9
9
|
const draft = normaliseDraft(input);
|
|
10
10
|
const { sources } = draft;
|
|
11
11
|
const missing = [];
|
|
@@ -14,22 +14,22 @@ export function pickTemplates(db, input) {
|
|
|
14
14
|
const text = [];
|
|
15
15
|
const image = [];
|
|
16
16
|
if (sources.article === "generate") {
|
|
17
|
-
body(db, "article", draft.articlePrompt, "articlePrompt", missing, text, "article");
|
|
17
|
+
body(db, "article", draft.articlePrompt, "articlePrompt", missing, text, "article", snapshot);
|
|
18
18
|
}
|
|
19
|
-
const intro = pickEntry(db, "intro", draft.intro, missing);
|
|
20
|
-
const outro = pickEntry(db, "outro", draft.outro, missing);
|
|
19
|
+
const intro = pickEntry(db, "intro", draft.intro, missing, snapshot);
|
|
20
|
+
const outro = pickEntry(db, "outro", draft.outro, missing, snapshot);
|
|
21
21
|
push(text, "intro", intro);
|
|
22
22
|
push(text, "outro", outro);
|
|
23
23
|
if (sources.images === "generate") {
|
|
24
24
|
for (const [index, picked] of draft.imagePrompts.entries()) {
|
|
25
25
|
const field = `imagePrompts.${String(index)}.name`;
|
|
26
|
-
body(db, "image", picked.name, field, missing, image, `imagePrompts.${String(index)}
|
|
26
|
+
body(db, "image", picked.name, field, missing, image, `imagePrompts.${String(index)}`, snapshot);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
// Both Generate modes read the same thumbnail template; Prompt by LLM sends it as the
|
|
30
30
|
// instruction rather than to the image provider.
|
|
31
31
|
if (sources.thumbnail === "from_prompt" || sources.thumbnail === "prompt_by_llm") {
|
|
32
|
-
body(db, "thumbnail", draft.thumbnailPrompt, "thumbnailPrompt", missing, image, "thumbnailPrompt");
|
|
32
|
+
body(db, "thumbnail", draft.thumbnailPrompt, "thumbnailPrompt", missing, image, "thumbnailPrompt", snapshot);
|
|
33
33
|
}
|
|
34
34
|
const fields = collectFields(text.map((picked) => picked.body), image.map((picked) => picked.body));
|
|
35
35
|
return {
|
|
@@ -48,24 +48,24 @@ export function renderPicked(picked, values) {
|
|
|
48
48
|
}
|
|
49
49
|
return rendered;
|
|
50
50
|
}
|
|
51
|
-
function body(db, kind, name, field, missing, into, key) {
|
|
51
|
+
function body(db, kind, name, field, missing, into, key, snapshot) {
|
|
52
52
|
// An unpicked prompt is admission's rule to state ("Pick an article prompt."), so this
|
|
53
53
|
// says nothing about it and the form marks the field once.
|
|
54
54
|
if (name === undefined || name.trim() === "") {
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
const prompt =
|
|
57
|
+
const prompt = snapshotPrompt(db, snapshot, kind, name);
|
|
58
58
|
if (prompt === undefined) {
|
|
59
59
|
missing.push({ field, message: `That ${kind} prompt no longer exists; pick another.` });
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
into.push({ key, body: prompt.body });
|
|
63
63
|
}
|
|
64
|
-
function pickEntry(db, category, choice, missing) {
|
|
64
|
+
function pickEntry(db, category, choice, missing, snapshot) {
|
|
65
65
|
if (choice === undefined) {
|
|
66
66
|
return undefined;
|
|
67
67
|
}
|
|
68
|
-
const entry =
|
|
68
|
+
const entry = snapshotEntry(db, snapshot, category, choice.name);
|
|
69
69
|
if (entry === undefined) {
|
|
70
70
|
missing.push({
|
|
71
71
|
field: category,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { entryCategories, entryModes, promptKinds } from "./model.js";
|
|
3
|
+
import { entryByName, promptByName } from "./repo.js";
|
|
4
|
+
const base = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
name: z.string(),
|
|
7
|
+
body: z.string(),
|
|
8
|
+
slots: z.array(z.string()).readonly(),
|
|
9
|
+
updatedAt: z.string(),
|
|
10
|
+
});
|
|
11
|
+
export const librarySnapshotSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
prompts: z
|
|
14
|
+
.array(base
|
|
15
|
+
.extend({ kind: z.enum(promptKinds) })
|
|
16
|
+
.strict()
|
|
17
|
+
.readonly())
|
|
18
|
+
.readonly(),
|
|
19
|
+
entries: z
|
|
20
|
+
.array(base
|
|
21
|
+
.extend({ category: z.enum(entryCategories), mode: z.enum(entryModes) })
|
|
22
|
+
.strict()
|
|
23
|
+
.readonly())
|
|
24
|
+
.readonly(),
|
|
25
|
+
})
|
|
26
|
+
.strict()
|
|
27
|
+
.readonly();
|
|
28
|
+
export function snapshotPrompt(db, snapshot, kind, name) {
|
|
29
|
+
return (snapshot?.prompts.find((row) => row.kind === kind && row.name.toLowerCase() === name.toLowerCase()) ?? promptByName(db, kind, name));
|
|
30
|
+
}
|
|
31
|
+
export function snapshotEntry(db, snapshot, category, name) {
|
|
32
|
+
return (snapshot?.entries.find((row) => row.category === category && row.name.toLowerCase() === name.toLowerCase()) ?? entryByName(db, category, name));
|
|
33
|
+
}
|