@gentbajko/slopify 2.3.1 → 2.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapter-registry.js +9 -2
- package/dist/adapters/host-cli/index.js +11 -0
- package/dist/adapters/host-cli/transport.js +6 -1
- package/dist/adapters/image/codex-output.js +59 -17
- package/dist/adapters/image/codex.js +92 -16
- package/dist/adapters/image/fal.js +99 -8
- package/dist/adapters/image/google.js +13 -1
- package/dist/adapters/image/openai.js +37 -15
- package/dist/adapters/image/reference.js +7 -0
- package/dist/adapters/image/replicate.js +48 -2
- package/dist/adapters/image/video.js +28 -0
- package/dist/adapters/llm/codex-models.js +2 -1
- package/dist/adapters/llm/codex-server-models.js +2 -1
- package/dist/adapters/llm/run-cli.js +14 -2
- package/dist/assets/fonts/Barlow-Bold.ttf +0 -0
- package/dist/assets/fonts/SOURCE.txt +6 -0
- package/dist/assets/models.yaml +89 -1
- package/dist/catalog/registry.js +24 -3
- package/dist/catalog/runtime-models.js +1 -1
- package/dist/catalog/schema.js +20 -1
- package/dist/catalog/store.js +2 -2
- package/dist/catalog/validate.js +47 -4
- package/dist/edge/docker-launch.js +4 -0
- package/dist/edge/docker-projects/engine.js +57 -1
- package/dist/edge/docker-projects/install.js +10 -2
- package/dist/edge/docker-projects/prune.js +66 -0
- package/dist/edge/events/hub.js +11 -0
- package/dist/edge/http/app.js +2 -0
- package/dist/edge/http/backups.js +40 -0
- package/dist/edge/http/host-cli.js +17 -2
- package/dist/edge/http/problem.js +1 -0
- package/dist/edge/http/providers.js +8 -0
- package/dist/edge/http/schedules.js +1 -1
- package/dist/edge/http/settings.js +44 -0
- package/dist/edge/http/timeouts.js +38 -0
- package/dist/host-cli/runtime.js +3 -2
- package/dist/kernel/db/migrations/0019-shorts-prompts.sql +16 -0
- package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
- package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
- package/dist/kernel/ports/host-cli.js +17 -2
- package/dist/kernel/ports/image.js +3 -1
- package/dist/kernel/ports/llm.js +3 -1
- package/dist/kernel/runner/attempt.js +5 -2
- package/dist/kernel/runner/providers.js +42 -2
- package/dist/main.js +55 -7
- package/dist/slices/admission/model.js +8 -0
- package/dist/slices/admission/rules.js +91 -2
- package/dist/slices/admission/schema.js +42 -1
- package/dist/slices/admission/start.js +53 -15
- package/dist/slices/backups/files.js +135 -0
- package/dist/slices/backups/folder.js +35 -0
- package/dist/slices/backups/model.js +65 -0
- package/dist/slices/backups/repo.js +28 -0
- package/dist/slices/backups/schedule.js +60 -0
- package/dist/slices/backups/service.js +311 -0
- package/dist/slices/batch/index.js +2 -0
- package/dist/slices/estimate/index.js +65 -2
- package/dist/slices/fonts/catalog.js +23 -0
- package/dist/slices/fonts/index.js +1 -1
- package/dist/slices/library/model.js +8 -1
- package/dist/slices/library/slots.js +13 -2
- package/dist/slices/library/snapshot.js +8 -2
- package/dist/slices/notifications/notifier.js +76 -0
- package/dist/slices/notifications/rules.js +81 -0
- package/dist/slices/notifications/send.js +40 -0
- package/dist/slices/notifications/settings.js +34 -0
- package/dist/slices/play-drafts/convert.js +72 -4
- package/dist/slices/play-drafts/readiness.js +7 -3
- package/dist/slices/play-drafts/repo.js +7 -1
- package/dist/slices/play-drafts/review-inputs.js +2 -0
- package/dist/slices/play-drafts/schema.js +44 -3
- package/dist/slices/play-drafts/service.js +6 -0
- package/dist/slices/play-drafts/uploads.js +2 -1
- package/dist/slices/project-templates/from-project.js +42 -0
- package/dist/slices/project-templates/setup.js +13 -0
- package/dist/slices/rebuild/preview-details.js +53 -3
- package/dist/slices/rebuild/preview-plan.js +2 -2
- package/dist/slices/rebuild/recipe-build.js +13 -3
- package/dist/slices/rebuild/recipe-edit.js +178 -0
- package/dist/slices/rebuild/recipe-exports.js +9 -4
- package/dist/slices/rebuild/recipe-input-schema.js +9 -1
- package/dist/slices/rebuild/recipe-legacy.js +12 -0
- package/dist/slices/rebuild/recipe-model.js +7 -1
- package/dist/slices/rebuild/recipe-provider-choice.js +15 -0
- package/dist/slices/rebuild/recipe-reference.js +44 -0
- package/dist/slices/rebuild/recipe-save.js +25 -1
- package/dist/slices/rebuild/recipe-shorts.js +186 -0
- package/dist/slices/rebuild/recipe-validation.js +33 -2
- package/dist/slices/rebuild/recipe-visual.js +29 -8
- package/dist/slices/rebuild/recipe-work.js +83 -1
- package/dist/slices/rebuild/runtime-actions.js +9 -2
- package/dist/slices/rebuild/runtime-animate.js +110 -0
- package/dist/slices/rebuild/runtime-export-edit.js +119 -0
- package/dist/slices/rebuild/runtime-export.js +13 -1
- package/dist/slices/rebuild/runtime-image.js +50 -0
- package/dist/slices/rebuild/runtime-local.js +6 -3
- package/dist/slices/rebuild/runtime-materialize.js +48 -0
- package/dist/slices/rebuild/runtime-piece-label.js +17 -0
- package/dist/slices/rebuild/runtime-provider.js +17 -10
- package/dist/slices/rebuild/runtime-publication.js +25 -4
- package/dist/slices/rebuild/runtime-run.js +11 -5
- package/dist/slices/rebuild/runtime-shorts.js +328 -0
- package/dist/slices/rebuild/runtime-store.js +7 -3
- package/dist/slices/rebuild/service-readiness.js +15 -1
- package/dist/slices/rebuild/transition-repo.js +40 -1
- package/dist/slices/revisions/adopt-content.js +7 -0
- package/dist/slices/revisions/adopt.js +7 -2
- package/dist/slices/revisions/mutation-assets.js +51 -9
- package/dist/slices/revisions/mutation-prepare.js +14 -14
- package/dist/slices/revisions/mutations.js +2 -1
- package/dist/slices/revisions/publish.js +20 -2
- package/dist/slices/revisions/schema.js +16 -1
- package/dist/slices/schedules/repo.js +1 -1
- package/dist/slices/schedules/service.js +6 -1
- package/dist/slices/settings/model.js +5 -0
- package/dist/slices/shorts/captions.js +272 -0
- package/dist/slices/shorts/clips.js +89 -0
- package/dist/slices/shorts/model.js +186 -0
- package/dist/slices/shorts/pick.js +232 -0
- package/dist/slices/shorts/prompts.js +68 -0
- package/dist/slices/shorts/render.js +163 -0
- package/dist/slices/storage/asset-name.js +9 -2
- package/dist/slices/storage/layout.js +18 -0
- package/dist/slices/storage/model.js +9 -0
- package/dist/slices/storage/portable.js +6 -3
- package/dist/slices/storage/reconcile.js +5 -1
- package/dist/slices/storage/schema.js +3 -0
- package/dist/slices/telemetry/model.js +3 -0
- package/dist/slices/video/cards.js +72 -0
- package/dist/slices/video/chapters.js +89 -0
- package/dist/slices/video/cuts.js +141 -0
- package/dist/slices/video/edit-list.js +36 -6
- package/dist/slices/video/edit-settings.js +191 -0
- package/dist/slices/video/ffmpeg.js +155 -29
- package/dist/slices/video/look.js +164 -0
- package/dist/slices/video/plan.js +68 -11
- package/dist/slices/video/slideshow.js +22 -6
- package/dist/slices/video/transitions.js +51 -0
- package/dist/slices/youtube/transcript.js +54 -0
- package/dist/web/assets/index-CktpxlJd.js +171 -0
- package/dist/web/assets/index-DB-vOnGU.css +1 -0
- package/dist/web/assets/{pdf-DR97Byps.js → pdf-BLF2i8c2.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DfXmBzCk.js +0 -168
- package/dist/web/assets/index-Y3RhfTEh.css +0 -1
|
@@ -4,6 +4,7 @@ import { providerError } from "../../kernel/ports/model.js";
|
|
|
4
4
|
import { httpFailure, missingKey, noImage, providerSaid, refusedImage, unreadable, } from "../explain.js";
|
|
5
5
|
import { retryAfter } from "../retry-after.js";
|
|
6
6
|
import { downloadImage } from "./bytes.js";
|
|
7
|
+
import { dataUri, downloadVideo } from "./video.js";
|
|
7
8
|
// The HTTP gateway adapter for Replicate: `fetch`, the injected clock and the downloader
|
|
8
9
|
// beside this file, no SDK. The `replicate` package wraps exactly the two requests below, so
|
|
9
10
|
// the platform's own `fetch` covers it.
|
|
@@ -40,6 +41,13 @@ export function replicateImage(deps) {
|
|
|
40
41
|
id: "replicate",
|
|
41
42
|
models: () => Promise.resolve(replicateModels),
|
|
42
43
|
generate: async (req) => {
|
|
44
|
+
// The Replicate models listed take no input image to draw from (the FLUX image inputs
|
|
45
|
+
// copy the picture's composition), so an establishing image is refused, not dropped.
|
|
46
|
+
if (req.reference !== undefined)
|
|
47
|
+
throw providerError({
|
|
48
|
+
kind: "unsupported",
|
|
49
|
+
message: "Replicate's image models can't use an establishing image as a reference. Choose Codex CLI, OpenAI, Google or fal.ai under Images → Provider on Play or in Edit project → Providers, or set Establishing image to Off in the Images section.",
|
|
50
|
+
});
|
|
43
51
|
const response = await deps.fetch(`${replicateBase}/models/${req.model}/predictions`, {
|
|
44
52
|
method: "POST",
|
|
45
53
|
signal: req.signal,
|
|
@@ -71,8 +79,46 @@ export function replicateImage(deps) {
|
|
|
71
79
|
signal: req.signal,
|
|
72
80
|
});
|
|
73
81
|
},
|
|
82
|
+
// The same prediction, polled the same way; a clip outlives `Prefer: wait` every time, so
|
|
83
|
+
// it is not asked for.
|
|
84
|
+
animate: async (req) => {
|
|
85
|
+
const response = await deps.fetch(`${replicateBase}/models/${req.model}/predictions`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
signal: req.signal,
|
|
88
|
+
headers: { ...auth(deps), "Content-Type": "application/json" },
|
|
89
|
+
body: JSON.stringify({
|
|
90
|
+
input: {
|
|
91
|
+
prompt: req.prompt,
|
|
92
|
+
[Object.hasOwn(videoImageField, req.model)
|
|
93
|
+
? (videoImageField[req.model] ?? "image")
|
|
94
|
+
: "image"]: dataUri(req.image),
|
|
95
|
+
duration: Math.round(req.seconds),
|
|
96
|
+
...(Object.hasOwn(videoResolution, req.model) ? { resolution: "720p" } : {}),
|
|
97
|
+
},
|
|
98
|
+
}),
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok)
|
|
101
|
+
throw await failure(response, "video clip request");
|
|
102
|
+
const url = await settle(deps, parse(await response.text()), req.signal);
|
|
103
|
+
return await downloadVideo({
|
|
104
|
+
fetch: deps.fetch,
|
|
105
|
+
provider: "Replicate",
|
|
106
|
+
url,
|
|
107
|
+
signal: req.signal,
|
|
108
|
+
});
|
|
109
|
+
},
|
|
74
110
|
};
|
|
75
111
|
}
|
|
112
|
+
// Replicate's image-to-video models name the still differently: Kling calls it
|
|
113
|
+
// `start_image`, the others `image`. Wan and Seedance default to larger frames than the
|
|
114
|
+
// render needs, so they are asked for 720p, which is what the catalogue prices.
|
|
115
|
+
const videoImageField = {
|
|
116
|
+
"kwaivgi/kling-v2.5-turbo-pro": "start_image",
|
|
117
|
+
};
|
|
118
|
+
const videoResolution = {
|
|
119
|
+
"wan-video/wan-2.5-i2v": true,
|
|
120
|
+
"bytedance/seedance-1-pro-fast": true,
|
|
121
|
+
};
|
|
76
122
|
// `Prefer: wait` answers `starting` when the model outlived its 60 s; Replicate's guidance
|
|
77
123
|
// is to poll `urls.get` until the prediction settles.
|
|
78
124
|
async function settle(deps, first, signal) {
|
|
@@ -144,7 +190,7 @@ function kindOf(status) {
|
|
|
144
190
|
// error contract first, which is not this adapter's to widen.
|
|
145
191
|
return "other";
|
|
146
192
|
}
|
|
147
|
-
async function failure(response) {
|
|
193
|
+
async function failure(response, subject = "image request") {
|
|
148
194
|
const text = await response.text().catch(() => "");
|
|
149
195
|
const parsed = errorBody.safeParse(safeJson(text));
|
|
150
196
|
// The provider's own words, through the redactor - an error body may quote the key back.
|
|
@@ -157,7 +203,7 @@ async function failure(response) {
|
|
|
157
203
|
provider: "Replicate",
|
|
158
204
|
status: response.status,
|
|
159
205
|
detail: message || response.statusText,
|
|
160
|
-
subject
|
|
206
|
+
subject,
|
|
161
207
|
}),
|
|
162
208
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
163
209
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { providerError } from "../../kernel/ports/model.js";
|
|
2
|
+
import { describeBytes } from "./bytes.js";
|
|
3
|
+
// What the two image-to-video adapters share: the still goes up inline as a data URI (the
|
|
4
|
+
// render sends a small JPEG, `slices/rebuild/runtime-animate.ts`), and the clip comes back
|
|
5
|
+
// as a link that is downloaded inside the attempt, like an image.
|
|
6
|
+
export function dataUri(image) {
|
|
7
|
+
return `data:${image.mime};base64,${Buffer.from(image.bytes).toString("base64")}`;
|
|
8
|
+
}
|
|
9
|
+
// An MP4 (or MOV) names its box type at byte 4.
|
|
10
|
+
export function sniffVideo(bytes) {
|
|
11
|
+
return bytes.length > 12 && Buffer.from(bytes.slice(4, 8)).toString("latin1") === "ftyp";
|
|
12
|
+
}
|
|
13
|
+
export async function downloadVideo(download) {
|
|
14
|
+
const response = await download.fetch(download.url, { signal: download.signal });
|
|
15
|
+
if (!response.ok)
|
|
16
|
+
// The link is not quoted back: a delivery link carries its own signature.
|
|
17
|
+
throw providerError({
|
|
18
|
+
kind: "other",
|
|
19
|
+
message: `${download.provider} made the video clip, but Slopify could not download it (error ${String(response.status)}). Check your internet connection, then use Retry stage.`,
|
|
20
|
+
});
|
|
21
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
22
|
+
if (!sniffVideo(bytes))
|
|
23
|
+
throw providerError({
|
|
24
|
+
kind: "other",
|
|
25
|
+
message: `${download.provider} sent back something that is not an MP4 video clip (${describeBytes(bytes)}). Use Retry stage; if it keeps happening, choose another image-to-video model under Animate images in Edit project → Inputs → Look.`,
|
|
26
|
+
});
|
|
27
|
+
return { bytes, mime: "video/mp4" };
|
|
28
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { homedir } from "node:os";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { thinkingModes as knownEfforts } from "../../kernel/ports/llm.js";
|
|
4
5
|
import { readCatalogueFile } from "./catalogue-files.js";
|
|
5
6
|
import { codexModelName, nodeCodexServerModels } from "./codex-server-models.js";
|
|
6
7
|
const safeText = z
|
|
@@ -40,7 +41,7 @@ export async function nodeCodexModels(env = process.env, binary = "codex", timeo
|
|
|
40
41
|
if (!unique.has(item.slug)) {
|
|
41
42
|
const thinkingModes = item.supported_reasoning_levels
|
|
42
43
|
?.map((level) => level.effort)
|
|
43
|
-
.filter((level) =>
|
|
44
|
+
.filter((level) => knownEfforts.includes(level));
|
|
44
45
|
unique.set(item.slug, {
|
|
45
46
|
id: item.slug,
|
|
46
47
|
name: codexModelName(item.display_name ?? item.slug),
|
|
@@ -4,6 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { cliCommand } from "../../kernel/cli-command.js";
|
|
7
|
+
import { thinkingModes as knownEfforts } from "../../kernel/ports/llm.js";
|
|
7
8
|
const text = z
|
|
8
9
|
.string()
|
|
9
10
|
.trim()
|
|
@@ -102,7 +103,7 @@ export async function nodeCodexServerModels(binary, env, timeoutMs = 15_000) {
|
|
|
102
103
|
continue;
|
|
103
104
|
const thinkingModes = row.supportedReasoningEfforts
|
|
104
105
|
?.map((item) => item.reasoningEffort)
|
|
105
|
-
.filter((effort) =>
|
|
106
|
+
.filter((effort) => knownEfforts.includes(effort));
|
|
106
107
|
models.set(row.model, {
|
|
107
108
|
id: row.model,
|
|
108
109
|
name: codexModelName(row.displayName),
|
|
@@ -87,12 +87,24 @@ export function nodeRunCli(binary, args, signal, options) {
|
|
|
87
87
|
child.stderr?.destroy();
|
|
88
88
|
}
|
|
89
89
|
};
|
|
90
|
-
|
|
90
|
+
// An abort asks politely, then forces after the same grace stopCliRun gives. A reader
|
|
91
|
+
// waiting on stdout only reaches stopCliRun once stdout closes, so a CLI that traps
|
|
92
|
+
// SIGTERM and keeps its pipe open would otherwise outlive the run that was cancelled.
|
|
93
|
+
let forced;
|
|
94
|
+
const abort = () => {
|
|
95
|
+
terminate();
|
|
96
|
+
forced ??= setTimeout(() => terminate(true), cliTerminationGraceMs);
|
|
97
|
+
forced.unref();
|
|
98
|
+
};
|
|
91
99
|
if (signal.aborted)
|
|
92
100
|
abort();
|
|
93
101
|
else
|
|
94
102
|
signal.addEventListener("abort", abort, { once: true });
|
|
95
|
-
void ended.then(() =>
|
|
103
|
+
void ended.then(() => {
|
|
104
|
+
signal.removeEventListener("abort", abort);
|
|
105
|
+
if (forced !== undefined)
|
|
106
|
+
clearTimeout(forced);
|
|
107
|
+
});
|
|
96
108
|
return {
|
|
97
109
|
...(inputWritten === undefined ? {} : { inputWritten }),
|
|
98
110
|
pid: child.pid,
|
|
Binary file
|
|
@@ -2,6 +2,12 @@ Barlow-Regular.ttf
|
|
|
2
2
|
Source: https://raw.githubusercontent.com/google/fonts/6cdf01867df0813c2390f90dff7dc66c87f14cf7/ofl/barlow/Barlow-Regular.ttf
|
|
3
3
|
SHA-256: 95aa02c7c43096e0dd44d787ba6216864a67157e402adab59b35572e0c1577ea
|
|
4
4
|
|
|
5
|
+
Barlow-Bold.ttf
|
|
6
|
+
Source: https://raw.githubusercontent.com/google/fonts/6cdf01867df0813c2390f90dff7dc66c87f14cf7/ofl/barlow/Barlow-Bold.ttf
|
|
7
|
+
SHA-256: 84e6a4d61e7c3e21f3c50ea6a4f7e5303a3467864c038be6ea3759bab8d547f9
|
|
8
|
+
Git blob: 2f20cbe667ca5cd871410dc50dc4b1a30c1d6267 (matches the google/fonts tree at that commit)
|
|
9
|
+
Licence: SIL Open Font License 1.1, OFL.txt below (the same licence and authors as Barlow-Regular)
|
|
10
|
+
|
|
5
11
|
OFL.txt
|
|
6
12
|
Source: https://raw.githubusercontent.com/google/fonts/6cdf01867df0813c2390f90dff7dc66c87f14cf7/ofl/barlow/OFL.txt
|
|
7
13
|
SHA-256: 186d750eb496a4c17a76385f82be6aea2ac1cf2de074a811d63786cf374ea73f
|
package/dist/assets/models.yaml
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
# A private copy in the Slopify data directory reloads automatically.
|
|
3
3
|
# Omitted pricing means unknown, never free. Provider concurrency is capped at five.
|
|
4
4
|
schemaVersion: 1
|
|
5
|
-
updatedAt: 2026-09-
|
|
5
|
+
updatedAt: 2026-09-26
|
|
6
6
|
providers:
|
|
7
7
|
openrouter:
|
|
8
8
|
maxConcurrent: 3
|
|
@@ -141,6 +141,7 @@ image:
|
|
|
141
141
|
source: https://developers.openai.com/api/docs/guides/image-generation
|
|
142
142
|
keywords:
|
|
143
143
|
- image
|
|
144
|
+
- reference
|
|
144
145
|
pricing:
|
|
145
146
|
note: Automatic quality and generated image tokens determine the charge; a fixed
|
|
146
147
|
image price is unavailable.
|
|
@@ -154,6 +155,7 @@ image:
|
|
|
154
155
|
source: https://developers.openai.com/api/docs/guides/image-generation
|
|
155
156
|
keywords:
|
|
156
157
|
- image
|
|
158
|
+
- reference
|
|
157
159
|
pricing:
|
|
158
160
|
note: Automatic quality and generated image tokens determine the charge; a fixed
|
|
159
161
|
image price is unavailable.
|
|
@@ -164,6 +166,7 @@ image:
|
|
|
164
166
|
source: https://developers.openai.com/api/docs/guides/image-generation
|
|
165
167
|
keywords:
|
|
166
168
|
- image
|
|
169
|
+
- reference
|
|
167
170
|
pricing:
|
|
168
171
|
note: Automatic quality and generated image tokens determine the charge; a fixed
|
|
169
172
|
image price is unavailable.
|
|
@@ -174,6 +177,7 @@ image:
|
|
|
174
177
|
source: https://ai.google.dev/gemini-api/docs/pricing
|
|
175
178
|
keywords:
|
|
176
179
|
- image
|
|
180
|
+
- reference
|
|
177
181
|
pricing:
|
|
178
182
|
perImage: 0.101
|
|
179
183
|
note: 2K image output price; prompt, thinking and other input tokens are
|
|
@@ -187,6 +191,7 @@ image:
|
|
|
187
191
|
source: https://ai.google.dev/gemini-api/docs/models
|
|
188
192
|
keywords:
|
|
189
193
|
- image
|
|
194
|
+
- reference
|
|
190
195
|
pricing:
|
|
191
196
|
note: Price depends on your account or generated output; estimate unavailable.
|
|
192
197
|
image:
|
|
@@ -198,6 +203,7 @@ image:
|
|
|
198
203
|
source: https://fal.ai/models/fal-ai/flux-2
|
|
199
204
|
keywords:
|
|
200
205
|
- image
|
|
206
|
+
- reference
|
|
201
207
|
pricing:
|
|
202
208
|
note: Price depends on your account or generated output; estimate unavailable.
|
|
203
209
|
image: *a4
|
|
@@ -207,6 +213,7 @@ image:
|
|
|
207
213
|
source: https://fal.ai/models/fal-ai/nano-banana-2
|
|
208
214
|
keywords:
|
|
209
215
|
- image
|
|
216
|
+
- reference
|
|
210
217
|
pricing:
|
|
211
218
|
note: Price depends on your account or generated output; estimate unavailable.
|
|
212
219
|
image: *a4
|
|
@@ -237,6 +244,87 @@ image:
|
|
|
237
244
|
pricing:
|
|
238
245
|
note: Price depends on your account or generated output; estimate unavailable.
|
|
239
246
|
image: *a4
|
|
247
|
+
# Image-to-video models, marked by the `video` keyword: Animate images turns a still into a
|
|
248
|
+
# 5-second clip with one of these. perImage is the price of one 5-second clip.
|
|
249
|
+
- provider: fal
|
|
250
|
+
id: fal-ai/kling-video/v2.5-turbo/pro/image-to-video
|
|
251
|
+
name: Kling 2.5 Turbo Pro (image to video)
|
|
252
|
+
source: https://fal.ai/models/fal-ai/kling-video/v2.5-turbo/pro/image-to-video
|
|
253
|
+
keywords:
|
|
254
|
+
- video
|
|
255
|
+
pricing:
|
|
256
|
+
perImage: 0.35
|
|
257
|
+
note: Public fal.ai rate verified 2026-09-26, one 5-second clip.
|
|
258
|
+
image:
|
|
259
|
+
aspectRatios:
|
|
260
|
+
- 16:9
|
|
261
|
+
- 9:16
|
|
262
|
+
- provider: fal
|
|
263
|
+
id: fal-ai/wan-25-preview/image-to-video
|
|
264
|
+
name: Wan 2.5 (image to video, 720p)
|
|
265
|
+
source: https://fal.ai/models/fal-ai/wan-25-preview/image-to-video
|
|
266
|
+
keywords:
|
|
267
|
+
- video
|
|
268
|
+
pricing:
|
|
269
|
+
perImage: 0.5
|
|
270
|
+
note: Public fal.ai rate verified 2026-09-26, $0.10 a second at 720p, one 5-second clip.
|
|
271
|
+
image:
|
|
272
|
+
aspectRatios:
|
|
273
|
+
- 16:9
|
|
274
|
+
- 9:16
|
|
275
|
+
- provider: fal
|
|
276
|
+
id: fal-ai/bytedance/seedance/v1/pro/fast/image-to-video
|
|
277
|
+
name: Seedance 1.0 Pro Fast (image to video, 720p)
|
|
278
|
+
source: https://fal.ai/models/fal-ai/bytedance/seedance/v1/pro/fast/image-to-video
|
|
279
|
+
keywords:
|
|
280
|
+
- video
|
|
281
|
+
pricing:
|
|
282
|
+
perImage: 0.12
|
|
283
|
+
note: fal.ai bills by video tokens; about $0.12 for a 5-second 720p clip (verified
|
|
284
|
+
2026-09-26), more for larger frames.
|
|
285
|
+
image:
|
|
286
|
+
aspectRatios:
|
|
287
|
+
- 16:9
|
|
288
|
+
- 9:16
|
|
289
|
+
- provider: replicate
|
|
290
|
+
id: kwaivgi/kling-v2.5-turbo-pro
|
|
291
|
+
name: Kling 2.5 Turbo Pro (image to video)
|
|
292
|
+
source: https://replicate.com/kwaivgi/kling-v2.5-turbo-pro
|
|
293
|
+
keywords:
|
|
294
|
+
- video
|
|
295
|
+
pricing:
|
|
296
|
+
perImage: 0.35
|
|
297
|
+
note: Public Replicate rate verified 2026-09-26, $0.07 a second, one 5-second clip.
|
|
298
|
+
image:
|
|
299
|
+
aspectRatios:
|
|
300
|
+
- 16:9
|
|
301
|
+
- 9:16
|
|
302
|
+
- provider: replicate
|
|
303
|
+
id: wan-video/wan-2.5-i2v
|
|
304
|
+
name: Wan 2.5 (image to video, 720p)
|
|
305
|
+
source: https://replicate.com/wan-video/wan-2.5-i2v
|
|
306
|
+
keywords:
|
|
307
|
+
- video
|
|
308
|
+
pricing:
|
|
309
|
+
perImage: 0.5
|
|
310
|
+
note: Public Replicate rate verified 2026-09-26, $0.10 a second at 720p, one 5-second clip.
|
|
311
|
+
image:
|
|
312
|
+
aspectRatios:
|
|
313
|
+
- 16:9
|
|
314
|
+
- 9:16
|
|
315
|
+
- provider: replicate
|
|
316
|
+
id: bytedance/seedance-1-pro-fast
|
|
317
|
+
name: Seedance 1 Pro Fast (image to video, 720p)
|
|
318
|
+
source: https://replicate.com/bytedance/seedance-1-pro-fast
|
|
319
|
+
keywords:
|
|
320
|
+
- video
|
|
321
|
+
pricing:
|
|
322
|
+
perImage: 0.125
|
|
323
|
+
note: Public Replicate rate verified 2026-09-26, $0.025 a second at 720p, one 5-second clip.
|
|
324
|
+
image:
|
|
325
|
+
aspectRatios:
|
|
326
|
+
- 16:9
|
|
327
|
+
- 9:16
|
|
240
328
|
tts:
|
|
241
329
|
- provider: elevenlabs
|
|
242
330
|
id: eleven_v3
|
package/dist/catalog/registry.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { thinkingModes } from "../kernel/ports/llm.js";
|
|
1
2
|
import { providerError } from "../kernel/ports/model.js";
|
|
2
3
|
import { isLocalCliProvider } from "../slices/settings/model.js";
|
|
4
|
+
import { referenceRefusal, takesReferenceImage } from "./schema.js";
|
|
3
5
|
export function curateRegistry(registry, catalogue) {
|
|
4
6
|
const requireModel = (provider, family, id) => {
|
|
5
7
|
const models = catalogue.models(provider, family);
|
|
@@ -67,7 +69,7 @@ export function curateRegistry(registry, catalogue) {
|
|
|
67
69
|
...m,
|
|
68
70
|
...("llm" in m && m.llm.thinking
|
|
69
71
|
? {
|
|
70
|
-
thinkingModes: Object.keys(m.llm.thinking).filter((mode) =>
|
|
72
|
+
thinkingModes: Object.keys(m.llm.thinking).filter((mode) => thinkingModes.includes(mode)),
|
|
71
73
|
}
|
|
72
74
|
: {}),
|
|
73
75
|
})),
|
|
@@ -100,10 +102,19 @@ export function curateRegistry(registry, catalogue) {
|
|
|
100
102
|
...port,
|
|
101
103
|
models: () => port.models(),
|
|
102
104
|
generate: async (request) => {
|
|
103
|
-
|
|
105
|
+
const selected = (await port.models()).find((model) => model.id === request.model);
|
|
106
|
+
if (selected === undefined)
|
|
104
107
|
throw providerError({
|
|
105
108
|
kind: "unsupported",
|
|
106
|
-
message:
|
|
109
|
+
message: request.model === "codex-imagegen"
|
|
110
|
+
? "Image generation is not available in the Codex CLI on your computer. Update Codex CLI, or choose another image provider in the Edit tab under Edit project, then use Resume."
|
|
111
|
+
: "The chosen Codex model for images is not in the Codex CLI's model list on your computer. Update Codex CLI, or pick another model for images: in the Edit tab, choose Edit project and change it under Providers, then use Resume.",
|
|
112
|
+
});
|
|
113
|
+
if (request.thinking !== undefined &&
|
|
114
|
+
!selected.thinkingModes?.includes(request.thinking))
|
|
115
|
+
throw providerError({
|
|
116
|
+
kind: "unsupported",
|
|
117
|
+
message: "The chosen Codex model does not support this effort for images. In the Edit tab, choose Edit project and change the images' Effort under Providers, then use Resume.",
|
|
107
118
|
});
|
|
108
119
|
return port.generate(request);
|
|
109
120
|
},
|
|
@@ -119,6 +130,16 @@ export function curateRegistry(registry, catalogue) {
|
|
|
119
130
|
kind: "unsupported",
|
|
120
131
|
message: "The chosen image model cannot make images in this video's shape. In the Edit tab, choose Edit project and change it under Providers, then use Resume.",
|
|
121
132
|
});
|
|
133
|
+
if (request.thinking !== undefined)
|
|
134
|
+
throw providerError({
|
|
135
|
+
kind: "unsupported",
|
|
136
|
+
message: "The chosen image model has no effort setting. In the Edit tab, choose Edit project and set the images' Effort back to Model default under Providers, then use Resume.",
|
|
137
|
+
});
|
|
138
|
+
if (request.reference !== undefined && !takesReferenceImage(model))
|
|
139
|
+
throw providerError({
|
|
140
|
+
kind: "unsupported",
|
|
141
|
+
message: referenceRefusal,
|
|
142
|
+
});
|
|
122
143
|
return port.generate(request);
|
|
123
144
|
},
|
|
124
145
|
};
|
|
@@ -6,7 +6,7 @@ export async function checkRuntimeModel(modelsFor, provider, family, id, thinkin
|
|
|
6
6
|
const model = (await modelsFor(provider, family)).find((row) => row.id === id);
|
|
7
7
|
if (model === undefined)
|
|
8
8
|
return "missing";
|
|
9
|
-
if (family
|
|
9
|
+
if (family !== "tts" && thinking !== undefined && !model.thinkingModes?.includes(thinking))
|
|
10
10
|
return "thinking";
|
|
11
11
|
return "available";
|
|
12
12
|
}
|
package/dist/catalog/schema.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { thinkingModes } from "../kernel/ports/llm.js";
|
|
2
3
|
import { providers } from "../slices/settings/model.js";
|
|
3
4
|
const money = z.number().finite().nonnegative();
|
|
4
5
|
const base = z.object({
|
|
@@ -29,7 +30,7 @@ export const llmModelSchema = base
|
|
|
29
30
|
maxOutputTokens: z.number().int().positive().optional(),
|
|
30
31
|
webSearch: z.boolean().default(false),
|
|
31
32
|
thinking: z
|
|
32
|
-
.partialRecord(z.enum(
|
|
33
|
+
.partialRecord(z.enum(thinkingModes), z
|
|
33
34
|
.object({
|
|
34
35
|
budget: z.number().int().min(-1).optional(),
|
|
35
36
|
level: z.enum(["minimal", "low", "medium", "high"]).optional(),
|
|
@@ -89,3 +90,21 @@ export const catalogueSchema = z
|
|
|
89
90
|
});
|
|
90
91
|
}
|
|
91
92
|
});
|
|
93
|
+
// Image-to-video models live in the image list, under the image providers that run them,
|
|
94
|
+
// marked by the `video` keyword; their perImage is the price of one clip. A catalogue that
|
|
95
|
+
// older Slopify versions read keeps its shape, and the image pickers leave these out.
|
|
96
|
+
export function isVideoModel(model) {
|
|
97
|
+
return model.keywords.includes("video");
|
|
98
|
+
}
|
|
99
|
+
// An image model that also takes an input image - so it can draw with the project's
|
|
100
|
+
// establishing image as its visual reference - carries the `reference` keyword, the same way
|
|
101
|
+
// and for the same reason the image-to-video models carry `video`.
|
|
102
|
+
export function takesReferenceImage(model) {
|
|
103
|
+
return model.keywords.includes("reference");
|
|
104
|
+
}
|
|
105
|
+
// What the run says when the chosen image model cannot take the establishing image.
|
|
106
|
+
export const referenceRefusal = "The chosen image model can't use an establishing image as a reference, so the images can't be drawn to match it. Choose an image model that can (Codex CLI, OpenAI, Google, or a fal.ai model listed with reference) under Images → Model on Play or in Edit project → Providers, or set Establishing image to Off in the Images section.";
|
|
107
|
+
// The image-to-video models a provider offers, as Animate images lists them.
|
|
108
|
+
export function videoModelsOf(catalogue, provider) {
|
|
109
|
+
return catalogue.image.filter((m) => m.provider === provider && m.enabled && !m.deprecated && isVideoModel(m));
|
|
110
|
+
}
|
package/dist/catalog/store.js
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSyn
|
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { parse } from "yaml";
|
|
4
4
|
import { isLocalCliProvider } from "../slices/settings/model.js";
|
|
5
|
-
import { catalogueSchema } from "./schema.js";
|
|
5
|
+
import { catalogueSchema, isVideoModel } from "./schema.js";
|
|
6
6
|
export const catalogueSource = "https://raw.githubusercontent.com/GentBajko/slopify/main/packages/app/src/assets/models.yaml";
|
|
7
7
|
export function parseCatalogue(text) {
|
|
8
8
|
if (Buffer.byteLength(text) > 1024 * 1024)
|
|
@@ -86,7 +86,7 @@ export function createCatalogueStore(deps) {
|
|
|
86
86
|
}
|
|
87
87
|
return {
|
|
88
88
|
read,
|
|
89
|
-
models: (provider, family) => read()[family].filter((m) => m.provider === provider && m.enabled && !m.deprecated),
|
|
89
|
+
models: (provider, family) => read()[family].filter((m) => m.provider === provider && m.enabled && !m.deprecated && !isVideoModel(m)),
|
|
90
90
|
status: () => ({ updatedAt: read().updatedAt, path, warning, source: catalogueSource }),
|
|
91
91
|
refresh: () => {
|
|
92
92
|
refreshing ??= refresh().finally(() => {
|
package/dist/catalog/validate.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { usesNarrationPreparation, usesYoutubeDescription, } from "../slices/admission/rules.js";
|
|
1
|
+
import { usesNarrationPreparation, usesReference, usesShorts, usesYoutubeDescription, } from "../slices/admission/rules.js";
|
|
2
2
|
import { isLocalCliProvider } from "../slices/settings/model.js";
|
|
3
|
+
import { usesAnimation } from "../slices/video/edit-settings.js";
|
|
4
|
+
import { referenceRefusal, takesReferenceImage, videoModelsOf } from "./schema.js";
|
|
3
5
|
export function modelFields(draft, catalogue) {
|
|
4
6
|
if (!catalogue)
|
|
5
7
|
return [];
|
|
@@ -10,7 +12,8 @@ export function modelFields(draft, catalogue) {
|
|
|
10
12
|
draft.intro?.mode === "llm" ||
|
|
11
13
|
draft.outro?.mode === "llm" ||
|
|
12
14
|
usesNarrationPreparation(draft) ||
|
|
13
|
-
usesYoutubeDescription(draft)
|
|
15
|
+
usesYoutubeDescription(draft) ||
|
|
16
|
+
usesShorts(draft);
|
|
14
17
|
const checks = [
|
|
15
18
|
{ field: "llm", family: "llm", choice: draft.llm, needed: needLlm },
|
|
16
19
|
{
|
|
@@ -24,7 +27,8 @@ export function modelFields(draft, catalogue) {
|
|
|
24
27
|
family: "image",
|
|
25
28
|
choice: draft.images,
|
|
26
29
|
needed: draft.sources.images === "generate" ||
|
|
27
|
-
["from_prompt", "prompt_by_llm"].includes(draft.sources.thumbnail)
|
|
30
|
+
["from_prompt", "prompt_by_llm"].includes(draft.sources.thumbnail) ||
|
|
31
|
+
usesShorts(draft),
|
|
28
32
|
},
|
|
29
33
|
];
|
|
30
34
|
for (const { field, family, choice, needed } of checks) {
|
|
@@ -41,16 +45,55 @@ export function modelFields(draft, catalogue) {
|
|
|
41
45
|
field,
|
|
42
46
|
message: "This model does not support that thinking setting. Choose another.",
|
|
43
47
|
});
|
|
48
|
+
else if ("image" in model && choice.thinking)
|
|
49
|
+
fields.push({
|
|
50
|
+
field: "images.thinking",
|
|
51
|
+
message: "This image model has no effort setting. Set Effort back to Model default.",
|
|
52
|
+
});
|
|
44
53
|
else if ("llm" in model && draft.sources.research === "generate" && !model.llm.webSearch)
|
|
45
54
|
fields.push({
|
|
46
55
|
field,
|
|
47
56
|
message: "This model cannot search the web for research. Choose another model or turn Research off.",
|
|
48
57
|
});
|
|
49
|
-
else if ("image" in model &&
|
|
58
|
+
else if ("image" in model &&
|
|
59
|
+
(draft.sources.images === "generate" ||
|
|
60
|
+
["from_prompt", "prompt_by_llm"].includes(draft.sources.thumbnail)) &&
|
|
61
|
+
!model.image.aspectRatios.includes(draft.format))
|
|
50
62
|
fields.push({
|
|
51
63
|
field,
|
|
52
64
|
message: "This image model cannot make images in this video's shape. Choose another model.",
|
|
53
65
|
});
|
|
66
|
+
// The shorts are always vertical, whatever shape the video is.
|
|
67
|
+
else if ("image" in model && usesShorts(draft) && !model.image.aspectRatios.includes("9:16"))
|
|
68
|
+
fields.push({
|
|
69
|
+
field,
|
|
70
|
+
message: "This image model cannot make the vertical (9:16) images Shorts need. Choose another image model, or turn Shorts off.",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// The establishing image needs a model that takes an input image; the catalogue says which.
|
|
74
|
+
// The Codex CLI always can, so only the keyed providers are looked up.
|
|
75
|
+
if (usesReference(draft) &&
|
|
76
|
+
draft.images !== undefined &&
|
|
77
|
+
!isLocalCliProvider(draft.images.provider)) {
|
|
78
|
+
const images = draft.images;
|
|
79
|
+
const model = catalogue.models(images.provider, "image").find((m) => m.id === images.model);
|
|
80
|
+
if (model !== undefined && !takesReferenceImage(model))
|
|
81
|
+
fields.push({ field: "reference.source", message: referenceRefusal });
|
|
82
|
+
}
|
|
83
|
+
// Animate images runs on the image provider's image-to-video models.
|
|
84
|
+
if (usesAnimation(draft) && draft.images !== undefined) {
|
|
85
|
+
const model = draft.videoEdit?.animateModel ?? "";
|
|
86
|
+
const offered = videoModelsOf(catalogue.read(), draft.images.provider);
|
|
87
|
+
if (offered.length === 0)
|
|
88
|
+
fields.push({
|
|
89
|
+
field: "videoEdit.animateModel",
|
|
90
|
+
message: "This image provider can't animate images. Choose fal.ai or Replicate as the image provider (on Play's Images rail, or in Edit project → Providers), or turn Animate images off.",
|
|
91
|
+
});
|
|
92
|
+
else if (model.trim() !== "" && !offered.some((one) => one.id === model))
|
|
93
|
+
fields.push({
|
|
94
|
+
field: "videoEdit.animateModel",
|
|
95
|
+
message: "This image-to-video model is no longer in Slopify's model list. Choose another under Animate images.",
|
|
96
|
+
});
|
|
54
97
|
}
|
|
55
98
|
return fields;
|
|
56
99
|
}
|
|
@@ -62,6 +62,10 @@ async function main() {
|
|
|
62
62
|
console.log(`Project files on this machine: ${result.projects}`);
|
|
63
63
|
if (result.recovery)
|
|
64
64
|
console.log(`Recovery volume retained: ${result.recovery}. Previous containers remain stopped.`);
|
|
65
|
+
for (const name of result.pruned.removed)
|
|
66
|
+
console.log(`Removed older recovery volume: ${name}`);
|
|
67
|
+
for (const problem of result.pruned.problems)
|
|
68
|
+
console.warn(problem);
|
|
65
69
|
}
|
|
66
70
|
finally {
|
|
67
71
|
process.off("SIGINT", abort);
|
|
@@ -4,6 +4,24 @@ import { setTimeout as delay } from "node:timers/promises";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { containerSchema, digestSchema, } from "./state.js";
|
|
6
6
|
import { contains } from "./tree.js";
|
|
7
|
+
const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
8
|
+
// A recovery volume says which update, installation and container made it. Earlier launchers
|
|
9
|
+
// set only the transaction label; `prune.ts` asks those for the update's own record instead.
|
|
10
|
+
export function recoveryLabels(j) {
|
|
11
|
+
return {
|
|
12
|
+
"io.slopify.transaction": j.id,
|
|
13
|
+
"io.slopify.installation": j.installation,
|
|
14
|
+
"io.slopify.container": j.name,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// The id a recovery volume of `volume` carries in its name, or null for any other volume.
|
|
18
|
+
export function recoveryId(volume, name) {
|
|
19
|
+
const prefix = `${volume}-recovery-`;
|
|
20
|
+
if (!name.startsWith(prefix))
|
|
21
|
+
return null;
|
|
22
|
+
const id = name.slice(prefix.length);
|
|
23
|
+
return uuid.test(id) ? id : null;
|
|
24
|
+
}
|
|
7
25
|
// Docker's exit code alone says nothing, so its last error line decides the advice: the daemon
|
|
8
26
|
// being stopped and the socket being off-limits are by far the most common first-run failures.
|
|
9
27
|
export function dockerFailure(args, result) {
|
|
@@ -294,9 +312,47 @@ export function dockerEngine(runner, signal, env) {
|
|
|
294
312
|
const all = (await command(["volume", "ls", "--format", "{{.Name}}"])).split(/\s+/);
|
|
295
313
|
if (all.includes(j.backup))
|
|
296
314
|
throw new Error("Recovery volume already exists; reconcile the journal before retrying.");
|
|
297
|
-
await command([
|
|
315
|
+
await command([
|
|
316
|
+
"volume",
|
|
317
|
+
"create",
|
|
318
|
+
...Object.entries(recoveryLabels(j)).flatMap(([key, value]) => [
|
|
319
|
+
"--label",
|
|
320
|
+
`${key}=${value}`,
|
|
321
|
+
]),
|
|
322
|
+
j.backup,
|
|
323
|
+
]);
|
|
298
324
|
return digestSchema.parse(await helper(j.image, [...source(j.volume, null), ...mount("volume", j.backup, "/backup")], "snapshot"));
|
|
299
325
|
},
|
|
326
|
+
recoveryVolumes: async (volume) => {
|
|
327
|
+
const names = (await command([
|
|
328
|
+
"volume",
|
|
329
|
+
"ls",
|
|
330
|
+
"--filter",
|
|
331
|
+
"label=io.slopify.transaction",
|
|
332
|
+
"--format",
|
|
333
|
+
"{{.Name}}",
|
|
334
|
+
]))
|
|
335
|
+
.split(/\s+/)
|
|
336
|
+
.filter((name) => recoveryId(volume, name) !== null);
|
|
337
|
+
const found = [];
|
|
338
|
+
for (const name of names) {
|
|
339
|
+
const labels = z
|
|
340
|
+
.record(z.string(), z.string())
|
|
341
|
+
.nullable()
|
|
342
|
+
.parse(JSON.parse(await command(["volume", "inspect", "--format", "{{json .Labels}}", name])));
|
|
343
|
+
found.push({
|
|
344
|
+
name,
|
|
345
|
+
transaction: labels?.["io.slopify.transaction"] ?? null,
|
|
346
|
+
installation: labels?.["io.slopify.installation"] ?? null,
|
|
347
|
+
container: labels?.["io.slopify.container"] ?? null,
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
return found;
|
|
351
|
+
},
|
|
352
|
+
// Never forced: Docker refuses a volume a container still mounts, and that refusal stands.
|
|
353
|
+
removeVolume: async (name) => {
|
|
354
|
+
await command(["volume", "rm", name]);
|
|
355
|
+
},
|
|
300
356
|
restore: async (j) => {
|
|
301
357
|
if (!j.backupDigest)
|
|
302
358
|
throw new Error("No verified private snapshot; automatic restore is unsafe.");
|