@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
|
@@ -14,11 +14,11 @@ export function exportSnapshot(deps, context, piece) {
|
|
|
14
14
|
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
15
15
|
.get(context.work.workId);
|
|
16
16
|
if (view === undefined || row === undefined)
|
|
17
|
-
throw new Error("
|
|
17
|
+
throw new Error("Slopify hit an internal error (the project version being exported is missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
18
18
|
const plan = executionPlan(deps, view, savedCatalogue(row.recipe_context));
|
|
19
19
|
const recipe = plan.recipes.find((one) => one.key === piece.key && one.fingerprint === piece.fingerprint);
|
|
20
20
|
if (recipe === undefined || recipe.deferred || recipe.unresolved)
|
|
21
|
-
throw new Error("
|
|
21
|
+
throw new Error("Slopify hit an internal error (the export no longer matches the project's saved inputs). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
22
22
|
return { view, plan };
|
|
23
23
|
}
|
|
24
24
|
export async function revisionAudio(deps, context, view) {
|
|
@@ -34,12 +34,12 @@ export async function revisionAudio(deps, context, view) {
|
|
|
34
34
|
(await (deps.measureAudio?.(path, context.signal) ??
|
|
35
35
|
probeDurationMs(deps.ffmpeg, path, context.signal, deps.log)));
|
|
36
36
|
if (!Number.isFinite(duration) || duration <= 0)
|
|
37
|
-
throw new Error("
|
|
37
|
+
throw new Error("Slopify couldn't read the length of the narration audio, so the file may be damaged or empty. Regenerate the narration (or upload your audio file again in Edit project → Narration), then Retry stage.");
|
|
38
38
|
return { path, seconds: duration / 1000 };
|
|
39
39
|
};
|
|
40
40
|
const body = await input("audio_body");
|
|
41
41
|
if (body === undefined)
|
|
42
|
-
throw new Error("The
|
|
42
|
+
throw new Error("The narration audio isn't finished yet. Let the Narration stage finish (Resume, or Retry stage on it), then retry this stage.");
|
|
43
43
|
return audioTimeline({
|
|
44
44
|
body,
|
|
45
45
|
intro: config.sources.audio === "generate" && config.intro !== undefined
|
|
@@ -49,6 +49,7 @@ export async function revisionAudio(deps, context, view) {
|
|
|
49
49
|
? await input("audio_outro")
|
|
50
50
|
: undefined,
|
|
51
51
|
gapSeconds: config.silenceGapSeconds,
|
|
52
|
+
edgeSeconds: config.edgeSilenceSeconds,
|
|
52
53
|
}, config.sources.video === "off" ? 1 / 48000 : 1 / 30);
|
|
53
54
|
}
|
|
54
55
|
export function revisionTranscript(deps, snapshot, kind) {
|
|
@@ -72,7 +73,7 @@ export function revisionTranscript(deps, snapshot, kind) {
|
|
|
72
73
|
Array.isArray(recipe.input.semantic) &&
|
|
73
74
|
typeof recipe.input.semantic[0] === "string")
|
|
74
75
|
return recipe.input.semantic[0];
|
|
75
|
-
throw new Error("
|
|
76
|
+
throw new Error("Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Resume, or Retry stage on it), or regenerate the missing chunk in Edit project → Narration.");
|
|
76
77
|
});
|
|
77
78
|
if (texts.length > 0)
|
|
78
79
|
return texts.join("\n");
|
|
@@ -84,7 +85,7 @@ export function revisionTranscript(deps, snapshot, kind) {
|
|
|
84
85
|
if (view.articleMarkdown !== null)
|
|
85
86
|
return plainText(splitEndMatter(view.articleMarkdown).body);
|
|
86
87
|
}
|
|
87
|
-
throw new Error(`The saved ${kind} narration
|
|
88
|
+
throw new Error(`The saved text of the ${kind === "body" ? "article" : kind} is missing, so it can't be matched to the narration. Use Re-run section on Article, then Retry stage.`);
|
|
88
89
|
}
|
|
89
90
|
export function retainedOutput(deps, view, row) {
|
|
90
91
|
const asset = z
|
|
@@ -3,8 +3,10 @@ import { extname, join, relative, sep } from "node:path";
|
|
|
3
3
|
import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
|
|
4
4
|
import { outputPath, projectDir } from "../storage/layout.js";
|
|
5
5
|
import { audioExportArgs } from "../video/audio-export.js";
|
|
6
|
-
import {
|
|
6
|
+
import { withPaths } from "../video/edit-list.js";
|
|
7
|
+
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
7
8
|
import { planRender } from "../video/plan.js";
|
|
9
|
+
import { renderSlideshow } from "../video/slideshow.js";
|
|
8
10
|
import { exportSnapshot, retainedOutput, revisionAudio, } from "./runtime-export-inputs.js";
|
|
9
11
|
import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
|
|
10
12
|
export async function executeExportRecipe(deps, context, piece) {
|
|
@@ -20,9 +22,9 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
20
22
|
const audio = await revisionAudio(deps, context, view);
|
|
21
23
|
const wav = piece.key === "export:wav";
|
|
22
24
|
if (!wav && piece.key !== "export:video")
|
|
23
|
-
throw new Error("
|
|
25
|
+
throw new Error("Slopify hit an internal error (unknown kind of export). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
24
26
|
if (wav && audio.length === 0)
|
|
25
|
-
throw new Error("
|
|
27
|
+
throw new Error("The audio export has nothing to export because narration is turned off for this project. Turn narration on in Edit project, then Retry stage.");
|
|
26
28
|
const filename = wav ? "audio.wav" : "video.mp4";
|
|
27
29
|
const pending = allocateAsset(deps, context.work.projectId, filename);
|
|
28
30
|
const prepared = [];
|
|
@@ -48,7 +50,7 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
48
50
|
const images = (wav ? [] : view.revision.content.imageOrder).map((key) => {
|
|
49
51
|
const row = view.outputs.find((one) => one.workKey === `image:${key}` && one.selected && one.available && one.state === "ready");
|
|
50
52
|
if (row === undefined)
|
|
51
|
-
throw new Error("The
|
|
53
|
+
throw new Error("The video export can't find one of the slideshow images. Use Re-run section on Images (or regenerate that image in Edit project → Images), then Retry stage.");
|
|
52
54
|
return outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
53
55
|
});
|
|
54
56
|
const plan = wav
|
|
@@ -56,6 +58,10 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
56
58
|
: planRender({
|
|
57
59
|
format: config.format,
|
|
58
60
|
gapSeconds: config.silenceGapSeconds,
|
|
61
|
+
edgeSeconds: config.edgeSilenceSeconds,
|
|
62
|
+
imageSeconds: config.imageSeconds,
|
|
63
|
+
zoomPercent: config.zoomPercent,
|
|
64
|
+
motionStyle: config.motionStyle,
|
|
59
65
|
body: segment("body"),
|
|
60
66
|
intro: segment("intro"),
|
|
61
67
|
outro: segment("outro"),
|
|
@@ -71,6 +77,7 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
71
77
|
channels: 2,
|
|
72
78
|
codec: "pcm_s16le",
|
|
73
79
|
gapSeconds: config.silenceGapSeconds,
|
|
80
|
+
edgeSeconds: config.edgeSilenceSeconds,
|
|
74
81
|
totalSeconds,
|
|
75
82
|
audio: audio.map((row) => ({
|
|
76
83
|
...row,
|
|
@@ -81,29 +88,38 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
81
88
|
: {
|
|
82
89
|
...plan,
|
|
83
90
|
output: pending.path,
|
|
84
|
-
|
|
85
|
-
...row,
|
|
86
|
-
path: row.path === null ? null : relativePath(row.path),
|
|
87
|
-
})),
|
|
88
|
-
images: plan.images.map((row) => ({ ...row, path: relativePath(row.path) })),
|
|
91
|
+
editList: withPaths(plan.editList, relativePath),
|
|
89
92
|
subtitles: config.subtitles,
|
|
90
93
|
};
|
|
91
94
|
if (!context.maySubmit(piece.id))
|
|
92
95
|
return "held";
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
-
}),
|
|
96
|
+
const onProgress = (elapsedMs) => context.emit({
|
|
97
|
+
type: "stage.progress",
|
|
98
|
+
projectId: context.work.projectId,
|
|
99
|
+
stage: "video",
|
|
100
|
+
current: Math.min(100, Math.round(elapsedMs / (totalSeconds * 10))),
|
|
101
|
+
total: 100,
|
|
106
102
|
});
|
|
103
|
+
if (plan === undefined)
|
|
104
|
+
await runFfmpeg({
|
|
105
|
+
bin: deps.ffmpeg,
|
|
106
|
+
args: audioExportArgs(audio, pending.absolutePath),
|
|
107
|
+
signal: context.signal,
|
|
108
|
+
log: deps.log,
|
|
109
|
+
onProgress,
|
|
110
|
+
});
|
|
111
|
+
else
|
|
112
|
+
await renderSlideshow({
|
|
113
|
+
bin: deps.ffmpeg,
|
|
114
|
+
edit: plan.editList,
|
|
115
|
+
output: plan.output,
|
|
116
|
+
burnSubtitles: burn,
|
|
117
|
+
cwd: directory,
|
|
118
|
+
scratch: projectDirectory,
|
|
119
|
+
signal: context.signal,
|
|
120
|
+
log: deps.log,
|
|
121
|
+
onProgress,
|
|
122
|
+
});
|
|
107
123
|
context.signal.throwIfAborted();
|
|
108
124
|
const asset = sealAsset(deps, pending);
|
|
109
125
|
prepared.push(preparedResult(deps, context, piece, wav ? "audio_export" : "video", asset, Math.round(totalSeconds * 1000), { subtitlesMode: mode }));
|
|
@@ -124,7 +140,7 @@ function captionDirectory(deps, context, snapshot) {
|
|
|
124
140
|
const ass = find("subtitle_ass");
|
|
125
141
|
const font = find("subtitle_font");
|
|
126
142
|
if (ass === undefined || font === undefined)
|
|
127
|
-
throw new Error("Burned
|
|
143
|
+
throw new Error("Burned-in captions are missing their caption file or font. Use Re-run section on Video (or choose the caption font again in Edit project → Subtitles), then Retry stage.");
|
|
128
144
|
const directory = mkdtempSync(join(projectDir(deps.paths, context.work.projectId), "render-"));
|
|
129
145
|
try {
|
|
130
146
|
mkdirSync(join(directory, "fonts"), { mode: 0o700 });
|
|
@@ -19,7 +19,7 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
19
19
|
const input = piece.input;
|
|
20
20
|
if (input.kind === "provided") {
|
|
21
21
|
if (input.assetId === null)
|
|
22
|
-
throw new Error("
|
|
22
|
+
throw new Error("A file you uploaded for this project is missing. Upload it again in Edit project, then Retry stage.");
|
|
23
23
|
const row = z
|
|
24
24
|
.object({ id: z.string(), project_id: z.string(), path: z.string(), created_at: z.string() })
|
|
25
25
|
.parse(deps.db
|
|
@@ -43,7 +43,7 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
43
43
|
return "done";
|
|
44
44
|
}
|
|
45
45
|
if (input.kind !== "local")
|
|
46
|
-
throw new Error("
|
|
46
|
+
throw new Error("Slopify hit an internal error (a local step was set up wrongly). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
47
47
|
if (input.operation === "narration-files-v1") {
|
|
48
48
|
await publishNarrationText(deps, context, piece);
|
|
49
49
|
return "done";
|
|
@@ -75,19 +75,19 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
75
75
|
});
|
|
76
76
|
return "done";
|
|
77
77
|
}
|
|
78
|
-
throw new Error(`
|
|
78
|
+
throw new Error(`Slopify hit an internal error (no handler for the step "${input.operation}"). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
|
|
79
79
|
}
|
|
80
80
|
async function concatenate(deps, context, piece) {
|
|
81
81
|
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
82
82
|
if (view === undefined)
|
|
83
|
-
throw new Error("
|
|
83
|
+
throw new Error("Slopify hit an internal error (the project version being built is missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
84
84
|
const row = deps.db
|
|
85
85
|
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
86
86
|
.get(context.work.workId);
|
|
87
87
|
const plan = executionPlan(deps, view, savedCatalogue(row?.recipe_context));
|
|
88
88
|
const recipe = plan.recipes.find((candidate) => candidate.key === piece.key && candidate.fingerprint === piece.fingerprint);
|
|
89
89
|
if (recipe === undefined)
|
|
90
|
-
throw new Error("
|
|
90
|
+
throw new Error("Slopify hit an internal error (the narration chunks to join no longer match the project's saved inputs). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
91
91
|
const files = recipe.dependsOn.map((key) => {
|
|
92
92
|
const input = view.pieces.find((candidate) => candidate.key === key &&
|
|
93
93
|
candidate.selected &&
|
|
@@ -95,7 +95,7 @@ async function concatenate(deps, context, piece) {
|
|
|
95
95
|
candidate.piece.state === "done" &&
|
|
96
96
|
candidate.fingerprint === plan.recipes.find((value) => value.key === key)?.fingerprint);
|
|
97
97
|
if (input?.assetId === null || input === undefined)
|
|
98
|
-
throw new Error("
|
|
98
|
+
throw new Error("One narration chunk has no saved audio, so the narration can't be joined together. Regenerate the missing chunk in Edit project → Narration, then Retry stage.");
|
|
99
99
|
const asset = deps.db
|
|
100
100
|
.prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
|
|
101
101
|
.get(input.assetId, context.work.projectId);
|
|
@@ -9,7 +9,7 @@ export function joinedNarration(parts) {
|
|
|
9
9
|
export function narrationTextParts(view, plan, segment) {
|
|
10
10
|
const concat = plan.recipes.find((row) => row.key === (segment === "body" ? "audio:body:concat" : `audio:${segment}`));
|
|
11
11
|
if (concat === undefined)
|
|
12
|
-
throw new Error(`
|
|
12
|
+
throw new Error(`Slopify hit an internal error (the ${segment} narration is missing from the project plan). Retry stage; if it happens again, use Download diagnostics in Settings and report it.`);
|
|
13
13
|
return concat.dependsOn.map((key) => {
|
|
14
14
|
const recipe = plan.recipes.find((row) => row.key === key);
|
|
15
15
|
const selected = view.pieces.find((row) => row.key === key &&
|
|
@@ -18,7 +18,7 @@ export function narrationTextParts(view, plan, segment) {
|
|
|
18
18
|
row.piece.state === "done" &&
|
|
19
19
|
row.fingerprint === recipe?.fingerprint);
|
|
20
20
|
if (selected === undefined || recipe === undefined)
|
|
21
|
-
throw new Error("
|
|
21
|
+
throw new Error("Some narration chunks aren't finished, so the full narration text isn't available. Let the Narration stage finish (Resume, or Retry stage on it), or regenerate the missing chunk in Edit project → Narration.");
|
|
22
22
|
const input = recipe.input;
|
|
23
23
|
if (input.kind === "provided" &&
|
|
24
24
|
Array.isArray(input.semantic) &&
|
|
@@ -29,13 +29,13 @@ export function narrationTextParts(view, plan, segment) {
|
|
|
29
29
|
requestText: null,
|
|
30
30
|
};
|
|
31
31
|
if (input.kind !== "tts")
|
|
32
|
-
throw new Error("
|
|
32
|
+
throw new Error("Slopify hit an internal error (a narration chunk's text was never worked out). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
33
33
|
const payload = z
|
|
34
34
|
.object({ text: z.string().optional(), spokenText: z.string().optional() })
|
|
35
35
|
.parse(JSON.parse(selected.piece.payload ?? "{}"));
|
|
36
36
|
if (input.spokenText !== undefined &&
|
|
37
37
|
(payload.spokenText !== input.spokenText || payload.text !== input.text))
|
|
38
|
-
throw new Error("
|
|
38
|
+
throw new Error("A narration chunk's audio no longer matches its prepared text. Regenerate that chunk in Edit project → Narration, then Retry stage.");
|
|
39
39
|
return {
|
|
40
40
|
logicalKey: input.logicalKey,
|
|
41
41
|
spokenText: input.spokenText === undefined ? (payload.text ?? input.text) : (payload.spokenText ?? ""),
|
|
@@ -45,7 +45,7 @@ export function narrationTextParts(view, plan, segment) {
|
|
|
45
45
|
}
|
|
46
46
|
export async function publishNarrationText(deps, context, piece) {
|
|
47
47
|
if (piece.input.kind !== "local")
|
|
48
|
-
throw new Error("
|
|
48
|
+
throw new Error("Slopify hit an internal error (the narration text file step was set up wrongly). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
49
49
|
const { segment } = z
|
|
50
50
|
.object({ segment: z.enum(["body", "intro", "outro"]) })
|
|
51
51
|
.parse(piece.input.values);
|
|
@@ -54,10 +54,10 @@ export async function publishNarrationText(deps, context, piece) {
|
|
|
54
54
|
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
55
55
|
.get(context.work.workId);
|
|
56
56
|
if (view === undefined)
|
|
57
|
-
throw new Error("
|
|
57
|
+
throw new Error("Slopify hit an internal error (the project version being narrated is missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
58
58
|
const plan = executionPlan(deps, view, savedCatalogue(row?.recipe_context));
|
|
59
59
|
if (!plan.recipes.some((recipe) => recipe.key === piece.key && recipe.fingerprint === piece.fingerprint))
|
|
60
|
-
throw new Error("
|
|
60
|
+
throw new Error("Slopify hit an internal error (the narration text no longer matches the project's saved inputs). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
61
61
|
const parts = narrationTextParts(view, plan, segment);
|
|
62
62
|
const outputs = preparedTexts(deps, context, piece, [
|
|
63
63
|
["narration_txt", `${segment}-narration.txt`, joinedNarration(parts)],
|
|
@@ -73,3 +73,42 @@ export async function publishNarrationText(deps, context, piece) {
|
|
|
73
73
|
}));
|
|
74
74
|
await publishResult(deps, context, piece, outputs, { segment });
|
|
75
75
|
}
|
|
76
|
+
// The narration's chunks in the order they are spoken, each with its clean text. A chunk is
|
|
77
|
+
// sent as one or more parts; the Narration editor numbers these chunks, so anything that
|
|
78
|
+
// names "chunk N" to a person counts from this list.
|
|
79
|
+
export function narrationChunks(parts) {
|
|
80
|
+
const chunks = [];
|
|
81
|
+
for (const part of parts) {
|
|
82
|
+
const last = chunks.at(-1);
|
|
83
|
+
if (last?.key === part.logicalKey)
|
|
84
|
+
chunks[chunks.length - 1] = {
|
|
85
|
+
key: last.key,
|
|
86
|
+
spokenText: `${last.spokenText}${part.spokenText}`,
|
|
87
|
+
};
|
|
88
|
+
else
|
|
89
|
+
chunks.push({ key: part.logicalKey, spokenText: part.spokenText });
|
|
90
|
+
}
|
|
91
|
+
return chunks;
|
|
92
|
+
}
|
|
93
|
+
// The current chunk keys of each narration segment, in spoken order, for the Narration
|
|
94
|
+
// editor. Undefined when the revision has no admitted work to read a plan from, or its
|
|
95
|
+
// narration is not complete yet; the editor then keeps its own listing.
|
|
96
|
+
export function narrationChunkOrder(deps, projectId, revisionId) {
|
|
97
|
+
const row = deps.db
|
|
98
|
+
.prepare("SELECT recipe_context FROM revision_work WHERE project_id=? AND revision_id=? AND recipe_context IS NOT NULL LIMIT 1")
|
|
99
|
+
.get(projectId, revisionId);
|
|
100
|
+
const view = executionView(deps, projectId, revisionId);
|
|
101
|
+
if (row === undefined || view === undefined)
|
|
102
|
+
return undefined;
|
|
103
|
+
const plan = executionPlan(deps, view, savedCatalogue(row.recipe_context));
|
|
104
|
+
const order = {};
|
|
105
|
+
for (const segment of ["intro", "body", "outro"]) {
|
|
106
|
+
try {
|
|
107
|
+
order[segment] = narrationChunks(narrationTextParts(view, plan, segment)).map((chunk) => chunk.key);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// A segment that is off, or not narrated yet, has no chunks to order.
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return order;
|
|
114
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
2
|
+
// What a failed step is called on screen, so its error says which image, chunk or topic to
|
|
3
|
+
// fix. Worked out only after a failure: numbering a narration chunk means building the plan.
|
|
4
|
+
// Undefined for steps that name their own place (captions, export) or have no single one.
|
|
5
|
+
export function pieceLabel(deps, context, piece) {
|
|
6
|
+
const { key } = piece;
|
|
7
|
+
if (key === "thumbnail:image" || key.startsWith("thumbnail:"))
|
|
8
|
+
return "Thumbnail";
|
|
9
|
+
if (key === "article:body")
|
|
10
|
+
return "Article";
|
|
11
|
+
if (key === "document:pdf")
|
|
12
|
+
return "Document";
|
|
13
|
+
if (key === "youtube:description")
|
|
14
|
+
return "YouTube description";
|
|
15
|
+
if (key === "entry:intro:text")
|
|
16
|
+
return "Intro text";
|
|
17
|
+
if (key === "entry:outro:text")
|
|
18
|
+
return "Outro text";
|
|
19
|
+
if (key === "research:planner")
|
|
20
|
+
return "Research plan";
|
|
21
|
+
if (key === "research:notes")
|
|
22
|
+
return "Research notes";
|
|
23
|
+
const topic = /^research:chapter:(\d+)$/.exec(key);
|
|
24
|
+
if (topic !== null)
|
|
25
|
+
return `Research topic ${topic[1] ?? ""}`;
|
|
26
|
+
const view = () => executionView(deps, context.work.projectId, context.work.revisionId);
|
|
27
|
+
if (key.startsWith("image:")) {
|
|
28
|
+
const at = view()?.revision.content.imageOrder.indexOf(key.slice("image:".length)) ?? -1;
|
|
29
|
+
return at === -1 ? "An image" : `Image ${String(at + 1)}`;
|
|
30
|
+
}
|
|
31
|
+
const narration = /^(?:narration:prepare:(?:intro|body|outro):)?audio:(intro|body|outro)(?::(.+))?$/.exec(key);
|
|
32
|
+
if (narration === null)
|
|
33
|
+
return undefined;
|
|
34
|
+
const segment = narration[1] ?? "body";
|
|
35
|
+
const prefix = segment === "body" ? "Narration" : `${segment === "intro" ? "Intro" : "Outro"} narration`;
|
|
36
|
+
if (narration[2] === undefined || narration[2] === "concat" || narration[2] === "future")
|
|
37
|
+
return prefix;
|
|
38
|
+
return narrationChunk(deps, context, segment, key, prefix) ?? prefix;
|
|
39
|
+
}
|
|
40
|
+
function narrationChunk(deps, context, segment, key, prefix) {
|
|
41
|
+
const row = deps.db
|
|
42
|
+
.prepare("SELECT recipe_context FROM revision_work WHERE id=?")
|
|
43
|
+
.get(context.work.workId);
|
|
44
|
+
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
45
|
+
if (row?.recipe_context === null || row === undefined || view === undefined)
|
|
46
|
+
return undefined;
|
|
47
|
+
const plan = executionPlan(deps, view, savedCatalogue(row.recipe_context));
|
|
48
|
+
const concat = plan.recipes.find((recipe) => recipe.key === (segment === "body" ? "audio:body:concat" : `audio:${segment}`));
|
|
49
|
+
if (concat === undefined)
|
|
50
|
+
return undefined;
|
|
51
|
+
// Parts are `<chunk>:<n>`; the chunk is everything before the last colon.
|
|
52
|
+
const chunkOf = (part) => part.slice(0, part.lastIndexOf(":"));
|
|
53
|
+
const chunks = [...new Set(concat.dependsOn.map(chunkOf))];
|
|
54
|
+
const preparing = key.startsWith("narration:prepare:");
|
|
55
|
+
const chunk = preparing ? key.slice(key.indexOf("audio:")) : chunkOf(key);
|
|
56
|
+
const at = chunks.indexOf(chunk);
|
|
57
|
+
if (at === -1)
|
|
58
|
+
return undefined;
|
|
59
|
+
const label = `${prefix} chunk ${String(at + 1)} of ${String(chunks.length)}`;
|
|
60
|
+
if (preparing)
|
|
61
|
+
return `${label} (preparing its text)`;
|
|
62
|
+
const parts = concat.dependsOn.filter((part) => chunkOf(part) === chunk);
|
|
63
|
+
return parts.length > 1
|
|
64
|
+
? `${label}, part ${String(parts.indexOf(key) + 1)} of ${String(parts.length)}`
|
|
65
|
+
: label;
|
|
66
|
+
}
|
|
@@ -117,13 +117,15 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
|
|
|
117
117
|
});
|
|
118
118
|
return "done";
|
|
119
119
|
}
|
|
120
|
-
throw new Error("
|
|
120
|
+
throw new Error("Slopify hit an internal error (this step has no provider request to run). Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
121
121
|
}
|
|
122
122
|
function checkAnswer(piece, answer) {
|
|
123
123
|
if (answer.text.trim() === "")
|
|
124
|
-
return "The
|
|
124
|
+
return "The AI model sent back an empty answer. Use Retry stage; if it keeps happening, choose another model in the Providers section of Edit project.";
|
|
125
125
|
if (piece.key === "research:planner")
|
|
126
|
-
return chaptersFrom(answer.text).length === 0
|
|
126
|
+
return chaptersFrom(answer.text).length === 0
|
|
127
|
+
? "The AI model's research plan listed no chapters, so research could not go on. Use Retry stage; if it keeps happening, choose another model in the Providers section of Edit project."
|
|
128
|
+
: undefined;
|
|
127
129
|
if (piece.key === "research:notes" || piece.key.startsWith("research:chapter:"))
|
|
128
130
|
return sourcedAnswer(piece.key, answer.text);
|
|
129
131
|
return undefined;
|
|
@@ -175,7 +177,7 @@ async function publishText(deps, context, piece, answer) {
|
|
|
175
177
|
: [];
|
|
176
178
|
const title = titles[Number(piece.key.split(":").at(-1)) - 1];
|
|
177
179
|
if (typeof title !== "string")
|
|
178
|
-
throw new Error("
|
|
180
|
+
throw new Error("Slopify hit an internal error (a research chapter has no title in the research plan). Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
179
181
|
const asset = writeAsset(deps, context.work.projectId, `research-${piece.key.split(":").at(-1)}.md`, Buffer.from(answer.text));
|
|
180
182
|
await publishResult(deps, context, piece, [], { title, notes: answer.text }, asset);
|
|
181
183
|
return;
|
|
@@ -29,7 +29,7 @@ export function preparedResult(deps, context, piece, role, asset, durationMs = n
|
|
|
29
29
|
export function preparedText(deps, context, piece, role, filename, text) {
|
|
30
30
|
const output = preparedTexts(deps, context, piece, [[role, filename, text]])[0];
|
|
31
31
|
if (output === undefined)
|
|
32
|
-
throw new Error("
|
|
32
|
+
throw new Error("Slopify hit an internal error (a text file could not be prepared). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
33
33
|
return output;
|
|
34
34
|
}
|
|
35
35
|
export function preparedTexts(deps, context, piece, texts) {
|
|
@@ -127,6 +127,7 @@ function pieceIndex(deps, context, piece) {
|
|
|
127
127
|
"subtitles:files",
|
|
128
128
|
"export:wav",
|
|
129
129
|
"export:video",
|
|
130
|
+
"document:pdf",
|
|
130
131
|
"narration:files:body",
|
|
131
132
|
"narration:files:intro",
|
|
132
133
|
"narration:files:outro",
|
|
@@ -168,10 +169,10 @@ export function retainPartialArticle(deps, context, piece, text) {
|
|
|
168
169
|
if (authority === undefined)
|
|
169
170
|
return;
|
|
170
171
|
if (authority.key !== piece.key || authority.fingerprint !== piece.fingerprint)
|
|
171
|
-
throw new Error("
|
|
172
|
+
throw new Error("Slopify hit an internal error (a partly written article doesn't match its saved step). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
172
173
|
const revision = revisionById(deps.db, context.work.projectId, context.work.revisionId);
|
|
173
174
|
if (revision === undefined)
|
|
174
|
-
throw new Error("
|
|
175
|
+
throw new Error("Slopify hit an internal error (the project version for a partly written article is missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
175
176
|
insertAsset(deps.db, result.asset);
|
|
176
177
|
insertManifestOutput(deps.db, revision, {
|
|
177
178
|
slot: `article:partial:${piece.id}`,
|
|
@@ -1,7 +1,10 @@
|
|
|
1
|
+
import { executeDocumentRecipe } from "./runtime-document.js";
|
|
1
2
|
import { executeExportRecipe } from "./runtime-export.js";
|
|
2
3
|
import { executeLocalRecipe } from "./runtime-local.js";
|
|
4
|
+
import { pieceLabel } from "./runtime-piece-label.js";
|
|
3
5
|
import { executeProviderRecipe } from "./runtime-provider.js";
|
|
4
6
|
import { executeSubtitleRecipe } from "./runtime-subtitles.js";
|
|
7
|
+
import { executeYoutubeRecipe } from "./runtime-youtube.js";
|
|
5
8
|
import { workPieces } from "./work-records.js";
|
|
6
9
|
export async function runRevisionInvocation(deps, context, providers) {
|
|
7
10
|
const pieces = workPieces(deps.db, context.work.workId).filter((piece) => !piece.key.startsWith("article:continuation:"));
|
|
@@ -15,9 +18,15 @@ export async function runRevisionInvocation(deps, context, providers) {
|
|
|
15
18
|
? await executeSubtitleRecipe(deps, context, piece)
|
|
16
19
|
: piece.key.startsWith("export:")
|
|
17
20
|
? await executeExportRecipe(deps, context, piece)
|
|
18
|
-
: piece.
|
|
19
|
-
? await
|
|
20
|
-
:
|
|
21
|
+
: piece.key.startsWith("document:")
|
|
22
|
+
? await executeDocumentRecipe(deps, context, piece)
|
|
23
|
+
: piece.key.startsWith("youtube:")
|
|
24
|
+
? await executeYoutubeRecipe(deps, context, providers, piece)
|
|
25
|
+
: piece.input.kind === "llm" ||
|
|
26
|
+
piece.input.kind === "tts" ||
|
|
27
|
+
piece.input.kind === "image"
|
|
28
|
+
? await executeProviderRecipe(deps, context, providers, piece)
|
|
29
|
+
: await executeLocalRecipe(deps, context, piece);
|
|
21
30
|
if (outcome === "held")
|
|
22
31
|
return outcome;
|
|
23
32
|
}
|
|
@@ -25,7 +34,16 @@ export async function runRevisionInvocation(deps, context, providers) {
|
|
|
25
34
|
deps.db
|
|
26
35
|
.prepare("UPDATE revision_work_pieces SET state=? WHERE id=? AND work_id=? AND state!='done'")
|
|
27
36
|
.run(context.signal.aborted ? "pending" : "failed", piece.id, context.work.workId);
|
|
28
|
-
|
|
37
|
+
if (context.signal.aborted || !(error instanceof Error))
|
|
38
|
+
throw error;
|
|
39
|
+
let label;
|
|
40
|
+
try {
|
|
41
|
+
label = pieceLabel(deps, context, piece);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// The error itself matters more than its label.
|
|
45
|
+
}
|
|
46
|
+
throw label === undefined ? error : new Error(`${label}: ${error.message}`, { cause: error });
|
|
29
47
|
}
|
|
30
48
|
}
|
|
31
49
|
deps.db
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { stageKinds, stageStates } from "../../kernel/pipeline.js";
|
|
3
|
+
import { sourceOf } from "../admission/model.js";
|
|
4
|
+
import { usesYoutubeDescription } from "../admission/rules.js";
|
|
3
5
|
import { currentRevisionId } from "../revisions/repo.js";
|
|
4
6
|
import { getRevisionView } from "../revisions/view.js";
|
|
5
7
|
import { settleScheduleRunsForProject } from "../schedules/repo.js";
|
|
@@ -31,7 +33,10 @@ export function executionStages(deps, projectId) {
|
|
|
31
33
|
.filter((row) => workPieces(deps.db, String(row.id)).some((piece) => {
|
|
32
34
|
if (!keys.has(piece.key))
|
|
33
35
|
return false;
|
|
34
|
-
|
|
36
|
+
// Hand-edited captions need no timing, unless the YouTube description reads it.
|
|
37
|
+
if (piece.key === "subtitles:timing" &&
|
|
38
|
+
view?.revision.content.subtitleCues !== undefined &&
|
|
39
|
+
!usesYoutubeDescription(view.revision.config))
|
|
35
40
|
return false;
|
|
36
41
|
if (piece.input.kind === "deferred" && piece.key.endsWith(":future")) {
|
|
37
42
|
const cacheKey = String(row.id);
|
|
@@ -101,7 +106,7 @@ export function executionStandings(deps, projectId, derived) {
|
|
|
101
106
|
return [];
|
|
102
107
|
const work = derived ?? executionStages(deps, projectId);
|
|
103
108
|
return stageKinds.map((kind) => {
|
|
104
|
-
const source = view.revision.config.sources
|
|
109
|
+
const source = sourceOf(view.revision.config.sources, kind);
|
|
105
110
|
const group = work
|
|
106
111
|
.filter((entry) => entry.kind === kind)
|
|
107
112
|
.map((entry) => {
|