@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,117 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { continuationLimit, continuationMessages } from "../article/continuation.js";
|
|
4
|
+
import { currentRevisionId, revisionById } from "../revisions/repo.js";
|
|
5
|
+
import { recipe } from "./recipe-model.js";
|
|
6
|
+
import { retainPartialArticle } from "./runtime-publication.js";
|
|
7
|
+
import { insertWorkPiece, workPieces } from "./work-records.js";
|
|
8
|
+
const answerSchema = z.object({
|
|
9
|
+
text: z.string(),
|
|
10
|
+
finishReason: z.string().nullable(),
|
|
11
|
+
usage: z.object({ inputTokens: z.number(), outputTokens: z.number() }).nullable(),
|
|
12
|
+
});
|
|
13
|
+
export async function executeArticleRequests(deps, context, providers, initial) {
|
|
14
|
+
if (initial.input.kind !== "llm")
|
|
15
|
+
throw new Error("Article generation needs a pinned LLM request.");
|
|
16
|
+
let piece = initial;
|
|
17
|
+
let text = "";
|
|
18
|
+
let inputTokens = 0;
|
|
19
|
+
let outputTokens = 0;
|
|
20
|
+
for (let part = 0; part <= continuationLimit; part += 1) {
|
|
21
|
+
if (piece.input.kind !== "llm")
|
|
22
|
+
throw new Error("An article continuation must contain an exact LLM request.");
|
|
23
|
+
const saved = deps.db
|
|
24
|
+
.prepare("SELECT result_json FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
25
|
+
.get(piece.id, context.work.workId);
|
|
26
|
+
let answer;
|
|
27
|
+
if (typeof saved?.result_json === "string")
|
|
28
|
+
answer = answerSchema.parse(JSON.parse(saved.result_json));
|
|
29
|
+
else {
|
|
30
|
+
const input = piece.input;
|
|
31
|
+
const result = await providers.forPiece(piece.id).llm({
|
|
32
|
+
provider: input.provider,
|
|
33
|
+
model: input.model,
|
|
34
|
+
messages: input.messages,
|
|
35
|
+
thinkingConfig: input.thinkingConfig,
|
|
36
|
+
...(input.thinking === null ? {} : { thinking: input.thinking }),
|
|
37
|
+
webSearch: input.webSearch,
|
|
38
|
+
previewLabel: part === 0 ? "Article" : `Continuation ${part}`,
|
|
39
|
+
check: (value) => value.text.trim() === ""
|
|
40
|
+
? "The article answered with nothing."
|
|
41
|
+
: part === continuationLimit && value.finishReason === "length"
|
|
42
|
+
? `The article was still unfinished after ${continuationLimit} continuations.`
|
|
43
|
+
: undefined,
|
|
44
|
+
}, (event) => {
|
|
45
|
+
if (event.type === "delta")
|
|
46
|
+
context.emit({
|
|
47
|
+
type: "article.delta",
|
|
48
|
+
projectId: context.work.projectId,
|
|
49
|
+
workPieceId: piece.id,
|
|
50
|
+
text: event.text,
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
if (!result.ok)
|
|
54
|
+
return result;
|
|
55
|
+
answer = result.value;
|
|
56
|
+
deps.db
|
|
57
|
+
.prepare("UPDATE revision_work_pieces SET result_json=? WHERE id=? AND work_id=? AND result_json IS NULL")
|
|
58
|
+
.run(JSON.stringify(answer), piece.id, context.work.workId);
|
|
59
|
+
}
|
|
60
|
+
text += answer.text;
|
|
61
|
+
inputTokens += answer.usage?.inputTokens ?? 0;
|
|
62
|
+
outputTokens += answer.usage?.outputTokens ?? 0;
|
|
63
|
+
if (answer.finishReason !== "length")
|
|
64
|
+
return {
|
|
65
|
+
ok: true,
|
|
66
|
+
value: { text, finishReason: answer.finishReason, usage: { inputTokens, outputTokens } },
|
|
67
|
+
};
|
|
68
|
+
retainPartialArticle(deps, context, piece, text);
|
|
69
|
+
const next = continuationPiece(deps, context, initial, part + 1, text);
|
|
70
|
+
if (next === undefined)
|
|
71
|
+
return { ok: false, reason: "held" };
|
|
72
|
+
piece = next;
|
|
73
|
+
}
|
|
74
|
+
throw new Error("The article exceeded its continuation limit.");
|
|
75
|
+
}
|
|
76
|
+
function continuationPiece(deps, context, initial, part, text) {
|
|
77
|
+
return transact(deps.db, () => {
|
|
78
|
+
const key = `article:continuation:${part}`;
|
|
79
|
+
const existing = workPieces(deps.db, context.work.workId).find((piece) => piece.key === key);
|
|
80
|
+
if (existing !== undefined)
|
|
81
|
+
return existing;
|
|
82
|
+
if (!context.maySubmit(initial.id) || initial.input.kind !== "llm")
|
|
83
|
+
return undefined;
|
|
84
|
+
const revision = revisionById(deps.db, context.work.projectId, context.work.revisionId);
|
|
85
|
+
const head = currentRevisionId(deps.db, context.work.projectId);
|
|
86
|
+
if (revision === undefined || head === undefined)
|
|
87
|
+
return undefined;
|
|
88
|
+
const anchor = deps.db
|
|
89
|
+
.prepare("SELECT logical_key,desired_fingerprint,work_key,fingerprint FROM revision_work_reservations WHERE revision_id=? AND work_id=? AND piece_id=?")
|
|
90
|
+
.get(head, context.work.workId, initial.id);
|
|
91
|
+
if (anchor === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
const value = recipe({ content: revision.content }, key, "article", {
|
|
94
|
+
...initial.input,
|
|
95
|
+
messages: continuationMessages(initial.input.messages, text),
|
|
96
|
+
}, [initial.key], { tokenKey: initial.key });
|
|
97
|
+
const piece = {
|
|
98
|
+
id: deps.ids.next(),
|
|
99
|
+
workId: context.work.workId,
|
|
100
|
+
key,
|
|
101
|
+
fingerprint: value.fingerprint,
|
|
102
|
+
requestFingerprint: value.requestFingerprint,
|
|
103
|
+
logicalFingerprint: value.logicalFingerprint,
|
|
104
|
+
input: value.input,
|
|
105
|
+
continuation: null,
|
|
106
|
+
generationToken: initial.generationToken,
|
|
107
|
+
state: "pending",
|
|
108
|
+
dispatchState: "allowed",
|
|
109
|
+
submittedAt: null,
|
|
110
|
+
};
|
|
111
|
+
insertWorkPiece(deps.db, piece);
|
|
112
|
+
deps.db
|
|
113
|
+
.prepare("INSERT INTO revision_work_reservations(project_id,revision_id,work_key,work_id,piece_id,fingerprint,logical_key,desired_fingerprint) VALUES (?,?,?,?,?,?,?,?)")
|
|
114
|
+
.run(context.work.projectId, head, key, context.work.workId, piece.id, piece.fingerprint, z.string().parse(anchor.logical_key ?? anchor.work_key), z.string().parse(anchor.desired_fingerprint ?? anchor.fingerprint));
|
|
115
|
+
return piece;
|
|
116
|
+
});
|
|
117
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { checkpointClosure, checkpointFingerprint } from "../checkpoints/fingerprint.js";
|
|
2
|
+
import { saveCheckpointSet } from "../checkpoints/repo.js";
|
|
3
|
+
import { currentRevisionId } from "../revisions/repo.js";
|
|
4
|
+
import { getRevisionView } from "../revisions/view.js";
|
|
5
|
+
import { executionCatalogue, executionPlan } from "./runtime-plan.js";
|
|
6
|
+
export function admitReviewedCheckpoints(deps, projectIds, reviewed, catalogue) {
|
|
7
|
+
return projectIds.flatMap((projectId, runIndex) => {
|
|
8
|
+
const gates = reviewed.filter((gate) => gate.runIndex === runIndex);
|
|
9
|
+
if (!gates.length)
|
|
10
|
+
return [];
|
|
11
|
+
const revisionId = currentRevisionId(deps.db, projectId);
|
|
12
|
+
const view = revisionId === undefined ? undefined : getRevisionView(deps, projectId, revisionId);
|
|
13
|
+
if (!view)
|
|
14
|
+
throw new Error("Checkpoint admission has no retained revision");
|
|
15
|
+
const plan = executionPlan(deps, view, executionCatalogue(catalogue, view.revision.config));
|
|
16
|
+
const checkpoints = gates.map((gate) => {
|
|
17
|
+
const work = deps.db
|
|
18
|
+
.prepare("SELECT id FROM revision_work WHERE project_id=? AND revision_id=? AND kind=? ORDER BY id LIMIT 1")
|
|
19
|
+
.get(projectId, view.revision.id, gate.stage);
|
|
20
|
+
if (typeof work?.id !== "string")
|
|
21
|
+
throw new Error("Checkpoint admission has no stage work");
|
|
22
|
+
return {
|
|
23
|
+
checkpointId: gate.checkpointId,
|
|
24
|
+
stage: gate.stage,
|
|
25
|
+
workId: work.id,
|
|
26
|
+
fingerprint: checkpointFingerprint(view.revision, checkpointClosure(gate.stage, plan.recipes)),
|
|
27
|
+
state: "held",
|
|
28
|
+
};
|
|
29
|
+
});
|
|
30
|
+
const saved = saveCheckpointSet(deps.db, {
|
|
31
|
+
projectId,
|
|
32
|
+
revisionId: view.revision.id,
|
|
33
|
+
checkpoints,
|
|
34
|
+
createdAt: deps.clock.now().toISOString(),
|
|
35
|
+
});
|
|
36
|
+
if (!saved.ok)
|
|
37
|
+
throw new Error("Checkpoint admission could not persist reviewed gates");
|
|
38
|
+
return saved.value.map((row) => {
|
|
39
|
+
const gate = gates.find((gate) => gate.checkpointId === row.checkpointId);
|
|
40
|
+
if (!gate)
|
|
41
|
+
throw new Error("Checkpoint admission lost its reviewed identity");
|
|
42
|
+
return { ...row, reviewedFingerprint: gate.fingerprint };
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { plainText } from "../article/plain.js";
|
|
4
|
+
import { splitEndMatter } from "../article/split.js";
|
|
5
|
+
import { outputPath } from "../storage/layout.js";
|
|
6
|
+
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
7
|
+
import { audioTimeline } from "../video/plan.js";
|
|
8
|
+
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
9
|
+
export function exportSnapshot(deps, context, piece) {
|
|
10
|
+
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
11
|
+
const row = deps.db
|
|
12
|
+
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
13
|
+
.get(context.work.workId);
|
|
14
|
+
if (view === undefined || row === undefined)
|
|
15
|
+
throw new Error("The export revision no longer exists.");
|
|
16
|
+
const plan = executionPlan(deps, view, savedCatalogue(row.recipe_context));
|
|
17
|
+
const recipe = plan.recipes.find((one) => one.key === piece.key && one.fingerprint === piece.fingerprint);
|
|
18
|
+
if (recipe === undefined || recipe.deferred || recipe.unresolved)
|
|
19
|
+
throw new Error("The export inputs do not match the admitted recipe.");
|
|
20
|
+
return { view, plan };
|
|
21
|
+
}
|
|
22
|
+
export async function revisionAudio(deps, context, view) {
|
|
23
|
+
const config = view.revision.config;
|
|
24
|
+
if (config.sources.audio === "off")
|
|
25
|
+
return [];
|
|
26
|
+
const input = async (role) => {
|
|
27
|
+
const row = view.outputs.find((one) => one.selected && one.available && one.state === "ready" && one.output.role === role);
|
|
28
|
+
if (row === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
const path = outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
31
|
+
const duration = row.output.durationMs ??
|
|
32
|
+
(await (deps.measureAudio?.(path, context.signal) ??
|
|
33
|
+
probeDurationMs(deps.ffmpeg, path, context.signal, deps.log)));
|
|
34
|
+
if (!Number.isFinite(duration) || duration <= 0)
|
|
35
|
+
throw new Error("The narration duration could not be measured.");
|
|
36
|
+
return { path, seconds: duration / 1000 };
|
|
37
|
+
};
|
|
38
|
+
const body = await input("audio_body");
|
|
39
|
+
if (body === undefined)
|
|
40
|
+
throw new Error("The revision has no complete narration.");
|
|
41
|
+
return audioTimeline({
|
|
42
|
+
body,
|
|
43
|
+
intro: config.sources.audio === "generate" && config.intro !== undefined
|
|
44
|
+
? await input("audio_intro")
|
|
45
|
+
: undefined,
|
|
46
|
+
outro: config.sources.audio === "generate" && config.outro !== undefined
|
|
47
|
+
? await input("audio_outro")
|
|
48
|
+
: undefined,
|
|
49
|
+
gapSeconds: config.silenceGapSeconds,
|
|
50
|
+
}, config.sources.video === "off" ? 1 / 48000 : 1 / 30);
|
|
51
|
+
}
|
|
52
|
+
export function revisionTranscript(deps, snapshot, kind) {
|
|
53
|
+
const { view, plan } = snapshot;
|
|
54
|
+
const concat = plan.recipes.find((one) => one.key === (kind === "body" ? "audio:body:concat" : `audio:${kind}`));
|
|
55
|
+
if (concat !== undefined) {
|
|
56
|
+
const texts = concat.dependsOn.map((key) => {
|
|
57
|
+
const row = view.pieces.find((one) => one.key === key && one.selected && one.available && one.piece.state === "done");
|
|
58
|
+
const payload = row?.piece.payload === null || row?.piece.payload === undefined
|
|
59
|
+
? undefined
|
|
60
|
+
: z.object({ text: z.string().optional() }).parse(JSON.parse(row.piece.payload));
|
|
61
|
+
if (payload?.text !== undefined)
|
|
62
|
+
return payload.text;
|
|
63
|
+
const recipe = plan.recipes.find((one) => one.key === key);
|
|
64
|
+
if (recipe?.input.kind === "tts")
|
|
65
|
+
return recipe.input.text;
|
|
66
|
+
if (recipe?.input.kind === "provided" &&
|
|
67
|
+
Array.isArray(recipe.input.semantic) &&
|
|
68
|
+
typeof recipe.input.semantic[0] === "string")
|
|
69
|
+
return recipe.input.semantic[0];
|
|
70
|
+
throw new Error("The saved narration transcript is incomplete.");
|
|
71
|
+
});
|
|
72
|
+
if (texts.length > 0)
|
|
73
|
+
return texts.join("\n");
|
|
74
|
+
}
|
|
75
|
+
if (kind === "body") {
|
|
76
|
+
const row = view.outputs.find((one) => one.selected && one.available && one.state === "ready" && one.output.role === "article_txt");
|
|
77
|
+
if (row !== undefined)
|
|
78
|
+
return readFileSync(outputPath(deps.paths, view.revision.projectId, row.output.path), "utf8");
|
|
79
|
+
if (view.articleMarkdown !== null)
|
|
80
|
+
return plainText(splitEndMatter(view.articleMarkdown).body);
|
|
81
|
+
}
|
|
82
|
+
throw new Error(`The saved ${kind} narration transcript is missing.`);
|
|
83
|
+
}
|
|
84
|
+
export function retainedOutput(deps, view, row) {
|
|
85
|
+
const asset = z
|
|
86
|
+
.object({
|
|
87
|
+
id: z.string(),
|
|
88
|
+
project_id: z.string(),
|
|
89
|
+
path: z.string(),
|
|
90
|
+
bytes: z.number(),
|
|
91
|
+
created_at: z.string(),
|
|
92
|
+
})
|
|
93
|
+
.parse(deps.db
|
|
94
|
+
.prepare("SELECT * FROM project_assets WHERE id=? AND project_id=?")
|
|
95
|
+
.get(row.assetId, view.revision.projectId));
|
|
96
|
+
return {
|
|
97
|
+
slot: row.slot,
|
|
98
|
+
workKey: row.workKey,
|
|
99
|
+
fingerprint: row.fingerprint,
|
|
100
|
+
asset: {
|
|
101
|
+
id: asset.id,
|
|
102
|
+
projectId: asset.project_id,
|
|
103
|
+
path: asset.path,
|
|
104
|
+
bytes: asset.bytes,
|
|
105
|
+
createdAt: asset.created_at,
|
|
106
|
+
},
|
|
107
|
+
output: { ...row.output, id: deps.ids.next(), createdAt: deps.clock.now().toISOString() },
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { extname, join, relative, sep } from "node:path";
|
|
3
|
+
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
4
|
+
import { outputPath, projectDir } from "../storage/layout.js";
|
|
5
|
+
import { audioExportArgs } from "../video/audio-export.js";
|
|
6
|
+
import { renderArgs, runFfmpeg } from "../video/ffmpeg.js";
|
|
7
|
+
import { planRender } from "../video/plan.js";
|
|
8
|
+
import { exportSnapshot, retainedOutput, revisionAudio, } from "./runtime-export-inputs.js";
|
|
9
|
+
import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
|
|
10
|
+
export async function executeExportRecipe(deps, context, piece) {
|
|
11
|
+
if (deps.db
|
|
12
|
+
.prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
13
|
+
.get(piece.id, context.work.workId)?.state === "done")
|
|
14
|
+
return "done";
|
|
15
|
+
if (!context.maySubmit(piece.id))
|
|
16
|
+
return "held";
|
|
17
|
+
context.signal.throwIfAborted();
|
|
18
|
+
const snapshot = exportSnapshot(deps, context, piece);
|
|
19
|
+
const { view } = snapshot;
|
|
20
|
+
const audio = await revisionAudio(deps, context, view);
|
|
21
|
+
const wav = piece.key === "export:wav";
|
|
22
|
+
if (!wav && piece.key !== "export:video")
|
|
23
|
+
throw new Error("Unknown media export recipe.");
|
|
24
|
+
if (wav && audio.length === 0)
|
|
25
|
+
throw new Error("Audio export needs complete narration.");
|
|
26
|
+
const filename = wav ? "audio.wav" : "video.mp4";
|
|
27
|
+
const pending = allocateAsset(deps, context.work.projectId, filename);
|
|
28
|
+
const prepared = [];
|
|
29
|
+
let directory;
|
|
30
|
+
try {
|
|
31
|
+
const captions = snapshot.view.outputs.filter((row) => row.selected && row.available && row.state === "ready" && row.workKey === "subtitles:files");
|
|
32
|
+
const burn = !wav && view.revision.config.subtitles?.mode === "burn-in";
|
|
33
|
+
if (burn)
|
|
34
|
+
directory = captionDirectory(deps, context, snapshot);
|
|
35
|
+
const mode = burn
|
|
36
|
+
? "burn-in"
|
|
37
|
+
: captions.some((row) => row.output.role === "subtitles_srt") &&
|
|
38
|
+
captions.some((row) => row.output.role === "subtitles_vtt")
|
|
39
|
+
? "files"
|
|
40
|
+
: "off";
|
|
41
|
+
const config = view.revision.config;
|
|
42
|
+
const segment = (kind) => {
|
|
43
|
+
const row = audio.find((one) => one.kind === kind);
|
|
44
|
+
return row?.path === null || row === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: { path: row.path, seconds: row.seconds };
|
|
47
|
+
};
|
|
48
|
+
const images = (wav ? [] : view.revision.content.imageOrder).map((key) => {
|
|
49
|
+
const row = view.outputs.find((one) => one.workKey === `image:${key}` && one.selected && one.available && one.state === "ready");
|
|
50
|
+
if (row === undefined)
|
|
51
|
+
throw new Error("The revision is missing an image selected for its slideshow.");
|
|
52
|
+
return outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
53
|
+
});
|
|
54
|
+
const plan = wav
|
|
55
|
+
? undefined
|
|
56
|
+
: planRender({
|
|
57
|
+
format: config.format,
|
|
58
|
+
gapSeconds: config.silenceGapSeconds,
|
|
59
|
+
body: segment("body"),
|
|
60
|
+
intro: segment("intro"),
|
|
61
|
+
outro: segment("outro"),
|
|
62
|
+
images,
|
|
63
|
+
output: pending.absolutePath,
|
|
64
|
+
});
|
|
65
|
+
const totalSeconds = plan?.totalSeconds ?? audio.reduce((sum, row) => sum + row.seconds, 0);
|
|
66
|
+
const projectDirectory = projectDir(deps.paths, context.work.projectId);
|
|
67
|
+
const relativePath = (path) => relative(projectDirectory, path).split(sep).join("/");
|
|
68
|
+
const record = plan === undefined
|
|
69
|
+
? {
|
|
70
|
+
sampleRate: 48000,
|
|
71
|
+
channels: 2,
|
|
72
|
+
codec: "pcm_s16le",
|
|
73
|
+
gapSeconds: config.silenceGapSeconds,
|
|
74
|
+
totalSeconds,
|
|
75
|
+
audio: audio.map((row) => ({
|
|
76
|
+
...row,
|
|
77
|
+
path: row.path === null ? null : relativePath(row.path),
|
|
78
|
+
})),
|
|
79
|
+
output: pending.path,
|
|
80
|
+
}
|
|
81
|
+
: {
|
|
82
|
+
...plan,
|
|
83
|
+
output: pending.path,
|
|
84
|
+
audio: plan.audio.map((row) => ({
|
|
85
|
+
...row,
|
|
86
|
+
path: row.path === null ? null : relativePath(row.path),
|
|
87
|
+
})),
|
|
88
|
+
images: plan.images.map((row) => ({ ...row, path: relativePath(row.path) })),
|
|
89
|
+
subtitles: config.subtitles,
|
|
90
|
+
};
|
|
91
|
+
if (!context.maySubmit(piece.id))
|
|
92
|
+
return "held";
|
|
93
|
+
await runFfmpeg({
|
|
94
|
+
bin: deps.ffmpeg,
|
|
95
|
+
args: plan === undefined ? audioExportArgs(audio, pending.absolutePath) : renderArgs(plan, burn),
|
|
96
|
+
cwd: directory,
|
|
97
|
+
signal: context.signal,
|
|
98
|
+
log: deps.log,
|
|
99
|
+
onProgress: (elapsedMs) => context.emit({
|
|
100
|
+
type: "stage.progress",
|
|
101
|
+
projectId: context.work.projectId,
|
|
102
|
+
stage: "video",
|
|
103
|
+
current: Math.min(100, Math.round(elapsedMs / (totalSeconds * 10))),
|
|
104
|
+
total: 100,
|
|
105
|
+
}),
|
|
106
|
+
});
|
|
107
|
+
context.signal.throwIfAborted();
|
|
108
|
+
const asset = sealAsset(deps, pending);
|
|
109
|
+
prepared.push(preparedResult(deps, context, piece, wav ? "audio_export" : "video", asset, Math.round(totalSeconds * 1000), { subtitlesMode: mode }));
|
|
110
|
+
prepared.push(preparedText(deps, context, piece, "render_params", "render.json", JSON.stringify(record)));
|
|
111
|
+
prepared.push(...captions.map((row) => retainedOutput(deps, view, row)));
|
|
112
|
+
await publishResult(deps, context, piece, prepared, { totalSeconds }, asset);
|
|
113
|
+
deps.count?.("stage.completed", { stage: "video" });
|
|
114
|
+
return "done";
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
discardPreparedAssets(deps, [pending, ...prepared.map((one) => one.asset)]);
|
|
118
|
+
if (directory !== undefined)
|
|
119
|
+
rmSync(directory, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function captionDirectory(deps, context, snapshot) {
|
|
123
|
+
const find = (role) => snapshot.view.outputs.find((row) => row.selected && row.available && row.state === "ready" && row.output.role === role);
|
|
124
|
+
const ass = find("subtitle_ass");
|
|
125
|
+
const font = find("subtitle_font");
|
|
126
|
+
if (ass === undefined || font === undefined)
|
|
127
|
+
throw new Error("Burned subtitles need prepared captions and their saved font.");
|
|
128
|
+
const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "render-"));
|
|
129
|
+
try {
|
|
130
|
+
mkdirSync(join(directory, "fonts"), { mode: 0o700 });
|
|
131
|
+
copyFileSync(outputPath(deps.paths, context.work.projectId, ass.output.path), join(directory, "subtitles.ass"));
|
|
132
|
+
copyFileSync(outputPath(deps.paths, context.work.projectId, font.output.path), join(directory, "fonts", `selected${extname(font.output.path)}`));
|
|
133
|
+
return directory;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
rmSync(directory, { recursive: true, force: true });
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { recipeInputSchema } from "./recipe-input-schema.js";
|
|
3
|
+
import { executionView } from "./runtime-plan.js";
|
|
4
|
+
export function frozenInstructions(deps, context, current) {
|
|
5
|
+
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
6
|
+
const selected = new Set(view?.pieces
|
|
7
|
+
.filter((row) => row.selected && row.piece.state === "done")
|
|
8
|
+
.map((row) => row.piece.id));
|
|
9
|
+
const rows = deps.db
|
|
10
|
+
.prepare(`SELECT p.id,p.work_id,p.work_key,p.input_json,p.result_json,p.state FROM revision_work_pieces p JOIN revision_work w ON w.id=p.work_id WHERE w.project_id=? AND w.kind=? ORDER BY w.rowid,p.rowid`)
|
|
11
|
+
.all(context.work.projectId, context.work.kind);
|
|
12
|
+
const sections = [];
|
|
13
|
+
for (const raw of rows) {
|
|
14
|
+
const row = z
|
|
15
|
+
.object({
|
|
16
|
+
id: z.string(),
|
|
17
|
+
work_id: z.string(),
|
|
18
|
+
work_key: z.string(),
|
|
19
|
+
input_json: z.string(),
|
|
20
|
+
result_json: z.string().nullable(),
|
|
21
|
+
state: z.string(),
|
|
22
|
+
})
|
|
23
|
+
.parse(raw);
|
|
24
|
+
if (row.id !== current.id &&
|
|
25
|
+
!selected.has(row.id) &&
|
|
26
|
+
!(row.work_id === current.workId && row.result_json !== null))
|
|
27
|
+
continue;
|
|
28
|
+
const input = recipeInputSchema.parse(JSON.parse(row.input_json));
|
|
29
|
+
if (input.kind !== "llm")
|
|
30
|
+
continue;
|
|
31
|
+
sections.push(`## ${label(row.work_key)}\n\n${input.messages.map((message) => `### ${message.role}\n\n${message.content}`).join("\n\n")}`);
|
|
32
|
+
}
|
|
33
|
+
return `${sections.join("\n\n")}\n`;
|
|
34
|
+
}
|
|
35
|
+
function label(key) {
|
|
36
|
+
if (key === "research:planner")
|
|
37
|
+
return "Research outline";
|
|
38
|
+
if (key === "research:notes")
|
|
39
|
+
return "Research synthesis";
|
|
40
|
+
if (key.startsWith("research:chapter:"))
|
|
41
|
+
return `Research chapter ${key.split(":").at(-1)}`;
|
|
42
|
+
if (key === "article:body")
|
|
43
|
+
return "Article";
|
|
44
|
+
if (key.startsWith("article:continuation:"))
|
|
45
|
+
return `Article continuation ${key.split(":").at(-1)}`;
|
|
46
|
+
if (key === "entry:intro:text")
|
|
47
|
+
return "Intro";
|
|
48
|
+
if (key === "entry:outro:text")
|
|
49
|
+
return "Outro";
|
|
50
|
+
return "Thumbnail instructions";
|
|
51
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { plainText } from "../article/plain.js";
|
|
5
|
+
import { splitEndMatter } from "../article/split.js";
|
|
6
|
+
import { joinNarration } from "../narration/concat.js";
|
|
7
|
+
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
8
|
+
import { outputPath } from "../storage/layout.js";
|
|
9
|
+
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
10
|
+
import { preparedResult, preparedText, preparedTexts, publishResult, } from "./runtime-publication.js";
|
|
11
|
+
export async function executeLocalRecipe(deps, context, piece) {
|
|
12
|
+
if (deps.db
|
|
13
|
+
.prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
14
|
+
.get(piece.id, context.work.workId)?.state === "done")
|
|
15
|
+
return "done";
|
|
16
|
+
if (!context.maySubmit(piece.id))
|
|
17
|
+
return "held";
|
|
18
|
+
const input = piece.input;
|
|
19
|
+
if (input.kind === "provided") {
|
|
20
|
+
if (input.assetId === null)
|
|
21
|
+
throw new Error("The provided recipe has no retained asset.");
|
|
22
|
+
const row = z
|
|
23
|
+
.object({ id: z.string(), project_id: z.string(), path: z.string(), created_at: z.string() })
|
|
24
|
+
.parse(deps.db
|
|
25
|
+
.prepare("SELECT * FROM project_assets WHERE id=? AND project_id=?")
|
|
26
|
+
.get(input.assetId, context.work.projectId));
|
|
27
|
+
const asset = {
|
|
28
|
+
id: row.id,
|
|
29
|
+
projectId: row.project_id,
|
|
30
|
+
path: row.path,
|
|
31
|
+
createdAt: row.created_at,
|
|
32
|
+
bytes: statSync(outputPath(deps.paths, row.project_id, row.path)).size,
|
|
33
|
+
};
|
|
34
|
+
const role = piece.key === "audio:provided"
|
|
35
|
+
? "audio_body"
|
|
36
|
+
: piece.key === "thumbnail:image"
|
|
37
|
+
? "thumbnail"
|
|
38
|
+
: piece.key.startsWith("image:")
|
|
39
|
+
? "image"
|
|
40
|
+
: undefined;
|
|
41
|
+
await publishResult(deps, context, piece, role === undefined ? [] : [preparedResult(deps, context, piece, role, asset)], {}, asset);
|
|
42
|
+
return "done";
|
|
43
|
+
}
|
|
44
|
+
if (input.kind !== "local")
|
|
45
|
+
throw new Error("Expected an exact local recipe.");
|
|
46
|
+
if (input.operation === "concat-narration") {
|
|
47
|
+
await concatenate(deps, context, piece);
|
|
48
|
+
return "done";
|
|
49
|
+
}
|
|
50
|
+
if (input.operation === "provided-article" || input.operation === "manual-article") {
|
|
51
|
+
const text = z.string().parse(input.values);
|
|
52
|
+
const parts = splitEndMatter(text);
|
|
53
|
+
await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
|
|
54
|
+
["article_md", "article.md", text],
|
|
55
|
+
["article_txt", "article.txt", plainText(parts.body)],
|
|
56
|
+
...(parts.sources ? [["sources", "sources.md", parts.sources]] : []),
|
|
57
|
+
...(parts.glossary ? [["glossary", "glossary.md", parts.glossary]] : []),
|
|
58
|
+
]), { text });
|
|
59
|
+
return "done";
|
|
60
|
+
}
|
|
61
|
+
if (input.operation === "provided-notes") {
|
|
62
|
+
const text = z.string().parse(input.values);
|
|
63
|
+
await publishResult(deps, context, piece, [preparedText(deps, context, piece, "notes", "notes.md", text)], { text });
|
|
64
|
+
return "done";
|
|
65
|
+
}
|
|
66
|
+
if (input.operation === "entry-text") {
|
|
67
|
+
await publishResult(deps, context, piece, [], {
|
|
68
|
+
text: z.string().parse(input.values),
|
|
69
|
+
category: piece.key.includes(":intro:") ? "intro" : "outro",
|
|
70
|
+
});
|
|
71
|
+
return "done";
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`No local executor for ${input.operation}.`);
|
|
74
|
+
}
|
|
75
|
+
async function concatenate(deps, context, piece) {
|
|
76
|
+
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
77
|
+
if (view === undefined)
|
|
78
|
+
throw new Error("The pinned revision is missing.");
|
|
79
|
+
const row = deps.db
|
|
80
|
+
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
81
|
+
.get(context.work.workId);
|
|
82
|
+
const plan = executionPlan(deps, view, savedCatalogue(row?.recipe_context));
|
|
83
|
+
const recipe = plan.recipes.find((candidate) => candidate.key === piece.key && candidate.fingerprint === piece.fingerprint);
|
|
84
|
+
if (recipe === undefined)
|
|
85
|
+
throw new Error("The pinned concatenation no longer matches its inputs.");
|
|
86
|
+
const files = recipe.dependsOn.map((key) => {
|
|
87
|
+
const input = view.pieces.find((candidate) => candidate.key === key &&
|
|
88
|
+
candidate.selected &&
|
|
89
|
+
candidate.available &&
|
|
90
|
+
candidate.piece.state === "done" &&
|
|
91
|
+
candidate.fingerprint === plan.recipes.find((value) => value.key === key)?.fingerprint);
|
|
92
|
+
if (input?.assetId === null || input === undefined)
|
|
93
|
+
throw new Error("A narration part has no retained audio.");
|
|
94
|
+
const asset = deps.db
|
|
95
|
+
.prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
|
|
96
|
+
.get(input.assetId, context.work.projectId);
|
|
97
|
+
return outputPath(deps.paths, context.work.projectId, z.string().parse(asset?.path));
|
|
98
|
+
});
|
|
99
|
+
const pending = allocateAsset(deps, context.work.projectId, "narration.mp3");
|
|
100
|
+
try {
|
|
101
|
+
const durationMs = await joinNarration({ bin: deps.ffmpeg, log: deps.log }, {
|
|
102
|
+
files,
|
|
103
|
+
output: pending.absolutePath,
|
|
104
|
+
listPath: join(dirname(pending.absolutePath), "parts.txt"),
|
|
105
|
+
signal: context.signal,
|
|
106
|
+
});
|
|
107
|
+
context.signal.throwIfAborted();
|
|
108
|
+
const asset = sealAsset(deps, pending);
|
|
109
|
+
const role = piece.key === "audio:intro"
|
|
110
|
+
? "audio_intro"
|
|
111
|
+
: piece.key === "audio:outro"
|
|
112
|
+
? "audio_outro"
|
|
113
|
+
: "audio_body";
|
|
114
|
+
await publishResult(deps, context, piece, [
|
|
115
|
+
preparedResult(deps, context, piece, role, asset, durationMs, view.revision.config.audio === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: {
|
|
118
|
+
provider: view.revision.config.audio.provider,
|
|
119
|
+
model: view.revision.config.audio.model,
|
|
120
|
+
voice: view.revision.config.audio.voice,
|
|
121
|
+
}),
|
|
122
|
+
], { durationMs }, asset);
|
|
123
|
+
for (const key of recipe.dependsOn) {
|
|
124
|
+
const retained = view.pieces.find((candidate) => candidate.key === key && candidate.selected && candidate.available);
|
|
125
|
+
if (retained?.publicationId === null || retained === undefined)
|
|
126
|
+
continue;
|
|
127
|
+
const owner = deps.db
|
|
128
|
+
.prepare("SELECT work_id FROM revision_work_pieces WHERE id=?")
|
|
129
|
+
.get(retained.publicationId);
|
|
130
|
+
if (typeof owner?.work_id === "string")
|
|
131
|
+
deps.audioPreviews?.clear(context.work.projectId, owner.work_id);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
finally {
|
|
135
|
+
discardPreparedAssets(deps, [pending]);
|
|
136
|
+
}
|
|
137
|
+
}
|