@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,50 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { outputPath } from "../storage/layout.js";
|
|
4
|
+
// The provider call an image step makes: its request, the effort when one is chosen and, while
|
|
5
|
+
// the project's Establishing image is on, that image's bytes as the visual reference.
|
|
6
|
+
export function imageCall(deps, projectId, input, previewLabel) {
|
|
7
|
+
const reference = referenceImage(deps, projectId, input);
|
|
8
|
+
return {
|
|
9
|
+
provider: input.provider,
|
|
10
|
+
model: input.model,
|
|
11
|
+
prompt: input.prompt,
|
|
12
|
+
aspect: input.aspect,
|
|
13
|
+
...(input.thinking === undefined ? {} : { thinking: input.thinking }),
|
|
14
|
+
...(reference === undefined ? {} : { reference }),
|
|
15
|
+
...(previewLabel === undefined ? {} : { previewLabel }),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
function referenceImage(deps, projectId, input) {
|
|
19
|
+
if (input.reference === undefined)
|
|
20
|
+
return undefined;
|
|
21
|
+
// The step waits for the establishing image, so a missing one went away since.
|
|
22
|
+
const missing = "The establishing image this image is drawn from is missing. Use Regenerate on the establishing image in the Images section (or upload it again in Edit project → Images), then use Retry stage.";
|
|
23
|
+
if (input.reference.assetId === null)
|
|
24
|
+
throw new Error(missing);
|
|
25
|
+
const row = z
|
|
26
|
+
.object({ path: z.string() })
|
|
27
|
+
.optional()
|
|
28
|
+
.parse(deps.db
|
|
29
|
+
.prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
|
|
30
|
+
.get(input.reference.assetId, projectId));
|
|
31
|
+
if (row === undefined)
|
|
32
|
+
throw new Error(missing);
|
|
33
|
+
let bytes;
|
|
34
|
+
try {
|
|
35
|
+
bytes = new Uint8Array(readFileSync(outputPath(deps.paths, projectId, row.path)));
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(missing);
|
|
39
|
+
}
|
|
40
|
+
const mime = imageMime(bytes);
|
|
41
|
+
if (mime === undefined)
|
|
42
|
+
throw new Error("The establishing image is not a PNG or JPEG file, so it can't be sent as a reference. Upload a PNG or JPEG in Edit project → Images → Establishing image, then use Retry stage.");
|
|
43
|
+
return { bytes, mime };
|
|
44
|
+
}
|
|
45
|
+
function imageMime(bytes) {
|
|
46
|
+
const png = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
|
47
|
+
if (png.every((value, index) => bytes[index] === value))
|
|
48
|
+
return "image/png";
|
|
49
|
+
return bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff ? "image/jpeg" : undefined;
|
|
50
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { referenceKey } from "../admission/model.js";
|
|
4
5
|
import { plainText } from "../article/plain.js";
|
|
5
6
|
import { splitEndMatter } from "../article/split.js";
|
|
6
7
|
import { joinNarration } from "../narration/concat.js";
|
|
@@ -36,9 +37,11 @@ export async function executeLocalRecipe(deps, context, piece) {
|
|
|
36
37
|
? "audio_body"
|
|
37
38
|
: piece.key === "thumbnail:image"
|
|
38
39
|
? "thumbnail"
|
|
39
|
-
: piece.key
|
|
40
|
-
? "
|
|
41
|
-
:
|
|
40
|
+
: piece.key === referenceKey
|
|
41
|
+
? "reference"
|
|
42
|
+
: piece.key.startsWith("image:")
|
|
43
|
+
? "image"
|
|
44
|
+
: undefined;
|
|
42
45
|
await publishResult(deps, context, piece, role === undefined ? [] : [preparedResult(deps, context, piece, role, asset)], {}, asset);
|
|
43
46
|
return "done";
|
|
44
47
|
}
|
|
@@ -6,6 +6,8 @@ export function pieceLabel(deps, context, piece) {
|
|
|
6
6
|
const { key } = piece;
|
|
7
7
|
if (key === "thumbnail:image" || key.startsWith("thumbnail:"))
|
|
8
8
|
return "Thumbnail";
|
|
9
|
+
if (key === "reference:image")
|
|
10
|
+
return "Establishing image";
|
|
9
11
|
if (key === "article:body")
|
|
10
12
|
return "Article";
|
|
11
13
|
if (key === "document:pdf")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { referenceKey } from "../admission/model.js";
|
|
1
2
|
import { plainText } from "../article/plain.js";
|
|
2
3
|
import { splitEndMatter } from "../article/split.js";
|
|
3
4
|
import { observeNarration } from "../narration/live.js";
|
|
@@ -9,6 +10,7 @@ import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
|
|
|
9
10
|
import { outputPath } from "../storage/layout.js";
|
|
10
11
|
import { probeDurationMs } from "../video/ffmpeg.js";
|
|
11
12
|
import { executeArticleRequests } from "./runtime-article.js";
|
|
13
|
+
import { imageCall } from "./runtime-image.js";
|
|
12
14
|
import { frozenInstructions } from "./runtime-instructions.js";
|
|
13
15
|
import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
|
|
14
16
|
import { preparedResult, preparedTexts, publishResult } from "./runtime-publication.js";
|
|
@@ -89,20 +91,25 @@ export async function executeProviderRecipe(deps, context, providers, piece) {
|
|
|
89
91
|
return "done";
|
|
90
92
|
}
|
|
91
93
|
if (input.kind === "image") {
|
|
92
|
-
const image = await wrapped.image({
|
|
93
|
-
provider: input.provider,
|
|
94
|
-
model: input.model,
|
|
95
|
-
prompt: input.prompt,
|
|
96
|
-
aspect: input.aspect,
|
|
97
|
-
});
|
|
98
|
-
if (!image.ok)
|
|
99
|
-
return "held";
|
|
100
|
-
const asset = writeAsset(deps, context.work.projectId, image.value.mime === "image/jpeg" ? "image.jpg" : "image.png", image.value.bytes);
|
|
101
94
|
const view = executionView(deps, context.work.projectId, context.work.revisionId);
|
|
102
95
|
const index = piece.key.startsWith("image:")
|
|
103
96
|
? (view?.revision.content.imageOrder.indexOf(piece.key.slice(6)) ?? 0) + 1
|
|
104
97
|
: undefined;
|
|
105
|
-
const
|
|
98
|
+
const image = await wrapped.image(imageCall(deps, context.work.projectId, input, piece.key === "thumbnail:image"
|
|
99
|
+
? "Thumbnail"
|
|
100
|
+
: piece.key === referenceKey
|
|
101
|
+
? "Establishing image"
|
|
102
|
+
: index === undefined
|
|
103
|
+
? "Image"
|
|
104
|
+
: `Image ${String(index)}`));
|
|
105
|
+
if (!image.ok)
|
|
106
|
+
return "held";
|
|
107
|
+
const asset = writeAsset(deps, context.work.projectId, image.value.mime === "image/jpeg" ? "image.jpg" : "image.png", image.value.bytes);
|
|
108
|
+
const output = preparedResult(deps, context, piece, piece.key === "thumbnail:image"
|
|
109
|
+
? "thumbnail"
|
|
110
|
+
: piece.key === referenceKey
|
|
111
|
+
? "reference"
|
|
112
|
+
: "image", asset, null, {
|
|
106
113
|
prompt: input.prompt,
|
|
107
114
|
provider: input.provider,
|
|
108
115
|
model: input.model,
|
|
@@ -14,6 +14,7 @@ import { probeDurationMs } from "../video/ffmpeg.js";
|
|
|
14
14
|
import { sentencesText, transcriptSentences } from "../youtube/transcript.js";
|
|
15
15
|
import { shortsPickKey } from "./recipe-shorts.js";
|
|
16
16
|
import { exportSnapshot, revisionAudio } from "./runtime-export-inputs.js";
|
|
17
|
+
import { imageCall } from "./runtime-image.js";
|
|
17
18
|
import { savedCatalogue } from "./runtime-plan.js";
|
|
18
19
|
import { preparedResult, preparedTexts, publishResult } from "./runtime-publication.js";
|
|
19
20
|
import { wordsSchema } from "./runtime-subtitles.js";
|
|
@@ -163,12 +164,9 @@ async function image(deps, context, providers, piece, clip, index) {
|
|
|
163
164
|
const input = piece.input;
|
|
164
165
|
if (input.kind !== "image")
|
|
165
166
|
throw setupError();
|
|
166
|
-
const made = await providers
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
prompt: input.prompt,
|
|
170
|
-
aspect: input.aspect,
|
|
171
|
-
});
|
|
167
|
+
const made = await providers
|
|
168
|
+
.forPiece(piece.id)
|
|
169
|
+
.image(imageCall(deps, context.work.projectId, input, `Short ${String(clip.number)} image ${String(index)}`));
|
|
172
170
|
if (!made.ok)
|
|
173
171
|
return "held";
|
|
174
172
|
const asset = writeAsset(deps, context.work.projectId, made.value.mime === "image/jpeg" ? "short-image.jpg" : "short-image.png", made.value.bytes);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { checkRuntimeModel } from "../../catalog/runtime-models.js";
|
|
2
|
+
import { referenceRefusal, takesReferenceImage } from "../../catalog/schema.js";
|
|
2
3
|
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
3
4
|
import { cliPathChanged } from "../settings/cli-paths.js";
|
|
4
5
|
import { isLocalCliProvider } from "../settings/model.js";
|
|
@@ -38,7 +39,7 @@ export async function checkReadiness(deps, snapshot, view) {
|
|
|
38
39
|
});
|
|
39
40
|
continue;
|
|
40
41
|
}
|
|
41
|
-
const result = await checkRuntimeModel(deps.modelsFor, choice.provider, choice.family, choice.model, choice.family === "
|
|
42
|
+
const result = await checkRuntimeModel(deps.modelsFor, choice.provider, choice.family, choice.model, choice.family === "tts" ? undefined : (choice.thinking ?? undefined));
|
|
42
43
|
if (result === "missing")
|
|
43
44
|
fields.push({
|
|
44
45
|
field: `${choice.family}.model`,
|
|
@@ -112,6 +113,19 @@ export function localReadiness(deps, snapshot, view, providers, catalogue) {
|
|
|
112
113
|
field: `work.${recipe.key}.aspect`,
|
|
113
114
|
message: "The model does not support this output shape.",
|
|
114
115
|
});
|
|
116
|
+
// Only the keyed providers' models are listed; the Codex CLI always takes a reference.
|
|
117
|
+
if (model !== undefined &&
|
|
118
|
+
input.reference !== undefined &&
|
|
119
|
+
!takesReferenceImage(model) &&
|
|
120
|
+
!fields.some((row) => row.field === "reference.source"))
|
|
121
|
+
fields.push({ field: "reference.source", message: referenceRefusal });
|
|
122
|
+
if (model !== undefined &&
|
|
123
|
+
input.thinking !== undefined &&
|
|
124
|
+
!fields.some((row) => row.field === "images.thinking"))
|
|
125
|
+
fields.push({
|
|
126
|
+
field: "images.thinking",
|
|
127
|
+
message: "This image model has no effort setting. Set the images' Effort back to Model default under Providers.",
|
|
128
|
+
});
|
|
115
129
|
}
|
|
116
130
|
}
|
|
117
131
|
return fields;
|
|
@@ -137,7 +137,7 @@ function stageForKey(key) {
|
|
|
137
137
|
return "article";
|
|
138
138
|
if (prefix === "audio" || prefix === "narration")
|
|
139
139
|
return "audio";
|
|
140
|
-
if (prefix === "image")
|
|
140
|
+
if (prefix === "image" || prefix === "reference")
|
|
141
141
|
return "images";
|
|
142
142
|
if (prefix === "thumbnail")
|
|
143
143
|
return "thumbnail";
|
|
@@ -30,6 +30,13 @@ export function baselineContent(deps, project, outputs, pieces, assets, template
|
|
|
30
30
|
if (asset !== undefined)
|
|
31
31
|
provided[stage] = asset.id;
|
|
32
32
|
}
|
|
33
|
+
// An uploaded establishing image, attached when the run started.
|
|
34
|
+
if (project.config.reference?.source === "provide") {
|
|
35
|
+
const output = outputs.find((row) => row.role === "reference");
|
|
36
|
+
const asset = output === undefined ? undefined : assets.get(output.path);
|
|
37
|
+
if (asset !== undefined)
|
|
38
|
+
provided.reference = asset.id;
|
|
39
|
+
}
|
|
33
40
|
const imageDefinitions = {};
|
|
34
41
|
const order = [];
|
|
35
42
|
const imagePieces = pieces.filter((row) => row.kind === "image");
|
|
@@ -13,7 +13,9 @@ import { getRevisionView } from "./view.js";
|
|
|
13
13
|
export function ensureBaseline(deps, projectId) {
|
|
14
14
|
return withProjectControl(deps.db, projectId, () => adoptBaseline(deps, projectId));
|
|
15
15
|
}
|
|
16
|
-
export function adoptBaseline(deps, projectId, templates
|
|
16
|
+
export function adoptBaseline(deps, projectId, templates,
|
|
17
|
+
// The Shorts step's background music a new run was started with, already a project asset.
|
|
18
|
+
supplied = {}) {
|
|
17
19
|
return transact(deps.db, () => {
|
|
18
20
|
const project = projectById(deps.db, projectId);
|
|
19
21
|
if (project === undefined)
|
|
@@ -29,7 +31,10 @@ export function adoptBaseline(deps, projectId, templates) {
|
|
|
29
31
|
const stages = stagesOf(deps.db, projectId);
|
|
30
32
|
const pieces = stages.flatMap((stage) => allPiecesOf(deps.db, stage.id));
|
|
31
33
|
const assets = adoptAssets(deps, projectId, outputs, pieces);
|
|
32
|
-
const content =
|
|
34
|
+
const content = {
|
|
35
|
+
...baselineContent(deps, project, outputs, pieces, assets, templates),
|
|
36
|
+
...(supplied.shortsMusic === undefined ? {} : { shortsMusic: supplied.shortsMusic }),
|
|
37
|
+
};
|
|
33
38
|
const fingerprints = baselineFingerprints(project, outputs, pieces, content, stages);
|
|
34
39
|
const revision = {
|
|
35
40
|
id: deps.ids.next(),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { statSync } from "node:fs";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { referenceKey } from "../admission/model.js";
|
|
4
|
+
import { usesReference } from "../admission/rules.js";
|
|
3
5
|
import { outputPath, stagingPath } from "../storage/layout.js";
|
|
4
6
|
import { stagedFileById } from "../storage/repo.js";
|
|
5
7
|
export function bindUpload(content, upload, assetId) {
|
|
@@ -31,7 +33,7 @@ export function validateUploads(deps, edit) {
|
|
|
31
33
|
for (const [index, upload] of (edit.uploads ?? []).entries()) {
|
|
32
34
|
const field = `uploads.${index}`;
|
|
33
35
|
const to = upload.destination;
|
|
34
|
-
if ((to.kind === "provided" && edit.config
|
|
36
|
+
if ((to.kind === "provided" && !providesOwnFile(edit.config, to.stage)) ||
|
|
35
37
|
(to.kind === "narration" && edit.config.sources.audio !== "generate") ||
|
|
36
38
|
(to.kind === "image" && edit.config.sources.images === "off"))
|
|
37
39
|
fields.push({
|
|
@@ -52,7 +54,13 @@ export function validateUploads(deps, edit) {
|
|
|
52
54
|
staged.add(upload.stagedFileId);
|
|
53
55
|
destinations.add(destination);
|
|
54
56
|
const file = stagedFileById(deps.db, upload.stagedFileId);
|
|
55
|
-
const kind = to.kind === "provided"
|
|
57
|
+
const kind = to.kind === "provided"
|
|
58
|
+
? to.stage === "reference"
|
|
59
|
+
? "images"
|
|
60
|
+
: to.stage
|
|
61
|
+
: to.kind === "image"
|
|
62
|
+
? "images"
|
|
63
|
+
: "audio";
|
|
56
64
|
if (file === undefined ||
|
|
57
65
|
file.state !== "staged" ||
|
|
58
66
|
file.stageKind !== kind ||
|
|
@@ -77,7 +85,7 @@ export function validateUploads(deps, edit) {
|
|
|
77
85
|
export function validateAssetReferences(deps, projectId, content, prepared) {
|
|
78
86
|
const fields = [];
|
|
79
87
|
const refs = [];
|
|
80
|
-
for (const key of ["research", "article", "audio", "thumbnail"]) {
|
|
88
|
+
for (const key of ["research", "article", "audio", "thumbnail", "reference"]) {
|
|
81
89
|
const id = content.provided[key];
|
|
82
90
|
if (id !== undefined)
|
|
83
91
|
refs.push([`content.provided.${key}`, id, key, false]);
|
|
@@ -94,7 +102,10 @@ export function validateAssetReferences(deps, projectId, content, prepared) {
|
|
|
94
102
|
audio: ["audio_body", "audio_intro", "audio_outro", "audio_export"],
|
|
95
103
|
images: ["image"],
|
|
96
104
|
thumbnail: ["thumbnail"],
|
|
105
|
+
reference: ["reference"],
|
|
97
106
|
};
|
|
107
|
+
// The establishing image is the Images stage's.
|
|
108
|
+
const stageOf = (kind) => (kind === "reference" ? "images" : kind);
|
|
98
109
|
for (const [field, id, kind, allowPiece] of refs) {
|
|
99
110
|
if (!prepared.some((row) => row.id === id && row.projectId === projectId) &&
|
|
100
111
|
deps.db
|
|
@@ -107,7 +118,7 @@ export function validateAssetReferences(deps, projectId, content, prepared) {
|
|
|
107
118
|
if (!prepared.some((row) => row.id === id) &&
|
|
108
119
|
deps.db
|
|
109
120
|
.prepare("SELECT 1 FROM revision_outputs WHERE project_id=? AND asset_id=? AND json_extract(descriptor,'$.stageKind')=? AND json_extract(descriptor,'$.role') IN (SELECT value FROM json_each(?))")
|
|
110
|
-
.get(projectId, id, kind, JSON.stringify(roles[kind])) === undefined &&
|
|
121
|
+
.get(projectId, id, stageOf(kind), JSON.stringify(roles[kind])) === undefined &&
|
|
111
122
|
(!allowPiece ||
|
|
112
123
|
deps.db
|
|
113
124
|
.prepare("SELECT 1 FROM revision_pieces WHERE project_id=? AND asset_id=? AND stage_kind=? AND json_extract(descriptor,'$.kind') IN (SELECT value FROM json_each(?))")
|
|
@@ -150,12 +161,25 @@ export function assetPath(deps, projectId, id) {
|
|
|
150
161
|
.prepare("SELECT path FROM project_assets WHERE project_id=? AND id=?")
|
|
151
162
|
.get(projectId, id)).path;
|
|
152
163
|
}
|
|
164
|
+
// A stage whose own file the project uses: Provide for audio and the thumbnail, Upload for the
|
|
165
|
+
// establishing image.
|
|
166
|
+
export function providesOwnFile(config, kind) {
|
|
167
|
+
return kind === "reference"
|
|
168
|
+
? usesReference(config) && config.reference?.source === "provide"
|
|
169
|
+
: config.sources[kind] === "provide";
|
|
170
|
+
}
|
|
171
|
+
// Where each provided file is published: its work key and output role.
|
|
172
|
+
export const providedOutput = {
|
|
173
|
+
audio: { workKey: "audio:provided", role: "audio_body" },
|
|
174
|
+
thumbnail: { workKey: "thumbnail:image", role: "thumbnail" },
|
|
175
|
+
reference: { workKey: referenceKey, role: "reference" },
|
|
176
|
+
};
|
|
153
177
|
export function providedAssetSelected(base, kind, assetId) {
|
|
154
|
-
return (base.revision.config
|
|
178
|
+
return (providesOwnFile(base.revision.config, kind) &&
|
|
155
179
|
base.outputs.some((row) => row.selected &&
|
|
156
180
|
row.assetId === assetId &&
|
|
157
|
-
row.workKey ===
|
|
158
|
-
row.output.role ===
|
|
181
|
+
row.workKey === providedOutput[kind].workKey &&
|
|
182
|
+
row.output.role === providedOutput[kind].role));
|
|
159
183
|
}
|
|
160
184
|
export function validateReplacementAvailability(deps, base, edit) {
|
|
161
185
|
const previous = new Set([
|
|
@@ -163,10 +187,10 @@ export function validateReplacementAvailability(deps, base, edit) {
|
|
|
163
187
|
...Object.values(base.revision.content.imageDefinitions).map((row) => row.assetId),
|
|
164
188
|
...Object.values(base.revision.content.narrationOverrides).flatMap((row) => row.kind === "asset" ? [row.assetId] : []),
|
|
165
189
|
]);
|
|
166
|
-
for (const kind of ["audio", "thumbnail"]) {
|
|
190
|
+
for (const kind of ["audio", "thumbnail", "reference"]) {
|
|
167
191
|
const assetId = edit.content.provided[kind];
|
|
168
192
|
if (assetId !== undefined &&
|
|
169
|
-
edit.config
|
|
193
|
+
providesOwnFile(edit.config, kind) &&
|
|
170
194
|
!providedAssetSelected(base, kind, assetId) &&
|
|
171
195
|
!edit.uploads?.some((row) => row.destination.kind === "provided" && row.destination.stage === kind))
|
|
172
196
|
previous.delete(assetId);
|
|
@@ -5,7 +5,7 @@ import { normalizeArticleIntent } from "../rebuild/recipe-save.js";
|
|
|
5
5
|
import { discardPreparedAssets } from "../storage/assets.js";
|
|
6
6
|
import { prepareStagedFile, prepareText } from "../storage/prepare.js";
|
|
7
7
|
import { outputSchema } from "../storage/schema.js";
|
|
8
|
-
import { bindUpload, measureAudio, providedAssetSelected, } from "./mutation-assets.js";
|
|
8
|
+
import { bindUpload, measureAudio, providedAssetSelected, providedOutput, providesOwnFile, } from "./mutation-assets.js";
|
|
9
9
|
export async function prepareEditAssets(deps, base, edit) {
|
|
10
10
|
const prepared = [];
|
|
11
11
|
const allocated = [];
|
|
@@ -16,8 +16,8 @@ export async function prepareEditAssets(deps, base, edit) {
|
|
|
16
16
|
const to = upload.destination;
|
|
17
17
|
const role = to.kind === "image"
|
|
18
18
|
? "image"
|
|
19
|
-
: to.kind === "provided" && to.stage
|
|
20
|
-
?
|
|
19
|
+
: to.kind === "provided" && to.stage !== "audio"
|
|
20
|
+
? providedOutput[to.stage].role
|
|
21
21
|
: "audio_body";
|
|
22
22
|
const result = prepareStagedFile(deps, {
|
|
23
23
|
projectId,
|
|
@@ -35,9 +35,7 @@ export async function prepareEditAssets(deps, base, edit) {
|
|
|
35
35
|
? to.key
|
|
36
36
|
: to.kind === "shortsMusic"
|
|
37
37
|
? "shorts:music"
|
|
38
|
-
: to.stage
|
|
39
|
-
? "audio:provided"
|
|
40
|
-
: "thumbnail:image";
|
|
38
|
+
: providedOutput[to.stage].workKey;
|
|
41
39
|
const item = {
|
|
42
40
|
...result,
|
|
43
41
|
upload,
|
|
@@ -93,21 +91,21 @@ export async function prepareEditAssets(deps, base, edit) {
|
|
|
93
91
|
slot: workKey,
|
|
94
92
|
});
|
|
95
93
|
}
|
|
96
|
-
for (const kind of ["audio", "thumbnail"]) {
|
|
94
|
+
for (const kind of ["audio", "thumbnail", "reference"]) {
|
|
97
95
|
const assetId = content.provided[kind];
|
|
98
|
-
if (edit.config
|
|
96
|
+
if (!providesOwnFile(edit.config, kind) ||
|
|
99
97
|
assetId === undefined ||
|
|
100
98
|
providedAssetSelected(base, kind, assetId) ||
|
|
101
99
|
prepared.some((row) => row.asset.id === assetId))
|
|
102
100
|
continue;
|
|
103
101
|
const existing = deps.db
|
|
104
102
|
.prepare("SELECT descriptor FROM revision_outputs WHERE project_id=? AND asset_id=? AND json_extract(descriptor,'$.stageKind')=? LIMIT 1")
|
|
105
|
-
.get(projectId, assetId, kind);
|
|
103
|
+
.get(projectId, assetId, kind === "reference" ? "images" : kind);
|
|
106
104
|
if (existing === undefined)
|
|
107
105
|
throw new Error("Validated replacement asset has no descriptor.");
|
|
108
106
|
const descriptor = outputSchema.parse(JSON.parse(z.string().parse(existing.descriptor)));
|
|
109
107
|
const asset = retainedAsset(deps, projectId, assetId);
|
|
110
|
-
const role = kind
|
|
108
|
+
const role = providedOutput[kind].role;
|
|
111
109
|
const item = {
|
|
112
110
|
asset,
|
|
113
111
|
output: {
|
|
@@ -32,6 +32,7 @@ export const revisionContentSchema = z
|
|
|
32
32
|
article: id.optional(),
|
|
33
33
|
audio: id.optional(),
|
|
34
34
|
thumbnail: id.optional(),
|
|
35
|
+
reference: id.optional(),
|
|
35
36
|
})
|
|
36
37
|
.strict(),
|
|
37
38
|
imageOrder: z.array(id).max(60),
|
|
@@ -91,7 +92,10 @@ export const revisionEditSchema = z
|
|
|
91
92
|
stagedFileId: id,
|
|
92
93
|
destination: z.discriminatedUnion("kind", [
|
|
93
94
|
z
|
|
94
|
-
.object({
|
|
95
|
+
.object({
|
|
96
|
+
kind: z.literal("provided"),
|
|
97
|
+
stage: z.enum(["audio", "thumbnail", "reference"]),
|
|
98
|
+
})
|
|
95
99
|
.strict(),
|
|
96
100
|
z.object({ kind: z.literal("image"), imageKey: id }).strict(),
|
|
97
101
|
z.object({ kind: z.literal("narration"), key: workKey }).strict(),
|
|
@@ -308,7 +308,7 @@ function parseRun(row) {
|
|
|
308
308
|
const internalRunError = "Slopify hit an internal error while starting this run. The next run will try again; if it keeps happening, use Download diagnostics in Settings and report it.";
|
|
309
309
|
const runErrors = {
|
|
310
310
|
"missing-template": "The template this schedule uses was deleted or changed. Edit the schedule and choose a template again.",
|
|
311
|
-
"unsupported-media": "This template uses audio, images
|
|
311
|
+
"unsupported-media": "This template uses audio, images, a thumbnail or shorts background music you supplied, which scheduled runs cannot use. Pick a template that generates these instead, or remove the music under Outputs → Export → More shorts options on Play and save the template again.",
|
|
312
312
|
"spend-limit": "Not started: the estimated cost was above this schedule's spend limit, or some prices were unknown. Raise the spend limit or choose models with known prices.",
|
|
313
313
|
readiness: "Not started: a provider was not ready, for example a missing API key, model or voice. Check Settings → Providers; the next run will try again.",
|
|
314
314
|
"not-found": "The template's settings are no longer valid, for example a prompt or voice it uses was deleted. Fix the template in Library > Templates and save it.",
|
|
@@ -152,7 +152,12 @@ function checkTemplate(deps, id, version) {
|
|
|
152
152
|
return { ok: false, reason: "missing-template" };
|
|
153
153
|
const sources = template.document.form.sources;
|
|
154
154
|
const mediaSources = ["audio", "images", "thumbnail"];
|
|
155
|
-
|
|
155
|
+
const { form } = template.document;
|
|
156
|
+
// A supplied file is attached again on Play, which a scheduled run cannot do; the shorts'
|
|
157
|
+
// background music is one too.
|
|
158
|
+
if (mediaSources.some((kind) => sources[kind] === "provide") ||
|
|
159
|
+
(form.shorts?.enabled === true && form.provided.shortsMusic) ||
|
|
160
|
+
(sources.images === "generate" && form.reference?.source === "provide"))
|
|
156
161
|
return { ok: false, reason: "unsupported-media" };
|
|
157
162
|
return { ok: true, value: true };
|
|
158
163
|
}
|
|
@@ -22,6 +22,11 @@ export const providerIds = [
|
|
|
22
22
|
export function isLocalCliProvider(id) {
|
|
23
23
|
return id === "claude-code" || id === "codex" || id === "gemini" || id === "codex-image";
|
|
24
24
|
}
|
|
25
|
+
// How many calls to a command-line tool run at once. Each Codex image is its own job - its
|
|
26
|
+
// own folder and thread - so four are drawn side by side; the text CLIs keep three.
|
|
27
|
+
export function localCliConcurrency(id) {
|
|
28
|
+
return id === "codex-image" ? 4 : 3;
|
|
29
|
+
}
|
|
25
30
|
export const providers = [
|
|
26
31
|
{ id: "openrouter", family: "llm", displayName: "OpenRouter", auth: "key" },
|
|
27
32
|
{
|
|
@@ -5,6 +5,14 @@ export function projectDir(paths, projectId) {
|
|
|
5
5
|
export function outputPath(paths, projectId, relativePath) {
|
|
6
6
|
return contained(projectDir(paths, projectId), relativePath);
|
|
7
7
|
}
|
|
8
|
+
// Scheduled backups (slices/backups) land here unless the user picks another folder. It sits
|
|
9
|
+
// inside the projects root because that is the one folder a Docker install shares with the
|
|
10
|
+
// host, so the archives end up on the user's disk and not only in the container's volume.
|
|
11
|
+
// Project folders are ULIDs, so the name never collides with one; reconcile leaves it alone.
|
|
12
|
+
export const backupsFolderName = "Backups";
|
|
13
|
+
export function defaultBackupsDir(paths) {
|
|
14
|
+
return contained(paths.projects, backupsFolderName);
|
|
15
|
+
}
|
|
8
16
|
export function stagingPath(paths, stagedFileId) {
|
|
9
17
|
return contained(paths.staging, stagedFileId);
|
|
10
18
|
}
|
|
@@ -78,6 +86,8 @@ export function outputFileName(role, index, extension, stageKind) {
|
|
|
78
86
|
return `audio-outro${extension}`;
|
|
79
87
|
case "thumbnail":
|
|
80
88
|
return `thumbnail${extension}`;
|
|
89
|
+
case "reference":
|
|
90
|
+
return `reference${extension}`;
|
|
81
91
|
case "image":
|
|
82
92
|
return `images/${String(index).padStart(3, "0")}${extension}`;
|
|
83
93
|
}
|
|
@@ -33,5 +33,8 @@ export const outputRoles = [
|
|
|
33
33
|
"short_video",
|
|
34
34
|
// An image of the slideshow brought to life by an image-to-video model (Animate images).
|
|
35
35
|
"animated_image",
|
|
36
|
+
// The Images stage's establishing image: made or uploaded first, drawn from by the other
|
|
37
|
+
// images, never shown in the video.
|
|
38
|
+
"reference",
|
|
36
39
|
];
|
|
37
40
|
export const stagedFileStates = ["copying", "staged"];
|
|
@@ -12,12 +12,13 @@ import { readFontMetadata } from "../fonts/sfnt.js";
|
|
|
12
12
|
import { lintEntry, lintPrompt } from "../library/lint.js";
|
|
13
13
|
import { entryCategories, entryModes, promptKinds } from "../library/model.js";
|
|
14
14
|
import { listEntries, listPrompts } from "../library/repo.js";
|
|
15
|
+
import { notificationUrlKey } from "../notifications/settings.js";
|
|
15
16
|
import { projectTemplateSchema } from "../project-templates/schema.js";
|
|
16
17
|
import { cliPathMaxLength } from "../settings/cli-paths.js";
|
|
17
18
|
import { appearances, providerById, providerIds } from "../settings/model.js";
|
|
18
19
|
import { listVoices } from "../settings/repo.js";
|
|
19
20
|
import { voiceIdMax, voiceNameMax } from "../settings/voices.js";
|
|
20
|
-
import { projectDir, stagingPath } from "./layout.js";
|
|
21
|
+
import { defaultBackupsDir, projectDir, stagingPath } from "./layout.js";
|
|
21
22
|
import { stageKinds } from "./model.js";
|
|
22
23
|
import { insertStagedFile, stagedFiles } from "./repo.js";
|
|
23
24
|
const record = z.record(z.string(), z.string());
|
|
@@ -326,7 +327,8 @@ const storedAppearance = z.enum(appearances);
|
|
|
326
327
|
function portableSettings(settings) {
|
|
327
328
|
const portable = {};
|
|
328
329
|
for (const [key, value] of Object.entries(settings)) {
|
|
329
|
-
|
|
330
|
+
// The Notification URL stays on this machine: an ntfy topic in it is as good as a password.
|
|
331
|
+
if (key === "tutorial.session" || key === notificationUrlKey)
|
|
330
332
|
continue;
|
|
331
333
|
const parsed = storedJson(value);
|
|
332
334
|
if (key === "silenceGapSeconds")
|
|
@@ -862,7 +864,8 @@ function planStagedImport(deps, archive, files) {
|
|
|
862
864
|
});
|
|
863
865
|
}
|
|
864
866
|
export function storageBytes(paths) {
|
|
865
|
-
|
|
867
|
+
// Scheduled backups default to a folder inside the projects root; they are not project files.
|
|
868
|
+
const projects = directoryBytes(paths.projects) - directoryBytes(defaultBackupsDir(paths));
|
|
866
869
|
const staging = directoryBytes(paths.staging);
|
|
867
870
|
return { data: directoryBytes(paths.dataDir), projects, staging };
|
|
868
871
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdirSync, rmSync, statSync, unlinkSync } from "node:fs";
|
|
2
2
|
import { join, relative, sep } from "node:path";
|
|
3
|
-
import { outputPath, stagingPath } from "./layout.js";
|
|
3
|
+
import { backupsFolderName, outputPath, stagingPath } from "./layout.js";
|
|
4
4
|
import { stagedFiles as stagedRows } from "./repo.js";
|
|
5
5
|
import { stagedFileReferenced } from "./staging-refs.js";
|
|
6
6
|
export function reconcileStorage(db, paths) {
|
|
@@ -33,6 +33,10 @@ export function reconcileStorage(db, paths) {
|
|
|
33
33
|
let orphanFiles = 0;
|
|
34
34
|
for (const entry of readdirSync(paths.projects, { withFileTypes: true })) {
|
|
35
35
|
const path = join(paths.projects, entry.name);
|
|
36
|
+
// The scheduled backups' default folder: its archives belong to slices/backups, which
|
|
37
|
+
// prunes them by its own rules.
|
|
38
|
+
if (entry.name === backupsFolderName && entry.isDirectory())
|
|
39
|
+
continue;
|
|
36
40
|
if (!entry.isDirectory()) {
|
|
37
41
|
unlinkSync(path);
|
|
38
42
|
orphanFiles += 1;
|