@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
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { startRun } from "../admission/start.js";
|
|
4
|
+
import { enqueueBatch, pumpQueue } from "../batch/index.js";
|
|
5
|
+
import { admitReviewedCheckpoints } from "../rebuild/runtime-checkpoints.js";
|
|
6
|
+
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
7
|
+
import { checkDraftReadiness, localDraftReadiness } from "./readiness.js";
|
|
8
|
+
import { attachmentRows, draftRow } from "./repo.js";
|
|
9
|
+
import { readDraft } from "./service.js";
|
|
10
|
+
import { executionSchema, insertStartReceipt, markDraftStartedAndReleaseRefs, readStartReceipt, releaseStartClaim, replayResult, requireStartingIdentity, } from "./start-repo.js";
|
|
11
|
+
const inputSchema = z
|
|
12
|
+
.object({ draftId: z.uuid(), baseVersion: z.number().int().positive(), reviewId: z.uuid() })
|
|
13
|
+
.strict();
|
|
14
|
+
export async function startPlayDraft(deps, input) {
|
|
15
|
+
const parsed = inputSchema.safeParse(input);
|
|
16
|
+
if (!parsed.success)
|
|
17
|
+
return { ok: false, reason: "invalid-edit", currentVersion: null, fields: [] };
|
|
18
|
+
input = parsed.data;
|
|
19
|
+
const claimed = transact(deps.db, () => {
|
|
20
|
+
const receipt = readStartReceipt(deps.db, input.reviewId);
|
|
21
|
+
if (receipt)
|
|
22
|
+
return replayResult(receipt, input);
|
|
23
|
+
const row = draftRow(deps.db, input.draftId);
|
|
24
|
+
if (!row)
|
|
25
|
+
return { ok: false, reason: "not-found", currentVersion: null, fields: [] };
|
|
26
|
+
const view = readDraft(deps, input.draftId);
|
|
27
|
+
if (!view.ok)
|
|
28
|
+
return view;
|
|
29
|
+
if (row.state === "started")
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
reason: "already-started",
|
|
33
|
+
currentVersion: row.version,
|
|
34
|
+
fields: [],
|
|
35
|
+
};
|
|
36
|
+
if (row.state === "starting" && row.start_id !== input.reviewId)
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
reason: "pending-start",
|
|
40
|
+
currentVersion: row.version,
|
|
41
|
+
reviewId: row.start_id ?? input.reviewId,
|
|
42
|
+
fields: [],
|
|
43
|
+
};
|
|
44
|
+
if (row.version !== input.baseVersion || view.value.review?.id !== input.reviewId)
|
|
45
|
+
return {
|
|
46
|
+
ok: false,
|
|
47
|
+
reason: "stale-review",
|
|
48
|
+
currentVersion: row.version,
|
|
49
|
+
fields: [],
|
|
50
|
+
};
|
|
51
|
+
if (row.state === "active") {
|
|
52
|
+
deps.db
|
|
53
|
+
.prepare("UPDATE play_drafts SET state='starting',start_id=? WHERE id=? AND version=? AND state='active'")
|
|
54
|
+
.run(input.reviewId, input.draftId, input.baseVersion);
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, value: view.value.review };
|
|
57
|
+
});
|
|
58
|
+
if (!claimed.ok) {
|
|
59
|
+
releaseStartClaim(deps, input, claimed.reason === "stale-review");
|
|
60
|
+
return claimed;
|
|
61
|
+
}
|
|
62
|
+
if ("requestId" in claimed.value)
|
|
63
|
+
return { ok: true, value: claimed.value };
|
|
64
|
+
const identity = requireStartingIdentity(deps, input);
|
|
65
|
+
if (!identity.ok) {
|
|
66
|
+
releaseStartClaim(deps, input, identity.reason === "stale-review");
|
|
67
|
+
return identity;
|
|
68
|
+
}
|
|
69
|
+
const row = draftRow(deps.db, input.draftId);
|
|
70
|
+
if (row?.review_json == null)
|
|
71
|
+
throw new Error("Pending Start lost its review");
|
|
72
|
+
const { catalogue } = executionSchema.parse(JSON.parse(row.review_json)).execution;
|
|
73
|
+
const captured = {
|
|
74
|
+
...deps,
|
|
75
|
+
catalogue: {
|
|
76
|
+
...deps.catalogue,
|
|
77
|
+
read: () => catalogue,
|
|
78
|
+
models: (provider, family) => catalogue[family].filter((m) => m.provider === provider && m.enabled && !m.deprecated),
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
const checked = await checkDraftReadiness(captured, claimed.value.runs);
|
|
82
|
+
const oldFiles = attachmentRows(deps.db, input.draftId).flatMap((a) => a.staged_file_id === null ? [] : [a.staged_file_id]);
|
|
83
|
+
const result = transact(deps.db, () => {
|
|
84
|
+
const receipt = readStartReceipt(deps.db, input.reviewId);
|
|
85
|
+
if (receipt)
|
|
86
|
+
return replayResult(receipt, input);
|
|
87
|
+
const starting = requireStartingIdentity(deps, input);
|
|
88
|
+
if (!starting.ok)
|
|
89
|
+
return starting;
|
|
90
|
+
const fields = [
|
|
91
|
+
...checked.fields,
|
|
92
|
+
...localDraftReadiness(captured, starting.value.runs, checked.providers),
|
|
93
|
+
];
|
|
94
|
+
if (fields.length)
|
|
95
|
+
return { ok: false, reason: "readiness", currentVersion: input.baseVersion, fields };
|
|
96
|
+
const runs = starting.value.runs;
|
|
97
|
+
const first = runs[0];
|
|
98
|
+
if (!first)
|
|
99
|
+
throw new Error("Reviewed run list was empty");
|
|
100
|
+
const queue = runs.length === 1 ? [] : enqueueBatch(captured, input.reviewId, runs, true);
|
|
101
|
+
const projectIds = runs.length === 1
|
|
102
|
+
? [startRun(captured, first.draft, first.rendered, true, first.templates).project.id]
|
|
103
|
+
: queue.map((e) => e.projectId);
|
|
104
|
+
const checkpointSet = admitReviewedCheckpoints(captured, projectIds, starting.value.checkpointSet ?? [], catalogue);
|
|
105
|
+
const value = {
|
|
106
|
+
requestId: input.reviewId,
|
|
107
|
+
projectIds,
|
|
108
|
+
queue,
|
|
109
|
+
replayed: false,
|
|
110
|
+
...(checkpointSet.length ? { checkpointSet } : {}),
|
|
111
|
+
};
|
|
112
|
+
insertStartReceipt(deps, input, value);
|
|
113
|
+
markDraftStartedAndReleaseRefs(deps.db, input);
|
|
114
|
+
return { ok: true, value };
|
|
115
|
+
});
|
|
116
|
+
if (!result.ok) {
|
|
117
|
+
releaseStartClaim(deps, input, result.reason === "stale-review");
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
if (!result.value.replayed) {
|
|
121
|
+
afterCommit(deps, () => deps.recordStarted(result.value.projectIds));
|
|
122
|
+
for (const id of oldFiles)
|
|
123
|
+
afterCommit(deps, () => releaseStagedFile(deps, id));
|
|
124
|
+
afterCommit(deps, () => {
|
|
125
|
+
if (result.value.queue.length)
|
|
126
|
+
pumpQueue(deps.db, deps.runner);
|
|
127
|
+
else
|
|
128
|
+
for (const id of result.value.projectIds)
|
|
129
|
+
deps.runner.tick(id);
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
function afterCommit(deps, run) {
|
|
135
|
+
try {
|
|
136
|
+
run();
|
|
137
|
+
}
|
|
138
|
+
catch {
|
|
139
|
+
try {
|
|
140
|
+
deps.log.write("warn", "play.start.after-commit", {
|
|
141
|
+
detail: "Start committed; a post-commit action failed.",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
/* The durable receipt remains authoritative even if logging fails. */
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { stageUpload } from "../storage/staging.js";
|
|
3
|
+
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
4
|
+
import { attachmentRow, draftRow } from "./repo.js";
|
|
5
|
+
import { draftAttachmentSchema } from "./schema.js";
|
|
6
|
+
export async function uploadDraftAttachment(deps, input) {
|
|
7
|
+
const row = z.uuid().safeParse(input.draftId).success
|
|
8
|
+
? draftRow(deps.db, input.draftId)
|
|
9
|
+
: undefined;
|
|
10
|
+
const refuse = (reason) => ({
|
|
11
|
+
ok: false,
|
|
12
|
+
reason,
|
|
13
|
+
currentVersion: row?.version ?? null,
|
|
14
|
+
fields: [],
|
|
15
|
+
...(row?.state === "starting" && row.review_id !== null ? { reviewId: row.review_id } : {}),
|
|
16
|
+
});
|
|
17
|
+
if (!z.uuid().safeParse(input.draftId).success || !z.uuid().safeParse(input.attachmentId).success)
|
|
18
|
+
return refuse("invalid-edit");
|
|
19
|
+
if (row === undefined)
|
|
20
|
+
return refuse("not-found");
|
|
21
|
+
if (row.state === "starting")
|
|
22
|
+
return refuse("pending-start");
|
|
23
|
+
if (row.state === "started")
|
|
24
|
+
return refuse("already-started");
|
|
25
|
+
const attachment = attachmentRow(deps.db, input.attachmentId);
|
|
26
|
+
if (attachment === undefined || attachment.draft_id !== input.draftId)
|
|
27
|
+
return refuse("not-found");
|
|
28
|
+
if (attachment.staged_file_id !== null || attachment.status !== "pending")
|
|
29
|
+
return refuse("conflict");
|
|
30
|
+
const storage = { ...deps, emit: () => undefined };
|
|
31
|
+
const result = await stageUpload(storage, {
|
|
32
|
+
stageKind: attachment.kind,
|
|
33
|
+
originalFilename: attachment.original_filename,
|
|
34
|
+
content: input.content,
|
|
35
|
+
onAllocated(file) {
|
|
36
|
+
const bound = deps.db
|
|
37
|
+
.prepare("UPDATE play_draft_attachments SET staged_file_id=?,error=NULL WHERE id=? AND draft_id=? AND staged_file_id IS NULL AND status='pending' AND EXISTS (SELECT 1 FROM play_drafts WHERE id=? AND state='active')")
|
|
38
|
+
.run(file.id, input.attachmentId, input.draftId, input.draftId);
|
|
39
|
+
if (Number(bound.changes) !== 1)
|
|
40
|
+
throw new Error("Upload attachment changed before allocation.");
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
if (!result.ok) {
|
|
44
|
+
deps.db
|
|
45
|
+
.prepare("UPDATE play_draft_attachments SET status='reattach',error=? WHERE id=? AND draft_id=? AND staged_file_id IS NULL")
|
|
46
|
+
.run("Upload failed. Reattach the file.", input.attachmentId, input.draftId);
|
|
47
|
+
return refuse("invalid-edit");
|
|
48
|
+
}
|
|
49
|
+
const updated = deps.db
|
|
50
|
+
.prepare("UPDATE play_draft_attachments SET status='ready',error=NULL WHERE id=? AND draft_id=? AND staged_file_id=?")
|
|
51
|
+
.run(input.attachmentId, input.draftId, result.file.id);
|
|
52
|
+
releaseStagedFile(storage, result.file.id);
|
|
53
|
+
if (Number(updated.changes) !== 1)
|
|
54
|
+
return refuse("not-found");
|
|
55
|
+
return {
|
|
56
|
+
ok: true,
|
|
57
|
+
value: draftAttachmentSchema.parse({
|
|
58
|
+
id: attachment.id,
|
|
59
|
+
kind: attachment.kind,
|
|
60
|
+
name: attachment.original_filename,
|
|
61
|
+
state: "ready",
|
|
62
|
+
stagedFileId: result.file.id,
|
|
63
|
+
bytes: result.file.bytes,
|
|
64
|
+
error: null,
|
|
65
|
+
}),
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { projectById } from "../admission/repo.js";
|
|
4
|
+
import { detectSlots } from "../admission/substitute.js";
|
|
5
|
+
import { listCheckpoints } from "../checkpoints/repo.js";
|
|
6
|
+
import { requestHash } from "../play-drafts/repo.js";
|
|
7
|
+
import { currentRevisionId, revisionById } from "../revisions/repo.js";
|
|
8
|
+
import { createTemplate, readTemplate } from "./service.js";
|
|
9
|
+
export const projectTemplateCreateSchema = z
|
|
10
|
+
.object({
|
|
11
|
+
id: z.uuid(),
|
|
12
|
+
name: z.string().trim().min(1).max(200),
|
|
13
|
+
projectId: z.string().min(1).max(64),
|
|
14
|
+
revisionId: z.string().min(1).max(64),
|
|
15
|
+
})
|
|
16
|
+
.strict()
|
|
17
|
+
.readonly();
|
|
18
|
+
export const templateFromProjectSchema = projectTemplateCreateSchema;
|
|
19
|
+
export function createTemplateFromProject(deps, input) {
|
|
20
|
+
const parsed = projectTemplateCreateSchema.safeParse(input);
|
|
21
|
+
if (!parsed.success)
|
|
22
|
+
return { ok: false, reason: "invalid-input" };
|
|
23
|
+
return transact(deps.db, () => {
|
|
24
|
+
const hash = requestHash({ operation: "template-from-project", ...parsed.data });
|
|
25
|
+
const old = deps.db
|
|
26
|
+
.prepare("SELECT creation_hash FROM project_templates WHERE id=?")
|
|
27
|
+
.get(parsed.data.id);
|
|
28
|
+
if (old)
|
|
29
|
+
return old.creation_hash === hash
|
|
30
|
+
? readTemplate(deps, parsed.data.id, 1)
|
|
31
|
+
: { ok: false, reason: "conflict" };
|
|
32
|
+
const project = projectById(deps.db, parsed.data.projectId);
|
|
33
|
+
if (project === undefined)
|
|
34
|
+
return { ok: false, reason: "not-found" };
|
|
35
|
+
const current = currentRevisionId(deps.db, project.id);
|
|
36
|
+
if (current === undefined || current !== parsed.data.revisionId)
|
|
37
|
+
return { ok: false, reason: "conflict" };
|
|
38
|
+
const revision = revisionById(deps.db, project.id, current);
|
|
39
|
+
if (revision === undefined)
|
|
40
|
+
return { ok: false, reason: "conflict" };
|
|
41
|
+
if (revision.config.sources.images === "generate" &&
|
|
42
|
+
revision.content.imageOrder.some((key) => revision.content.imageDefinitions[key]?.source === "provide"))
|
|
43
|
+
return { ok: false, reason: "invalid-input" };
|
|
44
|
+
const result = createTemplate(deps, {
|
|
45
|
+
id: parsed.data.id,
|
|
46
|
+
name: parsed.data.name,
|
|
47
|
+
document: documentFromProject(deps, revision),
|
|
48
|
+
});
|
|
49
|
+
if (result.ok)
|
|
50
|
+
deps.db
|
|
51
|
+
.prepare("UPDATE project_templates SET creation_hash=? WHERE id=?")
|
|
52
|
+
.run(hash, parsed.data.id);
|
|
53
|
+
return result;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function documentFromProject(deps, revision) {
|
|
57
|
+
const config = revision.config;
|
|
58
|
+
const prompts = [];
|
|
59
|
+
const entries = [];
|
|
60
|
+
const addPrompt = (kind, name, key, literal) => {
|
|
61
|
+
if (!name)
|
|
62
|
+
return;
|
|
63
|
+
const body = literal ?? revision.content.promptTemplates[key] ?? config.rendered[key];
|
|
64
|
+
if (body === null || body === undefined)
|
|
65
|
+
return;
|
|
66
|
+
prompts.push({
|
|
67
|
+
id: deps.uuid(),
|
|
68
|
+
kind,
|
|
69
|
+
name,
|
|
70
|
+
body,
|
|
71
|
+
slots: [...detectSlots(body).names],
|
|
72
|
+
updatedAt: revision.createdAt,
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
addPrompt("article", config.articlePrompt, "article");
|
|
76
|
+
const definitions = revision.content.imageOrder.flatMap((key) => {
|
|
77
|
+
const definition = revision.content.imageDefinitions[key];
|
|
78
|
+
return definition === undefined ? [] : [definition];
|
|
79
|
+
});
|
|
80
|
+
const imagePrompts = config.sources.images === "generate" && definitions.length > 0
|
|
81
|
+
? definitions.map((definition, index) => {
|
|
82
|
+
const key = definition.templateKey ?? "";
|
|
83
|
+
const original = revision.content.promptTemplates[key];
|
|
84
|
+
const body = original && definition.prompt === config.rendered[key]
|
|
85
|
+
? original
|
|
86
|
+
: (definition.prompt ?? original ?? "");
|
|
87
|
+
const name = `Project image ${index + 1}`;
|
|
88
|
+
addPrompt("image", name, key, body);
|
|
89
|
+
return { name, number: "1" };
|
|
90
|
+
})
|
|
91
|
+
: config.imagePrompts.map((prompt, index) => {
|
|
92
|
+
addPrompt("image", prompt.name, `imagePrompts.${index}`);
|
|
93
|
+
return { name: prompt.name, number: String(prompt.number) };
|
|
94
|
+
});
|
|
95
|
+
addPrompt("thumbnail", config.thumbnailPrompt, "thumbnailPrompt");
|
|
96
|
+
for (const category of ["intro", "outro"]) {
|
|
97
|
+
const choice = config[category];
|
|
98
|
+
const body = revision.content.promptTemplates[category] ?? config.rendered[category];
|
|
99
|
+
if (choice !== undefined && body !== null && body !== undefined)
|
|
100
|
+
entries.push({
|
|
101
|
+
id: deps.uuid(),
|
|
102
|
+
category,
|
|
103
|
+
mode: choice.mode,
|
|
104
|
+
name: choice.name,
|
|
105
|
+
body,
|
|
106
|
+
slots: [...detectSlots(body).names],
|
|
107
|
+
updatedAt: revision.createdAt,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
const attachment = (name) => ({ attachmentId: deps.uuid(), name });
|
|
111
|
+
const provided = {
|
|
112
|
+
research: config.sources.research === "provide" ? (config.provided.research ?? "") : "",
|
|
113
|
+
article: config.sources.article === "provide"
|
|
114
|
+
? (revision.content.articleMarkdown ?? config.provided.article ?? "")
|
|
115
|
+
: "",
|
|
116
|
+
audio: config.sources.audio === "provide" ? attachment("Audio from project") : null,
|
|
117
|
+
thumbnail: config.sources.thumbnail === "provide" ? attachment("Thumbnail from project") : null,
|
|
118
|
+
images: config.sources.images === "provide"
|
|
119
|
+
? (definitions.length ? definitions : (config.provided.images ?? [])).map((_, index) => attachment(`Image ${index + 1}`))
|
|
120
|
+
: [],
|
|
121
|
+
};
|
|
122
|
+
return {
|
|
123
|
+
schemaVersion: 1,
|
|
124
|
+
librarySnapshot: { prompts, entries },
|
|
125
|
+
form: {
|
|
126
|
+
title: config.title,
|
|
127
|
+
format: config.format,
|
|
128
|
+
sources: config.sources,
|
|
129
|
+
checkpoints: listCheckpoints(deps.db, revision.projectId, revision.id).map((gate) => gate.stage),
|
|
130
|
+
llm: config.llm ?? { provider: "", model: "" },
|
|
131
|
+
audio: config.audio ?? { provider: "", model: "", voice: "" },
|
|
132
|
+
images: config.images ?? { provider: "", model: "" },
|
|
133
|
+
articlePrompt: config.articlePrompt ?? "",
|
|
134
|
+
imagePrompts,
|
|
135
|
+
thumbnailPrompt: config.thumbnailPrompt ?? "",
|
|
136
|
+
intro: config.intro?.name ?? "",
|
|
137
|
+
outro: config.outro?.name ?? "",
|
|
138
|
+
chunking: {
|
|
139
|
+
mode: config.chunking?.mode ?? "whole",
|
|
140
|
+
words: config.chunking?.words === undefined ? "500" : String(config.chunking.words),
|
|
141
|
+
characters: config.chunking?.characters === undefined ? "3000" : String(config.chunking.characters),
|
|
142
|
+
},
|
|
143
|
+
subtitles: {
|
|
144
|
+
mode: config.subtitles?.mode ?? "off",
|
|
145
|
+
language: "en",
|
|
146
|
+
fontId: config.subtitles?.fontId ?? "default",
|
|
147
|
+
fontSize: config.subtitles?.fontSize === undefined ? "48" : String(config.subtitles.fontSize),
|
|
148
|
+
position: config.subtitles?.position ?? "bottom",
|
|
149
|
+
},
|
|
150
|
+
values: config.values,
|
|
151
|
+
provided,
|
|
152
|
+
},
|
|
153
|
+
section: "content",
|
|
154
|
+
variants: [],
|
|
155
|
+
expectedWords: "1500",
|
|
156
|
+
previewText: "Every story begins with a word.",
|
|
157
|
+
fontUpload: null,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { projectTemplateSchema } from "./schema.js";
|
|
3
|
+
const select = `SELECT t.id,r.version,r.name,t.created_at AS createdAt,r.created_at AS updatedAt,r.document_json AS document FROM project_templates t JOIN project_template_revisions r ON r.template_id=t.id`;
|
|
4
|
+
const templateSummarySchema = z
|
|
5
|
+
.object({
|
|
6
|
+
id: z.uuid(),
|
|
7
|
+
name: z.string(),
|
|
8
|
+
version: z.number().int().positive(),
|
|
9
|
+
createdAt: z.string(),
|
|
10
|
+
updatedAt: z.string(),
|
|
11
|
+
})
|
|
12
|
+
.strict()
|
|
13
|
+
.readonly();
|
|
14
|
+
export function templateById(db, id, version) {
|
|
15
|
+
const row = db
|
|
16
|
+
.prepare(`${select} WHERE t.id=? AND r.version=${version === undefined ? "t.head_version" : "?"}`)
|
|
17
|
+
.get(...(version === undefined ? [id] : [id, version]));
|
|
18
|
+
if (row === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
if (typeof row.document !== "string")
|
|
21
|
+
throw new Error("Template document is missing");
|
|
22
|
+
return projectTemplateSchema.parse({ ...row, document: JSON.parse(row.document) });
|
|
23
|
+
}
|
|
24
|
+
export function templateSummaries(db) {
|
|
25
|
+
return db
|
|
26
|
+
.prepare(`${select} WHERE r.version=t.head_version ORDER BY lower(r.name),t.id`)
|
|
27
|
+
.all()
|
|
28
|
+
.map((row) => {
|
|
29
|
+
const parsed = templateSummarySchema.parse({
|
|
30
|
+
id: row.id,
|
|
31
|
+
name: row.name,
|
|
32
|
+
version: row.version,
|
|
33
|
+
createdAt: row.createdAt,
|
|
34
|
+
updatedAt: row.updatedAt,
|
|
35
|
+
});
|
|
36
|
+
return parsed;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export function insertTemplateRevision(db, template) {
|
|
40
|
+
db.prepare("INSERT INTO project_template_revisions(template_id,version,name,document_json,created_at) VALUES (?,?,?,?,?)").run(template.id, template.version, template.name, JSON.stringify(template.document), template.updatedAt);
|
|
41
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { playDraftDocumentSchema } from "../play-drafts/schema.js";
|
|
3
|
+
export const templateCreateSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
id: z.uuid(),
|
|
6
|
+
name: z.string().trim().min(1).max(200),
|
|
7
|
+
document: playDraftDocumentSchema,
|
|
8
|
+
})
|
|
9
|
+
.strict()
|
|
10
|
+
.readonly();
|
|
11
|
+
export const templateUpdateSchema = templateCreateSchema
|
|
12
|
+
.unwrap()
|
|
13
|
+
.extend({
|
|
14
|
+
baseVersion: z.number().int().positive(),
|
|
15
|
+
mutationId: z.uuid(),
|
|
16
|
+
})
|
|
17
|
+
.strict()
|
|
18
|
+
.readonly();
|
|
19
|
+
export const templateDeleteSchema = z
|
|
20
|
+
.object({ id: z.uuid(), baseVersion: z.number().int().positive() })
|
|
21
|
+
.strict()
|
|
22
|
+
.readonly();
|
|
23
|
+
export const templateInstantiateSchema = z
|
|
24
|
+
.object({ templateId: z.uuid(), id: z.uuid(), version: z.number().int().positive() })
|
|
25
|
+
.strict()
|
|
26
|
+
.readonly();
|
|
27
|
+
export const projectTemplateSchema = z
|
|
28
|
+
.object({
|
|
29
|
+
id: z.uuid(),
|
|
30
|
+
name: z.string(),
|
|
31
|
+
version: z.number().int().positive(),
|
|
32
|
+
createdAt: z.string(),
|
|
33
|
+
updatedAt: z.string(),
|
|
34
|
+
document: playDraftDocumentSchema,
|
|
35
|
+
})
|
|
36
|
+
.strict()
|
|
37
|
+
.readonly();
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { requestHash } from "../play-drafts/repo.js";
|
|
4
|
+
import { createDraft, readDraft } from "../play-drafts/service.js";
|
|
5
|
+
import { insertTemplateRevision, templateById, templateSummaries } from "./repo.js";
|
|
6
|
+
import { templateCreateSchema, templateDeleteSchema, templateInstantiateSchema, templateUpdateSchema, } from "./schema.js";
|
|
7
|
+
import { freshTemplateDraft, templateSetup } from "./setup.js";
|
|
8
|
+
export function listTemplates(deps) {
|
|
9
|
+
return templateSummaries(deps.db);
|
|
10
|
+
}
|
|
11
|
+
export function readTemplate(deps, id, version) {
|
|
12
|
+
if (!z.uuid().safeParse(id).success ||
|
|
13
|
+
(version !== undefined && !z.number().int().positive().safeParse(version).success))
|
|
14
|
+
return { ok: false, reason: "invalid-input" };
|
|
15
|
+
const value = templateById(deps.db, id, version);
|
|
16
|
+
return value ? { ok: true, value } : { ok: false, reason: "not-found" };
|
|
17
|
+
}
|
|
18
|
+
export function createTemplate(deps, input) {
|
|
19
|
+
const parsed = templateCreateSchema.safeParse(input);
|
|
20
|
+
if (!parsed.success)
|
|
21
|
+
return { ok: false, reason: "invalid-input" };
|
|
22
|
+
return transact(deps.db, () => {
|
|
23
|
+
const { id, name, document } = parsed.data;
|
|
24
|
+
const hash = requestHash(parsed.data);
|
|
25
|
+
const old = deps.db.prepare("SELECT creation_hash FROM project_templates WHERE id=?").get(id);
|
|
26
|
+
if (old)
|
|
27
|
+
return old.creation_hash === hash
|
|
28
|
+
? readTemplate(deps, id, 1)
|
|
29
|
+
: { ok: false, reason: "conflict" };
|
|
30
|
+
const setup = templateSetup(deps, document);
|
|
31
|
+
if (!setup.ok)
|
|
32
|
+
return setup;
|
|
33
|
+
const at = deps.clock.now().toISOString();
|
|
34
|
+
const value = {
|
|
35
|
+
id,
|
|
36
|
+
name,
|
|
37
|
+
document: setup.value,
|
|
38
|
+
version: 1,
|
|
39
|
+
createdAt: at,
|
|
40
|
+
updatedAt: at,
|
|
41
|
+
};
|
|
42
|
+
deps.db
|
|
43
|
+
.prepare("INSERT INTO project_templates(id,head_version,creation_hash,created_at) VALUES (?,1,?,?)")
|
|
44
|
+
.run(id, hash, at);
|
|
45
|
+
insertTemplateRevision(deps.db, value);
|
|
46
|
+
return { ok: true, value };
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export function updateTemplate(deps, input) {
|
|
50
|
+
const parsed = templateUpdateSchema.safeParse(input);
|
|
51
|
+
if (!parsed.success)
|
|
52
|
+
return { ok: false, reason: "invalid-input" };
|
|
53
|
+
return transact(deps.db, () => {
|
|
54
|
+
const { id, baseVersion, mutationId, name, document } = parsed.data;
|
|
55
|
+
const previous = templateById(deps.db, id);
|
|
56
|
+
if (!previous)
|
|
57
|
+
return { ok: false, reason: "not-found" };
|
|
58
|
+
const row = deps.db
|
|
59
|
+
.prepare("SELECT mutation_id,mutation_hash FROM project_templates WHERE id=?")
|
|
60
|
+
.get(id);
|
|
61
|
+
const hash = requestHash(parsed.data);
|
|
62
|
+
if (row?.mutation_id === mutationId)
|
|
63
|
+
return row.mutation_hash === hash
|
|
64
|
+
? { ok: true, value: previous }
|
|
65
|
+
: { ok: false, reason: "conflict" };
|
|
66
|
+
if (previous.version !== baseVersion)
|
|
67
|
+
return { ok: false, reason: "conflict" };
|
|
68
|
+
const setup = templateSetup(deps, document);
|
|
69
|
+
if (!setup.ok)
|
|
70
|
+
return setup;
|
|
71
|
+
const value = {
|
|
72
|
+
...previous,
|
|
73
|
+
name,
|
|
74
|
+
document: setup.value,
|
|
75
|
+
version: baseVersion + 1,
|
|
76
|
+
updatedAt: deps.clock.now().toISOString(),
|
|
77
|
+
};
|
|
78
|
+
insertTemplateRevision(deps.db, value);
|
|
79
|
+
deps.db
|
|
80
|
+
.prepare("UPDATE project_templates SET head_version=?,mutation_id=?,mutation_hash=? WHERE id=? AND head_version=?")
|
|
81
|
+
.run(value.version, mutationId, hash, id, baseVersion);
|
|
82
|
+
return { ok: true, value };
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
export function deleteTemplate(deps, input) {
|
|
86
|
+
const parsed = templateDeleteSchema.safeParse(input);
|
|
87
|
+
if (!parsed.success)
|
|
88
|
+
return { ok: false, reason: "invalid-input" };
|
|
89
|
+
return transact(deps.db, () => {
|
|
90
|
+
const previous = templateById(deps.db, parsed.data.id);
|
|
91
|
+
if (!previous)
|
|
92
|
+
return { ok: false, reason: "not-found" };
|
|
93
|
+
if (previous.version !== parsed.data.baseVersion)
|
|
94
|
+
return { ok: false, reason: "conflict" };
|
|
95
|
+
if (deps.db
|
|
96
|
+
.prepare("SELECT 1 FROM schedules WHERE template_id=? AND deleted_at IS NULL LIMIT 1")
|
|
97
|
+
.get(parsed.data.id))
|
|
98
|
+
return { ok: false, reason: "referenced-by-schedule" };
|
|
99
|
+
deps.db.prepare("DELETE FROM project_templates WHERE id=?").run(parsed.data.id);
|
|
100
|
+
return { ok: true, value: { deleted: true } };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export function instantiateTemplate(deps, input) {
|
|
104
|
+
const parsed = templateInstantiateSchema.safeParse(input);
|
|
105
|
+
if (!parsed.success)
|
|
106
|
+
return { ok: false, reason: "invalid-input" };
|
|
107
|
+
return transact(deps.db, () => {
|
|
108
|
+
const { templateId, id, version } = parsed.data;
|
|
109
|
+
const receipt = deps.db
|
|
110
|
+
.prepare("SELECT template_id,template_version FROM project_template_instantiations WHERE draft_id=?")
|
|
111
|
+
.get(id);
|
|
112
|
+
if (receipt) {
|
|
113
|
+
if (receipt.template_id !== templateId || receipt.template_version !== version)
|
|
114
|
+
return { ok: false, reason: "conflict" };
|
|
115
|
+
const draft = readDraft(deps, id);
|
|
116
|
+
return draft.ok ? draft : { ok: false, reason: "conflict" };
|
|
117
|
+
}
|
|
118
|
+
const template = templateById(deps.db, templateId, version);
|
|
119
|
+
if (!template)
|
|
120
|
+
return { ok: false, reason: "not-found" };
|
|
121
|
+
const result = createDraft(deps, {
|
|
122
|
+
id,
|
|
123
|
+
document: freshTemplateDraft(deps, template.document, { id: templateId, version }),
|
|
124
|
+
});
|
|
125
|
+
if (!result.ok)
|
|
126
|
+
return { ok: false, reason: "conflict" };
|
|
127
|
+
deps.db.prepare("UPDATE play_draft_attachments SET status='reattach' WHERE draft_id=?").run(id);
|
|
128
|
+
deps.db
|
|
129
|
+
.prepare("INSERT INTO project_template_instantiations(draft_id,template_id,template_version) VALUES (?,?,?)")
|
|
130
|
+
.run(id, templateId, version);
|
|
131
|
+
const draft = readDraft(deps, id);
|
|
132
|
+
if (!draft.ok)
|
|
133
|
+
throw new Error("Created template draft could not be read");
|
|
134
|
+
return draft;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { snapshotEntry, snapshotPrompt } from "../library/snapshot.js";
|
|
2
|
+
export function templateSetup(deps, input) {
|
|
3
|
+
const prompts = [];
|
|
4
|
+
const entries = [];
|
|
5
|
+
const choices = [
|
|
6
|
+
{ kind: "article", name: input.form.articlePrompt },
|
|
7
|
+
...input.form.imagePrompts.map((row) => ({ kind: "image", name: row.name })),
|
|
8
|
+
{ kind: "thumbnail", name: input.form.thumbnailPrompt },
|
|
9
|
+
];
|
|
10
|
+
for (const choice of choices) {
|
|
11
|
+
if (choice.name === "")
|
|
12
|
+
continue;
|
|
13
|
+
const row = snapshotPrompt(deps.db, input.librarySnapshot, choice.kind, choice.name);
|
|
14
|
+
if (!row)
|
|
15
|
+
return { ok: false, reason: "missing-prompt" };
|
|
16
|
+
if (!prompts.some((saved) => saved.kind === row.kind && saved.name === row.name))
|
|
17
|
+
prompts.push(row);
|
|
18
|
+
}
|
|
19
|
+
for (const category of ["intro", "outro"]) {
|
|
20
|
+
if (input.form[category] === "")
|
|
21
|
+
continue;
|
|
22
|
+
const row = snapshotEntry(deps.db, input.librarySnapshot, category, input.form[category]);
|
|
23
|
+
if (!row)
|
|
24
|
+
return { ok: false, reason: "missing-prompt" };
|
|
25
|
+
entries.push(row);
|
|
26
|
+
}
|
|
27
|
+
const { templateSource: _source, ...document } = input;
|
|
28
|
+
return {
|
|
29
|
+
ok: true,
|
|
30
|
+
value: { ...document, fontUpload: null, librarySnapshot: { prompts, entries } },
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function freshTemplateDraft(deps, document, source) {
|
|
34
|
+
const provided = document.form.provided;
|
|
35
|
+
const fresh = (file) => ({
|
|
36
|
+
attachmentId: deps.uuid(),
|
|
37
|
+
name: file.name,
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
...document,
|
|
41
|
+
section: "content",
|
|
42
|
+
fontUpload: null,
|
|
43
|
+
templateSource: source,
|
|
44
|
+
variants: document.variants.map((variant) => ({ ...variant, id: deps.uuid() })),
|
|
45
|
+
form: {
|
|
46
|
+
...document.form,
|
|
47
|
+
provided: {
|
|
48
|
+
...provided,
|
|
49
|
+
audio: provided.audio === null ? null : fresh(provided.audio),
|
|
50
|
+
thumbnail: provided.thumbnail === null ? null : fresh(provided.thumbnail),
|
|
51
|
+
images: provided.images.map(fresh),
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|