@gentbajko/slopify 3.0.10 → 3.0.12
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/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/patch-notes/3.0.11.md +22 -0
- package/dist/patch-notes/3.0.12.md +25 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/admission/substitute.js +7 -2
- package/dist/slices/images/appearance.js +107 -0
- package/dist/slices/images/scenes.js +50 -1
- package/dist/slices/rebuild/recipe-appearance.js +82 -0
- package/dist/slices/rebuild/recipe-build.js +9 -5
- package/dist/slices/rebuild/recipe-model.js +2 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +2 -1
- package/dist/slices/rebuild/recipe-reference.js +22 -2
- package/dist/slices/rebuild/recipe-scenes.js +25 -4
- package/dist/slices/rebuild/recipe-validation.js +2 -2
- package/dist/slices/rebuild/recipe-visual.js +96 -38
- package/dist/slices/rebuild/recipe-work.js +4 -2
- package/dist/slices/rebuild/runtime-provider.js +16 -0
- package/dist/slices/revisions/publication-rules.js +11 -3
- package/dist/tutorials/Play-Images.md +10 -0
- package/dist/tutorials/Play-Outputs.md +1 -1
- package/dist/web/assets/index-BPstEgjc.js +309 -0
- package/dist/web/assets/index-BbbbM56z.css +1 -0
- package/dist/web/assets/{pdf-BlAr9_2d.js → pdf-DVDjUG4j.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/web/assets/index-CVbfz76X.js +0 -310
- package/dist/web/assets/index-Cvuxycwt.css +0 -1
|
@@ -3,6 +3,7 @@ import { usesShortMode } from "../admission/short-mode.js";
|
|
|
3
3
|
import { render } from "../admission/substitute.js";
|
|
4
4
|
import { withoutScene, withScene } from "../images/scenes.js";
|
|
5
5
|
import { usesAmbientBed } from "../video/ambient-bed.js";
|
|
6
|
+
import { lookWait, withLooks } from "./recipe-appearance.js";
|
|
6
7
|
import { castFor } from "./recipe-cast.js";
|
|
7
8
|
import { noMaster } from "./recipe-loudness.js";
|
|
8
9
|
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
@@ -18,7 +19,9 @@ timing = null,
|
|
|
18
19
|
// Level the volume: the levelled narration and the master (`recipe-loudness.ts`).
|
|
19
20
|
master = noMaster,
|
|
20
21
|
// Each image's own scene, for the images whose prompt asks for one (`recipe-scenes.ts`).
|
|
21
|
-
scenes
|
|
22
|
+
scenes,
|
|
23
|
+
// How the subject and characters look, for prompts with `{{Appearance}}`.
|
|
24
|
+
appearance) {
|
|
22
25
|
const recipes = [];
|
|
23
26
|
const imageKeys = config.sources.images === "off" ? [] : content.imageOrder;
|
|
24
27
|
for (const key of imageKeys) {
|
|
@@ -30,15 +33,37 @@ scenes) {
|
|
|
30
33
|
: content.promptTemplates[image.templateKey];
|
|
31
34
|
const at = scenes?.keys.indexOf(key) ?? -1;
|
|
32
35
|
if (scenes !== undefined && at !== -1 && image.source === "generate") {
|
|
33
|
-
recipes.push(sceneImage(config, content, key, raw ?? image.prompt ?? "", scenes, at, reference));
|
|
36
|
+
recipes.push(sceneImage(config, content, key, raw ?? image.prompt ?? "", scenes, at, reference, appearance));
|
|
34
37
|
continue;
|
|
35
38
|
}
|
|
36
39
|
// With Scenes from the article off, a prompt's `{{Scene}}` line is left out.
|
|
37
|
-
const
|
|
40
|
+
const written = raw === undefined || raw === null
|
|
38
41
|
? image.prompt === null || image.prompt === undefined
|
|
39
42
|
? image.prompt
|
|
40
43
|
: withoutScene(image.prompt)
|
|
41
44
|
: render(withoutScene(raw), config.values);
|
|
45
|
+
const prompt = written === null || written === undefined || image.source !== "generate"
|
|
46
|
+
? written
|
|
47
|
+
: withLooks(written, appearance);
|
|
48
|
+
// Waiting for the looks: the same wait an image with a scene has, on the lookup instead.
|
|
49
|
+
if (prompt === null && written !== null && written !== undefined) {
|
|
50
|
+
const wait = lookWait(written, appearance);
|
|
51
|
+
recipes.push(recipe({ content }, `image:${key}`, "images", {
|
|
52
|
+
kind: "deferred",
|
|
53
|
+
version: 1,
|
|
54
|
+
operation: "image-scene",
|
|
55
|
+
template: [
|
|
56
|
+
...wait.template,
|
|
57
|
+
written,
|
|
58
|
+
config.images?.provider ?? null,
|
|
59
|
+
config.images?.model ?? null,
|
|
60
|
+
config.format,
|
|
61
|
+
...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
|
|
62
|
+
...(reference === undefined ? [] : [reference.input.fingerprint]),
|
|
63
|
+
],
|
|
64
|
+
}, [...wait.dependsOn, ...(reference === undefined ? [] : [reference.key])]));
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
42
67
|
recipes.push(recipe({ content }, `image:${key}`, "images", image.source === "provide"
|
|
43
68
|
? { kind: "provided", version: 1, assetId: image.assetId, semantic: null }
|
|
44
69
|
: {
|
|
@@ -125,9 +150,12 @@ scenes) {
|
|
|
125
150
|
// An image drawn from its own scene. Until the scenes are written it waits as a deferred
|
|
126
151
|
// request that names the scenes step and the image's place among them, so writing them turns
|
|
127
152
|
// it into its image request (materialization), and writing them again draws it again.
|
|
128
|
-
function sceneImage(config, content, key, body, scenes, at, reference) {
|
|
153
|
+
function sceneImage(config, content, key, body, scenes, at, reference, appearance) {
|
|
129
154
|
const scene = scenes.scenes?.[at];
|
|
130
|
-
const prompt = scene === undefined
|
|
155
|
+
const prompt = scene === undefined
|
|
156
|
+
? undefined
|
|
157
|
+
: (withLooks(withScene(render(body, config.values), scene), appearance, scene) ?? undefined);
|
|
158
|
+
const wait = lookWait(body, appearance);
|
|
131
159
|
return recipe({ content }, `image:${key}`, "images", prompt === undefined
|
|
132
160
|
? {
|
|
133
161
|
kind: "deferred",
|
|
@@ -141,6 +169,7 @@ function sceneImage(config, content, key, body, scenes, at, reference) {
|
|
|
141
169
|
config.format,
|
|
142
170
|
...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
|
|
143
171
|
...(reference === undefined ? [] : [reference.input.fingerprint]),
|
|
172
|
+
...wait.template,
|
|
144
173
|
],
|
|
145
174
|
}
|
|
146
175
|
: {
|
|
@@ -151,7 +180,7 @@ function sceneImage(config, content, key, body, scenes, at, reference) {
|
|
|
151
180
|
prompt,
|
|
152
181
|
...(reference === undefined ? {} : { reference: reference.input }),
|
|
153
182
|
...castField(castFor(config, prompt)),
|
|
154
|
-
}, [scenes.recipe.key, ...(reference === undefined ? [] : [reference.key])]);
|
|
183
|
+
}, [scenes.recipe.key, ...wait.dependsOn, ...(reference === undefined ? [] : [reference.key])]);
|
|
155
184
|
}
|
|
156
185
|
// What the ambient bed adds to the render's fingerprint: its settings and, for the user's own
|
|
157
186
|
// file, the project asset it plays. Undefined without a bed.
|
|
@@ -170,7 +199,11 @@ export function ambientBedValues(config, content) {
|
|
|
170
199
|
}
|
|
171
200
|
export function thumbnailRecipes(context, textRecipes,
|
|
172
201
|
// The establishing image, when it is on and the thumbnail is drawn from it too.
|
|
173
|
-
reference
|
|
202
|
+
reference,
|
|
203
|
+
// Scenes from the article: a thumbnail prompt with `{{Scene}}` takes one per thumbnail.
|
|
204
|
+
scenes,
|
|
205
|
+
// How the subject and characters look, for a prompt with `{{Appearance}}`.
|
|
206
|
+
appearance) {
|
|
174
207
|
const { config, content } = context;
|
|
175
208
|
if (config.sources.thumbnail === "off")
|
|
176
209
|
return [];
|
|
@@ -184,40 +217,65 @@ reference) {
|
|
|
184
217
|
}, [], { unresolved: content.provided.thumbnail === undefined }),
|
|
185
218
|
];
|
|
186
219
|
const promptRecipe = textRecipes.find((value) => value.key === "thumbnail:prompt");
|
|
187
|
-
const
|
|
220
|
+
const written = config.sources.thumbnail === "prompt_by_llm" && promptRecipe !== undefined
|
|
188
221
|
? matchingText(context, promptRecipe, "prompt")
|
|
189
222
|
: renderedPrompt(context, "thumbnailPrompt");
|
|
223
|
+
const sceneFor = (variant) => scenes === undefined || scenes.thumbnailCount === 0
|
|
224
|
+
? undefined
|
|
225
|
+
: scenes.thumbnails?.[variant - 1];
|
|
226
|
+
const withScenes = scenes !== undefined && scenes.thumbnailCount > 0;
|
|
190
227
|
const variants = Array.from({ length: thumbnailCountOf(config) }, (_, index) => index + 1);
|
|
191
|
-
return variants.map((variant) =>
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
return variants.map((variant) => {
|
|
229
|
+
const scene = sceneFor(variant);
|
|
230
|
+
// With a scene the thumbnail waits for it; with the switch off a `{{Scene}}` line is left
|
|
231
|
+
// out, as the images do.
|
|
232
|
+
const sceneFilled = written === null
|
|
233
|
+
? null
|
|
234
|
+
: withScenes
|
|
235
|
+
? scene === undefined
|
|
236
|
+
? null
|
|
237
|
+
: withScene(written, scene)
|
|
238
|
+
: withoutScene(written);
|
|
239
|
+
// A thumbnail stands for the subject, so it always has the subject's look.
|
|
240
|
+
const prompt = sceneFilled === null
|
|
241
|
+
? null
|
|
242
|
+
: withLooks(sceneFilled, appearance, scene, { subjectAlways: true });
|
|
243
|
+
const wait = lookWait(written, appearance);
|
|
244
|
+
return recipe(context, thumbnailKey(variant), "thumbnail", prompt === null
|
|
245
|
+
? {
|
|
246
|
+
kind: "deferred",
|
|
247
|
+
version: 1,
|
|
248
|
+
operation: "thumbnail-image",
|
|
249
|
+
template: [
|
|
250
|
+
promptRecipe?.fingerprint ?? null,
|
|
251
|
+
config.images?.provider ?? null,
|
|
252
|
+
config.images?.model ?? null,
|
|
253
|
+
config.format,
|
|
254
|
+
// Only what is in use, so a thumbnail made before these existed keeps its
|
|
255
|
+
// fingerprint.
|
|
256
|
+
...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
|
|
257
|
+
...(reference === undefined ? [] : [reference.input.fingerprint]),
|
|
258
|
+
...(variant === 1 ? [] : [["thumbnail-variant", variant]]),
|
|
259
|
+
...(withScenes ? [["thumbnail-scene", scenes.recipe.fingerprint]] : []),
|
|
260
|
+
...wait.template,
|
|
261
|
+
],
|
|
262
|
+
}
|
|
263
|
+
: {
|
|
264
|
+
kind: "image",
|
|
265
|
+
version: 1,
|
|
266
|
+
...imageChoice(config),
|
|
267
|
+
aspect: config.format,
|
|
268
|
+
prompt: thumbnailVariantPrompt(prompt, variant),
|
|
269
|
+
...(reference === undefined ? {} : { reference: reference.input }),
|
|
270
|
+
// The thumbnail stands for the whole video, like the establishing image.
|
|
271
|
+
...castField(castFor(config, config.title, prompt)),
|
|
272
|
+
}, [
|
|
273
|
+
...(promptRecipe === undefined ? [] : [promptRecipe.key]),
|
|
274
|
+
...(reference === undefined ? [] : [reference.key]),
|
|
275
|
+
...(withScenes ? [scenes.recipe.key] : []),
|
|
276
|
+
...wait.dependsOn,
|
|
277
|
+
]);
|
|
278
|
+
});
|
|
221
279
|
}
|
|
222
280
|
// The second and third thumbnails are the same prompt asked for another composition, so the
|
|
223
281
|
// three test one idea against itself rather than three ideas. The first is the prompt as it
|
|
@@ -281,7 +281,9 @@ export function priceRecipe(work, value, config) {
|
|
|
281
281
|
// Scenes from the article: an image waiting for its scene is one image, and the scenes are
|
|
282
282
|
// one call that reads the article (its length is known once the article is written).
|
|
283
283
|
if (input.kind === "deferred" &&
|
|
284
|
-
(input.operation === "image-scene" ||
|
|
284
|
+
(input.operation === "image-scene" ||
|
|
285
|
+
input.operation === "image-scenes" ||
|
|
286
|
+
input.operation === "image-appearance")) {
|
|
285
287
|
const choice = config === undefined ? undefined : recipeProviderChoice(value, config);
|
|
286
288
|
if (choice !== undefined && input.operation === "image-scene")
|
|
287
289
|
return { kind: "image", stage: work.key, provider: choice.provider, model: choice.model };
|
|
@@ -292,7 +294,7 @@ export function priceRecipe(work, value, config) {
|
|
|
292
294
|
provider: choice.provider,
|
|
293
295
|
model: choice.model,
|
|
294
296
|
inputCharacters: 90000,
|
|
295
|
-
outputCharacters: 4000,
|
|
297
|
+
outputCharacters: input.operation === "image-appearance" ? 6000 : 4000,
|
|
296
298
|
detail: "The article is read when the step runs; its length is estimated. Retries are excluded.",
|
|
297
299
|
};
|
|
298
300
|
}
|
|
@@ -2,6 +2,7 @@ import { basename } from "node:path";
|
|
|
2
2
|
import { referenceKey, thumbnailVariant } from "../admission/model.js";
|
|
3
3
|
import { plainText } from "../article/plain.js";
|
|
4
4
|
import { splitEndMatter } from "../article/split.js";
|
|
5
|
+
import { checkAppearance } from "../images/appearance.js";
|
|
5
6
|
import { checkScenes, sceneCountOf } from "../images/scenes.js";
|
|
6
7
|
import { spokenPassage } from "../narration/describe.js";
|
|
7
8
|
import { observeNarration } from "../narration/live.js";
|
|
@@ -15,6 +16,7 @@ import { outputPath } from "../storage/layout.js";
|
|
|
15
16
|
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
16
17
|
import { parseAttribution } from "../voices/attribution.js";
|
|
17
18
|
import { parseScript } from "../voices/script.js";
|
|
19
|
+
import { imageAppearanceKey } from "./recipe-appearance.js";
|
|
18
20
|
import { imageScenesKey } from "./recipe-scenes.js";
|
|
19
21
|
import { executeArticleRequests } from "./runtime-article.js";
|
|
20
22
|
import { imageCall } from "./runtime-image.js";
|
|
@@ -200,6 +202,11 @@ function checkAnswer(piece, answer) {
|
|
|
200
202
|
const checked = checkScenes(answer.text, sceneCountOf(piece.input.messages) ?? 0);
|
|
201
203
|
return checked.ok ? undefined : checked.reason;
|
|
202
204
|
}
|
|
205
|
+
// The looks it looked up: at least the video's subject.
|
|
206
|
+
if (piece.key === imageAppearanceKey && piece.input.kind === "llm") {
|
|
207
|
+
const checked = checkAppearance(answer.text);
|
|
208
|
+
return checked.ok ? undefined : checked.reason;
|
|
209
|
+
}
|
|
203
210
|
if (piece.key === "research:planner")
|
|
204
211
|
return chaptersFrom(answer.text).length === 0
|
|
205
212
|
? "The AI model's research plan listed no chapters, so research could not go on. Use Try again; if it keeps happening, choose another model in the Providers section of Edit project."
|
|
@@ -226,6 +233,15 @@ async function publishText(deps, context, piece, answer) {
|
|
|
226
233
|
]), { scenes: checked.scenes, text: answer.text });
|
|
227
234
|
return;
|
|
228
235
|
}
|
|
236
|
+
if (piece.key === imageAppearanceKey && piece.input.kind === "llm") {
|
|
237
|
+
const checked = checkAppearance(answer.text);
|
|
238
|
+
if (!checked.ok)
|
|
239
|
+
throw new Error(checked.reason);
|
|
240
|
+
await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
|
|
241
|
+
["instructions", "instructions.md", frozenInstructions(deps, context, piece)],
|
|
242
|
+
]), { appearance: checked.appearance, text: answer.text });
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
229
245
|
if (piece.input.kind === "llm" && piece.input.preparation !== undefined) {
|
|
230
246
|
const checked = validatePreparation(answer.text, piece.input.preparation.source);
|
|
231
247
|
if (!checked.ok)
|
|
@@ -3,7 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { workExists } from "../../kernel/runner/work-authority.js";
|
|
4
4
|
import { pieceFile } from "../storage/reconcile.js";
|
|
5
5
|
import { outputSchema } from "../storage/schema.js";
|
|
6
|
-
import { outputsForRevision, piecesForRevision, revisionById } from "./repo.js";
|
|
6
|
+
import { currentRevisionId, outputsForRevision, piecesForRevision, revisionById } from "./repo.js";
|
|
7
7
|
import { stagePieceSchema } from "./schema.js";
|
|
8
8
|
export function publicationAuthority(db, publication) {
|
|
9
9
|
const { work, pieceId, publicationId } = publication;
|
|
@@ -42,6 +42,14 @@ export function validatePublication(db, publication, outputs, pieces) {
|
|
|
42
42
|
.prepare("SELECT logical_fingerprint FROM revision_work_pieces WHERE id=? AND work_id=?")
|
|
43
43
|
.get(publication.pieceId, publication.work.workId);
|
|
44
44
|
const revision = revisionById(db, publication.work.projectId, publication.work.revisionId);
|
|
45
|
+
// The captions an export carries are the ones selected when it started. An edit while it ran
|
|
46
|
+
// (a thumbnail redo, say) makes a newer revision: captions made after that are selected there
|
|
47
|
+
// only, so the current revision counts as well as the work's own.
|
|
48
|
+
const head = currentRevisionId(db, publication.work.projectId);
|
|
49
|
+
const captionRevisions = [
|
|
50
|
+
publication.work.revisionId,
|
|
51
|
+
...(head === undefined || head === publication.work.revisionId ? [] : [head]),
|
|
52
|
+
];
|
|
45
53
|
for (const row of outputs) {
|
|
46
54
|
const retainedMember = ((authority?.key === "subtitles:files" &&
|
|
47
55
|
((row.workKey === "export:wav" && row.output.role === "audio_export") ||
|
|
@@ -51,12 +59,12 @@ export function validatePublication(db, publication, outputs, pieces) {
|
|
|
51
59
|
((authority?.key === "export:wav" || authority?.key === "export:video") &&
|
|
52
60
|
row.workKey === "subtitles:files" &&
|
|
53
61
|
["subtitles_srt", "subtitles_vtt", "subtitle_ass", "subtitle_font"].includes(row.output.role))) &&
|
|
54
|
-
outputsForRevision(db, publication.work.projectId,
|
|
62
|
+
captionRevisions.some((revisionId) => outputsForRevision(db, publication.work.projectId, revisionId).some((old) => old.selected &&
|
|
55
63
|
old.state === "ready" &&
|
|
56
64
|
old.workKey === row.workKey &&
|
|
57
65
|
old.output.role === row.output.role &&
|
|
58
66
|
old.assetId === row.asset.id &&
|
|
59
|
-
old.fingerprint === row.fingerprint);
|
|
67
|
+
old.fingerprint === row.fingerprint));
|
|
60
68
|
if (!retainedMember &&
|
|
61
69
|
row.fingerprint !== revision?.fingerprints[row.workKey] &&
|
|
62
70
|
!(row.workKey === authority?.key &&
|
|
@@ -118,9 +118,19 @@ Where the scene goes:
|
|
|
118
118
|
- A prompt with `{{Scene}}` gets the scene there, for example a line `Scene: {{Scene}}` under the prompt's first sentence. `{{Scene}}` is never a field to fill in: Slopify writes it.
|
|
119
119
|
- A prompt without it gets the scene as a line of its own after its first paragraph, so any prompt works unchanged.
|
|
120
120
|
- With the switch off, a line holding `{{Scene}}` is left out and the prompt is drawn as it always was.
|
|
121
|
+
- A **thumbnail** prompt with `{{Scene}}` gets a scene from the same call: the article's most striking moment, the one that would make someone click, taken from anywhere in the article. With **Thumbnails** set to 3, each thumbnail gets a different moment, so the A/B test compares ideas as well as compositions. A thumbnail prompt without `{{Scene}}` is drawn as it always was.
|
|
121
122
|
|
|
122
123
|
The style, composition and everything else in the prompt stay as written; only the scene changes from image to image. The scenes are written once the article exists, and each image waits for them. Switching it on or off in **Edit project** remakes the images.
|
|
123
124
|
|
|
125
|
+
## Looks looked up: {{Appearance}}
|
|
126
|
+
|
|
127
|
+
An image model draws a named figure from whatever it half remembers, so the same character can look different in every image. Put `{{Appearance}}` in an image, thumbnail or establishing prompt, for example a line `Looks: {{Appearance}}`, and Slopify looks the looks up instead.
|
|
128
|
+
|
|
129
|
+
- Once the article exists, one call to the project's AI model **searches the web** for how the video's subject and every named character in the article look: build, face, clothing, colours and the marks that make them recognisable. Where depictions disagree, it describes the most iconic one.
|
|
130
|
+
- With **Scenes from the article** on, each picture gets the looks of the figures its scene names (up to three besides the subject), so a scene of a place or of someone else isn't redrawn with the subject in it; a scene that names no one leaves the looks line out. A thumbnail always gets the subject's look. Without scenes, every picture gets the subject's look.
|
|
131
|
+
- There is no switch: a prompt with `{{Appearance}}` turns it on, and prompts without it are drawn as they always were. `{{Appearance}}` is never a field to fill in.
|
|
132
|
+
- It costs one more AI call per run, with web search. The pictures that use it wait for it.
|
|
133
|
+
|
|
124
134
|
## How long each image stays on screen
|
|
125
135
|
|
|
126
136
|
**Seconds per image** (1 to 600, default 15) is in the **Video and style** row, beside **Cuts**, **Zoom** and **Motion**. When cuts follow the narration, it is the target length and a cut waits for a sentence end. When images run out, they start again. See [Play Video and Style](Play-Video-and-Style).
|
|
@@ -32,7 +32,7 @@ The Thumbnail part has its own **Source** switch:
|
|
|
32
32
|
| Option | What it does | Default |
|
|
33
33
|
| --- | --- | --- |
|
|
34
34
|
| **Thumbnail prompt** | The Library thumbnail prompt, keywords filled in. **From prompt** sends it to the image model as it is; **Prompt by LLM** gives it to the text model with the title and article to write the image prompt. | None |
|
|
35
|
-
| **Thumbnails** (**1** or **3**) | **3** makes two more thumbnails from the same prompt with different compositions, for YouTube's Test & compare. Each costs one more image, so 3 thumbnails are 3 image calls. | 1 |
|
|
35
|
+
| **Thumbnails** (**1** or **3**) | **3** makes two more thumbnails from the same prompt with different compositions, for YouTube's Test & compare. Each costs one more image, so 3 thumbnails are 3 image calls. With a thumbnail prompt that has `{{Scene}}` and Images → **Scenes from the article** on, each also shows a different moment of the article (see [Play Images](Play-Images#scenes-from-the-article)). | 1 |
|
|
36
36
|
|
|
37
37
|
### Make three thumbnails for Test & compare
|
|
38
38
|
|