@gentbajko/slopify 2.3.1 → 2.5.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/dist/adapter-registry.js +9 -2
- package/dist/adapters/host-cli/index.js +11 -0
- package/dist/adapters/host-cli/transport.js +6 -1
- package/dist/adapters/image/codex-output.js +59 -17
- package/dist/adapters/image/codex.js +92 -16
- package/dist/adapters/image/fal.js +99 -8
- package/dist/adapters/image/google.js +13 -1
- package/dist/adapters/image/openai.js +37 -15
- package/dist/adapters/image/reference.js +7 -0
- package/dist/adapters/image/replicate.js +48 -2
- package/dist/adapters/image/video.js +28 -0
- package/dist/adapters/llm/codex-models.js +2 -1
- package/dist/adapters/llm/codex-server-models.js +2 -1
- package/dist/adapters/llm/run-cli.js +14 -2
- package/dist/assets/fonts/Barlow-Bold.ttf +0 -0
- package/dist/assets/fonts/SOURCE.txt +6 -0
- package/dist/assets/models.yaml +89 -1
- package/dist/catalog/registry.js +24 -3
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +20 -1
- package/dist/catalog/store.js +2 -2
- package/dist/catalog/validate.js +47 -4
- package/dist/edge/docker-launch.js +4 -0
- package/dist/edge/docker-projects/engine.js +57 -1
- package/dist/edge/docker-projects/install.js +10 -2
- package/dist/edge/docker-projects/prune.js +66 -0
- package/dist/edge/events/hub.js +11 -0
- package/dist/edge/http/app.js +2 -0
- package/dist/edge/http/backups.js +40 -0
- package/dist/edge/http/host-cli.js +17 -2
- package/dist/edge/http/problem.js +1 -0
- package/dist/edge/http/providers.js +8 -0
- package/dist/edge/http/schedules.js +1 -1
- package/dist/edge/http/settings.js +44 -0
- package/dist/edge/http/timeouts.js +38 -0
- package/dist/host-cli/runtime.js +3 -2
- package/dist/kernel/db/migrations/0019-shorts-prompts.sql +16 -0
- package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
- package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
- package/dist/kernel/ports/host-cli.js +17 -2
- package/dist/kernel/ports/image.js +3 -1
- package/dist/kernel/ports/llm.js +3 -1
- package/dist/kernel/runner/attempt.js +5 -2
- package/dist/kernel/runner/providers.js +42 -2
- package/dist/main.js +55 -7
- package/dist/slices/admission/model.js +8 -0
- package/dist/slices/admission/rules.js +91 -2
- package/dist/slices/admission/schema.js +42 -1
- package/dist/slices/admission/start.js +53 -15
- package/dist/slices/backups/files.js +135 -0
- package/dist/slices/backups/folder.js +35 -0
- package/dist/slices/backups/model.js +65 -0
- package/dist/slices/backups/repo.js +28 -0
- package/dist/slices/backups/schedule.js +60 -0
- package/dist/slices/backups/service.js +311 -0
- package/dist/slices/batch/index.js +2 -0
- package/dist/slices/estimate/index.js +65 -2
- package/dist/slices/fonts/catalog.js +23 -0
- package/dist/slices/fonts/index.js +1 -1
- package/dist/slices/library/model.js +8 -1
- package/dist/slices/library/slots.js +13 -2
- package/dist/slices/library/snapshot.js +8 -2
- package/dist/slices/notifications/notifier.js +76 -0
- package/dist/slices/notifications/rules.js +81 -0
- package/dist/slices/notifications/send.js +40 -0
- package/dist/slices/notifications/settings.js +34 -0
- package/dist/slices/play-drafts/convert.js +72 -4
- package/dist/slices/play-drafts/readiness.js +7 -3
- package/dist/slices/play-drafts/repo.js +7 -1
- package/dist/slices/play-drafts/review-inputs.js +2 -0
- package/dist/slices/play-drafts/schema.js +44 -3
- package/dist/slices/play-drafts/service.js +6 -0
- package/dist/slices/play-drafts/uploads.js +2 -1
- package/dist/slices/project-templates/from-project.js +42 -0
- package/dist/slices/project-templates/setup.js +13 -0
- package/dist/slices/rebuild/preview-details.js +53 -3
- package/dist/slices/rebuild/preview-plan.js +2 -2
- package/dist/slices/rebuild/recipe-build.js +13 -3
- package/dist/slices/rebuild/recipe-edit.js +178 -0
- package/dist/slices/rebuild/recipe-exports.js +9 -4
- package/dist/slices/rebuild/recipe-input-schema.js +9 -1
- package/dist/slices/rebuild/recipe-legacy.js +12 -0
- package/dist/slices/rebuild/recipe-model.js +7 -1
- package/dist/slices/rebuild/recipe-provider-choice.js +15 -0
- package/dist/slices/rebuild/recipe-reference.js +44 -0
- package/dist/slices/rebuild/recipe-save.js +25 -1
- package/dist/slices/rebuild/recipe-shorts.js +186 -0
- package/dist/slices/rebuild/recipe-validation.js +33 -2
- package/dist/slices/rebuild/recipe-visual.js +29 -8
- package/dist/slices/rebuild/recipe-work.js +83 -1
- package/dist/slices/rebuild/runtime-actions.js +9 -2
- package/dist/slices/rebuild/runtime-animate.js +110 -0
- package/dist/slices/rebuild/runtime-export-edit.js +119 -0
- package/dist/slices/rebuild/runtime-export.js +13 -1
- package/dist/slices/rebuild/runtime-image.js +50 -0
- package/dist/slices/rebuild/runtime-local.js +6 -3
- package/dist/slices/rebuild/runtime-materialize.js +48 -0
- package/dist/slices/rebuild/runtime-piece-label.js +17 -0
- package/dist/slices/rebuild/runtime-provider.js +17 -10
- package/dist/slices/rebuild/runtime-publication.js +25 -4
- package/dist/slices/rebuild/runtime-run.js +11 -5
- package/dist/slices/rebuild/runtime-shorts.js +328 -0
- package/dist/slices/rebuild/runtime-store.js +7 -3
- package/dist/slices/rebuild/service-readiness.js +15 -1
- package/dist/slices/rebuild/transition-repo.js +40 -1
- package/dist/slices/revisions/adopt-content.js +7 -0
- package/dist/slices/revisions/adopt.js +7 -2
- package/dist/slices/revisions/mutation-assets.js +51 -9
- package/dist/slices/revisions/mutation-prepare.js +14 -14
- package/dist/slices/revisions/mutations.js +2 -1
- package/dist/slices/revisions/publish.js +20 -2
- package/dist/slices/revisions/schema.js +16 -1
- package/dist/slices/schedules/repo.js +1 -1
- package/dist/slices/schedules/service.js +6 -1
- package/dist/slices/settings/model.js +5 -0
- package/dist/slices/shorts/captions.js +272 -0
- package/dist/slices/shorts/clips.js +89 -0
- package/dist/slices/shorts/model.js +186 -0
- package/dist/slices/shorts/pick.js +232 -0
- package/dist/slices/shorts/prompts.js +68 -0
- package/dist/slices/shorts/render.js +163 -0
- package/dist/slices/storage/asset-name.js +9 -2
- package/dist/slices/storage/layout.js +18 -0
- package/dist/slices/storage/model.js +9 -0
- package/dist/slices/storage/portable.js +6 -3
- package/dist/slices/storage/reconcile.js +5 -1
- package/dist/slices/storage/schema.js +3 -0
- package/dist/slices/telemetry/model.js +3 -0
- package/dist/slices/video/cards.js +72 -0
- package/dist/slices/video/chapters.js +89 -0
- package/dist/slices/video/cuts.js +141 -0
- package/dist/slices/video/edit-list.js +36 -6
- package/dist/slices/video/edit-settings.js +191 -0
- package/dist/slices/video/ffmpeg.js +155 -29
- package/dist/slices/video/look.js +164 -0
- package/dist/slices/video/plan.js +68 -11
- package/dist/slices/video/slideshow.js +22 -6
- package/dist/slices/video/transitions.js +51 -0
- package/dist/slices/youtube/transcript.js +54 -0
- package/dist/web/assets/index-CktpxlJd.js +171 -0
- package/dist/web/assets/index-DB-vOnGU.css +1 -0
- package/dist/web/assets/{pdf-DR97Byps.js → pdf-BLF2i8c2.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DfXmBzCk.js +0 -168
- package/dist/web/assets/index-Y3RhfTEh.css +0 -1
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { usesYoutubeDescription } from "../admission/rules.js";
|
|
5
|
+
import { resolveFont } from "../fonts/index.js";
|
|
6
|
+
import { outputPath } from "../storage/layout.js";
|
|
7
|
+
import { headingChapters } from "../video/chapters.js";
|
|
8
|
+
import { sentenceCutPoints } from "../video/cuts.js";
|
|
9
|
+
import { usesChapterCards, usesNarrationCuts, videoEditOf, } from "../video/edit-settings.js";
|
|
10
|
+
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
11
|
+
import { wordsSchema } from "./runtime-subtitles.js";
|
|
12
|
+
// The containers an uploaded clip may come in; anything else in the images is a still.
|
|
13
|
+
const clipExtensions = new Set([".mp4", ".mov", ".m4v", ".webm", ".mkv"]);
|
|
14
|
+
export function isClipPath(path) {
|
|
15
|
+
return clipExtensions.has(extname(path).toLowerCase());
|
|
16
|
+
}
|
|
17
|
+
export async function exportEdit(deps, context, view, images) {
|
|
18
|
+
const config = view.revision.config;
|
|
19
|
+
const order = view.revision.content.imageOrder;
|
|
20
|
+
const ready = (key) => view.outputs.find((row) => row.workKey === key && row.selected && row.available && row.state === "ready");
|
|
21
|
+
// Clips: an animated image's clip first, then an uploaded clip in the image's own place.
|
|
22
|
+
const clips = [];
|
|
23
|
+
for (const [index, key] of order.entries()) {
|
|
24
|
+
const animated = ready(`animate:${key}`);
|
|
25
|
+
const path = animated?.output.role === "animated_image"
|
|
26
|
+
? outputPath(deps.paths, context.work.projectId, animated.output.path)
|
|
27
|
+
: images[index] !== undefined && isClipPath(images[index] ?? "")
|
|
28
|
+
? images[index]
|
|
29
|
+
: undefined;
|
|
30
|
+
clips.push(path === undefined
|
|
31
|
+
? undefined
|
|
32
|
+
: { kind: "video", path, seconds: await clipSeconds(deps, context, path, index + 1) });
|
|
33
|
+
}
|
|
34
|
+
const warnings = view.pieces.flatMap((row) => {
|
|
35
|
+
if (!row.key.startsWith("animate:") || !row.selected || row.piece.state !== "done")
|
|
36
|
+
return [];
|
|
37
|
+
const parsed = z
|
|
38
|
+
.object({ fallback: z.string() })
|
|
39
|
+
.safeParse(JSON.parse(row.piece.payload ?? "null"));
|
|
40
|
+
return parsed.success ? [parsed.data.fallback] : [];
|
|
41
|
+
});
|
|
42
|
+
if (config.videoEdit === undefined)
|
|
43
|
+
return {
|
|
44
|
+
edit: clips.some((clip) => clip !== undefined) ? { clips } : undefined,
|
|
45
|
+
warnings,
|
|
46
|
+
settings: undefined,
|
|
47
|
+
};
|
|
48
|
+
const settings = videoEditOf(config);
|
|
49
|
+
const cuts = usesNarrationCuts(config);
|
|
50
|
+
const cards = usesChapterCards(config);
|
|
51
|
+
const words = cuts || cards ? timingWords(deps, context, view) : [];
|
|
52
|
+
const chapters = cuts || cards ? chaptersOf(view, config, words) : [];
|
|
53
|
+
const font = cards
|
|
54
|
+
? await resolveFont(deps.paths, config.subtitles?.fontId ?? "default")
|
|
55
|
+
: undefined;
|
|
56
|
+
return {
|
|
57
|
+
edit: {
|
|
58
|
+
clips,
|
|
59
|
+
...(cuts
|
|
60
|
+
? {
|
|
61
|
+
narration: {
|
|
62
|
+
cutPoints: sentenceCutPoints(words),
|
|
63
|
+
chapterStarts: chapters.map((chapter) => chapter.start),
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
: {}),
|
|
67
|
+
...(settings.transition === "cut"
|
|
68
|
+
? {}
|
|
69
|
+
: { transition: { kind: settings.transition, seconds: settings.transitionSeconds } }),
|
|
70
|
+
look: {
|
|
71
|
+
vignette: settings.vignette,
|
|
72
|
+
grain: settings.grain,
|
|
73
|
+
grade: settings.grade,
|
|
74
|
+
atmosphere: settings.atmosphere,
|
|
75
|
+
},
|
|
76
|
+
...(font === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: { cards: { chapters, font: { path: font.path, name: font.assName } } }),
|
|
79
|
+
},
|
|
80
|
+
warnings,
|
|
81
|
+
settings,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function timingWords(deps, context, view) {
|
|
85
|
+
const timing = view.outputs.find((row) => row.selected &&
|
|
86
|
+
row.available &&
|
|
87
|
+
row.state === "ready" &&
|
|
88
|
+
row.workKey === "subtitles:timing" &&
|
|
89
|
+
row.output.role === "subtitle_words");
|
|
90
|
+
if (timing === undefined)
|
|
91
|
+
throw new Error("The narration's word timing, which the video's cuts and chapter cards follow, is missing. Use Re-run section on Video, then Retry stage.");
|
|
92
|
+
return wordsSchema.parse(JSON.parse(readFileSync(outputPath(deps.paths, context.work.projectId, timing.output.path), "utf8"))).words;
|
|
93
|
+
}
|
|
94
|
+
// The YouTube description's chapters when that step runs, the article's headings otherwise.
|
|
95
|
+
function chaptersOf(view, config, words) {
|
|
96
|
+
if (usesYoutubeDescription(config)) {
|
|
97
|
+
const row = view.pieces.find((one) => one.key === "youtube:description" && one.selected && one.piece.state === "done");
|
|
98
|
+
const parsed = z
|
|
99
|
+
.object({ chapters: z.array(z.object({ start: z.number(), title: z.string() })) })
|
|
100
|
+
.safeParse(JSON.parse(row?.piece.payload ?? "null"));
|
|
101
|
+
if (parsed.success)
|
|
102
|
+
return parsed.data.chapters;
|
|
103
|
+
}
|
|
104
|
+
return headingChapters(view.articleMarkdown ?? "", words);
|
|
105
|
+
}
|
|
106
|
+
async function clipSeconds(deps, context, path, place) {
|
|
107
|
+
let ms;
|
|
108
|
+
try {
|
|
109
|
+
ms = await probeDurationMs(deps.ffmpeg, path, context.signal, deps.log);
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
if (context.signal.aborted)
|
|
113
|
+
throw error;
|
|
114
|
+
ms = 0;
|
|
115
|
+
}
|
|
116
|
+
if (!(ms > 0))
|
|
117
|
+
throw new Error(`Image ${String(place)} is a video clip Slopify can't read, so the video can't show it. Replace it with another clip or an image in Edit project → Images, then Retry stage.`);
|
|
118
|
+
return ms / 1000;
|
|
119
|
+
}
|
|
@@ -7,6 +7,7 @@ import { withPaths } from "../video/edit-list.js";
|
|
|
7
7
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
8
8
|
import { planRender } from "../video/plan.js";
|
|
9
9
|
import { renderSlideshow } from "../video/slideshow.js";
|
|
10
|
+
import { exportEdit } from "./runtime-export-edit.js";
|
|
10
11
|
import { exportSnapshot, retainedOutput, revisionAudio, } from "./runtime-export-inputs.js";
|
|
11
12
|
import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
|
|
12
13
|
export async function executeExportRecipe(deps, context, piece) {
|
|
@@ -53,6 +54,7 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
53
54
|
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.");
|
|
54
55
|
return outputPath(deps.paths, context.work.projectId, row.output.path);
|
|
55
56
|
});
|
|
57
|
+
const edited = wav ? undefined : await exportEdit(deps, context, view, images);
|
|
56
58
|
const plan = wav
|
|
57
59
|
? undefined
|
|
58
60
|
: planRender({
|
|
@@ -67,6 +69,7 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
67
69
|
outro: segment("outro"),
|
|
68
70
|
images,
|
|
69
71
|
output: pending.absolutePath,
|
|
72
|
+
edit: edited?.edit,
|
|
70
73
|
});
|
|
71
74
|
const totalSeconds = plan?.totalSeconds ?? audio.reduce((sum, row) => sum + row.seconds, 0);
|
|
72
75
|
const projectDirectory = projectDir(deps.paths, context.work.projectId);
|
|
@@ -90,6 +93,10 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
90
93
|
output: pending.path,
|
|
91
94
|
editList: withPaths(plan.editList, relativePath),
|
|
92
95
|
subtitles: config.subtitles,
|
|
96
|
+
...(edited?.settings === undefined ? {} : { videoEdit: edited.settings }),
|
|
97
|
+
...(edited === undefined || edited.warnings.length === 0
|
|
98
|
+
? {}
|
|
99
|
+
: { warnings: edited.warnings }),
|
|
93
100
|
};
|
|
94
101
|
if (!context.maySubmit(piece.id))
|
|
95
102
|
return "held";
|
|
@@ -122,7 +129,12 @@ export async function executeExportRecipe(deps, context, piece) {
|
|
|
122
129
|
});
|
|
123
130
|
context.signal.throwIfAborted();
|
|
124
131
|
const asset = sealAsset(deps, pending);
|
|
125
|
-
prepared.push(preparedResult(deps, context, piece, wav ? "audio_export" : "video", asset, Math.round(totalSeconds * 1000), {
|
|
132
|
+
prepared.push(preparedResult(deps, context, piece, wav ? "audio_export" : "video", asset, Math.round(totalSeconds * 1000), {
|
|
133
|
+
subtitlesMode: mode,
|
|
134
|
+
...(edited === undefined || edited.warnings.length === 0
|
|
135
|
+
? {}
|
|
136
|
+
: { warnings: edited.warnings }),
|
|
137
|
+
}));
|
|
126
138
|
prepared.push(preparedText(deps, context, piece, "render_params", "render.json", JSON.stringify(record)));
|
|
127
139
|
prepared.push(...captions.map((row) => retainedOutput(deps, view, row)));
|
|
128
140
|
await publishResult(deps, context, piece, prepared, { totalSeconds }, asset);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { outputPath } from "../storage/layout.js";
|
|
4
|
+
// The provider call an image step makes: its request, the effort when one is chosen and, while
|
|
5
|
+
// the project's Establishing image is on, that image's bytes as the visual reference.
|
|
6
|
+
export function imageCall(deps, projectId, input, previewLabel) {
|
|
7
|
+
const reference = referenceImage(deps, projectId, input);
|
|
8
|
+
return {
|
|
9
|
+
provider: input.provider,
|
|
10
|
+
model: input.model,
|
|
11
|
+
prompt: input.prompt,
|
|
12
|
+
aspect: input.aspect,
|
|
13
|
+
...(input.thinking === undefined ? {} : { thinking: input.thinking }),
|
|
14
|
+
...(reference === undefined ? {} : { reference }),
|
|
15
|
+
...(previewLabel === undefined ? {} : { previewLabel }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function referenceImage(deps, projectId, input) {
|
|
19
|
+
if (input.reference === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
// The step waits for the establishing image, so a missing one went away since.
|
|
22
|
+
const missing = "The establishing image this image is drawn from is missing. Use Regenerate on the establishing image in the Images section (or upload it again in Edit project → Images), then use Retry stage.";
|
|
23
|
+
if (input.reference.assetId === null)
|
|
24
|
+
throw new Error(missing);
|
|
25
|
+
const row = z
|
|
26
|
+
.object({ path: z.string() })
|
|
27
|
+
.optional()
|
|
28
|
+
.parse(deps.db
|
|
29
|
+
.prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
|
|
30
|
+
.get(input.reference.assetId, projectId));
|
|
31
|
+
if (row === undefined)
|
|
32
|
+
throw new Error(missing);
|
|
33
|
+
let bytes;
|
|
34
|
+
try {
|
|
35
|
+
bytes = new Uint8Array(readFileSync(outputPath(deps.paths, projectId, row.path)));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(missing);
|
|
39
|
+
}
|
|
40
|
+
const mime = imageMime(bytes);
|
|
41
|
+
if (mime === undefined)
|
|
42
|
+
throw new Error("The establishing image is not a PNG or JPEG file, so it can't be sent as a reference. Upload a PNG or JPEG in Edit project → Images → Establishing image, then use Retry stage.");
|
|
43
|
+
return { bytes, mime };
|
|
44
|
+
}
|
|
45
|
+
function imageMime(bytes) {
|
|
46
|
+
const png = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
47
|
+
if (png.every((value, index) => bytes[index] === value))
|
|
48
|
+
return "image/png";
|
|
49
|
+
return bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff ? "image/jpeg" : undefined;
|
|
50
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { referenceKey } from "../admission/model.js";
|
|
4
5
|
import { plainText } from "../article/plain.js";
|
|
5
6
|
import { splitEndMatter } from "../article/split.js";
|
|
6
7
|
import { joinNarration } from "../narration/concat.js";
|
|
@@ -36,9 +37,11 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
36
37
|
? "audio_body"
|
|
37
38
|
: piece.key === "thumbnail:image"
|
|
38
39
|
? "thumbnail"
|
|
39
|
-
: piece.key
|
|
40
|
-
? "
|
|
41
|
-
:
|
|
40
|
+
: piece.key === referenceKey
|
|
41
|
+
? "reference"
|
|
42
|
+
: piece.key.startsWith("image:")
|
|
43
|
+
? "image"
|
|
44
|
+
: undefined;
|
|
42
45
|
await publishResult(deps, context, piece, role === undefined ? [] : [preparedResult(deps, context, piece, role, asset)], {}, asset);
|
|
43
46
|
return "done";
|
|
44
47
|
}
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { transact } from "../../kernel/db/tx.js";
|
|
3
3
|
import { currentRevisionId } from "../revisions/repo.js";
|
|
4
4
|
import { getRevisionView } from "../revisions/view.js";
|
|
5
|
+
import { recipeInputSchema } from "./recipe-input-schema.js";
|
|
5
6
|
import { insertInvocation } from "./runtime-admission.js";
|
|
6
7
|
import { bindNarrationReuse, narrationOrdinal } from "./runtime-narration-reuse.js";
|
|
7
8
|
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
@@ -56,6 +57,14 @@ export function materializeAdmittedWork(deps, projectId) {
|
|
|
56
57
|
continue;
|
|
57
58
|
if (plan.work.find((row) => row.key === recipe.key)?.disposition === "review")
|
|
58
59
|
continue;
|
|
60
|
+
// A step waiting on its inputs changes fingerprint each time one lands: every
|
|
61
|
+
// narration chunk moves the joined audio, the timing, the export and the YouTube
|
|
62
|
+
// description after it. The waiting row is the same request with newer inputs, so it
|
|
63
|
+
// takes them in place; minting a row per landing left one never-run row per chunk.
|
|
64
|
+
if (own !== undefined &&
|
|
65
|
+
plan.work.find((row) => row.key === recipe.key)?.disposition !== "reuse" &&
|
|
66
|
+
refreshWaiting(deps, head, own, recipe))
|
|
67
|
+
continue;
|
|
59
68
|
if (own !== undefined) {
|
|
60
69
|
deps.db
|
|
61
70
|
.prepare("UPDATE revision_work SET state='done' WHERE id=? AND state='pending'")
|
|
@@ -89,6 +98,34 @@ export function materializeAdmittedWork(deps, projectId) {
|
|
|
89
98
|
}
|
|
90
99
|
});
|
|
91
100
|
}
|
|
101
|
+
// Only a row nothing has touched: still pending, one unsent piece, no attempt. Anything
|
|
102
|
+
// further along keeps the old path, which retires it and admits a fresh row.
|
|
103
|
+
function refreshWaiting(deps, head, own, recipe) {
|
|
104
|
+
const pieces = workPieces(deps.db, own.work_id);
|
|
105
|
+
const piece = pieces[0];
|
|
106
|
+
if (pieces.length !== 1 ||
|
|
107
|
+
piece === undefined ||
|
|
108
|
+
piece.key !== recipe.key ||
|
|
109
|
+
piece.state !== "pending" ||
|
|
110
|
+
piece.submittedAt !== null ||
|
|
111
|
+
piece.continuation !== null)
|
|
112
|
+
return false;
|
|
113
|
+
const waiting = deps.db
|
|
114
|
+
.prepare("SELECT 1 FROM revision_work w WHERE w.id=? AND w.state='pending' AND NOT EXISTS(SELECT 1 FROM attempts a WHERE a.work_id=w.id)")
|
|
115
|
+
.get(own.work_id);
|
|
116
|
+
if (waiting === undefined)
|
|
117
|
+
return false;
|
|
118
|
+
deps.db
|
|
119
|
+
.prepare("UPDATE revision_work SET fingerprint=? WHERE id=?")
|
|
120
|
+
.run(recipe.fingerprint, own.work_id);
|
|
121
|
+
deps.db
|
|
122
|
+
.prepare("UPDATE revision_work_pieces SET fingerprint=?,request_fingerprint=?,logical_fingerprint=?,input_json=? WHERE id=?")
|
|
123
|
+
.run(recipe.fingerprint, recipe.requestFingerprint, recipe.logicalFingerprint, JSON.stringify(recipeInputSchema.parse(recipe.input)), piece.id);
|
|
124
|
+
deps.db
|
|
125
|
+
.prepare("UPDATE revision_work_reservations SET fingerprint=? WHERE revision_id=? AND work_id=? AND work_key=?")
|
|
126
|
+
.run(recipe.fingerprint, head, own.work_id, recipe.key);
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
92
129
|
function desiredAnchor(deps, projectId, head, row) {
|
|
93
130
|
const view = getRevisionView(deps, projectId, head);
|
|
94
131
|
return (view?.revision.fingerprints[row.logical_key ?? row.work_key] ===
|
|
@@ -103,5 +140,16 @@ function futureKey(recipe) {
|
|
|
103
140
|
return "research:planner";
|
|
104
141
|
if (recipe.key === "article:continuation")
|
|
105
142
|
return "article:body";
|
|
143
|
+
// A short's images and render unfold from its image prompts, admitted with them: under the
|
|
144
|
+
// pick on a first run, or on their own when one short is made again and the pick is kept.
|
|
145
|
+
// The prompts unfold from the pick's answer.
|
|
146
|
+
const clip = /^(shorts:\d+):(?:image:\d+|render)$/.exec(recipe.key);
|
|
147
|
+
if (clip?.[1] !== undefined)
|
|
148
|
+
return `${clip[1]}:prompts`;
|
|
149
|
+
if (recipe.key.startsWith("shorts:"))
|
|
150
|
+
return "shorts:pick";
|
|
151
|
+
// The chapter openers' clips unfold from the deferred request that stood in for them.
|
|
152
|
+
if (recipe.key.startsWith("animate:"))
|
|
153
|
+
return "animate:future";
|
|
106
154
|
return recipe.key;
|
|
107
155
|
}
|
|
@@ -6,12 +6,25 @@ export function pieceLabel(deps, context, piece) {
|
|
|
6
6
|
const { key } = piece;
|
|
7
7
|
if (key === "thumbnail:image" || key.startsWith("thumbnail:"))
|
|
8
8
|
return "Thumbnail";
|
|
9
|
+
if (key === "reference:image")
|
|
10
|
+
return "Establishing image";
|
|
9
11
|
if (key === "article:body")
|
|
10
12
|
return "Article";
|
|
11
13
|
if (key === "document:pdf")
|
|
12
14
|
return "Document";
|
|
13
15
|
if (key === "youtube:description")
|
|
14
16
|
return "YouTube description";
|
|
17
|
+
if (key === "shorts:pick" || key === "shorts:future")
|
|
18
|
+
return "Shorts";
|
|
19
|
+
if (key === "animate:future")
|
|
20
|
+
return "Animated images";
|
|
21
|
+
const short = /^shorts:(\d+):(prompts|image:(\d+)|render)$/.exec(key);
|
|
22
|
+
if (short !== null)
|
|
23
|
+
return short[2] === "prompts"
|
|
24
|
+
? `Short ${short[1]} image prompts`
|
|
25
|
+
: short[2] === "render"
|
|
26
|
+
? `Short ${short[1]}`
|
|
27
|
+
: `Short ${short[1]} image ${short[3]}`;
|
|
15
28
|
if (key === "entry:intro:text")
|
|
16
29
|
return "Intro text";
|
|
17
30
|
if (key === "entry:outro:text")
|
|
@@ -28,6 +41,10 @@ export function pieceLabel(deps, context, piece) {
|
|
|
28
41
|
const at = view()?.revision.content.imageOrder.indexOf(key.slice("image:".length)) ?? -1;
|
|
29
42
|
return at === -1 ? "An image" : `Image ${String(at + 1)}`;
|
|
30
43
|
}
|
|
44
|
+
if (key.startsWith("animate:")) {
|
|
45
|
+
const at = view()?.revision.content.imageOrder.indexOf(key.slice("animate:".length)) ?? -1;
|
|
46
|
+
return at === -1 ? "An animated image" : `Animated image ${String(at + 1)}`;
|
|
47
|
+
}
|
|
31
48
|
const narration = /^(?:narration:prepare:(?:intro|body|outro):)?audio:(intro|body|outro)(?::(.+))?$/.exec(key);
|
|
32
49
|
if (narration === null)
|
|
33
50
|
return undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { referenceKey } from "../admission/model.js";
|
|
1
2
|
import { plainText } from "../article/plain.js";
|
|
2
3
|
import { splitEndMatter } from "../article/split.js";
|
|
3
4
|
import { observeNarration } from "../narration/live.js";
|
|
@@ -9,6 +10,7 @@ import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
|
|
|
9
10
|
import { outputPath } from "../storage/layout.js";
|
|
10
11
|
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
11
12
|
import { executeArticleRequests } from "./runtime-article.js";
|
|
13
|
+
import { imageCall } from "./runtime-image.js";
|
|
12
14
|
import { frozenInstructions } from "./runtime-instructions.js";
|
|
13
15
|
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
14
16
|
import { preparedResult, preparedTexts, publishResult } from "./runtime-publication.js";
|
|
@@ -89,20 +91,25 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
|
|
|
89
91
|
return "done";
|
|
90
92
|
}
|
|
91
93
|
if (input.kind === "image") {
|
|
92
|
-
const image = await wrapped.image({
|
|
93
|
-
provider: input.provider,
|
|
94
|
-
model: input.model,
|
|
95
|
-
prompt: input.prompt,
|
|
96
|
-
aspect: input.aspect,
|
|
97
|
-
});
|
|
98
|
-
if (!image.ok)
|
|
99
|
-
return "held";
|
|
100
|
-
const asset = writeAsset(deps, context.work.projectId, image.value.mime === "image/jpeg" ? "image.jpg" : "image.png", image.value.bytes);
|
|
101
94
|
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
102
95
|
const index = piece.key.startsWith("image:")
|
|
103
96
|
? (view?.revision.content.imageOrder.indexOf(piece.key.slice(6)) ?? 0) + 1
|
|
104
97
|
: undefined;
|
|
105
|
-
const
|
|
98
|
+
const image = await wrapped.image(imageCall(deps, context.work.projectId, input, piece.key === "thumbnail:image"
|
|
99
|
+
? "Thumbnail"
|
|
100
|
+
: piece.key === referenceKey
|
|
101
|
+
? "Establishing image"
|
|
102
|
+
: index === undefined
|
|
103
|
+
? "Image"
|
|
104
|
+
: `Image ${String(index)}`));
|
|
105
|
+
if (!image.ok)
|
|
106
|
+
return "held";
|
|
107
|
+
const asset = writeAsset(deps, context.work.projectId, image.value.mime === "image/jpeg" ? "image.jpg" : "image.png", image.value.bytes);
|
|
108
|
+
const output = preparedResult(deps, context, piece, piece.key === "thumbnail:image"
|
|
109
|
+
? "thumbnail"
|
|
110
|
+
: piece.key === referenceKey
|
|
111
|
+
? "reference"
|
|
112
|
+
: "image", asset, null, {
|
|
106
113
|
prompt: input.prompt,
|
|
107
114
|
provider: input.provider,
|
|
108
115
|
model: input.model,
|
|
@@ -8,7 +8,13 @@ import { rebindPublishedNarration } from "./runtime-narration-publication.js";
|
|
|
8
8
|
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
9
9
|
export function preparedResult(deps, context, piece, role, asset, durationMs = null, meta = {}) {
|
|
10
10
|
return {
|
|
11
|
-
|
|
11
|
+
// A project holds many images, and one video and several images per short.
|
|
12
|
+
slot: role === "image" ||
|
|
13
|
+
role === "short_image" ||
|
|
14
|
+
role === "short_video" ||
|
|
15
|
+
role === "animated_image"
|
|
16
|
+
? piece.key
|
|
17
|
+
: `${context.work.kind}:${role}`,
|
|
12
18
|
workKey: piece.key,
|
|
13
19
|
fingerprint: piece.logicalFingerprint ?? piece.fingerprint,
|
|
14
20
|
asset,
|
|
@@ -46,7 +52,7 @@ export function preparedTexts(deps, context, piece, texts) {
|
|
|
46
52
|
throw error;
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
|
-
export async function publishResult(deps, context, piece, outputs, payload, asset = null) {
|
|
55
|
+
export async function publishResult(deps, context, piece, outputs, payload, asset = null, retired) {
|
|
50
56
|
try {
|
|
51
57
|
const record = {
|
|
52
58
|
key: piece.key,
|
|
@@ -67,7 +73,7 @@ export async function publishResult(deps, context, piece, outputs, payload, asse
|
|
|
67
73
|
},
|
|
68
74
|
};
|
|
69
75
|
const result = transact(deps.db, () => {
|
|
70
|
-
const committed = commitRevisionOutputs(deps, { work: context.work, pieceId: piece.id, publicationId: piece.id }, outputs, [record]);
|
|
76
|
+
const committed = commitRevisionOutputs(deps, { work: context.work, pieceId: piece.id, publicationId: piece.id }, outputs, [record], retired);
|
|
71
77
|
rebindPublishedNarration(deps, context.work, piece);
|
|
72
78
|
deps.db
|
|
73
79
|
.prepare("UPDATE revision_work_pieces SET state='done' WHERE id=? AND work_id=?")
|
|
@@ -93,7 +99,7 @@ function pieceKind(piece) {
|
|
|
93
99
|
return "chunk";
|
|
94
100
|
if (piece.input.kind === "provided" && /^audio:(intro|outro):\d+$/.test(piece.key))
|
|
95
101
|
return "segment";
|
|
96
|
-
if (piece.key.startsWith("image:"))
|
|
102
|
+
if (piece.key.startsWith("image:") || piece.key.startsWith("animate:"))
|
|
97
103
|
return "image";
|
|
98
104
|
if (piece.key.startsWith("research:chapter:"))
|
|
99
105
|
return "chapter";
|
|
@@ -117,6 +123,21 @@ function pieceIndex(deps, context, piece) {
|
|
|
117
123
|
return 1;
|
|
118
124
|
if (piece.key === "entry:outro:text")
|
|
119
125
|
return 2;
|
|
126
|
+
// The shorts' steps take places of their own, past every other step of the Video stage:
|
|
127
|
+
// the pick, then per short its prompts, its images and its render.
|
|
128
|
+
const short = /^shorts:(?:(\d+):(prompts|image:(\d+)|render)|pick)$/.exec(piece.key);
|
|
129
|
+
if (short !== null)
|
|
130
|
+
return short[1] === undefined
|
|
131
|
+
? 10000
|
|
132
|
+
: 10000 +
|
|
133
|
+
Number(short[1]) * 1000 +
|
|
134
|
+
(short[2] === "prompts" ? 0 : short[2] === "render" ? 999 : Number(short[3]));
|
|
135
|
+
// Animated images take places past the shorts', one per image of the slideshow.
|
|
136
|
+
if (piece.key.startsWith("animate:")) {
|
|
137
|
+
const order = executionView(deps, context.work.projectId, context.work.revisionId)?.revision
|
|
138
|
+
.content.imageOrder;
|
|
139
|
+
return 900000 + (order?.indexOf(piece.key.slice("animate:".length)) ?? -1) + 1;
|
|
140
|
+
}
|
|
120
141
|
const localOrder = [
|
|
121
142
|
"audio:body:concat",
|
|
122
143
|
"audio:intro",
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { executeAnimateRecipe } from "./runtime-animate.js";
|
|
1
2
|
import { executeDocumentRecipe } from "./runtime-document.js";
|
|
2
3
|
import { executeExportRecipe } from "./runtime-export.js";
|
|
3
4
|
import { executeLocalRecipe } from "./runtime-local.js";
|
|
4
5
|
import { pieceLabel } from "./runtime-piece-label.js";
|
|
5
6
|
import { executeProviderRecipe } from "./runtime-provider.js";
|
|
7
|
+
import { executeShortsRecipe } from "./runtime-shorts.js";
|
|
6
8
|
import { executeSubtitleRecipe } from "./runtime-subtitles.js";
|
|
7
9
|
import { executeYoutubeRecipe } from "./runtime-youtube.js";
|
|
8
10
|
import { workPieces } from "./work-records.js";
|
|
@@ -22,11 +24,15 @@ export async function runRevisionInvocation(deps, context, providers) {
|
|
|
22
24
|
? await executeDocumentRecipe(deps, context, piece)
|
|
23
25
|
: piece.key.startsWith("youtube:")
|
|
24
26
|
? await executeYoutubeRecipe(deps, context, providers, piece)
|
|
25
|
-
: piece.
|
|
26
|
-
|
|
27
|
-
piece.
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
: piece.key.startsWith("shorts:")
|
|
28
|
+
? await executeShortsRecipe(deps, context, providers, piece)
|
|
29
|
+
: piece.key.startsWith("animate:")
|
|
30
|
+
? await executeAnimateRecipe(deps, context, providers, piece)
|
|
31
|
+
: piece.input.kind === "llm" ||
|
|
32
|
+
piece.input.kind === "tts" ||
|
|
33
|
+
piece.input.kind === "image"
|
|
34
|
+
? await executeProviderRecipe(deps, context, providers, piece)
|
|
35
|
+
: await executeLocalRecipe(deps, context, piece);
|
|
30
36
|
if (outcome === "held")
|
|
31
37
|
return outcome;
|
|
32
38
|
}
|