@gentbajko/slopify 2.0.2 → 2.1.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/SUBTITLES.md +1 -1
- package/dist/adapters/alignment/protocol.js +11 -1
- package/dist/adapters/alignment/runner.js +4 -1
- package/dist/adapters/alignment/threads.js +11 -0
- package/dist/adapters/alignment/worker.js +66 -9
- package/dist/adapters/explain.js +93 -0
- package/dist/adapters/ffmpeg.js +3 -3
- package/dist/adapters/host-cli/index.js +34 -3
- package/dist/adapters/host-cli/transport.js +4 -4
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +12 -12
- package/dist/adapters/image/codex.js +24 -8
- package/dist/adapters/image/fal.js +12 -6
- package/dist/adapters/image/google.js +18 -8
- package/dist/adapters/image/models.js +8 -2
- package/dist/adapters/image/openai.js +15 -6
- package/dist/adapters/image/replicate.js +16 -7
- package/dist/adapters/llm/claude-code-models.js +1 -1
- package/dist/adapters/llm/claude-code.js +16 -7
- package/dist/adapters/llm/cli-login-error.js +2 -1
- package/dist/adapters/llm/codex-models.js +1 -1
- package/dist/adapters/llm/codex.js +5 -6
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-models.js +1 -1
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +15 -11
- package/dist/adapters/llm/openrouter.js +21 -9
- package/dist/adapters/llm/run-cli.js +14 -11
- package/dist/adapters/tts/cartesia.js +10 -3
- package/dist/adapters/tts/elevenlabs.js +18 -6
- package/dist/adapters/tts/inworld-async.js +47 -16
- package/dist/adapters/tts/inworld.js +25 -15
- package/dist/adapters/tts/openai.js +18 -6
- package/dist/assets/document/SOURCE.txt +7 -0
- package/dist/assets/document/background.jpg +0 -0
- package/dist/assets/document/fonts/Cinzel-Black.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Bold.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Medium.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Regular.ttf +0 -0
- package/dist/assets/document/fonts/OFL.txt +93 -0
- package/dist/assets/document/fonts/SOURCE.txt +11 -0
- package/dist/catalog/registry.js +13 -10
- package/dist/catalog/store.js +1 -1
- package/dist/catalog/validate.js +16 -6
- package/dist/edge/cli.js +33 -7
- package/dist/edge/docker-launch.js +6 -6
- package/dist/edge/docker-projects/engine.js +34 -13
- package/dist/edge/docker-projects/install.js +8 -8
- package/dist/edge/docker-projects/state.js +11 -7
- package/dist/edge/docker-projects/tree.js +6 -6
- package/dist/edge/docker.js +13 -13
- package/dist/edge/host-cli.js +14 -0
- package/dist/edge/http/actions.js +27 -25
- package/dist/edge/http/app.js +3 -3
- package/dist/edge/http/audio-preview.js +1 -1
- package/dist/edge/http/checkpoints.js +5 -1
- package/dist/edge/http/diagnostics.js +3 -1
- package/dist/edge/http/draft-files.js +2 -2
- package/dist/edge/http/draft-problem.js +11 -1
- package/dist/edge/http/files.js +4 -4
- package/dist/edge/http/folder-location-schema.js +5 -8
- package/dist/edge/http/folder-location.js +13 -8
- package/dist/edge/http/fonts.js +22 -10
- package/dist/edge/http/host-cli.js +61 -14
- package/dist/edge/http/multipart.js +7 -5
- package/dist/edge/http/open-folder.js +2 -2
- package/dist/edge/http/planning.js +4 -4
- package/dist/edge/http/problem.js +2 -2
- package/dist/edge/http/project-templates.js +7 -3
- package/dist/edge/http/projects.js +8 -8
- package/dist/edge/http/prompts.js +9 -4
- package/dist/edge/http/providers.js +15 -7
- package/dist/edge/http/revision-files.js +8 -4
- package/dist/edge/http/revisions.js +30 -4
- package/dist/edge/http/schedules.js +9 -9
- package/dist/edge/http/settings.js +15 -9
- package/dist/edge/http/staging.js +5 -5
- package/dist/edge/http/storage.js +4 -4
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/tutorial.js +2 -2
- package/dist/edge/http/update.js +3 -3
- package/dist/host-cli/install.js +28 -6
- package/dist/host-cli/open-folder.js +95 -0
- package/dist/host-cli/paths.js +6 -6
- package/dist/host-cli/runtime.js +2 -0
- package/dist/host-cli/server.js +1 -1
- package/dist/host-cli/service.js +11 -11
- package/dist/host-cli/status.js +7 -3
- package/dist/kernel/config/index.js +6 -4
- package/dist/kernel/db/migrate.js +27 -2
- package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
- package/dist/kernel/db/migrations/0014-document-stage.sql +44 -0
- package/dist/kernel/db/migrations/0015-description-prompts.sql +13 -0
- package/dist/kernel/lock.js +4 -3
- package/dist/kernel/pipeline.js +11 -1
- package/dist/kernel/ports/host-cli.js +14 -0
- package/dist/kernel/ports/subtitles.js +15 -1
- package/dist/kernel/runner/attempt.js +26 -2
- package/dist/kernel/runner/graph.js +6 -1
- package/dist/main.js +8 -4
- package/dist/slices/admission/model.js +7 -1
- package/dist/slices/admission/rules.js +98 -22
- package/dist/slices/admission/schema.js +16 -3
- package/dist/slices/admission/start.js +3 -2
- package/dist/slices/article/continuation.js +4 -2
- package/dist/slices/article/plain.js +2 -2
- package/dist/slices/article/provided-entries.js +3 -3
- package/dist/slices/article/run.js +2 -2
- package/dist/slices/article/segments.js +5 -3
- package/dist/slices/control/providers.js +20 -7
- package/dist/slices/document/blocks.js +129 -0
- package/dist/slices/document/flow.js +117 -0
- package/dist/slices/document/fonts.js +54 -0
- package/dist/slices/document/model.js +11 -0
- package/dist/slices/document/pages.js +198 -0
- package/dist/slices/document/render.js +242 -0
- package/dist/slices/document/sources.js +35 -0
- package/dist/slices/document/theme.js +158 -0
- package/dist/slices/document/writer.js +66 -0
- package/dist/slices/estimate/index.js +14 -2
- package/dist/slices/fonts/catalog.js +1 -1
- package/dist/slices/fonts/upload.js +1 -1
- package/dist/slices/images/run.js +5 -5
- package/dist/slices/library/lint.js +13 -7
- package/dist/slices/library/model.js +1 -1
- package/dist/slices/library/slots.js +6 -3
- package/dist/slices/narration/concat.js +1 -1
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +2 -2
- package/dist/slices/narration/run.js +7 -7
- package/dist/slices/narration/steering.js +3 -3
- package/dist/slices/play-drafts/convert.js +35 -5
- package/dist/slices/play-drafts/readiness.js +15 -8
- package/dist/slices/play-drafts/review-inputs.js +7 -4
- package/dist/slices/play-drafts/review.js +3 -3
- package/dist/slices/play-drafts/schema.js +22 -5
- package/dist/slices/play-drafts/service.js +3 -1
- package/dist/slices/play-drafts/uploads.js +1 -1
- package/dist/slices/project-templates/from-project.js +10 -0
- package/dist/slices/project-templates/setup.js +1 -0
- package/dist/slices/rebuild/admission-repo.js +2 -2
- package/dist/slices/rebuild/preview-details.js +11 -1
- package/dist/slices/rebuild/recipe-audio.js +3 -0
- package/dist/slices/rebuild/recipe-build.js +6 -1
- package/dist/slices/rebuild/recipe-document.js +36 -0
- package/dist/slices/rebuild/recipe-exports.js +14 -2
- package/dist/slices/rebuild/recipe-fixture.js +4 -0
- package/dist/slices/rebuild/recipe-legacy.js +6 -1
- package/dist/slices/rebuild/recipe-model.js +2 -0
- package/dist/slices/rebuild/recipe-narration-text.js +1 -1
- package/dist/slices/rebuild/recipe-provider-choice.js +3 -0
- package/dist/slices/rebuild/recipe-save.js +15 -3
- package/dist/slices/rebuild/recipe-validation.js +15 -2
- package/dist/slices/rebuild/recipe-visual.js +11 -2
- package/dist/slices/rebuild/recipe-work.js +15 -0
- package/dist/slices/rebuild/recipe-youtube.js +33 -0
- package/dist/slices/rebuild/recovery-selection.js +2 -0
- package/dist/slices/rebuild/recovery.js +12 -7
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/runtime-actions.js +2 -0
- package/dist/slices/rebuild/runtime-admission.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
- package/dist/slices/rebuild/runtime-document.js +85 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +7 -6
- package/dist/slices/rebuild/runtime-export.js +39 -23
- package/dist/slices/rebuild/runtime-local.js +6 -6
- package/dist/slices/rebuild/runtime-narration-text.js +46 -7
- package/dist/slices/rebuild/runtime-piece-label.js +66 -0
- package/dist/slices/rebuild/runtime-provider.js +6 -4
- package/dist/slices/rebuild/runtime-publication.js +4 -3
- package/dist/slices/rebuild/runtime-run.js +22 -4
- package/dist/slices/rebuild/runtime-store.js +7 -2
- package/dist/slices/rebuild/runtime-subtitles.js +82 -16
- package/dist/slices/rebuild/runtime-youtube.js +84 -0
- package/dist/slices/rebuild/transition-repo.js +8 -6
- package/dist/slices/reruns/cascade.js +6 -4
- package/dist/slices/reruns/index.js +1 -1
- package/dist/slices/research/run.js +9 -7
- package/dist/slices/research/synthesis.js +4 -2
- package/dist/slices/revisions/mutation-assets.js +26 -8
- package/dist/slices/revisions/mutation-cues.js +4 -2
- package/dist/slices/revisions/projection.js +2 -1
- package/dist/slices/revisions/rules.js +18 -9
- package/dist/slices/revisions/schema.js +1 -1
- package/dist/slices/schedules/repo.js +55 -11
- package/dist/slices/schedules/scheduler.js +25 -8
- package/dist/slices/schedules/schema.js +19 -4
- package/dist/slices/schedules/service.js +2 -0
- package/dist/slices/settings/cli-paths.js +7 -4
- package/dist/slices/settings/models.js +4 -4
- package/dist/slices/settings/playback.js +2 -2
- package/dist/slices/storage/assets.js +5 -5
- package/dist/slices/storage/downloads.js +1 -0
- package/dist/slices/storage/layout.js +7 -1
- package/dist/slices/storage/model.js +3 -0
- package/dist/slices/subtitles/captions.js +1 -1
- package/dist/slices/subtitles/prepare.js +7 -6
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/telemetry/model.js +1 -1
- package/dist/slices/thumbnail/run.js +8 -8
- package/dist/slices/video/audio-export.js +11 -3
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/edit-list.js +73 -0
- package/dist/slices/video/ffmpeg.js +129 -61
- package/dist/slices/video/motion.js +51 -0
- package/dist/slices/video/plan.js +62 -28
- package/dist/slices/video/reuse-audio.js +2 -0
- package/dist/slices/video/run.js +23 -12
- package/dist/slices/video/slideshow.js +48 -0
- package/dist/slices/video/write-export.js +17 -25
- package/dist/slices/youtube/answer.js +199 -0
- package/dist/slices/youtube/model.js +27 -0
- package/dist/slices/youtube/timestamps.js +24 -0
- package/dist/slices/youtube/transcript.js +47 -0
- package/dist/updater/plan.js +1 -1
- package/dist/updater/service.js +4 -3
- package/dist/updater/worker.js +6 -4
- package/dist/web/assets/index-B6NbhpjD.css +1 -0
- package/dist/web/assets/index-ComF5gwP.js +142 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -2
- package/dist/web/assets/index-CZNiHm8P.css +0 -1
- package/dist/web/assets/index-DPgGLX5m.js +0 -137
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { sourceOf } from "../admission/model.js";
|
|
2
|
+
import { documentThemeOf } from "../document/model.js";
|
|
3
|
+
import { builtInTheme } from "../document/theme.js";
|
|
4
|
+
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
5
|
+
// Bumped when the renderer's output changes for the same inputs, so documents made by an
|
|
6
|
+
// older one read as outdated rather than current.
|
|
7
|
+
const renderer = "document-v1";
|
|
8
|
+
// The PDF is made from the saved article (whose end matter feeds the Sources page), the
|
|
9
|
+
// research notes' links, the thumbnail as a cover and the resolved theme. It waits for
|
|
10
|
+
// nothing else, so narration and images run beside it.
|
|
11
|
+
export function documentRecipes(context, text, thumbnail) {
|
|
12
|
+
const { config } = context;
|
|
13
|
+
if (sourceOf(config.sources, "document") !== "generate")
|
|
14
|
+
return [];
|
|
15
|
+
const notes = text.recipes.find((value) => value.key === "research:notes");
|
|
16
|
+
const cover = thumbnail.find((value) => value.key === "thumbnail:image");
|
|
17
|
+
const theme = builtInTheme(documentThemeOf(config.document));
|
|
18
|
+
return [
|
|
19
|
+
recipe(context, "document:pdf", "document", {
|
|
20
|
+
kind: "local",
|
|
21
|
+
version: 1,
|
|
22
|
+
operation: "render-document",
|
|
23
|
+
values: [
|
|
24
|
+
renderer,
|
|
25
|
+
config.title,
|
|
26
|
+
JSON.parse(JSON.stringify(theme)),
|
|
27
|
+
resourceIdentity(context, text.article),
|
|
28
|
+
notes === undefined ? null : resourceIdentity(context, notes),
|
|
29
|
+
cover === undefined ? null : resourceIdentity(context, cover),
|
|
30
|
+
],
|
|
31
|
+
}, [
|
|
32
|
+
text.article.key,
|
|
33
|
+
...[notes, cover].flatMap((value) => (value === undefined ? [] : [value.key])),
|
|
34
|
+
]),
|
|
35
|
+
];
|
|
36
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { usesYoutubeDescription } from "../admission/rules.js";
|
|
1
2
|
import { audioExportArgs } from "../video/audio-export-args.js";
|
|
2
3
|
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
3
4
|
export function exportRecipes(context, audio) {
|
|
@@ -15,15 +16,26 @@ export function exportRecipes(context, audio) {
|
|
|
15
16
|
audioExportArgs([{ kind: "body", path: "$body", seconds: 0 }], "$output"),
|
|
16
17
|
],
|
|
17
18
|
}, audio.keys));
|
|
18
|
-
|
|
19
|
+
const captions = config.subtitles !== undefined && config.subtitles.mode !== "off";
|
|
20
|
+
// The YouTube description's chapters use the same word timing, so it runs for them even
|
|
21
|
+
// with captions off; only the caption files below wait for captions.
|
|
22
|
+
if (!captions && !usesYoutubeDescription(config))
|
|
19
23
|
return recipes;
|
|
20
24
|
const timing = recipe(context, "subtitles:timing", "video", {
|
|
21
25
|
kind: "local",
|
|
22
26
|
version: 1,
|
|
23
27
|
operation: "wav2vec2-en-a19f851-v2-omissions",
|
|
24
|
-
|
|
28
|
+
// The lead-in moves every word, so the edge silence is part of the timing.
|
|
29
|
+
values: [
|
|
30
|
+
audio.timeline,
|
|
31
|
+
config.silenceGapSeconds,
|
|
32
|
+
config.subtitles?.language ?? "en",
|
|
33
|
+
config.edgeSilenceSeconds,
|
|
34
|
+
],
|
|
25
35
|
}, audio.keys);
|
|
26
36
|
recipes.push(timing);
|
|
37
|
+
if (config.subtitles === undefined || config.subtitles.mode === "off")
|
|
38
|
+
return recipes;
|
|
27
39
|
const cues = content.subtitleCues;
|
|
28
40
|
const cueRecipe = cues === undefined
|
|
29
41
|
? recipe(context, "subtitles:cues", "video", {
|
|
@@ -17,6 +17,10 @@ export const config = {
|
|
|
17
17
|
values: {},
|
|
18
18
|
provided: { article: "First paragraph.\n\nSecond paragraph." },
|
|
19
19
|
silenceGapSeconds: 0,
|
|
20
|
+
imageSeconds: 15,
|
|
21
|
+
zoomPercent: 22.5,
|
|
22
|
+
motionStyle: "zoom",
|
|
23
|
+
edgeSilenceSeconds: 0,
|
|
20
24
|
rendered: { article: "Write an article." },
|
|
21
25
|
};
|
|
22
26
|
export const content = {
|
|
@@ -54,6 +54,11 @@ export function legacyOutputWorkKey(output, config) {
|
|
|
54
54
|
return "export:wav";
|
|
55
55
|
case "video":
|
|
56
56
|
return "export:video";
|
|
57
|
+
case "document_pdf":
|
|
58
|
+
return "document:pdf";
|
|
59
|
+
case "youtube_description":
|
|
60
|
+
case "youtube_tags":
|
|
61
|
+
return "youtube:description";
|
|
57
62
|
case "subtitles_srt":
|
|
58
63
|
case "subtitles_vtt":
|
|
59
64
|
case "subtitle_ass":
|
|
@@ -117,7 +122,7 @@ export function legacyImageOutput(piece, outputs) {
|
|
|
117
122
|
row.meta.promptName === undefined ||
|
|
118
123
|
row.meta.promptName === promptName)));
|
|
119
124
|
if (matches.length > 1)
|
|
120
|
-
throw new Error(`
|
|
125
|
+
throw new Error(`Slopify hit an internal error (an image from an older Slopify version, ${piece.id}, matches multiple outputs). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
|
|
121
126
|
return matches[0];
|
|
122
127
|
}
|
|
123
128
|
const legacyImageMetadata = z
|
|
@@ -26,7 +26,7 @@ export function narrationFileRecipe(context, segment, parts) {
|
|
|
26
26
|
];
|
|
27
27
|
if (part.input.kind === "provided")
|
|
28
28
|
return [resourceIdentity(context, part), part.input.semantic, null];
|
|
29
|
-
throw new Error("
|
|
29
|
+
throw new Error("Slopify hit an internal error (a narration chunk has an unexpected source). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
30
30
|
}),
|
|
31
31
|
},
|
|
32
32
|
}, dependencies, { unresolved: parts.length === 0 });
|
|
@@ -2,6 +2,9 @@ export function recipeProviderChoice(recipe, config) {
|
|
|
2
2
|
const input = recipe.input;
|
|
3
3
|
if (input.kind === "llm" || input.kind === "tts" || input.kind === "image")
|
|
4
4
|
return { ...input, family: input.kind };
|
|
5
|
+
// The YouTube description's request is built when it runs, from the project's LLM row.
|
|
6
|
+
if (input.kind === "local" && input.operation === "youtube-description-v1")
|
|
7
|
+
return config.llm === undefined ? undefined : { ...config.llm, family: "llm" };
|
|
5
8
|
if (input.kind !== "deferred")
|
|
6
9
|
return undefined;
|
|
7
10
|
const family = input.operation === "narration-preparation"
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { stageKinds } from "../../kernel/pipeline.js";
|
|
2
|
-
import {
|
|
2
|
+
import { sourceOf } from "../admission/model.js";
|
|
3
|
+
import { allowedSources, edgeSilenceSecondsProblem, imageSecondsProblem, normaliseDraft, silenceGapSecondsMax, zoomPercentProblem, } from "../admission/rules.js";
|
|
3
4
|
import { render } from "../admission/substitute.js";
|
|
4
5
|
import { buildRecipes } from "./recipe-build.js";
|
|
5
6
|
import { selectedReference } from "./recipe-model.js";
|
|
@@ -153,10 +154,10 @@ function normalizeImages(content) {
|
|
|
153
154
|
export function validateRevisionEdit(config, content) {
|
|
154
155
|
const fields = [];
|
|
155
156
|
for (const kind of stageKinds)
|
|
156
|
-
if (!allowedSources[kind].includes(config.sources
|
|
157
|
+
if (!allowedSources[kind].includes(sourceOf(config.sources, kind)))
|
|
157
158
|
fields.push({
|
|
158
159
|
field: `sources.${kind}`,
|
|
159
|
-
message: `The ${kind} stage cannot be set to ${config.sources
|
|
160
|
+
message: `The ${kind} stage cannot be set to ${sourceOf(config.sources, kind)}.`,
|
|
160
161
|
});
|
|
161
162
|
if (config.sources.images === "provide" &&
|
|
162
163
|
content.imageOrder.some((key) => content.imageDefinitions[key]?.source === "generate"))
|
|
@@ -187,6 +188,17 @@ export function validateRevisionEdit(config, content) {
|
|
|
187
188
|
field: "silenceGapSeconds",
|
|
188
189
|
message: `Use a gap between 0 and ${silenceGapSecondsMax} seconds.`,
|
|
189
190
|
});
|
|
191
|
+
const imageProblem = config.sources.video === "off" ? undefined : imageSecondsProblem(config.imageSeconds);
|
|
192
|
+
if (imageProblem !== undefined)
|
|
193
|
+
fields.push({ field: "imageSeconds", message: imageProblem });
|
|
194
|
+
const zoomProblem = config.sources.video === "off" ? undefined : zoomPercentProblem(config.zoomPercent);
|
|
195
|
+
if (zoomProblem !== undefined)
|
|
196
|
+
fields.push({ field: "zoomPercent", message: zoomProblem });
|
|
197
|
+
const edgeProblem = config.sources.audio === "off"
|
|
198
|
+
? undefined
|
|
199
|
+
: edgeSilenceSecondsProblem(config.edgeSilenceSeconds);
|
|
200
|
+
if (edgeProblem !== undefined)
|
|
201
|
+
fields.push({ field: "edgeSilenceSeconds", message: edgeProblem });
|
|
190
202
|
if (config.sources.article === "generate" &&
|
|
191
203
|
content.articleEdited === true &&
|
|
192
204
|
!content.articleMarkdown?.trim())
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import { narrationPreparationFields, usesNarrationPreparation, } from "../admission/rules.js";
|
|
1
|
+
import { narrationPreparationFields, usesNarrationPreparation, usesYoutubeDescription, youtubeDescriptionFields, } from "../admission/rules.js";
|
|
2
2
|
import { detectSlots, render } from "../admission/substitute.js";
|
|
3
3
|
export function validateRecipeInputs(config, content) {
|
|
4
|
-
const fields = [
|
|
4
|
+
const fields = [
|
|
5
|
+
...narrationPreparationFields(config),
|
|
6
|
+
...youtubeDescriptionFields(config),
|
|
7
|
+
];
|
|
5
8
|
const llm = (config.sources.research === "generate" && config.sources.article !== "provide") ||
|
|
6
9
|
(config.sources.article === "generate" && !content.articleEdited) ||
|
|
7
10
|
config.sources.thumbnail === "prompt_by_llm" ||
|
|
8
11
|
usesNarrationPreparation(config) ||
|
|
12
|
+
usesYoutubeDescription(config) ||
|
|
9
13
|
(config.sources.audio === "generate" &&
|
|
10
14
|
(config.intro?.mode === "llm" || config.outro?.mode === "llm"));
|
|
11
15
|
if (llm && (!config.llm?.provider.trim() || !config.llm.model.trim()))
|
|
@@ -43,6 +47,15 @@ export function validateRecipeInputs(config, content) {
|
|
|
43
47
|
raw: content.promptTemplates.narration ?? null,
|
|
44
48
|
literal: config.rendered.narration,
|
|
45
49
|
});
|
|
50
|
+
// Only a picked Description prompt is checked: none picked uses the built-in one.
|
|
51
|
+
if (usesYoutubeDescription(config) &&
|
|
52
|
+
config.descriptionPrompt?.trim() &&
|
|
53
|
+
content.promptTemplates.description != null)
|
|
54
|
+
prompts.push({
|
|
55
|
+
field: "rendered.description",
|
|
56
|
+
raw: content.promptTemplates.description,
|
|
57
|
+
literal: config.rendered.description,
|
|
58
|
+
});
|
|
46
59
|
if ((config.sources.article === "generate" && !content.articleEdited) ||
|
|
47
60
|
(config.sources.research === "generate" && config.sources.article !== "provide"))
|
|
48
61
|
prompts.push({
|
|
@@ -7,7 +7,7 @@ export function visualRecipes(config, content, audioFingerprint, captionFingerpr
|
|
|
7
7
|
for (const key of imageKeys) {
|
|
8
8
|
const image = content.imageDefinitions[key];
|
|
9
9
|
if (image === undefined)
|
|
10
|
-
throw new Error("
|
|
10
|
+
throw new Error("Slopify hit an internal error (one of the project's images has no saved settings). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
11
11
|
const raw = image.templateKey === undefined || image.templateKey === null
|
|
12
12
|
? undefined
|
|
13
13
|
: content.promptTemplates[image.templateKey];
|
|
@@ -45,7 +45,16 @@ export function visualRecipes(config, content, audioFingerprint, captionFingerpr
|
|
|
45
45
|
recipes.map((value) => value.fingerprint),
|
|
46
46
|
audioFingerprint,
|
|
47
47
|
config.subtitles?.mode === "burn-in" ? captionFingerprint : null,
|
|
48
|
-
|
|
48
|
+
// Only here: how long a still is held and how it moves change the render,
|
|
49
|
+
// never an image.
|
|
50
|
+
config.imageSeconds,
|
|
51
|
+
config.zoomPercent,
|
|
52
|
+
// Left out for "zoom", which is what every video did before the setting
|
|
53
|
+
// existed, so a project that keeps it keeps its render; the other styles
|
|
54
|
+
// each render differently.
|
|
55
|
+
...(config.motionStyle === "zoom" ? [] : [config.motionStyle]),
|
|
56
|
+
// v1 split the timeline evenly across the images; v2 cycles them.
|
|
57
|
+
"slideshow-zoom-v2",
|
|
49
58
|
],
|
|
50
59
|
}, [
|
|
51
60
|
...recipes.map((value) => value.key),
|
|
@@ -177,6 +177,9 @@ function retainedFor(row, logical, manifest, available) {
|
|
|
177
177
|
pieceIds: [],
|
|
178
178
|
}));
|
|
179
179
|
}
|
|
180
|
+
// ceiling: the timed transcript of a 20-minute narration, about 3,000 words. The real one
|
|
181
|
+
// is only built once the word timing lands.
|
|
182
|
+
const youtubeTranscriptEstimate = 20000;
|
|
180
183
|
export function priceRecipe(work, value) {
|
|
181
184
|
if (value === undefined || work.disposition !== "generate")
|
|
182
185
|
return {
|
|
@@ -214,6 +217,18 @@ export function priceRecipe(work, value) {
|
|
|
214
217
|
};
|
|
215
218
|
if (input.kind === "image")
|
|
216
219
|
return { kind: "image", stage: work.key, provider: input.provider, model: input.model };
|
|
220
|
+
if (input.kind === "local" && input.operation === "youtube-description-v1") {
|
|
221
|
+
const [, provider, model, , prompt] = Array.isArray(input.values) ? input.values : [];
|
|
222
|
+
return {
|
|
223
|
+
kind: "llm",
|
|
224
|
+
stage: work.key,
|
|
225
|
+
provider: typeof provider === "string" ? provider : "",
|
|
226
|
+
model: typeof model === "string" ? model : "",
|
|
227
|
+
inputCharacters: (typeof prompt === "string" ? prompt.length : 0) + youtubeTranscriptEstimate,
|
|
228
|
+
outputCharacters: 2400,
|
|
229
|
+
detail: "The timed transcript is built when the step runs; its length is estimated. Retries are excluded.",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
217
232
|
return {
|
|
218
233
|
kind: "tts-estimate",
|
|
219
234
|
stage: work.key,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { usesYoutubeDescription } from "../admission/rules.js";
|
|
2
|
+
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
3
|
+
import { renderedPrompt } from "./recipe-text.js";
|
|
4
|
+
// The YouTube description is written from the word timing, so it waits for
|
|
5
|
+
// `subtitles:timing` and nothing else: it runs beside the render, and a change to the
|
|
6
|
+
// motion, zoom, images or captions' look leaves it current. The transcript is only known once
|
|
7
|
+
// the timing lands, so the request is built when the step runs (`runtime-youtube.ts`); what
|
|
8
|
+
// identifies it here is the timing it reads, the prompt, the model and the title.
|
|
9
|
+
export function youtubeRecipes(context, exports) {
|
|
10
|
+
const { config } = context;
|
|
11
|
+
const timing = exports.find((value) => value.key === "subtitles:timing");
|
|
12
|
+
if (!usesYoutubeDescription(config) || timing === undefined)
|
|
13
|
+
return [];
|
|
14
|
+
return [
|
|
15
|
+
recipe(context, "youtube:description", "video", {
|
|
16
|
+
kind: "local",
|
|
17
|
+
version: 1,
|
|
18
|
+
operation: "youtube-description-v1",
|
|
19
|
+
values: [
|
|
20
|
+
resourceIdentity(context, timing),
|
|
21
|
+
config.llm?.provider ?? "",
|
|
22
|
+
config.llm?.model ?? "",
|
|
23
|
+
config.llm?.thinking ?? null,
|
|
24
|
+
// Blank means the built-in prompt; the step fills it in, so a new built-in wording
|
|
25
|
+
// ships as a new operation version rather than a silent change here.
|
|
26
|
+
config.descriptionPrompt?.trim() ? renderedPrompt(context, "description") : "",
|
|
27
|
+
config.title,
|
|
28
|
+
],
|
|
29
|
+
}, [timing.key],
|
|
30
|
+
// A paid LLM call: readiness, the charge warning and the estimate treat it like one.
|
|
31
|
+
{ kind: "provider" }),
|
|
32
|
+
];
|
|
33
|
+
}
|
|
@@ -16,6 +16,8 @@ export function sectionRoots(view, plan, stage) {
|
|
|
16
16
|
["body-narration", "intro-narration", "outro-narration"].includes(row.input.operation)));
|
|
17
17
|
if (stage === "article")
|
|
18
18
|
return row.key === "article:body" && row.kind === "provider";
|
|
19
|
+
if (stage === "document")
|
|
20
|
+
return row.key === "document:pdf";
|
|
19
21
|
return row.kind === "provider";
|
|
20
22
|
})
|
|
21
23
|
.map((row) => row.key);
|
|
@@ -78,7 +78,7 @@ export async function recoverProject(deps, projectId, raw) {
|
|
|
78
78
|
const prior = retainedPreviewPlan(deps, base, deps.catalogue.read());
|
|
79
79
|
const action = input.action;
|
|
80
80
|
if (action.kind !== "rerun")
|
|
81
|
-
throw new Error("
|
|
81
|
+
throw new Error("Slopify hit an internal error (a recovery edit has nothing to re-run). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
82
82
|
const affected = dependentClosure(prior.recipes, sectionRoots(base, prior, action.stage));
|
|
83
83
|
const savedIntent = deps.db
|
|
84
84
|
.prepare("SELECT result_revision_id FROM revision_mutations WHERE project_id=? AND idempotency_key=?")
|
|
@@ -158,16 +158,21 @@ export async function recoverProject(deps, projectId, raw) {
|
|
|
158
158
|
fields: blocked.map((row) => ({ field: row.key, message: row.reason })),
|
|
159
159
|
});
|
|
160
160
|
if (input.action.kind === "rerun" &&
|
|
161
|
-
input.action.stage === "video" &&
|
|
161
|
+
(input.action.stage === "video" || input.action.stage === "document") &&
|
|
162
162
|
preview.value.preview.work.some((row) => row.kind === "provider" && row.disposition !== "reuse"))
|
|
163
163
|
return finish({
|
|
164
164
|
ok: false,
|
|
165
165
|
reason: "readiness",
|
|
166
166
|
fields: [
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
167
|
+
input.action.stage === "video"
|
|
168
|
+
? {
|
|
169
|
+
field: "video",
|
|
170
|
+
message: "Source media is incomplete. Use Resume to recover it before rerunning the export.",
|
|
171
|
+
}
|
|
172
|
+
: {
|
|
173
|
+
field: "document",
|
|
174
|
+
message: "The article or thumbnail the document is made from isn't finished. Use Resume to finish it, then Re-run section on Document.",
|
|
175
|
+
},
|
|
171
176
|
],
|
|
172
177
|
});
|
|
173
178
|
const conflict = rerunKeys.length > 0 ? activeConflict(deps, projectId, rerunKeys) : undefined;
|
|
@@ -176,7 +181,7 @@ export async function recoverProject(deps, projectId, raw) {
|
|
|
176
181
|
storePreview(deps, preview.value.preview, preview.value.execution);
|
|
177
182
|
const stored = previewById(deps.db, projectId, preview.value.preview.id);
|
|
178
183
|
if (stored === undefined)
|
|
179
|
-
throw new Error("
|
|
184
|
+
throw new Error("Slopify hit an internal error (the saved recovery preview is missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
180
185
|
return {
|
|
181
186
|
plan: { preview: stored, execution: preview.value.execution },
|
|
182
187
|
view,
|
|
@@ -7,7 +7,7 @@ export { transitionRevisionWork } from "./transition-repo.js";
|
|
|
7
7
|
export function publicationTargets(db, publication, workKey, workFingerprint) {
|
|
8
8
|
const { work, pieceId, publicationId } = publication;
|
|
9
9
|
if (publicationId !== (pieceId ?? work.workId))
|
|
10
|
-
throw new Error("
|
|
10
|
+
throw new Error("Slopify hit an internal error (a finished result is filed under the wrong step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
11
11
|
if (!workExists(db, work))
|
|
12
12
|
return [];
|
|
13
13
|
if (pieceId !== null) {
|
|
@@ -15,9 +15,9 @@ export function publicationTargets(db, publication, workKey, workFingerprint) {
|
|
|
15
15
|
.prepare("SELECT work_key,fingerprint FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
16
16
|
.get(pieceId, work.workId);
|
|
17
17
|
if (piece === undefined)
|
|
18
|
-
throw new Error("
|
|
18
|
+
throw new Error("Slopify hit an internal error (a finished result belongs to a different step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
19
19
|
if (piece.work_key !== workKey || piece.fingerprint !== workFingerprint)
|
|
20
|
-
throw new Error("
|
|
20
|
+
throw new Error("Slopify hit an internal error (a finished result doesn't match its saved step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
21
21
|
}
|
|
22
22
|
const currentId = currentRevisionId(db, work.projectId);
|
|
23
23
|
const owns = (revisionId) => {
|
|
@@ -18,11 +18,11 @@ export function reservationKey(recipe, view) {
|
|
|
18
18
|
export function admitInitialRevision(deps, view, catalogue) {
|
|
19
19
|
transact(deps.db, () => {
|
|
20
20
|
if (currentRevisionId(deps.db, view.revision.projectId) !== view.revision.id)
|
|
21
|
-
throw new Error("
|
|
21
|
+
throw new Error("The project was changed while this run was starting. Try again.");
|
|
22
22
|
if (deps.db
|
|
23
23
|
.prepare("SELECT 1 FROM revision_work WHERE project_id=? LIMIT 1")
|
|
24
24
|
.get(view.revision.projectId) !== undefined)
|
|
25
|
-
throw new Error("
|
|
25
|
+
throw new Error("Slopify hit an internal error (this project was started twice). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
26
26
|
const snapshot = executionCatalogue(catalogue, view.revision.config);
|
|
27
27
|
const plan = executionPlan(deps, view, snapshot);
|
|
28
28
|
for (const recipe of plan.recipes) {
|
|
@@ -32,7 +32,7 @@ export function admitInitialRevision(deps, view, catalogue) {
|
|
|
32
32
|
const logicalKey = recipe.input.kind === "tts" ? `${recipe.input.logicalKey}:1` : recipe.key;
|
|
33
33
|
const anchor = view.revision.fingerprints[logicalKey] ?? desired;
|
|
34
34
|
if (anchor === undefined)
|
|
35
|
-
throw new Error(`
|
|
35
|
+
throw new Error(`Slopify hit an internal error (step ${recipe.key} has no saved starting point). Try again; if it happens again, use Download diagnostics in Settings and report it.`);
|
|
36
36
|
const reuse = plan.work.find((row) => row.key === recipe.key)?.disposition === "reuse";
|
|
37
37
|
if (reuse)
|
|
38
38
|
bindNarrationReuse(deps, view, recipe, narrationOrdinal(plan.recipes, recipe.key));
|
|
@@ -46,7 +46,7 @@ export function insertInvocation(deps, view, recipe, catalogue, anchor, complete
|
|
|
46
46
|
.prepare("SELECT id FROM stages WHERE project_id=? AND kind=?")
|
|
47
47
|
.get(projectId, recipe.stage);
|
|
48
48
|
if (typeof stage?.id !== "string")
|
|
49
|
-
throw new Error("
|
|
49
|
+
throw new Error("Slopify hit an internal error (a step has no matching stage). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
50
50
|
const work = {
|
|
51
51
|
projectId,
|
|
52
52
|
revisionId,
|
|
@@ -12,14 +12,14 @@ const answerSchema = z.object({
|
|
|
12
12
|
});
|
|
13
13
|
export async function executeArticleRequests(deps, context, providers, initial) {
|
|
14
14
|
if (initial.input.kind !== "llm")
|
|
15
|
-
throw new Error("
|
|
15
|
+
throw new Error("Slopify hit an internal error (the article step has no AI request). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
16
16
|
let piece = initial;
|
|
17
17
|
let text = "";
|
|
18
18
|
let inputTokens = 0;
|
|
19
19
|
let outputTokens = 0;
|
|
20
20
|
for (let part = 0; part <= continuationLimit; part += 1) {
|
|
21
21
|
if (piece.input.kind !== "llm")
|
|
22
|
-
throw new Error("
|
|
22
|
+
throw new Error("Slopify hit an internal error (an article continuation has no AI request). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
23
23
|
const saved = deps.db
|
|
24
24
|
.prepare("SELECT result_json FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
25
25
|
.get(piece.id, context.work.workId);
|
|
@@ -38,9 +38,9 @@ export async function executeArticleRequests(deps, context, providers, initial)
|
|
|
38
38
|
webSearch: input.webSearch,
|
|
39
39
|
previewLabel: part === 0 ? "Article" : `Continuation ${part}`,
|
|
40
40
|
check: (value) => value.text.trim() === ""
|
|
41
|
-
? "The article
|
|
41
|
+
? "The AI model returned an empty article. Retry stage; if it keeps happening, choose a different model in Edit project → Providers."
|
|
42
42
|
: part === continuationLimit && value.finishReason === "length"
|
|
43
|
-
? `The article was still unfinished after ${continuationLimit} continuations.`
|
|
43
|
+
? `The article was still unfinished after ${continuationLimit} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Retry stage.`
|
|
44
44
|
: undefined,
|
|
45
45
|
}, (event) => {
|
|
46
46
|
if (event.type === "delta")
|
|
@@ -72,7 +72,7 @@ export async function executeArticleRequests(deps, context, providers, initial)
|
|
|
72
72
|
return { ok: false, reason: "held" };
|
|
73
73
|
piece = next;
|
|
74
74
|
}
|
|
75
|
-
throw new Error(
|
|
75
|
+
throw new Error(`The article was still unfinished after ${continuationLimit} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Retry stage.`);
|
|
76
76
|
}
|
|
77
77
|
function continuationPiece(deps, context, initial, part, text) {
|
|
78
78
|
return transact(deps.db, () => {
|
|
@@ -11,14 +11,14 @@ export function admitReviewedCheckpoints(deps, projectIds, reviewed, catalogue)
|
|
|
11
11
|
const revisionId = currentRevisionId(deps.db, projectId);
|
|
12
12
|
const view = revisionId === undefined ? undefined : getRevisionView(deps, projectId, revisionId);
|
|
13
13
|
if (!view)
|
|
14
|
-
throw new Error("
|
|
14
|
+
throw new Error("Slopify hit an internal error (the project version to review is missing). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
15
15
|
const plan = executionPlan(deps, view, executionCatalogue(catalogue, view.revision.config));
|
|
16
16
|
const checkpoints = gates.map((gate) => {
|
|
17
17
|
const work = deps.db
|
|
18
18
|
.prepare("SELECT id FROM revision_work WHERE project_id=? AND revision_id=? AND kind=? ORDER BY id LIMIT 1")
|
|
19
19
|
.get(projectId, view.revision.id, gate.stage);
|
|
20
20
|
if (typeof work?.id !== "string")
|
|
21
|
-
throw new Error("
|
|
21
|
+
throw new Error("Slopify hit an internal error (a review checkpoint has no matching stage). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
22
22
|
return {
|
|
23
23
|
checkpointId: gate.checkpointId,
|
|
24
24
|
stage: gate.stage,
|
|
@@ -34,11 +34,11 @@ export function admitReviewedCheckpoints(deps, projectIds, reviewed, catalogue)
|
|
|
34
34
|
createdAt: deps.clock.now().toISOString(),
|
|
35
35
|
});
|
|
36
36
|
if (!saved.ok)
|
|
37
|
-
throw new Error("
|
|
37
|
+
throw new Error("Slopify hit an internal error (the review checkpoints could not be saved). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
38
38
|
return saved.value.map((row) => {
|
|
39
39
|
const gate = gates.find((gate) => gate.checkpointId === row.checkpointId);
|
|
40
40
|
if (!gate)
|
|
41
|
-
throw new Error("
|
|
41
|
+
throw new Error("Slopify hit an internal error (a saved review checkpoint could not be matched). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
42
42
|
return { ...row, reviewedFingerprint: gate.fingerprint };
|
|
43
43
|
});
|
|
44
44
|
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readDocumentAssets } from "../document/fonts.js";
|
|
3
|
+
import { documentThemeOf } from "../document/model.js";
|
|
4
|
+
import { renderDocument } from "../document/render.js";
|
|
5
|
+
import { builtInTheme } from "../document/theme.js";
|
|
6
|
+
import { writeAsset } from "../storage/assets.js";
|
|
7
|
+
import { outputPath } from "../storage/layout.js";
|
|
8
|
+
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
9
|
+
import { preparedResult, publishResult } from "./runtime-publication.js";
|
|
10
|
+
// Lays the saved article out as a PDF. Local and quick: no provider, no charge.
|
|
11
|
+
export async function executeDocumentRecipe(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
|
+
context.signal.throwIfAborted();
|
|
19
|
+
const view = documentView(deps, context, piece);
|
|
20
|
+
const { projectId } = context.work;
|
|
21
|
+
const text = (role, workKey) => {
|
|
22
|
+
const row = ready(view, role, workKey);
|
|
23
|
+
return row === undefined
|
|
24
|
+
? null
|
|
25
|
+
: readFileSync(outputPath(deps.paths, projectId, row.output.path), "utf8");
|
|
26
|
+
};
|
|
27
|
+
const config = view.revision.config;
|
|
28
|
+
const article = view.revision.content.articleEdited === true || config.sources.article === "provide"
|
|
29
|
+
? view.articleMarkdown
|
|
30
|
+
: text("article_md", "article:body");
|
|
31
|
+
if (article === null)
|
|
32
|
+
throw new Error("The article isn't finished yet, so there's nothing to make the document from. Let the Article stage finish (Resume, or Retry stage on Article), then use Retry stage on Document.");
|
|
33
|
+
const thumbnail = ready(view, "thumbnail", "thumbnail:image");
|
|
34
|
+
let cover = null;
|
|
35
|
+
if (config.sources.thumbnail !== "off" && thumbnail !== undefined) {
|
|
36
|
+
try {
|
|
37
|
+
cover = new Uint8Array(readFileSync(outputPath(deps.paths, projectId, thumbnail.output.path)));
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
throw new Error(`The document's cover is the project's thumbnail, and its file can't be read (${error instanceof Error ? error.message : String(error)}). Regenerate or upload the thumbnail again (Re-run section on Thumbnail, or Edit project → Thumbnail), then use Retry stage on Document.`, { cause: error });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const rendered = renderDocument({
|
|
44
|
+
title: config.title,
|
|
45
|
+
articleMarkdown: article,
|
|
46
|
+
researchNotes: config.sources.research === "off" ? null : text("notes", "research:notes"),
|
|
47
|
+
cover,
|
|
48
|
+
theme: builtInTheme(documentThemeOf(config.document)),
|
|
49
|
+
writtenOn: deps.clock.now(),
|
|
50
|
+
assets: readDocumentAssets(),
|
|
51
|
+
});
|
|
52
|
+
if (rendered.cover === "unsupported")
|
|
53
|
+
deps.log.write("warn", "document.cover", {
|
|
54
|
+
projectId,
|
|
55
|
+
stage: "document",
|
|
56
|
+
detail: "The thumbnail is not a PNG, JPEG or WebP image, so the document has no cover.",
|
|
57
|
+
});
|
|
58
|
+
context.signal.throwIfAborted();
|
|
59
|
+
if (!context.maySubmit(piece.id))
|
|
60
|
+
return "held";
|
|
61
|
+
const asset = writeAsset(deps, projectId, "document.pdf", rendered.bytes);
|
|
62
|
+
// publishResult discards the file itself when the publication doesn't commit.
|
|
63
|
+
await publishResult(deps, context, piece, [preparedResult(deps, context, piece, "document_pdf", asset)], { pages: rendered.pages, words: rendered.words, cover: rendered.cover });
|
|
64
|
+
deps.count?.("stage.completed", { stage: "document" });
|
|
65
|
+
return "done";
|
|
66
|
+
}
|
|
67
|
+
function documentView(deps, context, piece) {
|
|
68
|
+
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
69
|
+
const row = deps.db
|
|
70
|
+
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
71
|
+
.get(context.work.workId);
|
|
72
|
+
if (view === undefined || row === undefined)
|
|
73
|
+
throw new Error("Slopify hit an internal error (the project version the document belongs to is missing). Use Retry stage on Document; if it happens again, use Download diagnostics in Settings and report it.");
|
|
74
|
+
const recipe = executionPlan(deps, view, savedCatalogue(row.recipe_context)).recipes.find((one) => one.key === piece.key && one.fingerprint === piece.fingerprint);
|
|
75
|
+
if (recipe === undefined || recipe.unresolved)
|
|
76
|
+
throw new Error("The project changed after this document was queued, so it no longer matches the saved article. Use Rebuild affected outputs (or Retry stage on Document) to make it from the current version.");
|
|
77
|
+
return view;
|
|
78
|
+
}
|
|
79
|
+
function ready(view, role, workKey) {
|
|
80
|
+
return view.outputs.find((row) => row.selected &&
|
|
81
|
+
row.available &&
|
|
82
|
+
row.state === "ready" &&
|
|
83
|
+
row.workKey === workKey &&
|
|
84
|
+
row.output.role === role);
|
|
85
|
+
}
|