@gentbajko/slopify 3.0.8 → 3.0.10
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/main.js +5 -18
- package/dist/patch-notes/3.0.10.md +24 -0
- package/dist/patch-notes/3.0.9.md +27 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/admission/rules.js +71 -2
- package/dist/slices/admission/schema.js +2 -0
- package/dist/slices/admission/substitute.js +5 -2
- package/dist/slices/estimate/index.js +3 -0
- package/dist/slices/images/scenes.js +103 -0
- package/dist/slices/play-drafts/convert.js +5 -1
- package/dist/slices/play-drafts/schema.js +2 -0
- package/dist/slices/project-templates/from-project.js +1 -0
- package/dist/slices/rebuild/preview-details.js +3 -1
- package/dist/slices/rebuild/recipe-build.js +5 -1
- package/dist/slices/rebuild/recipe-model.js +3 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +3 -1
- package/dist/slices/rebuild/recipe-save.js +3 -2
- package/dist/slices/rebuild/recipe-scenes.js +61 -0
- package/dist/slices/rebuild/recipe-text.js +4 -3
- package/dist/slices/rebuild/recipe-validation.js +3 -2
- package/dist/slices/rebuild/recipe-visual.js +46 -2
- package/dist/slices/rebuild/recipe-work.js +18 -0
- package/dist/slices/rebuild/runtime-document.js +1 -1
- package/dist/slices/rebuild/runtime-piece-label.js +2 -0
- package/dist/slices/rebuild/runtime-plan.js +1 -1
- package/dist/slices/rebuild/runtime-provider.js +16 -0
- package/dist/slices/revisions/mutation-prepare.js +1 -1
- package/dist/slices/revisions/view.js +3 -1
- package/dist/tutorials/Play-Images.md +16 -0
- package/dist/tutorials/Play-Title-and-Article.md +1 -0
- package/dist/updater/service.js +13 -2
- package/dist/updater/work-in-progress.js +59 -0
- package/dist/web/assets/index-CVbfz76X.js +310 -0
- package/dist/web/assets/index-Cvuxycwt.css +1 -0
- package/dist/web/assets/{pdf-BhFuoBjr.js → pdf-BlAr9_2d.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-C-Ib18Ec.css +0 -1
- package/dist/web/assets/index-Cdb0Jh3P.js +0 -310
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { thumbnailCountOf, thumbnailKey } from "../admission/model.js";
|
|
2
2
|
import { usesShortMode } from "../admission/short-mode.js";
|
|
3
3
|
import { render } from "../admission/substitute.js";
|
|
4
|
+
import { withoutScene, withScene } from "../images/scenes.js";
|
|
4
5
|
import { usesAmbientBed } from "../video/ambient-bed.js";
|
|
5
6
|
import { castFor } from "./recipe-cast.js";
|
|
6
7
|
import { noMaster } from "./recipe-loudness.js";
|
|
@@ -15,7 +16,9 @@ reference,
|
|
|
15
16
|
// The word timing's identity, which a short's captions are drawn from.
|
|
16
17
|
timing = null,
|
|
17
18
|
// Level the volume: the levelled narration and the master (`recipe-loudness.ts`).
|
|
18
|
-
master = noMaster
|
|
19
|
+
master = noMaster,
|
|
20
|
+
// Each image's own scene, for the images whose prompt asks for one (`recipe-scenes.ts`).
|
|
21
|
+
scenes) {
|
|
19
22
|
const recipes = [];
|
|
20
23
|
const imageKeys = config.sources.images === "off" ? [] : content.imageOrder;
|
|
21
24
|
for (const key of imageKeys) {
|
|
@@ -25,7 +28,17 @@ master = noMaster) {
|
|
|
25
28
|
const raw = image.templateKey === undefined || image.templateKey === null
|
|
26
29
|
? undefined
|
|
27
30
|
: content.promptTemplates[image.templateKey];
|
|
28
|
-
const
|
|
31
|
+
const at = scenes?.keys.indexOf(key) ?? -1;
|
|
32
|
+
if (scenes !== undefined && at !== -1 && image.source === "generate") {
|
|
33
|
+
recipes.push(sceneImage(config, content, key, raw ?? image.prompt ?? "", scenes, at, reference));
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
// With Scenes from the article off, a prompt's `{{Scene}}` line is left out.
|
|
37
|
+
const prompt = raw === undefined || raw === null
|
|
38
|
+
? image.prompt === null || image.prompt === undefined
|
|
39
|
+
? image.prompt
|
|
40
|
+
: withoutScene(image.prompt)
|
|
41
|
+
: render(withoutScene(raw), config.values);
|
|
29
42
|
recipes.push(recipe({ content }, `image:${key}`, "images", image.source === "provide"
|
|
30
43
|
? { kind: "provided", version: 1, assetId: image.assetId, semantic: null }
|
|
31
44
|
: {
|
|
@@ -109,6 +122,37 @@ master = noMaster) {
|
|
|
109
122
|
}
|
|
110
123
|
return recipes;
|
|
111
124
|
}
|
|
125
|
+
// An image drawn from its own scene. Until the scenes are written it waits as a deferred
|
|
126
|
+
// request that names the scenes step and the image's place among them, so writing them turns
|
|
127
|
+
// it into its image request (materialization), and writing them again draws it again.
|
|
128
|
+
function sceneImage(config, content, key, body, scenes, at, reference) {
|
|
129
|
+
const scene = scenes.scenes?.[at];
|
|
130
|
+
const prompt = scene === undefined ? undefined : withScene(render(body, config.values), scene);
|
|
131
|
+
return recipe({ content }, `image:${key}`, "images", prompt === undefined
|
|
132
|
+
? {
|
|
133
|
+
kind: "deferred",
|
|
134
|
+
version: 1,
|
|
135
|
+
operation: "image-scene",
|
|
136
|
+
template: [
|
|
137
|
+
scenes.recipe.fingerprint,
|
|
138
|
+
at,
|
|
139
|
+
config.images?.provider ?? null,
|
|
140
|
+
config.images?.model ?? null,
|
|
141
|
+
config.format,
|
|
142
|
+
...(config.images?.thinking === undefined ? [] : [config.images.thinking]),
|
|
143
|
+
...(reference === undefined ? [] : [reference.input.fingerprint]),
|
|
144
|
+
],
|
|
145
|
+
}
|
|
146
|
+
: {
|
|
147
|
+
kind: "image",
|
|
148
|
+
version: 1,
|
|
149
|
+
...imageChoice(config),
|
|
150
|
+
aspect: config.format,
|
|
151
|
+
prompt,
|
|
152
|
+
...(reference === undefined ? {} : { reference: reference.input }),
|
|
153
|
+
...castField(castFor(config, prompt)),
|
|
154
|
+
}, [scenes.recipe.key, ...(reference === undefined ? [] : [reference.key])]);
|
|
155
|
+
}
|
|
112
156
|
// What the ambient bed adds to the render's fingerprint: its settings and, for the user's own
|
|
113
157
|
// file, the project asset it plays. Undefined without a bed.
|
|
114
158
|
export function ambientBedValues(config, content) {
|
|
@@ -278,6 +278,24 @@ export function priceRecipe(work, value, config) {
|
|
|
278
278
|
const onPlan = config === undefined ? undefined : cliRequest(work, value, config);
|
|
279
279
|
if (onPlan !== undefined)
|
|
280
280
|
return onPlan;
|
|
281
|
+
// Scenes from the article: an image waiting for its scene is one image, and the scenes are
|
|
282
|
+
// one call that reads the article (its length is known once the article is written).
|
|
283
|
+
if (input.kind === "deferred" &&
|
|
284
|
+
(input.operation === "image-scene" || input.operation === "image-scenes")) {
|
|
285
|
+
const choice = config === undefined ? undefined : recipeProviderChoice(value, config);
|
|
286
|
+
if (choice !== undefined && input.operation === "image-scene")
|
|
287
|
+
return { kind: "image", stage: work.key, provider: choice.provider, model: choice.model };
|
|
288
|
+
if (choice !== undefined)
|
|
289
|
+
return {
|
|
290
|
+
kind: "llm",
|
|
291
|
+
stage: work.key,
|
|
292
|
+
provider: choice.provider,
|
|
293
|
+
model: choice.model,
|
|
294
|
+
inputCharacters: 90000,
|
|
295
|
+
outputCharacters: 4000,
|
|
296
|
+
detail: "The article is read when the step runs; its length is estimated. Retries are excluded.",
|
|
297
|
+
};
|
|
298
|
+
}
|
|
281
299
|
if (input.kind === "deferred" && input.operation === "narration-preparation")
|
|
282
300
|
return {
|
|
283
301
|
kind: "unknown",
|
|
@@ -24,7 +24,7 @@ export async function executeDocumentRecipe(deps, context, piece) {
|
|
|
24
24
|
: readFileSync(outputPath(deps.paths, projectId, row.output.path), "utf8");
|
|
25
25
|
};
|
|
26
26
|
const config = view.revision.config;
|
|
27
|
-
const article = view.revision.content.articleEdited === true || config.sources.article
|
|
27
|
+
const article = view.revision.content.articleEdited === true || config.sources.article !== "generate"
|
|
28
28
|
? view.articleMarkdown
|
|
29
29
|
: text("article_md", "article:body");
|
|
30
30
|
if (article === null)
|
|
@@ -45,6 +45,8 @@ export function pieceLabel(deps, context, piece) {
|
|
|
45
45
|
return "Shorts";
|
|
46
46
|
if (key === "animate:future")
|
|
47
47
|
return "Animated images";
|
|
48
|
+
if (key === "images:scenes")
|
|
49
|
+
return "Image scenes";
|
|
48
50
|
const short = /^shorts:(\d+):(prompts|image:(\d+)|render)$/.exec(key);
|
|
49
51
|
if (short !== null)
|
|
50
52
|
return short[2] === "prompts"
|
|
@@ -35,7 +35,7 @@ export function executionPlan(deps, view, catalogue) {
|
|
|
35
35
|
const payloads = view.pieces.filter((piece) => piece.selected && piece.piece.state === "done" && piece.piece.payload !== null);
|
|
36
36
|
const resolved = {
|
|
37
37
|
articleMarkdown: view.revision.content.articleEdited === true ||
|
|
38
|
-
view.revision.config.sources.article
|
|
38
|
+
view.revision.config.sources.article !== "generate"
|
|
39
39
|
? view.articleMarkdown
|
|
40
40
|
: textOutput("article_md"),
|
|
41
41
|
researchNotes: textOutput("notes"),
|
|
@@ -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 { checkScenes, sceneCountOf } from "../images/scenes.js";
|
|
5
6
|
import { spokenPassage } from "../narration/describe.js";
|
|
6
7
|
import { observeNarration } from "../narration/live.js";
|
|
7
8
|
import { validatePreparation } from "../narration/preparation.js";
|
|
@@ -14,6 +15,7 @@ import { outputPath } from "../storage/layout.js";
|
|
|
14
15
|
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
15
16
|
import { parseAttribution } from "../voices/attribution.js";
|
|
16
17
|
import { parseScript } from "../voices/script.js";
|
|
18
|
+
import { imageScenesKey } from "./recipe-scenes.js";
|
|
17
19
|
import { executeArticleRequests } from "./runtime-article.js";
|
|
18
20
|
import { imageCall } from "./runtime-image.js";
|
|
19
21
|
import { frozenInstructions } from "./runtime-instructions.js";
|
|
@@ -193,6 +195,11 @@ function checkAnswer(piece, answer) {
|
|
|
193
195
|
return spokenPassage(answer.text) === ""
|
|
194
196
|
? "The AI model's description had no words a narrator could say. Use Try again; if it keeps happening, choose another model in the Providers section of Edit project, or turn off Describe tables and figures there."
|
|
195
197
|
: undefined;
|
|
198
|
+
// Scenes from the article: one scene per image, as many as the request lists.
|
|
199
|
+
if (piece.key === imageScenesKey && piece.input.kind === "llm") {
|
|
200
|
+
const checked = checkScenes(answer.text, sceneCountOf(piece.input.messages) ?? 0);
|
|
201
|
+
return checked.ok ? undefined : checked.reason;
|
|
202
|
+
}
|
|
196
203
|
if (piece.key === "research:planner")
|
|
197
204
|
return chaptersFrom(answer.text).length === 0
|
|
198
205
|
? "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."
|
|
@@ -210,6 +217,15 @@ function attributionSource(messages) {
|
|
|
210
217
|
return messages.at(-1)?.content ?? "";
|
|
211
218
|
}
|
|
212
219
|
async function publishText(deps, context, piece, answer) {
|
|
220
|
+
if (piece.key === imageScenesKey && piece.input.kind === "llm") {
|
|
221
|
+
const checked = checkScenes(answer.text, sceneCountOf(piece.input.messages) ?? 0);
|
|
222
|
+
if (!checked.ok)
|
|
223
|
+
throw new Error(checked.reason);
|
|
224
|
+
await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
|
|
225
|
+
["instructions", "instructions.md", frozenInstructions(deps, context, piece)],
|
|
226
|
+
]), { scenes: checked.scenes, text: answer.text });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
213
229
|
if (piece.input.kind === "llm" && piece.input.preparation !== undefined) {
|
|
214
230
|
const checked = validatePreparation(answer.text, piece.input.preparation.source);
|
|
215
231
|
if (!checked.ok)
|
|
@@ -121,7 +121,7 @@ export async function prepareEditAssets(deps, base, edit) {
|
|
|
121
121
|
};
|
|
122
122
|
prepared.push(item);
|
|
123
123
|
}
|
|
124
|
-
const article = edit.config.sources.article
|
|
124
|
+
const article = edit.config.sources.article !== "generate" || content.articleEdited === true
|
|
125
125
|
? (content.articleMarkdown ?? edit.config.provided.article)
|
|
126
126
|
: undefined;
|
|
127
127
|
if (article !== undefined &&
|
|
@@ -40,5 +40,7 @@ export function resolvedArticleMarkdown(deps, revision, outputs) {
|
|
|
40
40
|
return readFileSync(outputPath(deps.paths, revision.projectId, article.output.path), "utf8");
|
|
41
41
|
return revision.config.sources.article === "provide"
|
|
42
42
|
? (revision.config.provided.article ?? null)
|
|
43
|
-
:
|
|
43
|
+
: revision.config.sources.article === "off"
|
|
44
|
+
? ""
|
|
45
|
+
: null;
|
|
44
46
|
}
|
|
@@ -105,6 +105,22 @@ How the count is worked out:
|
|
|
105
105
|
|
|
106
106
|
Turning it on also switches **Motion** (in the **Video and style** row) from **Zoom in and out** to **Mix of both**, so a long video pans and zooms by turns. A **Pan across** or **Still** you picked stays, and you can pick Zoom again.
|
|
107
107
|
|
|
108
|
+
## Scenes from the article
|
|
109
|
+
|
|
110
|
+
Without it, every image of a prompt is drawn from the same text, so three images of one prompt tend to show the same picture three times. **Scenes from the article** gives every image its own moment.
|
|
111
|
+
|
|
112
|
+
| Option | What it does | Default |
|
|
113
|
+
| --- | --- | --- |
|
|
114
|
+
| **Scenes from the article** | Before the images are drawn, the project's AI model reads the article once and writes a short scene for each image. Scene 1 comes from the article's opening and the last from its ending, in the order the images are shown. Each image is drawn from its prompt with its scene added. One more AI call per run. | Off |
|
|
115
|
+
|
|
116
|
+
Where the scene goes:
|
|
117
|
+
|
|
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
|
+
- A prompt without it gets the scene as a line of its own after its first paragraph, so any prompt works unchanged.
|
|
120
|
+
- With the switch off, a line holding `{{Scene}}` is left out and the prompt is drawn as it always was.
|
|
121
|
+
|
|
122
|
+
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
|
+
|
|
108
124
|
## How long each image stays on screen
|
|
109
125
|
|
|
110
126
|
**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).
|
|
@@ -25,6 +25,7 @@ The Article row starts with a **Source** switch:
|
|
|
25
25
|
| --- | --- | --- |
|
|
26
26
|
| **Generate** (default) | The text model writes the article from the article prompt, in one call. | One text-model call |
|
|
27
27
|
| **Provide** | Slopify narrates the text you paste, word for word. No text model writes one. | No text-model cost for the article |
|
|
28
|
+
| **Off** | No article at all, for a project of images or a thumbnail made from prompts. Choosing it switches off what reads the article (research, narration, captions, the YouTube description, shorts, the PDF, Scenes from the article) and takes those sections out of Play; the thumbnail offers From prompt and Provide only. | Nothing |
|
|
28
29
|
|
|
29
30
|
The article is what the narration reads and what the PDF lays out.
|
|
30
31
|
|
package/dist/updater/service.js
CHANGED
|
@@ -2,6 +2,7 @@ import { timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { setImmediate } from "node:timers";
|
|
3
3
|
import { isStableVersion, isUpdateToken, newerVersion } from "./model.js";
|
|
4
4
|
const waitCheckMs = 5_000;
|
|
5
|
+
const idleLooksToInstall = 2;
|
|
5
6
|
export function createUpdater(deps) {
|
|
6
7
|
let latestVersion = null;
|
|
7
8
|
let checkedAt = -Infinity;
|
|
@@ -66,13 +67,23 @@ export function createUpdater(deps) {
|
|
|
66
67
|
deps.report(error);
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
|
-
// Looks again every few seconds and installs
|
|
70
|
+
// Looks again every few seconds and installs once no work is running. Idle has to hold for
|
|
71
|
+
// two looks in a row, so the moment between one step finishing and the next starting is
|
|
72
|
+
// never mistaken for the end of the work.
|
|
70
73
|
function wait(version) {
|
|
71
74
|
pending = version;
|
|
72
75
|
status = "waiting";
|
|
73
76
|
error = undefined;
|
|
77
|
+
let idleLooks = 0;
|
|
74
78
|
stopWatching = every(() => {
|
|
75
|
-
if (status !== "waiting"
|
|
79
|
+
if (status !== "waiting")
|
|
80
|
+
return;
|
|
81
|
+
if (busyNow()) {
|
|
82
|
+
idleLooks = 0;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
idleLooks += 1;
|
|
86
|
+
if (idleLooks < idleLooksToInstall)
|
|
76
87
|
return;
|
|
77
88
|
stopWatching?.();
|
|
78
89
|
stopWatching = undefined;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { derive } from "../kernel/runner/graph.js";
|
|
2
|
+
import { projectPaused, stagesOf } from "../slices/admission/repo.js";
|
|
3
|
+
// A scheduled run this close is treated as running: a restart then could land on its time,
|
|
4
|
+
// and a schedule that skips missed runs would skip it.
|
|
5
|
+
export const scheduledSoonMs = 10 * 60_000;
|
|
6
|
+
// What an update must wait for, named for "installs when … finishes"; undefined when nothing
|
|
7
|
+
// is. An update never installs while a job is going: a step running or about to run, a step
|
|
8
|
+
// waiting to try again, a batch with videos still to make, a narration chunk about to be
|
|
9
|
+
// recorded again, a schedule writing topics or due to start a run.
|
|
10
|
+
export function workInProgress(db, inflight, now) {
|
|
11
|
+
const projects = db
|
|
12
|
+
.prepare("SELECT id, title FROM projects WHERE id NOT IN (SELECT project_id FROM project_trash)")
|
|
13
|
+
.all()
|
|
14
|
+
.flatMap((row) => typeof row.id === "string" && typeof row.title === "string"
|
|
15
|
+
? [{ id: row.id, title: row.title }]
|
|
16
|
+
: []);
|
|
17
|
+
for (const project of projects) {
|
|
18
|
+
if (inflight(project.id))
|
|
19
|
+
return project.title;
|
|
20
|
+
if (projectPaused(db, project.id))
|
|
21
|
+
continue;
|
|
22
|
+
// Running, or waiting to try a step again (`derive`); or between two steps, with work
|
|
23
|
+
// admitted and free to start. Work held for a person (a review checkpoint) doesn't count.
|
|
24
|
+
const state = derive(stagesOf(db, project.id));
|
|
25
|
+
if (state === "running")
|
|
26
|
+
return project.title;
|
|
27
|
+
if (state === "pending" &&
|
|
28
|
+
db
|
|
29
|
+
.prepare("SELECT 1 FROM revision_work WHERE project_id=? AND state IN ('pending','running') AND dispatch_state='allowed' LIMIT 1")
|
|
30
|
+
.get(project.id) !== undefined)
|
|
31
|
+
return project.title;
|
|
32
|
+
}
|
|
33
|
+
const queued = db
|
|
34
|
+
.prepare(`SELECT p.title FROM project_queue q JOIN projects p ON p.id=q.project_id
|
|
35
|
+
WHERE q.state IN ('queued','active')
|
|
36
|
+
AND q.project_id NOT IN (SELECT project_id FROM project_trash)
|
|
37
|
+
AND q.project_id NOT IN (SELECT project_id FROM project_controls WHERE paused=1)
|
|
38
|
+
LIMIT 1`)
|
|
39
|
+
.get();
|
|
40
|
+
if (typeof queued?.title === "string")
|
|
41
|
+
return queued.title;
|
|
42
|
+
const retry = db
|
|
43
|
+
.prepare(`SELECT p.title FROM narration_retries r JOIN projects p ON p.id=r.project_id
|
|
44
|
+
WHERE r.state='pending' LIMIT 1`)
|
|
45
|
+
.get();
|
|
46
|
+
if (typeof retry?.title === "string")
|
|
47
|
+
return retry.title;
|
|
48
|
+
const schedule = db
|
|
49
|
+
.prepare(`SELECT name FROM schedules WHERE deleted_at IS NULL AND (
|
|
50
|
+
(status='active' AND next_run_at IS NOT NULL AND next_run_at <= ?)
|
|
51
|
+
OR (topics_generating_at IS NOT NULL
|
|
52
|
+
AND (topics_generated_at IS NULL OR topics_generated_at < topics_generating_at)
|
|
53
|
+
AND (topics_failed_at IS NULL OR topics_failed_at < topics_generating_at))
|
|
54
|
+
) LIMIT 1`)
|
|
55
|
+
.get(new Date(now + scheduledSoonMs).toISOString());
|
|
56
|
+
if (typeof schedule?.name === "string")
|
|
57
|
+
return schedule.name;
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|