@gentbajko/slopify 2.4.0 → 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 +8 -1
- 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 +22 -2
- 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 +7 -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/models.yaml +7 -0
- package/dist/catalog/registry.js +24 -3
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +10 -1
- package/dist/catalog/validate.js +17 -2
- 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/schedules.js +1 -1
- package/dist/edge/http/settings.js +44 -0
- package/dist/host-cli/runtime.js +3 -2
- 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 +1 -1
- package/dist/kernel/runner/providers.js +22 -2
- package/dist/main.js +52 -7
- package/dist/slices/admission/model.js +8 -0
- package/dist/slices/admission/rules.js +47 -0
- package/dist/slices/admission/schema.js +11 -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 +9 -1
- package/dist/slices/library/slots.js +8 -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 +35 -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 +20 -2
- 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 +20 -0
- package/dist/slices/project-templates/setup.js +11 -0
- package/dist/slices/rebuild/preview-details.js +24 -2
- package/dist/slices/rebuild/recipe-build.js +9 -3
- package/dist/slices/rebuild/recipe-input-schema.js +8 -1
- package/dist/slices/rebuild/recipe-legacy.js +3 -0
- package/dist/slices/rebuild/recipe-reference.js +44 -0
- package/dist/slices/rebuild/recipe-shorts.js +11 -4
- package/dist/slices/rebuild/recipe-validation.js +15 -1
- package/dist/slices/rebuild/recipe-visual.js +20 -8
- package/dist/slices/rebuild/runtime-actions.js +9 -2
- package/dist/slices/rebuild/runtime-image.js +50 -0
- package/dist/slices/rebuild/runtime-local.js +6 -3
- package/dist/slices/rebuild/runtime-piece-label.js +2 -0
- package/dist/slices/rebuild/runtime-provider.js +17 -10
- package/dist/slices/rebuild/runtime-shorts.js +4 -6
- package/dist/slices/rebuild/service-readiness.js +15 -1
- package/dist/slices/rebuild/transition-repo.js +1 -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 +33 -9
- package/dist/slices/revisions/mutation-prepare.js +8 -10
- package/dist/slices/revisions/schema.js +5 -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/storage/layout.js +10 -0
- package/dist/slices/storage/model.js +3 -0
- package/dist/slices/storage/portable.js +6 -3
- package/dist/slices/storage/reconcile.js +5 -1
- package/dist/web/assets/index-CktpxlJd.js +171 -0
- package/dist/web/assets/index-DB-vOnGU.css +1 -0
- package/dist/web/assets/{pdf-CAJNT254.js → pdf-BLF2i8c2.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BGgYMm2u.css +0 -1
- package/dist/web/assets/index-BVVgtntL.js +0 -171
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { readSetting, writeSetting } from "../settings/repo.js";
|
|
2
|
+
import { notificationUrlProblem } from "./rules.js";
|
|
3
|
+
// Its own row in the key/value `settings` table. Backups leave it out
|
|
4
|
+
// (`slices/storage/portable.ts`): an ntfy topic in the URL is as good as a password.
|
|
5
|
+
export const notificationUrlKey = "notificationUrl";
|
|
6
|
+
export function readNotificationUrl(db) {
|
|
7
|
+
const stored = readSetting(db, notificationUrlKey);
|
|
8
|
+
if (stored === undefined)
|
|
9
|
+
return null;
|
|
10
|
+
let value;
|
|
11
|
+
try {
|
|
12
|
+
value = JSON.parse(stored);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
// A hand-edited row that no longer passes the rules is treated as not set rather than
|
|
18
|
+
// POSTed to.
|
|
19
|
+
return typeof value === "string" && value !== "" && notificationUrlProblem(value) === undefined
|
|
20
|
+
? value
|
|
21
|
+
: null;
|
|
22
|
+
}
|
|
23
|
+
export function saveNotificationUrl(db, raw) {
|
|
24
|
+
const problem = notificationUrlProblem(raw);
|
|
25
|
+
if (problem !== undefined)
|
|
26
|
+
return { ok: false, message: problem };
|
|
27
|
+
const url = raw.trim();
|
|
28
|
+
if (url === "") {
|
|
29
|
+
db.prepare("DELETE FROM settings WHERE key = ?").run(notificationUrlKey);
|
|
30
|
+
return { ok: true, url: null };
|
|
31
|
+
}
|
|
32
|
+
writeSetting(db, notificationUrlKey, JSON.stringify(url));
|
|
33
|
+
return { ok: true, url };
|
|
34
|
+
}
|
|
@@ -4,6 +4,14 @@ import { runDraftSchema } from "../admission/schema.js";
|
|
|
4
4
|
import { draftDocumentThemeOf } from "../document/model.js";
|
|
5
5
|
import { defaultShorts, shortsExtrasOf, shortsSettingsProblems, } from "../shorts/model.js";
|
|
6
6
|
import { defaultSubtitles } from "../subtitles/model.js";
|
|
7
|
+
// Said where the control is, since the music sits in a closed disclosure on the Export rail.
|
|
8
|
+
const musicMessages = {
|
|
9
|
+
uploading: "The shorts' background music is still uploading, so the run can't start yet. Wait for it to finish, or remove it under Outputs → Export → More shorts options → Background music.",
|
|
10
|
+
missing: "The shorts' background music file is no longer available (it was not uploaded again after the draft was copied, or the upload failed). Choose the file again under Outputs → Export → More shorts options → Background music, or remove it.",
|
|
11
|
+
};
|
|
12
|
+
function pickMusic(id) {
|
|
13
|
+
return id === undefined ? {} : { shortsMusic: id };
|
|
14
|
+
}
|
|
7
15
|
export function toAdmissionDraft(input) {
|
|
8
16
|
const { form } = input.document;
|
|
9
17
|
const fields = [];
|
|
@@ -49,15 +57,18 @@ export function toAdmissionDraft(input) {
|
|
|
49
57
|
});
|
|
50
58
|
return undefined;
|
|
51
59
|
};
|
|
52
|
-
const file = (ref, kind, field
|
|
60
|
+
const file = (ref, kind, field, messages = {
|
|
61
|
+
uploading: "This file is still uploading. Wait for it to finish.",
|
|
62
|
+
missing: "That upload is no longer available. Choose the file again.",
|
|
63
|
+
}) => {
|
|
53
64
|
const attachment = input.attachments.find((item) => item.id === ref?.attachmentId && item.kind === kind && item.name === ref.name);
|
|
54
65
|
if (attachment?.state === "ready" && attachment.stagedFileId !== null)
|
|
55
66
|
return attachment.stagedFileId;
|
|
56
67
|
fields.push({
|
|
57
68
|
field,
|
|
58
69
|
message: attachment?.state === "copying" || attachment?.state === "pending"
|
|
59
|
-
?
|
|
60
|
-
:
|
|
70
|
+
? messages.uploading
|
|
71
|
+
: messages.missing,
|
|
61
72
|
});
|
|
62
73
|
return undefined;
|
|
63
74
|
};
|
|
@@ -119,6 +130,19 @@ export function toAdmissionDraft(input) {
|
|
|
119
130
|
: form.audio
|
|
120
131
|
: undefined,
|
|
121
132
|
images: form.images,
|
|
133
|
+
// The establishing image belongs to generated images; Off, or images not generated,
|
|
134
|
+
// leaves it out, which is what every draft saved before it was.
|
|
135
|
+
...(sources.images === "generate" &&
|
|
136
|
+
form.reference !== undefined &&
|
|
137
|
+
form.reference.source !== "off"
|
|
138
|
+
? {
|
|
139
|
+
reference: {
|
|
140
|
+
source: form.reference.source,
|
|
141
|
+
...(form.reference.source === "prompt" ? { prompt: form.reference.prompt } : {}),
|
|
142
|
+
thumbnail: form.reference.thumbnail,
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
: {}),
|
|
122
146
|
articlePrompt: sources.article === "generate" ? form.articlePrompt : undefined,
|
|
123
147
|
...(sources.audio === "generate" && form.narrationPrompt?.trim()
|
|
124
148
|
? { narrationPrompt: form.narrationPrompt }
|
|
@@ -154,12 +178,20 @@ export function toAdmissionDraft(input) {
|
|
|
154
178
|
thumbnail: sources.thumbnail === "provide"
|
|
155
179
|
? file(form.provided.thumbnail, "thumbnail", "provided.thumbnail")
|
|
156
180
|
: undefined,
|
|
181
|
+
reference: sources.images === "generate" && form.reference?.source === "provide"
|
|
182
|
+
? file(form.provided.reference ?? null, "reference", "provided.reference")
|
|
183
|
+
: undefined,
|
|
157
184
|
images: sources.images === "provide"
|
|
158
185
|
? form.provided.images.flatMap((ref, index) => {
|
|
159
186
|
const id = file(ref, "images", `provided.images.${index}`);
|
|
160
187
|
return id === undefined ? [] : [id];
|
|
161
188
|
})
|
|
162
189
|
: [],
|
|
190
|
+
// Optional, so only a file that was attached is checked; with Shorts off it is kept on
|
|
191
|
+
// the draft for when Shorts is turned back on, and never reaches the run.
|
|
192
|
+
...(shortsOn && form.provided.shortsMusic
|
|
193
|
+
? pickMusic(file(form.provided.shortsMusic, "audio", "shorts.music", musicMessages))
|
|
194
|
+
: {}),
|
|
163
195
|
},
|
|
164
196
|
chunking: {
|
|
165
197
|
mode: chunkMode,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { fingerprint } from "../../kernel/runner/work.js";
|
|
3
|
+
import { draftAttachmentKinds } from "./schema.js";
|
|
3
4
|
const rowSchema = z.object({
|
|
4
5
|
id: z.string(),
|
|
5
6
|
schema_version: z.number(),
|
|
@@ -21,7 +22,7 @@ const attachmentSchema = z.object({
|
|
|
21
22
|
id: z.string(),
|
|
22
23
|
draft_id: z.string(),
|
|
23
24
|
staged_file_id: z.string().nullable(),
|
|
24
|
-
kind: z.enum(
|
|
25
|
+
kind: z.enum(draftAttachmentKinds),
|
|
25
26
|
original_filename: z.string(),
|
|
26
27
|
status: z.enum(["pending", "ready", "reattach"]),
|
|
27
28
|
error: z.string().nullable(),
|
|
@@ -52,6 +53,11 @@ export function attachmentRefs(document) {
|
|
|
52
53
|
...(p.audio === null ? [] : [{ ...p.audio, kind: "audio" }]),
|
|
53
54
|
...p.images.map((file) => ({ ...file, kind: "images" })),
|
|
54
55
|
...(p.thumbnail === null ? [] : [{ ...p.thumbnail, kind: "thumbnail" }]),
|
|
56
|
+
...(p.reference === undefined || p.reference === null
|
|
57
|
+
? []
|
|
58
|
+
: [{ ...p.reference, kind: "reference" }]),
|
|
59
|
+
// The shorts' music is staged like narration audio, so it is an audio attachment.
|
|
60
|
+
...(p.shortsMusic == null ? [] : [{ ...p.shortsMusic, kind: "audio" }]),
|
|
55
61
|
];
|
|
56
62
|
}
|
|
57
63
|
export function requestHash(value) {
|
|
@@ -110,7 +110,9 @@ export function resolveReviewInputs(deps, view, font) {
|
|
|
110
110
|
const activeIds = new Set(runs.flatMap((run) => [
|
|
111
111
|
run.draft.provided.audio,
|
|
112
112
|
run.draft.provided.thumbnail,
|
|
113
|
+
run.draft.provided.reference,
|
|
113
114
|
...(run.draft.provided.images ?? []),
|
|
115
|
+
run.draft.provided.shortsMusic,
|
|
114
116
|
]));
|
|
115
117
|
const attachmentIdentity = fresh.value.attachments.flatMap((file) => file.stagedFileId !== null && activeIds.has(file.stagedFileId)
|
|
116
118
|
? [{ id: file.id, stagedFileId: file.stagedFileId, bytes: file.bytes }]
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { formats, stageKinds } from "../../kernel/pipeline.js";
|
|
3
3
|
import { thinkingModes } from "../../kernel/ports/llm.js";
|
|
4
|
-
import { motionStyles, stageSources } from "../admission/model.js";
|
|
4
|
+
import { motionStyles, referenceSources, stageSources } from "../admission/model.js";
|
|
5
5
|
import { defaultEdgeSilenceSeconds, defaultImageSeconds, defaultMotionStyle, defaultZoomPercent, } from "../admission/rules.js";
|
|
6
6
|
import { runDraftSchema, videoEditSchema } from "../admission/schema.js";
|
|
7
7
|
import { checkpointRowSchema, checkpointStageSchema } from "../checkpoints/schema.js";
|
|
@@ -15,6 +15,8 @@ const values = z.record(text, text).readonly();
|
|
|
15
15
|
const provider = z
|
|
16
16
|
.object({ provider: text, model: text, thinking: z.enum(thinkingModes).optional() })
|
|
17
17
|
.strict();
|
|
18
|
+
// What a draft's uploads are for: a stage's files, or the Images stage's establishing image.
|
|
19
|
+
export const draftAttachmentKinds = ["audio", "images", "thumbnail", "reference"];
|
|
18
20
|
const file = z.object({ attachmentId: id, name: text }).strict().readonly();
|
|
19
21
|
export const playDraftFormSchema = z
|
|
20
22
|
.object({
|
|
@@ -45,6 +47,17 @@ export const playDraftFormSchema = z
|
|
|
45
47
|
})
|
|
46
48
|
.readonly(),
|
|
47
49
|
images: provider.readonly(),
|
|
50
|
+
// Absent on drafts and templates saved before the establishing image: Off. The prompt is
|
|
51
|
+
// an image prompt's name, "" when none is picked.
|
|
52
|
+
reference: z
|
|
53
|
+
.object({
|
|
54
|
+
source: z.enum(["off", ...referenceSources]),
|
|
55
|
+
prompt: text,
|
|
56
|
+
thumbnail: z.boolean(),
|
|
57
|
+
})
|
|
58
|
+
.strict()
|
|
59
|
+
.readonly()
|
|
60
|
+
.optional(),
|
|
48
61
|
articlePrompt: text,
|
|
49
62
|
narrationPrompt: text.optional(),
|
|
50
63
|
// Absent on drafts and templates saved before the YouTube description: off, built-in prompt.
|
|
@@ -105,6 +118,11 @@ export const playDraftFormSchema = z
|
|
|
105
118
|
audio: file.nullable(),
|
|
106
119
|
thumbnail: file.nullable(),
|
|
107
120
|
images: z.array(file).readonly(),
|
|
121
|
+
// The uploaded establishing image; absent on drafts saved before it.
|
|
122
|
+
reference: file.nullable().optional(),
|
|
123
|
+
// The Shorts step's background music, uploaded as an audio attachment. Absent on
|
|
124
|
+
// drafts and templates saved before Play offered it: no music.
|
|
125
|
+
shortsMusic: file.nullable().optional(),
|
|
108
126
|
})
|
|
109
127
|
.strict()
|
|
110
128
|
.readonly(),
|
|
@@ -150,7 +168,7 @@ export const discardDraftInputSchema = z
|
|
|
150
168
|
export const draftAttachmentSchema = z
|
|
151
169
|
.object({
|
|
152
170
|
id,
|
|
153
|
-
kind: z.enum(
|
|
171
|
+
kind: z.enum(draftAttachmentKinds),
|
|
154
172
|
name: text,
|
|
155
173
|
state: z.enum(["pending", "copying", "ready", "reattach"]),
|
|
156
174
|
stagedFileId: text.nullable(),
|
|
@@ -208,6 +208,12 @@ export function forkDraft(deps, input) {
|
|
|
208
208
|
audio: p.audio === null ? null : rewrite(p.audio),
|
|
209
209
|
thumbnail: p.thumbnail === null ? null : rewrite(p.thumbnail),
|
|
210
210
|
images: p.images.map(rewrite),
|
|
211
|
+
...(p.reference === undefined
|
|
212
|
+
? {}
|
|
213
|
+
: { reference: p.reference === null ? null : rewrite(p.reference) }),
|
|
214
|
+
...(p.shortsMusic === undefined
|
|
215
|
+
? {}
|
|
216
|
+
: { shortsMusic: p.shortsMusic === null ? null : rewrite(p.shortsMusic) }),
|
|
211
217
|
},
|
|
212
218
|
},
|
|
213
219
|
};
|
|
@@ -29,7 +29,8 @@ export async function uploadDraftAttachment(deps, input) {
|
|
|
29
29
|
return refuse("conflict");
|
|
30
30
|
const storage = { ...deps, emit: () => undefined };
|
|
31
31
|
const result = await stageUpload(storage, {
|
|
32
|
-
|
|
32
|
+
// The establishing image is the Images stage's upload.
|
|
33
|
+
stageKind: attachment.kind === "reference" ? "images" : attachment.kind,
|
|
33
34
|
originalFilename: attachment.original_filename,
|
|
34
35
|
content: input.content,
|
|
35
36
|
onAllocated(file) {
|
|
@@ -103,6 +103,10 @@ function documentFromProject(deps, revision) {
|
|
|
103
103
|
// and a snapshot holds one prompt per kind and name.
|
|
104
104
|
if (!prompts.some((prompt) => prompt.kind === "image" && prompt.name === config.shorts?.imagePrompt))
|
|
105
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");
|
|
106
110
|
for (const category of ["intro", "outro"]) {
|
|
107
111
|
const choice = config[category];
|
|
108
112
|
const body = revision.content.promptTemplates[category] ?? config.rendered[category];
|
|
@@ -125,9 +129,16 @@ function documentFromProject(deps, revision) {
|
|
|
125
129
|
: "",
|
|
126
130
|
audio: config.sources.audio === "provide" ? attachment("Audio from project") : null,
|
|
127
131
|
thumbnail: config.sources.thumbnail === "provide" ? attachment("Thumbnail from project") : null,
|
|
132
|
+
...(config.reference?.source === "provide"
|
|
133
|
+
? { reference: attachment("Establishing image from project") }
|
|
134
|
+
: {}),
|
|
128
135
|
images: config.sources.images === "provide"
|
|
129
136
|
? (definitions.length ? definitions : (config.provided.images ?? [])).map((_, index) => attachment(`Image ${index + 1}`))
|
|
130
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
|
+
: {}),
|
|
131
142
|
};
|
|
132
143
|
return {
|
|
133
144
|
schemaVersion: 1,
|
|
@@ -147,6 +158,15 @@ function documentFromProject(deps, revision) {
|
|
|
147
158
|
llm: config.llm ?? { provider: "", model: "" },
|
|
148
159
|
audio: config.audio ?? { provider: "", model: "", voice: "" },
|
|
149
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
|
+
}),
|
|
150
170
|
articlePrompt: config.articlePrompt ?? "",
|
|
151
171
|
...(config.narrationPrompt === undefined ? {} : { narrationPrompt: config.narrationPrompt }),
|
|
152
172
|
...(config.youtubeDescription === true ? { youtubeDescription: true } : {}),
|
|
@@ -10,6 +10,10 @@ export function templateSetup(deps, input) {
|
|
|
10
10
|
{ kind: "thumbnail", name: input.form.thumbnailPrompt },
|
|
11
11
|
{ kind: "shorts", name: input.form.shorts?.prompt ?? "" },
|
|
12
12
|
{ kind: "image", name: input.form.shorts?.imagePrompt ?? "" },
|
|
13
|
+
{
|
|
14
|
+
kind: "image",
|
|
15
|
+
name: input.form.reference?.source === "prompt" ? input.form.reference.prompt : "",
|
|
16
|
+
},
|
|
13
17
|
];
|
|
14
18
|
for (const choice of choices) {
|
|
15
19
|
if (choice.name === "")
|
|
@@ -53,6 +57,13 @@ export function freshTemplateDraft(deps, document, source) {
|
|
|
53
57
|
audio: provided.audio === null ? null : fresh(provided.audio),
|
|
54
58
|
thumbnail: provided.thumbnail === null ? null : fresh(provided.thumbnail),
|
|
55
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) }),
|
|
56
67
|
},
|
|
57
68
|
},
|
|
58
69
|
};
|
|
@@ -46,9 +46,19 @@ export function previewDetails(deps, view, allRecipes, selected) {
|
|
|
46
46
|
return [
|
|
47
47
|
{
|
|
48
48
|
key: row.key,
|
|
49
|
-
label:
|
|
49
|
+
label: row.key === "reference:image"
|
|
50
|
+
? "Establishing image"
|
|
51
|
+
: index < 0
|
|
52
|
+
? "Thumbnail image"
|
|
53
|
+
: `Image ${index + 1}`,
|
|
50
54
|
text: input.prompt,
|
|
51
|
-
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(" · "),
|
|
52
62
|
},
|
|
53
63
|
];
|
|
54
64
|
}
|
|
@@ -142,6 +152,12 @@ function inputChanges(parent, view) {
|
|
|
142
152
|
: `${String(after.shorts.minSeconds)}-${String(after.shorts.maxSeconds)}`);
|
|
143
153
|
add("Shorts prompt", before.shorts?.prompt, after.shorts?.prompt);
|
|
144
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");
|
|
145
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");
|
|
146
162
|
add("Shorts speed", before.shorts === undefined ? undefined : `${shortsSpeedOf(before.shorts).toFixed(2)}×`, after.shorts === undefined ? undefined : `${shortsSpeedOf(after.shorts).toFixed(2)}×`);
|
|
147
163
|
add("Shorts music", old.shortsMusic === undefined ? "None" : "Uploaded file", next.shortsMusic === undefined
|
|
@@ -191,3 +207,9 @@ function inputChanges(parent, view) {
|
|
|
191
207
|
add("Manual captions", old.subtitleCues?.cues, next.subtitleCues?.cues);
|
|
192
208
|
return changes;
|
|
193
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
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { referenceForThumbnail } from "../admission/rules.js";
|
|
1
2
|
import { audioRecipes } from "./recipe-audio.js";
|
|
2
3
|
import { documentRecipes } from "./recipe-document.js";
|
|
3
4
|
import { editPlan } from "./recipe-edit.js";
|
|
4
5
|
import { exportRecipes } from "./recipe-exports.js";
|
|
6
|
+
import { imageReference, referenceRecipe } from "./recipe-reference.js";
|
|
5
7
|
import { shortsRecipes } from "./recipe-shorts.js";
|
|
6
8
|
import { textRecipes } from "./recipe-text.js";
|
|
7
9
|
import { thumbnailRecipes, visualAssets, visualRecipes } from "./recipe-visual.js";
|
|
@@ -11,16 +13,20 @@ export function buildRecipes(context) {
|
|
|
11
13
|
const audio = audioRecipes(context, text);
|
|
12
14
|
const exports = exportRecipes(context, audio);
|
|
13
15
|
const captions = exports.find((value) => value.key === "subtitles:files");
|
|
14
|
-
|
|
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);
|
|
15
20
|
const youtube = youtubeRecipes(context, exports);
|
|
16
21
|
return [
|
|
17
22
|
...text.recipes,
|
|
18
23
|
...audio.recipes,
|
|
19
24
|
...exports,
|
|
20
25
|
...youtube,
|
|
21
|
-
...
|
|
26
|
+
...(reference === undefined ? [] : [reference]),
|
|
27
|
+
...shortsRecipes(context, exports, drawnFrom),
|
|
22
28
|
...thumbnail,
|
|
23
29
|
...documentRecipes(context, text, thumbnail),
|
|
24
|
-
...visualAssets(context, visualRecipes(context.config, context.content, audio.mediaFingerprint, captions?.fingerprint ?? null, (images) => editPlan(context, exports, youtube, images))),
|
|
30
|
+
...visualAssets(context, visualRecipes(context.config, context.content, audio.mediaFingerprint, captions?.fingerprint ?? null, (images) => editPlan(context, exports, youtube, images), drawnFrom)),
|
|
25
31
|
];
|
|
26
32
|
}
|
|
@@ -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(),
|
|
@@ -67,6 +69,11 @@ export const recipeInputSchema = z.discriminatedUnion("kind", [
|
|
|
67
69
|
prompt: z.string(),
|
|
68
70
|
aspect: z.enum(["16:9", "9:16"]),
|
|
69
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(),
|
|
70
77
|
})
|
|
71
78
|
.strict(),
|
|
72
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":
|
|
@@ -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
|
+
}
|
|
@@ -5,6 +5,7 @@ import { effectiveClips, pickedShortsOf } from "../shorts/clips.js";
|
|
|
5
5
|
import { defaultShortsImagePrompt, musicVolumeOf, shortImageCount, shortsSpeedOf, } from "../shorts/model.js";
|
|
6
6
|
import { imagePromptMessages } from "../shorts/prompts.js";
|
|
7
7
|
import { recipe, resourceIdentity, selectedReference, } from "./recipe-model.js";
|
|
8
|
+
import { imageChoice } from "./recipe-reference.js";
|
|
8
9
|
import { llmInput, renderedPrompt } from "./recipe-text.js";
|
|
9
10
|
// The Shorts step unfolds as its answers land, the way research's chapters unfold from the
|
|
10
11
|
// plan. `shorts:pick` reads the word timing, so it waits for `subtitles:timing` and runs
|
|
@@ -19,7 +20,9 @@ import { llmInput, renderedPrompt } from "./recipe-text.js";
|
|
|
19
20
|
// can be made again alone.
|
|
20
21
|
export const shortsPickKey = "shorts:pick";
|
|
21
22
|
export const shortsFutureKey = "shorts:future";
|
|
22
|
-
export function shortsRecipes(context, exports
|
|
23
|
+
export function shortsRecipes(context, exports,
|
|
24
|
+
// The establishing image the shorts' images are drawn from, when it is on.
|
|
25
|
+
reference) {
|
|
23
26
|
const { config } = context;
|
|
24
27
|
const shorts = config.shorts;
|
|
25
28
|
const timing = exports.find((value) => value.key === "subtitles:timing");
|
|
@@ -67,6 +70,10 @@ export function shortsRecipes(context, exports) {
|
|
|
67
70
|
shorts.maxSeconds,
|
|
68
71
|
config.llm?.provider ?? "",
|
|
69
72
|
config.llm?.model ?? "",
|
|
73
|
+
// Only what is in use, so shorts planned before these existed keep their
|
|
74
|
+
// fingerprint; the estimate reads the fields above by position.
|
|
75
|
+
...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
|
|
76
|
+
...(reference === undefined ? [] : [reference.input.fingerprint]),
|
|
70
77
|
],
|
|
71
78
|
}, [pick.key], { tokenKey: shortsPickKey }),
|
|
72
79
|
];
|
|
@@ -94,11 +101,11 @@ export function shortsRecipes(context, exports) {
|
|
|
94
101
|
const stills = written.map((prompt, index) => recipe(context, `${prefix}:image:${String(index + 1)}`, "video", {
|
|
95
102
|
kind: "image",
|
|
96
103
|
version: 1,
|
|
97
|
-
|
|
98
|
-
model: images.model,
|
|
104
|
+
...imageChoice(config),
|
|
99
105
|
prompt,
|
|
100
106
|
aspect: "9:16",
|
|
101
|
-
|
|
107
|
+
...(reference === undefined ? {} : { reference: reference.input }),
|
|
108
|
+
}, [prompts.key, ...(reference === undefined ? [] : [reference.key])], { token }));
|
|
102
109
|
recipes.push(...stills);
|
|
103
110
|
const extras = renderExtras(context, clip);
|
|
104
111
|
recipes.push(recipe(context, `${prefix}:render`, "video", {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { narrationPreparationFields, shortsFields, usesNarrationPreparation, usesShorts, usesYoutubeDescription, videoEditFields, youtubeDescriptionFields, } from "../admission/rules.js";
|
|
1
|
+
import { narrationPreparationFields, shortsFields, usesNarrationPreparation, usesReference, usesShorts, usesYoutubeDescription, videoEditFields, youtubeDescriptionFields, } from "../admission/rules.js";
|
|
2
2
|
import { detectSlots, render } from "../admission/substitute.js";
|
|
3
3
|
import { usesAnimation } from "../video/edit-settings.js";
|
|
4
4
|
export function validateRecipeInputs(config, content) {
|
|
@@ -93,6 +93,20 @@ export function validateRecipeInputs(config, content) {
|
|
|
93
93
|
raw: content.promptTemplates.thumbnailPrompt ?? null,
|
|
94
94
|
literal: config.rendered.thumbnailPrompt,
|
|
95
95
|
});
|
|
96
|
+
// The establishing image: its prompt is checked like the thumbnail's, an upload must be there.
|
|
97
|
+
if (usesReference(config) && config.reference?.source === "prompt")
|
|
98
|
+
prompts.push({
|
|
99
|
+
field: "rendered.referencePrompt",
|
|
100
|
+
raw: content.promptTemplates.referencePrompt ?? null,
|
|
101
|
+
literal: config.rendered.referencePrompt,
|
|
102
|
+
});
|
|
103
|
+
if (usesReference(config) &&
|
|
104
|
+
config.reference?.source === "provide" &&
|
|
105
|
+
content.provided.reference === undefined)
|
|
106
|
+
fields.push({
|
|
107
|
+
field: "content.provided.reference",
|
|
108
|
+
message: "Upload the establishing image, choose From a prompt instead, or set Establishing image to Off.",
|
|
109
|
+
});
|
|
96
110
|
if (config.sources.images !== "off")
|
|
97
111
|
for (const key of content.imageOrder) {
|
|
98
112
|
const image = content.imageDefinitions[key];
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { render } from "../admission/substitute.js";
|
|
2
2
|
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
3
|
+
import { imageChoice } from "./recipe-reference.js";
|
|
3
4
|
import { matchingText, renderedPrompt } from "./recipe-text.js";
|
|
4
5
|
export function visualRecipes(config, content, audioFingerprint, captionFingerprint,
|
|
5
6
|
// What the Video stage's edit settings add (`recipe-edit.ts`), from the images planned here.
|
|
6
|
-
edit
|
|
7
|
+
edit,
|
|
8
|
+
// The establishing image the generated images are drawn from, when it is on.
|
|
9
|
+
reference) {
|
|
7
10
|
const recipes = [];
|
|
8
11
|
const imageKeys = config.sources.images === "off" ? [] : content.imageOrder;
|
|
9
12
|
for (const key of imageKeys) {
|
|
@@ -19,11 +22,11 @@ edit) {
|
|
|
19
22
|
: {
|
|
20
23
|
kind: "image",
|
|
21
24
|
version: 1,
|
|
22
|
-
|
|
23
|
-
model: config.images?.model ?? "",
|
|
25
|
+
...imageChoice(config),
|
|
24
26
|
aspect: config.format,
|
|
25
27
|
prompt: prompt ?? "",
|
|
26
|
-
|
|
28
|
+
...(reference === undefined ? {} : { reference: reference.input }),
|
|
29
|
+
}, image.source === "provide" || reference === undefined ? [] : [reference.key], { unresolved: image.source === "provide" ? image.assetId === null : !prompt?.trim() }));
|
|
27
30
|
}
|
|
28
31
|
if (config.sources.video !== "off") {
|
|
29
32
|
const edited = edit?.([...recipes]) ?? { recipes: [], values: [], dependsOn: [] };
|
|
@@ -73,7 +76,9 @@ edit) {
|
|
|
73
76
|
}
|
|
74
77
|
return recipes;
|
|
75
78
|
}
|
|
76
|
-
export function thumbnailRecipes(context, textRecipes
|
|
79
|
+
export function thumbnailRecipes(context, textRecipes,
|
|
80
|
+
// The establishing image, when it is on and the thumbnail is drawn from it too.
|
|
81
|
+
reference) {
|
|
77
82
|
const { config, content } = context;
|
|
78
83
|
if (config.sources.thumbnail === "off")
|
|
79
84
|
return [];
|
|
@@ -101,16 +106,23 @@ export function thumbnailRecipes(context, textRecipes) {
|
|
|
101
106
|
config.images?.provider ?? null,
|
|
102
107
|
config.images?.model ?? null,
|
|
103
108
|
config.format,
|
|
109
|
+
// Only what is in use, so a thumbnail made before these existed keeps its
|
|
110
|
+
// fingerprint.
|
|
111
|
+
...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
|
|
112
|
+
...(reference === undefined ? [] : [reference.input.fingerprint]),
|
|
104
113
|
],
|
|
105
114
|
}
|
|
106
115
|
: {
|
|
107
116
|
kind: "image",
|
|
108
117
|
version: 1,
|
|
109
|
-
|
|
110
|
-
model: config.images?.model ?? "",
|
|
118
|
+
...imageChoice(config),
|
|
111
119
|
aspect: config.format,
|
|
112
120
|
prompt,
|
|
113
|
-
|
|
121
|
+
...(reference === undefined ? {} : { reference: reference.input }),
|
|
122
|
+
}, [
|
|
123
|
+
...(promptRecipe === undefined ? [] : [promptRecipe.key]),
|
|
124
|
+
...(reference === undefined ? [] : [reference.key]),
|
|
125
|
+
]),
|
|
114
126
|
];
|
|
115
127
|
}
|
|
116
128
|
export function visualAssets(context, recipes) {
|
|
@@ -12,7 +12,14 @@ export async function revisionAction(deps, projectId, action) {
|
|
|
12
12
|
const config = view.revision.config;
|
|
13
13
|
const content = view.revision.content;
|
|
14
14
|
const image = action.kind === "delete-image" || action.kind === "regenerate-image"
|
|
15
|
-
? view.outputs.find((row) => row.selected &&
|
|
15
|
+
? view.outputs.find((row) => row.selected &&
|
|
16
|
+
row.output.id === action.outputId &&
|
|
17
|
+
(row.output.role === "image" ||
|
|
18
|
+
// A generated establishing image is made again like any image; it is never deleted
|
|
19
|
+
// on its own (Edit project sets it Off), and an uploaded one has nothing to redo.
|
|
20
|
+
(action.kind === "regenerate-image" &&
|
|
21
|
+
row.output.role === "reference" &&
|
|
22
|
+
config.reference?.source === "prompt")))
|
|
16
23
|
: undefined;
|
|
17
24
|
if ((action.kind === "delete-image" || action.kind === "regenerate-image") && image === undefined)
|
|
18
25
|
return { ok: false, reason: "unknown-image" };
|
|
@@ -64,7 +71,7 @@ function keyStage(key) {
|
|
|
64
71
|
const prefix = key.split(":")[0];
|
|
65
72
|
if (prefix === "entry" || prefix === "article")
|
|
66
73
|
return "article";
|
|
67
|
-
if (prefix === "image")
|
|
74
|
+
if (prefix === "image" || prefix === "reference")
|
|
68
75
|
return "images";
|
|
69
76
|
if (prefix === "audio")
|
|
70
77
|
return "audio";
|