@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
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { splitText } from "../../kernel/ports/text.js";
|
|
4
|
+
import { insertPiece, piecesOf } from "../../kernel/runner/piece-repo.js";
|
|
5
|
+
const payload = z.object({ text: z.string(), file: z.string().optional() });
|
|
6
|
+
// Old whole-text plans can exceed a newly known account limit. Preserve finished
|
|
7
|
+
// pieces and their paths, and split only unfinished work before any request starts.
|
|
8
|
+
export function planLegacyNarration(deps, stageId, texts, maxCharacters, finished) {
|
|
9
|
+
const old = piecesOf(deps.db, stageId, "chunk");
|
|
10
|
+
const source = old.length
|
|
11
|
+
? old
|
|
12
|
+
: texts.map((text, idx) => ({
|
|
13
|
+
id: deps.ids.next(),
|
|
14
|
+
stageId,
|
|
15
|
+
kind: "chunk",
|
|
16
|
+
idx: idx + 1,
|
|
17
|
+
state: "pending",
|
|
18
|
+
payload: JSON.stringify({ text }),
|
|
19
|
+
}));
|
|
20
|
+
const planned = [];
|
|
21
|
+
for (const piece of source) {
|
|
22
|
+
const data = payload.parse(JSON.parse(piece.payload ?? "null"));
|
|
23
|
+
if (finished(piece)) {
|
|
24
|
+
planned.push({ ...piece, idx: planned.length + 1 });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
for (const [part, text] of splitText(data.text, maxCharacters).entries())
|
|
28
|
+
planned.push({
|
|
29
|
+
...piece,
|
|
30
|
+
id: part === 0 ? piece.id : deps.ids.next(),
|
|
31
|
+
idx: planned.length + 1,
|
|
32
|
+
state: "pending",
|
|
33
|
+
payload: JSON.stringify({ text }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (old.length &&
|
|
37
|
+
planned.length === old.length &&
|
|
38
|
+
planned.every((p, i) => p.id === old[i]?.id &&
|
|
39
|
+
payload.parse(JSON.parse(p.payload ?? "null")).text ===
|
|
40
|
+
payload.parse(JSON.parse(old[i]?.payload ?? "null")).text))
|
|
41
|
+
return old;
|
|
42
|
+
transact(deps.db, () => {
|
|
43
|
+
// Negative temporary indices avoid colliding with a retained completed piece.
|
|
44
|
+
deps.db
|
|
45
|
+
.prepare("UPDATE stage_pieces SET idx=-idx WHERE stage_id=? AND kind='chunk'")
|
|
46
|
+
.run(stageId);
|
|
47
|
+
const known = new Set(old.map((p) => p.id));
|
|
48
|
+
for (const piece of planned) {
|
|
49
|
+
if (known.has(piece.id))
|
|
50
|
+
deps.db
|
|
51
|
+
.prepare("UPDATE stage_pieces SET idx=?,state=?,payload=? WHERE id=?")
|
|
52
|
+
.run(piece.idx, piece.state, piece.payload, piece.id);
|
|
53
|
+
else
|
|
54
|
+
insertPiece(deps.db, piece);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return planned;
|
|
58
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export function observeNarration(store, projectId, key, label) {
|
|
1
|
+
export function observeNarration(store, projectId, key, label, origin) {
|
|
2
2
|
if (store === undefined)
|
|
3
3
|
return undefined;
|
|
4
4
|
let sink;
|
|
5
5
|
return (event) => {
|
|
6
6
|
switch (event.type) {
|
|
7
7
|
case "start":
|
|
8
|
-
sink = store.begin(projectId, key, label);
|
|
8
|
+
sink = store.begin(projectId, key, label, origin);
|
|
9
9
|
break;
|
|
10
10
|
case "chunk":
|
|
11
11
|
sink?.append(event.bytes);
|
|
@@ -1,58 +1,52 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { transact } from "../../kernel/db/tx.js";
|
|
3
1
|
import { splitText } from "../../kernel/ports/text.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
...piece,
|
|
30
|
-
id: part === 0 ? piece.id : deps.ids.next(),
|
|
31
|
-
idx: planned.length + 1,
|
|
32
|
-
state: "pending",
|
|
33
|
-
payload: JSON.stringify({ text }),
|
|
2
|
+
import { fingerprint } from "../../kernel/runner/work.js";
|
|
3
|
+
export function normalizeNarrationText(text) {
|
|
4
|
+
return text.replace(/\r\n?/g, "\n").trim();
|
|
5
|
+
}
|
|
6
|
+
export function narrationRequestFingerprint(input) {
|
|
7
|
+
return fingerprint([
|
|
8
|
+
"narration-request-v1",
|
|
9
|
+
input.provider,
|
|
10
|
+
input.model,
|
|
11
|
+
input.voice,
|
|
12
|
+
input.text,
|
|
13
|
+
null,
|
|
14
|
+
input.segment,
|
|
15
|
+
input.wholeText === null ? null : fingerprint(["whole-v1", input.wholeText]),
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
export function planNarration(input) {
|
|
19
|
+
return input.groups.flatMap((group) => {
|
|
20
|
+
const text = normalizeNarrationText(group.text);
|
|
21
|
+
return splitText(text, input.maxCharacters).map((part, index) => {
|
|
22
|
+
const requestFingerprint = narrationRequestFingerprint({
|
|
23
|
+
...input,
|
|
24
|
+
text: part,
|
|
25
|
+
segment: group.segment,
|
|
26
|
+
wholeText: group.wholeRequest ? text : null,
|
|
34
27
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
deps.db
|
|
51
|
-
.prepare("UPDATE stage_pieces SET idx=?,state=?,payload=? WHERE id=?")
|
|
52
|
-
.run(piece.idx, piece.state, piece.payload, piece.id);
|
|
53
|
-
else
|
|
54
|
-
insertPiece(deps.db, piece);
|
|
55
|
-
}
|
|
28
|
+
const workFingerprint = fingerprint([requestFingerprint, group.regenerationToken]);
|
|
29
|
+
const retained = group.regenerationToken === null
|
|
30
|
+
? input.retained.find((one) => one.available && one.requestFingerprint === requestFingerprint)
|
|
31
|
+
: input.retained.find((one) => one.available && one.fingerprint === workFingerprint);
|
|
32
|
+
return {
|
|
33
|
+
key: `${group.key}:${index + 1}`,
|
|
34
|
+
logicalKey: group.key,
|
|
35
|
+
logicalText: text,
|
|
36
|
+
segment: group.segment,
|
|
37
|
+
text: part,
|
|
38
|
+
requestFingerprint,
|
|
39
|
+
fingerprint: workFingerprint,
|
|
40
|
+
assetId: retained?.assetId ?? null,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
56
43
|
});
|
|
57
|
-
|
|
44
|
+
}
|
|
45
|
+
export function narrationRegenerationToken(tokens, logicalKey, segment) {
|
|
46
|
+
return tokens[logicalKey] ?? tokens[`audio:${segment}:future`] ?? null;
|
|
47
|
+
}
|
|
48
|
+
export function narrationRegenerationKey(key) {
|
|
49
|
+
return /^audio:(body:.+|intro|outro):[0-9]+$/.test(key)
|
|
50
|
+
? key.slice(0, key.lastIndexOf(":"))
|
|
51
|
+
: key;
|
|
58
52
|
}
|
|
@@ -10,8 +10,8 @@ import { insertOutput, outputsOf } from "../storage/repo.js";
|
|
|
10
10
|
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
11
11
|
import { chunkNarration, defaultChunking } from "./chunk.js";
|
|
12
12
|
import { joinNarration } from "./concat.js";
|
|
13
|
+
import { planLegacyNarration } from "./legacy-plan.js";
|
|
13
14
|
import { observeNarration } from "./live.js";
|
|
14
|
-
import { planNarration } from "./plan.js";
|
|
15
15
|
// An empty narration source fails the stage immediately with no retries. Thrown from the
|
|
16
16
|
// slice rather than from a provider call, so the attempt wrapper never sees it.
|
|
17
17
|
export const nothingToNarrate = "nothing to narrate";
|
|
@@ -53,15 +53,19 @@ export async function runNarration(deps, context, providers) {
|
|
|
53
53
|
if (texts.length === 0) {
|
|
54
54
|
throw new Error(nothingToNarrate);
|
|
55
55
|
}
|
|
56
|
-
const planned =
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
56
|
+
const planned = planLegacyNarration(deps, context.stage.id, texts, deps.maxCharacters?.(choice.provider, choice.model) ?? 1000000, (piece) => finished(deps, projectId, piece) !== undefined);
|
|
57
|
+
const result = await speakChunks(deps, context, providers, choice, planned);
|
|
58
|
+
if (!result.ok)
|
|
59
|
+
return "held";
|
|
60
|
+
await storeBody(deps, context, choice, result.value, outputs);
|
|
61
|
+
if ((await speakSegments(deps, context, providers, choice, outputs)) === "held")
|
|
62
|
+
return "held";
|
|
60
63
|
deps.log.write("info", "narration.done", {
|
|
61
64
|
projectId,
|
|
62
65
|
stage: "audio",
|
|
63
66
|
detail: `${String(texts.length)} chunks in ${chunking.mode} mode`,
|
|
64
67
|
});
|
|
68
|
+
return "done";
|
|
65
69
|
}
|
|
66
70
|
// The narration source: the plain-text article, written by the article stage or pasted in
|
|
67
71
|
// by a user whose article stage was set to Provide.
|
|
@@ -95,7 +99,11 @@ async function speakChunks(deps, context, providers, choice, pieces) {
|
|
|
95
99
|
voiceId: choice.voice,
|
|
96
100
|
text: carried.text,
|
|
97
101
|
}, observeNarration(deps.audioPreviews, projectId, piece.id, total === 1 ? "Body" : `Body part ${piece.idx} of ${total}`));
|
|
98
|
-
|
|
102
|
+
if (!spoken.ok) {
|
|
103
|
+
setPiece(deps.db, piece.id, "pending", piece.payload);
|
|
104
|
+
return spoken;
|
|
105
|
+
}
|
|
106
|
+
write(deps, projectId, file, spoken.value.bytes);
|
|
99
107
|
// The file is on disk before the row says so: a crash between the two leaves a
|
|
100
108
|
// file the reconcile collects, where the other order would leave a `done` chunk
|
|
101
109
|
// whose audio never existed and a concat that fails on every retry.
|
|
@@ -114,11 +122,13 @@ async function speakChunks(deps, context, providers, choice, pieces) {
|
|
|
114
122
|
const files = [];
|
|
115
123
|
for (const outcome of outcomes) {
|
|
116
124
|
if (!outcome.ok) {
|
|
125
|
+
if ("reason" in outcome)
|
|
126
|
+
return outcome;
|
|
117
127
|
throw outcome.error;
|
|
118
128
|
}
|
|
119
129
|
files.push(outcome.file);
|
|
120
130
|
}
|
|
121
|
-
return files;
|
|
131
|
+
return { ok: true, value: files };
|
|
122
132
|
}
|
|
123
133
|
// One body file, in chunk order, with its measured duration on the row.
|
|
124
134
|
async function storeBody(deps, context, choice, files, outputs) {
|
|
@@ -166,8 +176,10 @@ async function speakSegments(deps, context, providers, choice, outputs) {
|
|
|
166
176
|
voiceId: choice.voice,
|
|
167
177
|
text,
|
|
168
178
|
}, observeNarration(deps.audioPreviews, projectId, piece.id, segment.category === "intro" ? "Intro" : "Outro"));
|
|
179
|
+
if (!spoken.ok)
|
|
180
|
+
return "held";
|
|
169
181
|
const name = outputFileName(role, 1, ".mp3", "audio");
|
|
170
|
-
write(deps, projectId, name, spoken.bytes);
|
|
182
|
+
write(deps, projectId, name, spoken.value.bytes);
|
|
171
183
|
// Measured the same way the body is, because the render adds all three and the gaps to
|
|
172
184
|
// get the length of the video.
|
|
173
185
|
const durationMs = await probeDurationMs(deps.ffmpeg, outputPath(deps.paths, projectId, name), context.signal, deps.log);
|
|
@@ -175,6 +187,7 @@ async function speakSegments(deps, context, providers, choice, outputs) {
|
|
|
175
187
|
store(deps, projectId, role, name, durationMs, choice);
|
|
176
188
|
counted(deps, segment.category, durationMs, choice);
|
|
177
189
|
}
|
|
190
|
+
return "done";
|
|
178
191
|
}
|
|
179
192
|
// The segment pieces belong to the *article* stage, which is where they are written, so
|
|
180
193
|
// they are read under its id and never under this stage's own. They stay there because the
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { numberPerPromptMax } from "../admission/rules.js";
|
|
3
|
+
import { runDraftSchema } from "../admission/schema.js";
|
|
4
|
+
import { defaultSubtitles } from "../subtitles/model.js";
|
|
5
|
+
export function toAdmissionDraft(input) {
|
|
6
|
+
const { form } = input.document;
|
|
7
|
+
const fields = [];
|
|
8
|
+
const sources = {
|
|
9
|
+
...form.sources,
|
|
10
|
+
...(form.sources.article === "provide" ? { research: "off" } : {}),
|
|
11
|
+
...(form.sources.images === "off" ? { video: "off" } : {}),
|
|
12
|
+
};
|
|
13
|
+
const number = (raw, field, max, min = 1) => {
|
|
14
|
+
const parsed = z
|
|
15
|
+
.number()
|
|
16
|
+
.int()
|
|
17
|
+
.min(min)
|
|
18
|
+
.max(max)
|
|
19
|
+
.safeParse(raw.trim() === "" ? Number.NaN : Number(raw));
|
|
20
|
+
if (parsed.success)
|
|
21
|
+
return parsed.data;
|
|
22
|
+
fields.push({ field, message: `Enter a whole number between ${min} and ${max}.` });
|
|
23
|
+
return min;
|
|
24
|
+
};
|
|
25
|
+
const entry = (category) => {
|
|
26
|
+
const name = form[category];
|
|
27
|
+
if (sources.audio !== "generate" || name === "")
|
|
28
|
+
return undefined;
|
|
29
|
+
const saved = input.entries.find((candidate) => candidate.category === category && candidate.name.toLowerCase() === name.toLowerCase());
|
|
30
|
+
if (saved)
|
|
31
|
+
return { name: saved.name, mode: saved.mode };
|
|
32
|
+
fields.push({
|
|
33
|
+
field: category,
|
|
34
|
+
message: `That ${category} entry no longer exists; pick another.`,
|
|
35
|
+
});
|
|
36
|
+
return undefined;
|
|
37
|
+
};
|
|
38
|
+
const file = (ref, kind, field) => {
|
|
39
|
+
const attachment = input.attachments.find((item) => item.id === ref?.attachmentId && item.kind === kind && item.name === ref.name);
|
|
40
|
+
if (attachment?.state === "ready" && attachment.stagedFileId !== null)
|
|
41
|
+
return attachment.stagedFileId;
|
|
42
|
+
fields.push({
|
|
43
|
+
field,
|
|
44
|
+
message: attachment?.state === "copying" || attachment?.state === "pending"
|
|
45
|
+
? "This upload is still copying."
|
|
46
|
+
: "That upload is no longer available; pick the file again.",
|
|
47
|
+
});
|
|
48
|
+
return undefined;
|
|
49
|
+
};
|
|
50
|
+
const mode = sources.audio === "off"
|
|
51
|
+
? "off"
|
|
52
|
+
: sources.video === "off" && form.subtitles.mode === "burn-in"
|
|
53
|
+
? "files"
|
|
54
|
+
: form.subtitles.mode;
|
|
55
|
+
const chunkMode = sources.audio === "generate" ? form.chunking.mode : "whole";
|
|
56
|
+
const draft = {
|
|
57
|
+
...(form.checkpoints === undefined ? {} : { checkpoints: form.checkpoints }),
|
|
58
|
+
title: form.title,
|
|
59
|
+
format: form.format,
|
|
60
|
+
sources,
|
|
61
|
+
llm: form.llm,
|
|
62
|
+
audio: sources.audio === "generate" ? form.audio : undefined,
|
|
63
|
+
images: form.images,
|
|
64
|
+
articlePrompt: sources.article === "generate" ? form.articlePrompt : undefined,
|
|
65
|
+
imagePrompts: sources.images === "generate"
|
|
66
|
+
? form.imagePrompts.map((prompt, index) => ({
|
|
67
|
+
name: prompt.name,
|
|
68
|
+
number: number(prompt.number, `imagePrompts.${index}.number`, numberPerPromptMax),
|
|
69
|
+
}))
|
|
70
|
+
: [],
|
|
71
|
+
thumbnailPrompt: ["from_prompt", "prompt_by_llm"].includes(sources.thumbnail)
|
|
72
|
+
? form.thumbnailPrompt
|
|
73
|
+
: undefined,
|
|
74
|
+
intro: entry("intro"),
|
|
75
|
+
outro: entry("outro"),
|
|
76
|
+
values: form.values,
|
|
77
|
+
provided: {
|
|
78
|
+
research: sources.research === "provide" ? form.provided.research : undefined,
|
|
79
|
+
article: sources.article === "provide" ? form.provided.article : undefined,
|
|
80
|
+
audio: sources.audio === "provide"
|
|
81
|
+
? file(form.provided.audio, "audio", "provided.audio")
|
|
82
|
+
: undefined,
|
|
83
|
+
thumbnail: sources.thumbnail === "provide"
|
|
84
|
+
? file(form.provided.thumbnail, "thumbnail", "provided.thumbnail")
|
|
85
|
+
: undefined,
|
|
86
|
+
images: sources.images === "provide"
|
|
87
|
+
? form.provided.images.flatMap((ref, index) => {
|
|
88
|
+
const id = file(ref, "images", `provided.images.${index}`);
|
|
89
|
+
return id === undefined ? [] : [id];
|
|
90
|
+
})
|
|
91
|
+
: [],
|
|
92
|
+
},
|
|
93
|
+
chunking: {
|
|
94
|
+
mode: chunkMode,
|
|
95
|
+
...(chunkMode === "words"
|
|
96
|
+
? { words: number(form.chunking.words, "chunking.words", 10000) }
|
|
97
|
+
: {}),
|
|
98
|
+
...(chunkMode === "characters"
|
|
99
|
+
? { characters: number(form.chunking.characters, "chunking.characters", 1000000) }
|
|
100
|
+
: {}),
|
|
101
|
+
},
|
|
102
|
+
subtitles: mode === "off"
|
|
103
|
+
? defaultSubtitles
|
|
104
|
+
: {
|
|
105
|
+
...form.subtitles,
|
|
106
|
+
mode,
|
|
107
|
+
fontSize: number(form.subtitles.fontSize, "subtitles.fontSize", 120, 16),
|
|
108
|
+
},
|
|
109
|
+
silenceGapSeconds: input.silenceGapSeconds,
|
|
110
|
+
};
|
|
111
|
+
const parsed = runDraftSchema.safeParse(draft);
|
|
112
|
+
if (!parsed.success)
|
|
113
|
+
fields.push(...parsed.error.issues.map((issue) => ({
|
|
114
|
+
field: issue.path.join("."),
|
|
115
|
+
message: issue.message,
|
|
116
|
+
})));
|
|
117
|
+
return fields.length || !parsed.success
|
|
118
|
+
? { ok: false, fields }
|
|
119
|
+
: { ok: true, draft: parsed.data };
|
|
120
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { modelFields } from "../../catalog/validate.js";
|
|
2
|
+
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
3
|
+
import { cliPathStatus } from "../settings/cli-paths.js";
|
|
4
|
+
import { hasKey, listVoices } from "../settings/repo.js";
|
|
5
|
+
function choices(runs) {
|
|
6
|
+
const selected = runs
|
|
7
|
+
.flatMap(({ draft: d }) => [
|
|
8
|
+
{
|
|
9
|
+
field: "llm",
|
|
10
|
+
family: "llm",
|
|
11
|
+
choice: d.llm,
|
|
12
|
+
needed: d.sources.research === "generate" ||
|
|
13
|
+
d.sources.article === "generate" ||
|
|
14
|
+
d.sources.thumbnail === "prompt_by_llm" ||
|
|
15
|
+
d.intro?.mode === "llm" ||
|
|
16
|
+
d.outro?.mode === "llm",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
field: "audio",
|
|
20
|
+
family: "tts",
|
|
21
|
+
choice: d.audio,
|
|
22
|
+
needed: d.sources.audio === "generate",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
field: "images",
|
|
26
|
+
family: "image",
|
|
27
|
+
choice: d.images,
|
|
28
|
+
needed: d.sources.images === "generate" ||
|
|
29
|
+
["from_prompt", "prompt_by_llm"].includes(d.sources.thumbnail),
|
|
30
|
+
},
|
|
31
|
+
])
|
|
32
|
+
.flatMap((c) => (c.needed && c.choice ? [{ ...c, ...c.choice }] : []));
|
|
33
|
+
return [...new Map(selected.map((c) => [`${c.family}:${c.provider}:${c.model}`, c])).values()];
|
|
34
|
+
}
|
|
35
|
+
export async function checkDraftReadiness(deps, runs) {
|
|
36
|
+
const selected = choices(runs);
|
|
37
|
+
if (selected.length === 0)
|
|
38
|
+
return { fields: [], providers: [] };
|
|
39
|
+
const providers = await deps.providers();
|
|
40
|
+
const fields = [];
|
|
41
|
+
for (const c of selected) {
|
|
42
|
+
const provider = providers.find((p) => p.id === c.provider && p.family === c.family);
|
|
43
|
+
if (!provider || !readinessIsUsable(provider.readiness)) {
|
|
44
|
+
fields.push({
|
|
45
|
+
field: c.field,
|
|
46
|
+
message: provider?.readiness.kind === "cli" && provider.readiness.issue
|
|
47
|
+
? provider.readiness.issue
|
|
48
|
+
: "Configure this provider before starting.",
|
|
49
|
+
});
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const models = await deps.modelsFor(c.provider, c.family);
|
|
53
|
+
if (!models.some((m) => m.id === c.model))
|
|
54
|
+
fields.push({
|
|
55
|
+
field: `${c.field}.model`,
|
|
56
|
+
message: "Choose an available model before starting.",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return { fields: [...fields, ...localDraftReadiness(deps, runs, providers)], providers };
|
|
60
|
+
}
|
|
61
|
+
export function localDraftReadiness(deps, runs, providers) {
|
|
62
|
+
const fields = runs.flatMap((run) => modelFields(run.draft, deps.catalogue));
|
|
63
|
+
for (const c of choices(runs)) {
|
|
64
|
+
const provider = providers.find((p) => p.id === c.provider && p.family === c.family);
|
|
65
|
+
if (provider?.cliPath !== undefined &&
|
|
66
|
+
cliPathStatus(deps.db, provider.id).command !== provider.cliPath.command)
|
|
67
|
+
fields.push({
|
|
68
|
+
field: `${c.field}.cliPath`,
|
|
69
|
+
message: "The configured CLI command changed. Check readiness again.",
|
|
70
|
+
});
|
|
71
|
+
if (provider?.readiness.kind === "keyed" && !hasKey(deps.db, provider.id))
|
|
72
|
+
fields.push({ field: c.field, message: "Save the provider key before starting." });
|
|
73
|
+
}
|
|
74
|
+
for (const { draft } of runs)
|
|
75
|
+
if (draft.sources.audio === "generate" &&
|
|
76
|
+
draft.audio &&
|
|
77
|
+
!listVoices(deps.db).some((v) => v.provider === draft.audio?.provider && v.voiceId === draft.audio.voice))
|
|
78
|
+
fields.push({ field: "audio.voice", message: "Choose a saved voice before starting." });
|
|
79
|
+
return fields;
|
|
80
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fingerprint } from "../../kernel/runner/work.js";
|
|
3
|
+
const rowSchema = z.object({
|
|
4
|
+
id: z.string(),
|
|
5
|
+
schema_version: z.number(),
|
|
6
|
+
version: z.number(),
|
|
7
|
+
title: z.string(),
|
|
8
|
+
document_json: z.string(),
|
|
9
|
+
creation_hash: z.string(),
|
|
10
|
+
save_mutation_id: z.string().nullable(),
|
|
11
|
+
save_request_hash: z.string().nullable(),
|
|
12
|
+
created_at: z.string(),
|
|
13
|
+
updated_at: z.string(),
|
|
14
|
+
state: z.enum(["active", "starting", "started"]),
|
|
15
|
+
review_id: z.string().nullable(),
|
|
16
|
+
review_json: z.string().nullable(),
|
|
17
|
+
review_fingerprint: z.string().nullable(),
|
|
18
|
+
start_id: z.string().nullable(),
|
|
19
|
+
});
|
|
20
|
+
const attachmentSchema = z.object({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
draft_id: z.string(),
|
|
23
|
+
staged_file_id: z.string().nullable(),
|
|
24
|
+
kind: z.enum(["audio", "images", "thumbnail"]),
|
|
25
|
+
original_filename: z.string(),
|
|
26
|
+
status: z.enum(["pending", "ready", "reattach"]),
|
|
27
|
+
error: z.string().nullable(),
|
|
28
|
+
});
|
|
29
|
+
export function draftRow(db, id) {
|
|
30
|
+
const row = db.prepare("SELECT * FROM play_drafts WHERE id=?").get(id);
|
|
31
|
+
return row === undefined ? undefined : rowSchema.parse(row);
|
|
32
|
+
}
|
|
33
|
+
export function draftRows(db) {
|
|
34
|
+
return db
|
|
35
|
+
.prepare("SELECT * FROM play_drafts WHERE state IN ('active','starting') ORDER BY updated_at DESC,id")
|
|
36
|
+
.all()
|
|
37
|
+
.map((row) => rowSchema.parse(row));
|
|
38
|
+
}
|
|
39
|
+
export function attachmentRow(db, id) {
|
|
40
|
+
const row = db.prepare("SELECT * FROM play_draft_attachments WHERE id=?").get(id);
|
|
41
|
+
return row === undefined ? undefined : attachmentSchema.parse(row);
|
|
42
|
+
}
|
|
43
|
+
export function attachmentRows(db, id) {
|
|
44
|
+
return db
|
|
45
|
+
.prepare("SELECT * FROM play_draft_attachments WHERE draft_id=? ORDER BY rowid")
|
|
46
|
+
.all(id)
|
|
47
|
+
.map((row) => attachmentSchema.parse(row));
|
|
48
|
+
}
|
|
49
|
+
export function attachmentRefs(document) {
|
|
50
|
+
const p = document.form.provided;
|
|
51
|
+
return [
|
|
52
|
+
...(p.audio === null ? [] : [{ ...p.audio, kind: "audio" }]),
|
|
53
|
+
...p.images.map((file) => ({ ...file, kind: "images" })),
|
|
54
|
+
...(p.thumbnail === null ? [] : [{ ...p.thumbnail, kind: "thumbnail" }]),
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
export function requestHash(value) {
|
|
58
|
+
return fingerprint(z.json().parse(JSON.parse(JSON.stringify(value))));
|
|
59
|
+
}
|
|
60
|
+
export function insertDraft(db, id, document, hash, at) {
|
|
61
|
+
db.prepare("INSERT INTO play_drafts (id,schema_version,version,title,document_json,creation_hash,created_at,updated_at,state) VALUES (?,1,1,?,?,?,?,?,'active')").run(id, document.form.title, JSON.stringify(document), hash, at, at);
|
|
62
|
+
}
|
|
63
|
+
export function insertAttachment(db, id, ref, status, stagedId = null) {
|
|
64
|
+
db.prepare("INSERT INTO play_draft_attachments (id,draft_id,kind,original_filename,status,staged_file_id) VALUES (?,?,?,?,?,?)").run(ref.attachmentId, id, ref.kind, ref.name, status, stagedId);
|
|
65
|
+
}
|
|
66
|
+
export function syncAttachments(db, id, document) {
|
|
67
|
+
const refs = attachmentRefs(document);
|
|
68
|
+
const retained = new Set(refs.map((ref) => ref.attachmentId));
|
|
69
|
+
for (const old of attachmentRows(db, id))
|
|
70
|
+
if (!retained.has(old.id))
|
|
71
|
+
db.prepare("DELETE FROM play_draft_attachments WHERE id=?").run(old.id);
|
|
72
|
+
// Removed staged bytes remain until reference-aware storage cleanup runs.
|
|
73
|
+
for (const ref of refs)
|
|
74
|
+
if (attachmentRow(db, ref.attachmentId) === undefined)
|
|
75
|
+
insertAttachment(db, id, ref, "pending");
|
|
76
|
+
}
|