@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
|
@@ -7,6 +7,7 @@ import { listCheckpoints } from "../checkpoints/repo.js";
|
|
|
7
7
|
import { documentThemeOf } from "../document/model.js";
|
|
8
8
|
import { requestHash } from "../play-drafts/repo.js";
|
|
9
9
|
import { currentRevisionId, revisionById } from "../revisions/repo.js";
|
|
10
|
+
import { shortsExtrasForm } from "../shorts/model.js";
|
|
10
11
|
import { createTemplate, readTemplate } from "./service.js";
|
|
11
12
|
export const projectTemplateCreateSchema = z
|
|
12
13
|
.object({
|
|
@@ -97,6 +98,15 @@ function documentFromProject(deps, revision) {
|
|
|
97
98
|
return { name: prompt.name, number: String(prompt.number) };
|
|
98
99
|
});
|
|
99
100
|
addPrompt("thumbnail", config.thumbnailPrompt, "thumbnailPrompt");
|
|
101
|
+
addPrompt("shorts", config.shorts?.prompt, "shorts");
|
|
102
|
+
// The shorts' image style may be the same Library prompt one of the slideshow images uses,
|
|
103
|
+
// and a snapshot holds one prompt per kind and name.
|
|
104
|
+
if (!prompts.some((prompt) => prompt.kind === "image" && prompt.name === config.shorts?.imagePrompt))
|
|
105
|
+
addPrompt("image", config.shorts?.imagePrompt, "shortsImage");
|
|
106
|
+
// The establishing image's prompt, the same way.
|
|
107
|
+
if (config.reference?.source === "prompt" &&
|
|
108
|
+
!prompts.some((prompt) => prompt.kind === "image" && prompt.name === config.reference?.prompt))
|
|
109
|
+
addPrompt("image", config.reference.prompt, "referencePrompt");
|
|
100
110
|
for (const category of ["intro", "outro"]) {
|
|
101
111
|
const choice = config[category];
|
|
102
112
|
const body = revision.content.promptTemplates[category] ?? config.rendered[category];
|
|
@@ -119,9 +129,16 @@ function documentFromProject(deps, revision) {
|
|
|
119
129
|
: "",
|
|
120
130
|
audio: config.sources.audio === "provide" ? attachment("Audio from project") : null,
|
|
121
131
|
thumbnail: config.sources.thumbnail === "provide" ? attachment("Thumbnail from project") : null,
|
|
132
|
+
...(config.reference?.source === "provide"
|
|
133
|
+
? { reference: attachment("Establishing image from project") }
|
|
134
|
+
: {}),
|
|
122
135
|
images: config.sources.images === "provide"
|
|
123
136
|
? (definitions.length ? definitions : (config.provided.images ?? [])).map((_, index) => attachment(`Image ${index + 1}`))
|
|
124
137
|
: [],
|
|
138
|
+
// Named like the project's other files, to be attached again on Play.
|
|
139
|
+
...(config.shorts?.enabled === true && revision.content.shortsMusic !== undefined
|
|
140
|
+
? { shortsMusic: attachment("Music from project") }
|
|
141
|
+
: {}),
|
|
125
142
|
};
|
|
126
143
|
return {
|
|
127
144
|
schemaVersion: 1,
|
|
@@ -141,12 +158,37 @@ function documentFromProject(deps, revision) {
|
|
|
141
158
|
llm: config.llm ?? { provider: "", model: "" },
|
|
142
159
|
audio: config.audio ?? { provider: "", model: "", voice: "" },
|
|
143
160
|
images: config.images ?? { provider: "", model: "" },
|
|
161
|
+
...(config.reference === undefined
|
|
162
|
+
? {}
|
|
163
|
+
: {
|
|
164
|
+
reference: {
|
|
165
|
+
source: config.reference.source,
|
|
166
|
+
prompt: config.reference.prompt ?? "",
|
|
167
|
+
thumbnail: config.reference.thumbnail !== false,
|
|
168
|
+
},
|
|
169
|
+
}),
|
|
144
170
|
articlePrompt: config.articlePrompt ?? "",
|
|
145
171
|
...(config.narrationPrompt === undefined ? {} : { narrationPrompt: config.narrationPrompt }),
|
|
146
172
|
...(config.youtubeDescription === true ? { youtubeDescription: true } : {}),
|
|
147
173
|
...(config.descriptionPrompt === undefined
|
|
148
174
|
? {}
|
|
149
175
|
: { descriptionPrompt: config.descriptionPrompt }),
|
|
176
|
+
...(config.shorts === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: {
|
|
179
|
+
shorts: {
|
|
180
|
+
enabled: config.shorts.enabled,
|
|
181
|
+
count: String(config.shorts.count),
|
|
182
|
+
minSeconds: String(config.shorts.minSeconds),
|
|
183
|
+
maxSeconds: String(config.shorts.maxSeconds),
|
|
184
|
+
prompt: config.shorts.prompt ?? "",
|
|
185
|
+
imagePrompt: config.shorts.imagePrompt ?? "",
|
|
186
|
+
...shortsExtrasForm(config.shorts),
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
// A project without edit settings makes a template without them, so a video made from
|
|
190
|
+
// it cuts every N seconds like the project did.
|
|
191
|
+
...(config.videoEdit === undefined ? {} : { videoEdit: config.videoEdit }),
|
|
150
192
|
imagePrompts,
|
|
151
193
|
thumbnailPrompt: config.thumbnailPrompt ?? "",
|
|
152
194
|
intro: config.intro?.name ?? "",
|
|
@@ -8,6 +8,12 @@ export function templateSetup(deps, input) {
|
|
|
8
8
|
{ kind: "description", name: input.form.descriptionPrompt ?? "" },
|
|
9
9
|
...input.form.imagePrompts.map((row) => ({ kind: "image", name: row.name })),
|
|
10
10
|
{ kind: "thumbnail", name: input.form.thumbnailPrompt },
|
|
11
|
+
{ kind: "shorts", name: input.form.shorts?.prompt ?? "" },
|
|
12
|
+
{ kind: "image", name: input.form.shorts?.imagePrompt ?? "" },
|
|
13
|
+
{
|
|
14
|
+
kind: "image",
|
|
15
|
+
name: input.form.reference?.source === "prompt" ? input.form.reference.prompt : "",
|
|
16
|
+
},
|
|
11
17
|
];
|
|
12
18
|
for (const choice of choices) {
|
|
13
19
|
if (choice.name === "")
|
|
@@ -51,6 +57,13 @@ export function freshTemplateDraft(deps, document, source) {
|
|
|
51
57
|
audio: provided.audio === null ? null : fresh(provided.audio),
|
|
52
58
|
thumbnail: provided.thumbnail === null ? null : fresh(provided.thumbnail),
|
|
53
59
|
images: provided.images.map(fresh),
|
|
60
|
+
...(provided.reference === undefined
|
|
61
|
+
? {}
|
|
62
|
+
: { reference: provided.reference === null ? null : fresh(provided.reference) }),
|
|
63
|
+
// Like the images: the name is kept and the file is attached again on Play.
|
|
64
|
+
...(provided.shortsMusic === undefined
|
|
65
|
+
? {}
|
|
66
|
+
: { shortsMusic: provided.shortsMusic === null ? null : fresh(provided.shortsMusic) }),
|
|
54
67
|
},
|
|
55
68
|
},
|
|
56
69
|
};
|
|
@@ -3,6 +3,8 @@ import { sourceOf } from "../admission/model.js";
|
|
|
3
3
|
import { motionStyleLabels } from "../admission/rules.js";
|
|
4
4
|
import { documentThemeLabel } from "../document/model.js";
|
|
5
5
|
import { getRevisionView } from "../revisions/view.js";
|
|
6
|
+
import { musicVolumeOf, shortsSpeedOf } from "../shorts/model.js";
|
|
7
|
+
import { videoEditOf, videoEditRows } from "../video/edit-settings.js";
|
|
6
8
|
export function previewDetails(deps, view, allRecipes, selected) {
|
|
7
9
|
const parent = view.revision.parentId === null
|
|
8
10
|
? undefined
|
|
@@ -44,9 +46,19 @@ export function previewDetails(deps, view, allRecipes, selected) {
|
|
|
44
46
|
return [
|
|
45
47
|
{
|
|
46
48
|
key: row.key,
|
|
47
|
-
label:
|
|
49
|
+
label: row.key === "reference:image"
|
|
50
|
+
? "Establishing image"
|
|
51
|
+
: index < 0
|
|
52
|
+
? "Thumbnail image"
|
|
53
|
+
: `Image ${index + 1}`,
|
|
48
54
|
text: input.prompt,
|
|
49
|
-
settings: [
|
|
55
|
+
settings: [
|
|
56
|
+
input.provider,
|
|
57
|
+
input.model,
|
|
58
|
+
...(input.thinking === undefined ? [] : [`effort ${input.thinking}`]),
|
|
59
|
+
input.aspect,
|
|
60
|
+
...(input.reference === undefined ? [] : ["drawn from the establishing image"]),
|
|
61
|
+
].join(" · "),
|
|
50
62
|
},
|
|
51
63
|
];
|
|
52
64
|
}
|
|
@@ -62,7 +74,9 @@ export function previewDetails(deps, view, allRecipes, selected) {
|
|
|
62
74
|
? "Article"
|
|
63
75
|
: row.stage === "research"
|
|
64
76
|
? "Research"
|
|
65
|
-
: "
|
|
77
|
+
: row.key.startsWith("shorts:")
|
|
78
|
+
? "Short image prompts"
|
|
79
|
+
: "Thumbnail prompt";
|
|
66
80
|
return [
|
|
67
81
|
{
|
|
68
82
|
key: row.key,
|
|
@@ -129,11 +143,41 @@ function inputChanges(parent, view) {
|
|
|
129
143
|
add("Narration chunking", before.chunking, after.chunking);
|
|
130
144
|
add("YouTube description", before.youtubeDescription === true ? "On" : "Off", after.youtubeDescription === true ? "On" : "Off");
|
|
131
145
|
add("Description prompt", before.descriptionPrompt, after.descriptionPrompt);
|
|
146
|
+
add("Shorts", before.shorts?.enabled === true ? "On" : "Off", after.shorts?.enabled === true ? "On" : "Off");
|
|
147
|
+
add("Number of shorts", before.shorts?.count, after.shorts?.count);
|
|
148
|
+
add("Short length (seconds)", before.shorts === undefined
|
|
149
|
+
? undefined
|
|
150
|
+
: `${String(before.shorts.minSeconds)}-${String(before.shorts.maxSeconds)}`, after.shorts === undefined
|
|
151
|
+
? undefined
|
|
152
|
+
: `${String(after.shorts.minSeconds)}-${String(after.shorts.maxSeconds)}`);
|
|
153
|
+
add("Shorts prompt", before.shorts?.prompt, after.shorts?.prompt);
|
|
154
|
+
add("Shorts image prompt", before.shorts?.imagePrompt, after.shorts?.imagePrompt);
|
|
155
|
+
add("Establishing image", referenceLabel(before), referenceLabel(after));
|
|
156
|
+
add("Thumbnail drawn from the establishing image", before.reference === undefined
|
|
157
|
+
? undefined
|
|
158
|
+
: before.reference.thumbnail === false
|
|
159
|
+
? "Off"
|
|
160
|
+
: "On", after.reference === undefined ? undefined : after.reference.thumbnail === false ? "Off" : "On");
|
|
161
|
+
add("Title on the shorts", before.shorts === undefined ? undefined : before.shorts.titleOnScreen === true ? "On" : "Off", after.shorts === undefined ? undefined : after.shorts.titleOnScreen === true ? "On" : "Off");
|
|
162
|
+
add("Shorts speed", before.shorts === undefined ? undefined : `${shortsSpeedOf(before.shorts).toFixed(2)}×`, after.shorts === undefined ? undefined : `${shortsSpeedOf(after.shorts).toFixed(2)}×`);
|
|
163
|
+
add("Shorts music", old.shortsMusic === undefined ? "None" : "Uploaded file", next.shortsMusic === undefined
|
|
164
|
+
? "None"
|
|
165
|
+
: next.shortsMusic === old.shortsMusic
|
|
166
|
+
? "Uploaded file"
|
|
167
|
+
: "New uploaded file");
|
|
168
|
+
add("Shorts music volume (%)", before.shorts === undefined ? undefined : musicVolumeOf(before.shorts), after.shorts === undefined ? undefined : musicVolumeOf(after.shorts));
|
|
169
|
+
add("Full video link", before.shorts?.fullVideoLink, after.shorts?.fullVideoLink);
|
|
170
|
+
add("Short ranges set by hand", old.shortsRanges, next.shortsRanges);
|
|
132
171
|
add("Silence gap (seconds)", before.silenceGapSeconds, after.silenceGapSeconds);
|
|
133
172
|
add("Silence at start and end (seconds)", before.edgeSilenceSeconds, after.edgeSilenceSeconds);
|
|
134
173
|
add("Seconds per image", before.imageSeconds, after.imageSeconds);
|
|
135
174
|
add("Zoom (%)", before.zoomPercent, after.zoomPercent);
|
|
136
175
|
add("Motion", motionStyleLabels[before.motionStyle], motionStyleLabels[after.motionStyle]);
|
|
176
|
+
// Row by row, so turning on grain says "Film grain: Off → Subtle" and nothing else.
|
|
177
|
+
const editBefore = videoEditRows(videoEditOf(before));
|
|
178
|
+
const editAfter = new Map(videoEditRows(videoEditOf(after)));
|
|
179
|
+
for (const [label, value] of editBefore)
|
|
180
|
+
add(label, value, editAfter.get(label));
|
|
137
181
|
add("Intro", before.intro, after.intro);
|
|
138
182
|
add("Outro", before.outro, after.outro);
|
|
139
183
|
add("Subtitle settings", before.subtitles, after.subtitles);
|
|
@@ -163,3 +207,9 @@ function inputChanges(parent, view) {
|
|
|
163
207
|
add("Manual captions", old.subtitleCues?.cues, next.subtitleCues?.cues);
|
|
164
208
|
return changes;
|
|
165
209
|
}
|
|
210
|
+
function referenceLabel(config) {
|
|
211
|
+
const reference = config.reference;
|
|
212
|
+
if (reference === undefined)
|
|
213
|
+
return "Off";
|
|
214
|
+
return reference.source === "provide" ? "Uploaded image" : `Prompt: ${reference.prompt ?? ""}`;
|
|
215
|
+
}
|
|
@@ -8,7 +8,7 @@ import { hasSubmittedRequest, requiresNewSubmission, retainedPreviewPlan, } from
|
|
|
8
8
|
import { providedDependencyFingerprint } from "./provided-review.js";
|
|
9
9
|
import { recipeInputSchema } from "./recipe-input-schema.js";
|
|
10
10
|
import { recipeProviderChoice } from "./recipe-provider-choice.js";
|
|
11
|
-
import {
|
|
11
|
+
import { priceRecipes } from "./recipe-work.js";
|
|
12
12
|
import { reservationKey } from "./runtime-admission.js";
|
|
13
13
|
import { narrationOrdinal } from "./runtime-narration-reuse.js";
|
|
14
14
|
import { executionCatalogue } from "./runtime-plan.js";
|
|
@@ -166,7 +166,7 @@ export function planPreview(deps, view, catalogue, selection, id) {
|
|
|
166
166
|
state: row.state,
|
|
167
167
|
})),
|
|
168
168
|
providedReuseRequired: execution.reviews.map((row) => row.key),
|
|
169
|
-
costs: estimateRequests(work.
|
|
169
|
+
costs: estimateRequests(work.flatMap((row) => priceRecipes(row.inflight || !requiresNewSubmission(deps, view.revision.id, row.key, row.fingerprint)
|
|
170
170
|
? { ...row, disposition: "reuse" }
|
|
171
171
|
: row, recipes.find((recipe) => recipe.key === row.key))), snapshot),
|
|
172
172
|
wholeRequestNotice: plan.wholeRequestNotice,
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { referenceForThumbnail } from "../admission/rules.js";
|
|
1
2
|
import { audioRecipes } from "./recipe-audio.js";
|
|
2
3
|
import { documentRecipes } from "./recipe-document.js";
|
|
4
|
+
import { editPlan } from "./recipe-edit.js";
|
|
3
5
|
import { exportRecipes } from "./recipe-exports.js";
|
|
6
|
+
import { imageReference, referenceRecipe } from "./recipe-reference.js";
|
|
7
|
+
import { shortsRecipes } from "./recipe-shorts.js";
|
|
4
8
|
import { textRecipes } from "./recipe-text.js";
|
|
5
9
|
import { thumbnailRecipes, visualAssets, visualRecipes } from "./recipe-visual.js";
|
|
6
10
|
import { youtubeRecipes } from "./recipe-youtube.js";
|
|
@@ -9,14 +13,20 @@ export function buildRecipes(context) {
|
|
|
9
13
|
const audio = audioRecipes(context, text);
|
|
10
14
|
const exports = exportRecipes(context, audio);
|
|
11
15
|
const captions = exports.find((value) => value.key === "subtitles:files");
|
|
12
|
-
|
|
16
|
+
// The establishing image, when it is on: every other image is drawn from it.
|
|
17
|
+
const reference = referenceRecipe(context);
|
|
18
|
+
const drawnFrom = imageReference(context, reference);
|
|
19
|
+
const thumbnail = thumbnailRecipes(context, text.recipes, referenceForThumbnail(context.config) ? drawnFrom : undefined);
|
|
20
|
+
const youtube = youtubeRecipes(context, exports);
|
|
13
21
|
return [
|
|
14
22
|
...text.recipes,
|
|
15
23
|
...audio.recipes,
|
|
16
24
|
...exports,
|
|
17
|
-
...
|
|
25
|
+
...youtube,
|
|
26
|
+
...(reference === undefined ? [] : [reference]),
|
|
27
|
+
...shortsRecipes(context, exports, drawnFrom),
|
|
18
28
|
...thumbnail,
|
|
19
29
|
...documentRecipes(context, text, thumbnail),
|
|
20
|
-
...visualAssets(context, visualRecipes(context.config, context.content, audio.mediaFingerprint, captions?.fingerprint ?? null)),
|
|
30
|
+
...visualAssets(context, visualRecipes(context.config, context.content, audio.mediaFingerprint, captions?.fingerprint ?? null, (images) => editPlan(context, exports, youtube, images), drawnFrom)),
|
|
21
31
|
];
|
|
22
32
|
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { fingerprint } from "../../kernel/runner/work.js";
|
|
3
|
+
import { usesYoutubeDescription } from "../admission/rules.js";
|
|
4
|
+
import { headingChapters, topHeadings } from "../video/chapters.js";
|
|
5
|
+
import { chapterOpeningShots, sentenceCutPoints } from "../video/cuts.js";
|
|
6
|
+
import { animatedClipSeconds, animatedImageIndexes, animatePrompt, editNeedsTiming, usesAnimation, usesChapterCards, usesNarrationCuts, videoEditOf, } from "../video/edit-settings.js";
|
|
7
|
+
import { fps } from "../video/plan.js";
|
|
8
|
+
import { recipe, resourceIdentity, selectedReference, } from "./recipe-model.js";
|
|
9
|
+
// What the Video stage's edit settings add to the plan: the word timing and chapters the
|
|
10
|
+
// render reads, and a paid `animate:<image>` request for each image brought to life. Every
|
|
11
|
+
// setting left at today's behaviour adds nothing, so a project without them, or with them all
|
|
12
|
+
// off, plans the same export fingerprint it always did and no finished video turns outdated.
|
|
13
|
+
//
|
|
14
|
+
// "Chapter openers" can only be told once the word timing (and the YouTube chapters, when
|
|
15
|
+
// that step runs) has landed, so until then one deferred `animate:future` stands in for the
|
|
16
|
+
// requests and carries their cost, the way `shorts:future` does.
|
|
17
|
+
export const animateFutureKey = "animate:future";
|
|
18
|
+
export function editPlan(context, exports, youtube, images) {
|
|
19
|
+
const { config } = context;
|
|
20
|
+
const edit = videoEditOf(config);
|
|
21
|
+
if (config.sources.video !== "generate")
|
|
22
|
+
return { recipes: [], values: [], dependsOn: [] };
|
|
23
|
+
const timing = editNeedsTiming(config)
|
|
24
|
+
? exports.find((value) => value.key === "subtitles:timing")
|
|
25
|
+
: undefined;
|
|
26
|
+
const description = youtube.find((value) => value.key === "youtube:description");
|
|
27
|
+
const fromDescription = usesYoutubeDescription(config) && description !== undefined;
|
|
28
|
+
const values = [];
|
|
29
|
+
const dependsOn = [];
|
|
30
|
+
if (usesNarrationCuts(config))
|
|
31
|
+
values.push(["cuts", "narration-v1"]);
|
|
32
|
+
if (edit.transition !== "cut")
|
|
33
|
+
values.push(["transition", edit.transition, edit.transitionSeconds]);
|
|
34
|
+
if (edit.vignette !== "off" ||
|
|
35
|
+
edit.grain !== "off" ||
|
|
36
|
+
edit.grade !== "none" ||
|
|
37
|
+
edit.atmosphere !== "none")
|
|
38
|
+
values.push(["look-v1", edit.vignette, edit.grain, edit.grade, edit.atmosphere]);
|
|
39
|
+
if (usesChapterCards(config))
|
|
40
|
+
values.push(["cards-v1", config.subtitles?.fontId ?? "default"]);
|
|
41
|
+
if (timing !== undefined) {
|
|
42
|
+
values.push(["timing", resourceIdentity(context, timing)]);
|
|
43
|
+
dependsOn.push(timing.key);
|
|
44
|
+
if (fromDescription && description !== undefined) {
|
|
45
|
+
values.push(["chapters", resourceIdentity(context, description)]);
|
|
46
|
+
dependsOn.push(description.key);
|
|
47
|
+
}
|
|
48
|
+
else
|
|
49
|
+
values.push(["chapters", "headings", headingsIdentity(context)]);
|
|
50
|
+
}
|
|
51
|
+
const recipes = usesAnimation(config)
|
|
52
|
+
? animateRecipes(context, images, timing, fromDescription ? description : undefined)
|
|
53
|
+
: [];
|
|
54
|
+
if (recipes.length > 0) {
|
|
55
|
+
values.push(["animate-v1", recipes.map((value) => value.fingerprint)]);
|
|
56
|
+
dependsOn.push(...recipes.map((value) => value.key));
|
|
57
|
+
}
|
|
58
|
+
return { recipes, values, dependsOn };
|
|
59
|
+
}
|
|
60
|
+
function animateRecipes(context, images, timing, description) {
|
|
61
|
+
const { config, content } = context;
|
|
62
|
+
const edit = videoEditOf(config);
|
|
63
|
+
const provider = config.images?.provider ?? "";
|
|
64
|
+
// Only images the project draws are animated; an uploaded picture or clip is shown as it
|
|
65
|
+
// was given.
|
|
66
|
+
const drawn = (index) => {
|
|
67
|
+
const key = content.imageOrder[index];
|
|
68
|
+
return key !== undefined && content.imageDefinitions[key]?.source === "generate";
|
|
69
|
+
};
|
|
70
|
+
let indexes;
|
|
71
|
+
if (edit.animate === "every")
|
|
72
|
+
indexes = animatedImageIndexes(edit, content.imageOrder.length);
|
|
73
|
+
else {
|
|
74
|
+
const opening = openingImages(context, timing, description);
|
|
75
|
+
if (opening === undefined) {
|
|
76
|
+
const upper = Math.min(content.imageOrder.filter((_key, index) => drawn(index)).length, chapterEstimate(context));
|
|
77
|
+
return [
|
|
78
|
+
recipe(context, animateFutureKey, "video", {
|
|
79
|
+
kind: "deferred",
|
|
80
|
+
version: 1,
|
|
81
|
+
operation: "animate",
|
|
82
|
+
template: [
|
|
83
|
+
timing?.fingerprint ?? null,
|
|
84
|
+
description?.fingerprint ?? null,
|
|
85
|
+
provider,
|
|
86
|
+
edit.animateModel,
|
|
87
|
+
upper,
|
|
88
|
+
],
|
|
89
|
+
}, [
|
|
90
|
+
...(timing === undefined ? [] : [timing.key]),
|
|
91
|
+
...(description ? [description.key] : []),
|
|
92
|
+
]),
|
|
93
|
+
];
|
|
94
|
+
}
|
|
95
|
+
indexes = opening;
|
|
96
|
+
}
|
|
97
|
+
return indexes.filter(drawn).flatMap((index) => {
|
|
98
|
+
const key = content.imageOrder[index];
|
|
99
|
+
const still = images.find((value) => value.key === `image:${key ?? ""}`);
|
|
100
|
+
if (key === undefined || still === undefined)
|
|
101
|
+
return [];
|
|
102
|
+
return [
|
|
103
|
+
recipe(context, `animate:${key}`, "video", {
|
|
104
|
+
kind: "image",
|
|
105
|
+
version: 1,
|
|
106
|
+
provider,
|
|
107
|
+
model: edit.animateModel,
|
|
108
|
+
prompt: animatePrompt,
|
|
109
|
+
aspect: config.format,
|
|
110
|
+
animate: { image: still.fingerprint, seconds: animatedClipSeconds },
|
|
111
|
+
}, [still.key], { unresolved: still.unresolved }),
|
|
112
|
+
];
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// The images (0-based, in slideshow order) the video and its chapters open on, once the word
|
|
116
|
+
// timing and chapters are saved; undefined until then.
|
|
117
|
+
function openingImages(context, timing, description) {
|
|
118
|
+
const count = context.content.imageOrder.length;
|
|
119
|
+
if (count === 0)
|
|
120
|
+
return [];
|
|
121
|
+
const words = timing === undefined ? undefined : savedWords(context, timing);
|
|
122
|
+
if (words === undefined)
|
|
123
|
+
return undefined;
|
|
124
|
+
const chapters = description === undefined
|
|
125
|
+
? headingChapters(context.resolved.articleMarkdown ?? "", words)
|
|
126
|
+
: savedChapters(context, description);
|
|
127
|
+
if (chapters === undefined)
|
|
128
|
+
return undefined;
|
|
129
|
+
const shots = chapterOpeningShots({
|
|
130
|
+
fps,
|
|
131
|
+
imageSeconds: context.config.imageSeconds,
|
|
132
|
+
narration: usesNarrationCuts(context.config),
|
|
133
|
+
cutPoints: sentenceCutPoints(words),
|
|
134
|
+
chapterStarts: chapters.map((chapter) => chapter.start),
|
|
135
|
+
});
|
|
136
|
+
return [...new Set(shots.map((shot) => shot % count))];
|
|
137
|
+
}
|
|
138
|
+
const wordsPayload = z.object({
|
|
139
|
+
words: z.array(z.object({ text: z.string(), start: z.number(), end: z.number() })),
|
|
140
|
+
});
|
|
141
|
+
const chaptersPayload = z.object({
|
|
142
|
+
chapters: z.array(z.object({ start: z.number(), title: z.string() })),
|
|
143
|
+
});
|
|
144
|
+
function savedPayload(context, value) {
|
|
145
|
+
const piece = context.manifest.pieces.find((row) => row.key === value.key &&
|
|
146
|
+
selectedReference(row) &&
|
|
147
|
+
row.piece.state === "done" &&
|
|
148
|
+
row.fingerprint === value.fingerprint);
|
|
149
|
+
if (piece?.piece.payload === undefined || piece.piece.payload === null)
|
|
150
|
+
return undefined;
|
|
151
|
+
try {
|
|
152
|
+
return JSON.parse(piece.piece.payload);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function savedWords(context, timing) {
|
|
159
|
+
const parsed = wordsPayload.safeParse(savedPayload(context, timing));
|
|
160
|
+
return parsed.success ? parsed.data.words : undefined;
|
|
161
|
+
}
|
|
162
|
+
function savedChapters(context, description) {
|
|
163
|
+
const parsed = chaptersPayload.safeParse(savedPayload(context, description));
|
|
164
|
+
return parsed.success ? parsed.data.chapters : undefined;
|
|
165
|
+
}
|
|
166
|
+
// The article's chapter headings, which decide where heading chapters fall; null while the
|
|
167
|
+
// article is not written yet.
|
|
168
|
+
function headingsIdentity(context) {
|
|
169
|
+
const markdown = context.resolved.articleMarkdown;
|
|
170
|
+
return markdown === null ? null : fingerprint(topHeadings(markdown).map((one) => one.title));
|
|
171
|
+
}
|
|
172
|
+
// ceiling: how many chapters "Chapter openers" is charged for before they are known: the
|
|
173
|
+
// article's headings and the opening, or twelve (the most a YouTube description is asked for)
|
|
174
|
+
// while the article is not written yet.
|
|
175
|
+
function chapterEstimate(context) {
|
|
176
|
+
const markdown = context.resolved.articleMarkdown;
|
|
177
|
+
return markdown === null ? 12 : topHeadings(markdown).length + 1;
|
|
178
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { usesYoutubeDescription } from "../admission/rules.js";
|
|
1
|
+
import { usesShorts, usesYoutubeDescription } from "../admission/rules.js";
|
|
2
2
|
import { audioExportArgs } from "../video/audio-export-args.js";
|
|
3
|
+
import { editNeedsTiming } from "../video/edit-settings.js";
|
|
3
4
|
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
4
5
|
export function exportRecipes(context, audio) {
|
|
5
6
|
const { config, content } = context;
|
|
@@ -17,9 +18,13 @@ export function exportRecipes(context, audio) {
|
|
|
17
18
|
],
|
|
18
19
|
}, audio.keys));
|
|
19
20
|
const captions = config.subtitles !== undefined && config.subtitles.mode !== "off";
|
|
20
|
-
// The YouTube description's chapters
|
|
21
|
-
//
|
|
22
|
-
|
|
21
|
+
// The YouTube description's chapters, the shorts' clips and captions, and the video's cuts,
|
|
22
|
+
// chapter cards and chapter openers use the same word timing, so it runs for them even with
|
|
23
|
+
// captions off; only the caption files below wait for captions.
|
|
24
|
+
if (!captions &&
|
|
25
|
+
!usesYoutubeDescription(config) &&
|
|
26
|
+
!usesShorts(config) &&
|
|
27
|
+
!editNeedsTiming(config))
|
|
23
28
|
return recipes;
|
|
24
29
|
const timing = recipe(context, "subtitles:timing", "video", {
|
|
25
30
|
kind: "local",
|
|
@@ -23,7 +23,9 @@ export const recipeInputSchema = z.discriminatedUnion("kind", [
|
|
|
23
23
|
.object({
|
|
24
24
|
budget: z.number().finite().optional(),
|
|
25
25
|
level: z.enum(["minimal", "low", "medium", "high"]).optional(),
|
|
26
|
-
effort: z
|
|
26
|
+
effort: z
|
|
27
|
+
.enum(["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"])
|
|
28
|
+
.optional(),
|
|
27
29
|
})
|
|
28
30
|
.strict()
|
|
29
31
|
.nullable(),
|
|
@@ -66,6 +68,12 @@ export const recipeInputSchema = z.discriminatedUnion("kind", [
|
|
|
66
68
|
model: z.string(),
|
|
67
69
|
prompt: z.string(),
|
|
68
70
|
aspect: z.enum(["16:9", "9:16"]),
|
|
71
|
+
animate: z.object({ image: z.string(), seconds: z.number() }).strict().optional(),
|
|
72
|
+
thinking: z.enum(thinkingModes).optional(),
|
|
73
|
+
reference: z
|
|
74
|
+
.object({ fingerprint: z.string(), assetId: z.string().nullable() })
|
|
75
|
+
.strict()
|
|
76
|
+
.optional(),
|
|
69
77
|
})
|
|
70
78
|
.strict(),
|
|
71
79
|
z
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { referenceKey } from "../admission/model.js";
|
|
2
3
|
import { buildRecipes } from "./recipe-build.js";
|
|
3
4
|
export function baselineFingerprints(project, outputs, pieces, content, stages = []) {
|
|
4
5
|
const recipes = buildRecipes({
|
|
@@ -34,6 +35,8 @@ export function legacyOutputWorkKey(output, config) {
|
|
|
34
35
|
return `image:${output.id}`;
|
|
35
36
|
case "thumbnail":
|
|
36
37
|
return "thumbnail:image";
|
|
38
|
+
case "reference":
|
|
39
|
+
return referenceKey;
|
|
37
40
|
case "notes":
|
|
38
41
|
return "research:notes";
|
|
39
42
|
case "article_md":
|
|
@@ -59,6 +62,15 @@ export function legacyOutputWorkKey(output, config) {
|
|
|
59
62
|
case "youtube_description":
|
|
60
63
|
case "youtube_tags":
|
|
61
64
|
return "youtube:description";
|
|
65
|
+
case "shorts":
|
|
66
|
+
return "shorts:pick";
|
|
67
|
+
case "short_image":
|
|
68
|
+
return `shorts:${String(output.meta.short ?? 1)}:image:${String(output.meta.index ?? 1)}`;
|
|
69
|
+
case "short_video":
|
|
70
|
+
return `shorts:${String(output.meta.short ?? 1)}:render`;
|
|
71
|
+
// Only ever made by a revision, never by the legacy stages.
|
|
72
|
+
case "animated_image":
|
|
73
|
+
return `animate:${String(output.meta.index ?? 1)}`;
|
|
62
74
|
case "subtitles_srt":
|
|
63
75
|
case "subtitles_vtt":
|
|
64
76
|
case "subtitle_ass":
|
|
@@ -17,6 +17,8 @@ export const localOperations = [
|
|
|
17
17
|
"narration-files-v1",
|
|
18
18
|
"render-document",
|
|
19
19
|
"youtube-description-v1",
|
|
20
|
+
"shorts-pick-v1",
|
|
21
|
+
"short-render-v1",
|
|
20
22
|
];
|
|
21
23
|
export const deferredOperations = [
|
|
22
24
|
"narration-preparation",
|
|
@@ -30,6 +32,8 @@ export const deferredOperations = [
|
|
|
30
32
|
"intro-narration",
|
|
31
33
|
"outro-narration",
|
|
32
34
|
"resolve-revision-recipe",
|
|
35
|
+
"shorts",
|
|
36
|
+
"animate",
|
|
33
37
|
];
|
|
34
38
|
export function recipe(context, key, stage, input, dependsOn = [], options = {}) {
|
|
35
39
|
const requestFingerprint = input.kind === "tts"
|
|
@@ -40,7 +44,9 @@ export function recipe(context, key, stage, input, dependsOn = [], options = {})
|
|
|
40
44
|
: fingerprint(JSON.parse(JSON.stringify(input, (_key, value) => typeof value === "number" ? z.number().finite().parse(value) : value)));
|
|
41
45
|
const token = input.kind === "tts"
|
|
42
46
|
? narrationRegenerationToken(context.content.regenerationTokens, input.logicalKey, input.segment)
|
|
43
|
-
:
|
|
47
|
+
: options.token !== undefined
|
|
48
|
+
? options.token
|
|
49
|
+
: (context.content.regenerationTokens[options.tokenKey ?? key] ?? null);
|
|
44
50
|
const researchToken = stage === "research" ? context.content.regenerationTokens["research:all"] : undefined;
|
|
45
51
|
const workFingerprint = fingerprint([
|
|
46
52
|
requestFingerprint,
|
|
@@ -5,6 +5,21 @@ export function recipeProviderChoice(recipe, config) {
|
|
|
5
5
|
// The YouTube description's request is built when it runs, from the project's LLM row.
|
|
6
6
|
if (input.kind === "local" && input.operation === "youtube-description-v1")
|
|
7
7
|
return config.llm === undefined ? undefined : { ...config.llm, family: "llm" };
|
|
8
|
+
// So is the shorts' pick; what stands in for the rest of the step until it lands is
|
|
9
|
+
// mostly images, which are its cost.
|
|
10
|
+
if (input.kind === "local" && input.operation === "shorts-pick-v1")
|
|
11
|
+
return config.llm === undefined ? undefined : { ...config.llm, family: "llm" };
|
|
12
|
+
if (input.kind === "deferred" && input.operation === "shorts")
|
|
13
|
+
return config.images === undefined ? undefined : { ...config.images, family: "image" };
|
|
14
|
+
// The chapter openers' clips are asked of the image provider's image-to-video model.
|
|
15
|
+
if (input.kind === "deferred" && input.operation === "animate")
|
|
16
|
+
return config.images === undefined
|
|
17
|
+
? undefined
|
|
18
|
+
: {
|
|
19
|
+
provider: config.images.provider,
|
|
20
|
+
model: config.videoEdit?.animateModel ?? "",
|
|
21
|
+
family: "image",
|
|
22
|
+
};
|
|
8
23
|
if (input.kind !== "deferred")
|
|
9
24
|
return undefined;
|
|
10
25
|
const family = input.operation === "narration-preparation"
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { referenceKey } from "../admission/model.js";
|
|
2
|
+
import { usesReference } from "../admission/rules.js";
|
|
3
|
+
import { recipe, selectedAsset, } from "./recipe-model.js";
|
|
4
|
+
import { renderedPrompt } from "./recipe-text.js";
|
|
5
|
+
// The establishing image (`RunDraft.reference`): one step of the Images stage, made before
|
|
6
|
+
// every other image of the run and never part of the video. Made from its library prompt, whose
|
|
7
|
+
// keywords are filled like every other image prompt's, or the file the user uploaded.
|
|
8
|
+
export const referencePromptKey = "referencePrompt";
|
|
9
|
+
// The image provider's choice as an image request carries it: the effort only when one is set,
|
|
10
|
+
// so a request made without one keeps the fingerprint it always had.
|
|
11
|
+
export function imageChoice(config) {
|
|
12
|
+
return {
|
|
13
|
+
provider: config.images?.provider ?? "",
|
|
14
|
+
model: config.images?.model ?? "",
|
|
15
|
+
...(config.images?.thinking === undefined ? {} : { thinking: config.images.thinking }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export function referenceRecipe(context) {
|
|
19
|
+
const { config, content } = context;
|
|
20
|
+
if (!usesReference(config) || config.reference === undefined)
|
|
21
|
+
return undefined;
|
|
22
|
+
if (config.reference.source === "provide")
|
|
23
|
+
return recipe(context, referenceKey, "images", {
|
|
24
|
+
kind: "provided",
|
|
25
|
+
version: 1,
|
|
26
|
+
assetId: content.provided.reference ?? null,
|
|
27
|
+
semantic: "reference",
|
|
28
|
+
}, [], { unresolved: content.provided.reference === undefined });
|
|
29
|
+
const prompt = renderedPrompt(context, referencePromptKey);
|
|
30
|
+
return recipe(context, referenceKey, "images", { kind: "image", version: 1, ...imageChoice(config), aspect: config.format, prompt }, [], { unresolved: prompt.trim() === "" });
|
|
31
|
+
}
|
|
32
|
+
// Which image the others are drawn from: the step's fingerprint, so making it again or
|
|
33
|
+
// changing it marks every image drawn from it outdated, and the asset it made.
|
|
34
|
+
export function imageReference(context, reference) {
|
|
35
|
+
return reference === undefined
|
|
36
|
+
? undefined
|
|
37
|
+
: {
|
|
38
|
+
key: reference.key,
|
|
39
|
+
input: {
|
|
40
|
+
fingerprint: reference.fingerprint,
|
|
41
|
+
assetId: selectedAsset(context, reference.key, reference.fingerprint),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|