@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 { dirname, join } from "node:path";
|
|
|
4
4
|
import { readVersion } from "../../kernel/version.js";
|
|
5
5
|
import { assertVolumeClaims } from "./claims.js";
|
|
6
6
|
import { readCommittedJournal } from "./committed.js";
|
|
7
|
+
import { pruneRecoveryVolumes } from "./prune.js";
|
|
7
8
|
import { assertSourceIdentity, recoverInstallation } from "./recover.js";
|
|
8
9
|
import { journalSchema, privateDirectory, readState, receiptSchema, selectProjects, writeState, } from "./state.js";
|
|
9
10
|
import { assertIdentity, assertWritableTree, identity, isMissing, privateTree, safePath, syncDirectory, treeDigest, } from "./tree.js";
|
|
@@ -91,7 +92,12 @@ export async function installProjects(c, e, recovery) {
|
|
|
91
92
|
if (!old.running)
|
|
92
93
|
await e.command(["start", old.id]);
|
|
93
94
|
await e.health(old.id, j.token, expectedVersion);
|
|
94
|
-
return {
|
|
95
|
+
return {
|
|
96
|
+
url: `http://${old.port}`,
|
|
97
|
+
projects,
|
|
98
|
+
recovery: j.backup,
|
|
99
|
+
pruned: { removed: [], problems: [] },
|
|
100
|
+
};
|
|
95
101
|
}
|
|
96
102
|
const id = randomUUID();
|
|
97
103
|
const directory = join(c.directory, id);
|
|
@@ -229,7 +235,9 @@ export async function installProjects(c, e, recovery) {
|
|
|
229
235
|
const ready = await e.inspect(candidate);
|
|
230
236
|
if (!ready?.port)
|
|
231
237
|
throw new Error("Slopify started but Docker did not report which port it is on. Run the same command again to finish.");
|
|
232
|
-
|
|
238
|
+
// Only now, with the new container committed and healthy, are older snapshots redundant.
|
|
239
|
+
const pruned = await pruneRecoveryVolumes(c, e, j);
|
|
240
|
+
return { url: `http://${ready.port}`, projects, recovery: j.backup, pruned };
|
|
233
241
|
}
|
|
234
242
|
catch (cause) {
|
|
235
243
|
const current = await readState(receiptPath, receiptSchema, c.uid);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { recoveryId } from "./engine.js";
|
|
3
|
+
import { journalSchema, readState } from "./state.js";
|
|
4
|
+
// Phases an update's record is left in once it is over, one way or the other. Anything else
|
|
5
|
+
// is an update that never finished, and its volume may be the only copy of the data.
|
|
6
|
+
const settled = new Set(["healthy", "committed", "restored", "rolled-back"]);
|
|
7
|
+
// After an update is committed and healthy, keep only its own recovery volume. An older one is
|
|
8
|
+
// removed only when all of these hold, so a volume some other installation, container or person
|
|
9
|
+
// made is never touched:
|
|
10
|
+
// - its name is exactly `<this data volume>-recovery-<uuid>`, never the data volume itself;
|
|
11
|
+
// - its io.slopify.transaction label is that same uuid (every launcher has set it);
|
|
12
|
+
// - the container label, which this launcher adds, names this container (earlier launchers
|
|
13
|
+
// set none, and for those the next check decides alone);
|
|
14
|
+
// - the update's own record, <install dir>/<uuid>/journal.json in this container's private
|
|
15
|
+
// folder, exists and names this container, data volume and Docker, the installation the
|
|
16
|
+
// volume's label names, and exactly this volume as its backup, and says that update is
|
|
17
|
+
// over. A first install that was rolled back has an installation id of its own, so the
|
|
18
|
+
// id is matched against the volume's label rather than the current update's.
|
|
19
|
+
// Docker refuses to remove a volume a container still uses; that refusal is reported, not
|
|
20
|
+
// forced. Nothing here can fail the update, which has already finished.
|
|
21
|
+
export async function pruneRecoveryVolumes(c, e, j) {
|
|
22
|
+
const removed = [];
|
|
23
|
+
const problems = [];
|
|
24
|
+
let volumes;
|
|
25
|
+
try {
|
|
26
|
+
volumes = await e.recoveryVolumes(c.volume);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return {
|
|
30
|
+
removed,
|
|
31
|
+
problems: [
|
|
32
|
+
`Couldn't list older recovery volumes to tidy up, so all of them are kept. See them with docker volume ls --filter label=io.slopify.transaction and remove the ones you no longer need with docker volume rm <name>. ${reason(error)}`,
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
for (const volume of volumes) {
|
|
37
|
+
const id = recoveryId(c.volume, volume.name);
|
|
38
|
+
if (id === null || id === j.id || volume.name === j.backup || volume.name === c.volume)
|
|
39
|
+
continue;
|
|
40
|
+
if (volume.transaction !== id)
|
|
41
|
+
continue;
|
|
42
|
+
if (volume.container !== null && volume.container !== c.name)
|
|
43
|
+
continue;
|
|
44
|
+
const record = await readState(join(c.directory, id, "journal.json"), journalSchema, c.uid).catch(() => null);
|
|
45
|
+
if (record === null ||
|
|
46
|
+
record.id !== id ||
|
|
47
|
+
record.name !== c.name ||
|
|
48
|
+
record.volume !== c.volume ||
|
|
49
|
+
record.daemon !== j.daemon ||
|
|
50
|
+
(volume.installation !== null && volume.installation !== record.installation) ||
|
|
51
|
+
record.backup !== volume.name ||
|
|
52
|
+
!settled.has(record.phase))
|
|
53
|
+
continue;
|
|
54
|
+
try {
|
|
55
|
+
await e.removeVolume(volume.name);
|
|
56
|
+
removed.push(volume.name);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
problems.push(`Couldn't remove the older recovery volume ${volume.name}, so it is kept. Once nothing uses it, remove it with docker volume rm ${volume.name}. ${reason(error)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return { removed, problems };
|
|
63
|
+
}
|
|
64
|
+
function reason(error) {
|
|
65
|
+
return error instanceof Error ? `Why: ${error.message}` : "";
|
|
66
|
+
}
|
package/dist/edge/events/hub.js
CHANGED
|
@@ -87,6 +87,17 @@ export function createHub(deps) {
|
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
89
|
}
|
|
90
|
+
// Every project event also goes to `observe` (the run notifier), after the open pages have
|
|
91
|
+
// it, whether or not a page is open to receive it.
|
|
92
|
+
export function observedHub(hub, observe) {
|
|
93
|
+
return {
|
|
94
|
+
...hub,
|
|
95
|
+
emit: (projectId, event) => {
|
|
96
|
+
hub.emit(projectId, event);
|
|
97
|
+
observe(event);
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
90
101
|
function messageOf(error) {
|
|
91
102
|
return error instanceof Error ? error.message : String(error);
|
|
92
103
|
}
|
package/dist/edge/http/app.js
CHANGED
|
@@ -4,6 +4,7 @@ import { Hono } from "hono";
|
|
|
4
4
|
import { streamSSE } from "hono/streaming";
|
|
5
5
|
import { actionRoutes } from "./actions.js";
|
|
6
6
|
import { audioPreviewRoutes } from "./audio-preview.js";
|
|
7
|
+
import { backupRoutes } from "./backups.js";
|
|
7
8
|
import { checkpointRoutes } from "./checkpoints.js";
|
|
8
9
|
import { diagnosticsRoutes } from "./diagnostics.js";
|
|
9
10
|
import { documentThemeRoutes } from "./document-themes.js";
|
|
@@ -41,6 +42,7 @@ function apiRoutes(deps, startedAt) {
|
|
|
41
42
|
}))
|
|
42
43
|
.route("/staging", stagingRoutes(deps))
|
|
43
44
|
.route("/storage", storageRoutes(deps))
|
|
45
|
+
.route("/backups", backupRoutes(deps.backups))
|
|
44
46
|
.route("/drafts", draftRoutes(deps.drafts))
|
|
45
47
|
.route("/diagnostics", diagnosticsRoutes(deps))
|
|
46
48
|
.route("/project-templates", projectTemplateRoutes(deps.drafts))
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { HTTPException } from "hono/http-exception";
|
|
4
|
+
import { backupConfigInputSchema } from "../../slices/backups/model.js";
|
|
5
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
6
|
+
// Settings → Backups: the schedule, the folder, the last result and Back up now. Writing an
|
|
7
|
+
// archive takes minutes, so Back up now answers at once and the screen polls this view.
|
|
8
|
+
export function backupRoutes(service) {
|
|
9
|
+
const backups = () => {
|
|
10
|
+
if (service === undefined)
|
|
11
|
+
throw new HTTPException(404);
|
|
12
|
+
return service;
|
|
13
|
+
};
|
|
14
|
+
return new Hono()
|
|
15
|
+
.get("/", async (c) => {
|
|
16
|
+
c.header("Cache-Control", "no-store");
|
|
17
|
+
return c.json(await backups().view());
|
|
18
|
+
})
|
|
19
|
+
.put("/", zValidator("json", backupConfigInputSchema, onInvalid), async (c) => {
|
|
20
|
+
const saved = backups().save(c.req.valid("json"));
|
|
21
|
+
if (!saved.ok)
|
|
22
|
+
return problem(c, {
|
|
23
|
+
status: 400,
|
|
24
|
+
title: titleOf(400),
|
|
25
|
+
detail: saved.detail,
|
|
26
|
+
extensions: { field: "folder" },
|
|
27
|
+
});
|
|
28
|
+
return c.json(await backups().view());
|
|
29
|
+
})
|
|
30
|
+
.post("/run", async (c) => {
|
|
31
|
+
const started = backups().runNow();
|
|
32
|
+
if (!started.ok)
|
|
33
|
+
return problem(c, {
|
|
34
|
+
status: started.status,
|
|
35
|
+
title: titleOf(started.status),
|
|
36
|
+
detail: started.detail,
|
|
37
|
+
});
|
|
38
|
+
return c.json(await backups().view(), 202);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
@@ -6,6 +6,7 @@ import { z } from "zod";
|
|
|
6
6
|
import { sniffImage } from "../../adapters/image/bytes.js";
|
|
7
7
|
import { redact } from "../../kernel/log.js";
|
|
8
8
|
import { bridgeLimits, HostFolderRefused, hostCliIds, hostCliProtocol, hostFrameSchema, hostImageSchema, hostLlmIds, hostLlmSchema, hostModelsSchema, hostOpenFolderSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
|
|
9
|
+
import { agentImageTimeoutMs } from "../../kernel/ports/image.js";
|
|
9
10
|
import { isProviderError, providerError } from "../../kernel/ports/model.js";
|
|
10
11
|
export function hostGate() {
|
|
11
12
|
let accepting = true;
|
|
@@ -226,9 +227,23 @@ export function hostCliRoutes(options) {
|
|
|
226
227
|
const job = c.get("job");
|
|
227
228
|
const controller = new AbortController();
|
|
228
229
|
const signal = AbortSignal.any([job.signal, controller.signal]);
|
|
229
|
-
const timer = setTimeout(() => controller.abort(),
|
|
230
|
+
const timer = setTimeout(() => controller.abort(), agentImageTimeoutMs);
|
|
230
231
|
try {
|
|
231
|
-
const
|
|
232
|
+
const { reference, thinking, ...rest } = body.data;
|
|
233
|
+
const image = reference === undefined
|
|
234
|
+
? undefined
|
|
235
|
+
: {
|
|
236
|
+
bytes: new Uint8Array(Buffer.from(reference.base64, "base64")),
|
|
237
|
+
mime: reference.mime,
|
|
238
|
+
};
|
|
239
|
+
if (image !== undefined && sniffImage(image.bytes) !== image.mime)
|
|
240
|
+
return invalid(c);
|
|
241
|
+
const result = await ports.image.generate({
|
|
242
|
+
...rest,
|
|
243
|
+
...(thinking === undefined ? {} : { thinking }),
|
|
244
|
+
...(image === undefined ? {} : { reference: image }),
|
|
245
|
+
signal,
|
|
246
|
+
});
|
|
232
247
|
if (signal.aborted)
|
|
233
248
|
throw providerError({ kind: "unavailable", message: unavailable });
|
|
234
249
|
if (result.bytes.byteLength > bridgeLimits.image || sniffImage(result.bytes) !== result.mime)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { zValidator } from "@hono/zod-validator";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { videoModelsOf } from "../../catalog/schema.js";
|
|
4
5
|
import { cliPathMaxLength, saveCliPath } from "../../slices/settings/cli-paths.js";
|
|
5
6
|
import { keyStatus, removeProviderKey, saveProviderKey } from "../../slices/settings/keys.js";
|
|
6
7
|
import { isLocalCliProvider, providerById, providerIds } from "../../slices/settings/model.js";
|
|
@@ -56,6 +57,13 @@ export function providerRoutes(deps) {
|
|
|
56
57
|
.get("/", async (c) => c.json({ providers: await providerStatuses(readiness) }))
|
|
57
58
|
.get("/:id/models", zValidator("param", providerParam, onInvalid), async (c) => {
|
|
58
59
|
const { id } = c.req.valid("param");
|
|
60
|
+
// Animate images lists the provider's image-to-video models, which only the catalogue
|
|
61
|
+
// names.
|
|
62
|
+
if (c.req.query("video") === "1")
|
|
63
|
+
return c.json({
|
|
64
|
+
models: deps.catalogue === undefined ? [] : videoModelsOf(deps.catalogue.read(), id),
|
|
65
|
+
allowsCustom: false,
|
|
66
|
+
});
|
|
59
67
|
if (deps.catalogue && !isLocalCliProvider(id))
|
|
60
68
|
return c.json({
|
|
61
69
|
models: deps.catalogue.models(id, providerById(id).family).map((m) => ({
|
|
@@ -18,7 +18,7 @@ function refused(c, result) {
|
|
|
18
18
|
conflict: "This schedule changed while you were editing, possibly because a run just started. Reload the page to see the latest, then make your change again.",
|
|
19
19
|
"cancel-required": "This schedule is still active. Cancel it first, then delete it. Completed schedules can be deleted straight away.",
|
|
20
20
|
"missing-template": "The template you picked was deleted or changed. Choose a template again, then save.",
|
|
21
|
-
"unsupported-media": "This template uses audio, images
|
|
21
|
+
"unsupported-media": "This template uses audio, images, a thumbnail or shorts background music you supplied, so it cannot run on a schedule: a scheduled run has no one to attach the file again. Pick a template that generates these instead, or remove the music under Outputs → Export → More shorts options on Play and save the template again.",
|
|
22
22
|
"not-due": "This one-off time has already passed. Choose a time in the future.",
|
|
23
23
|
"spend-limit": "The estimated cost is above this schedule's spend limit, or some prices are unknown. Raise the spend limit or choose models with known prices.",
|
|
24
24
|
readiness: "A provider this template needs is not ready, for example a missing API key, model or voice. Check Settings → Providers, then try again.",
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { zValidator } from "@hono/zod-validator";
|
|
2
2
|
import { Hono } from "hono";
|
|
3
3
|
import { z } from "zod";
|
|
4
|
+
import { notificationUrlMax, notificationUrlProblem, testNotice, webhookBody, } from "../../slices/notifications/rules.js";
|
|
5
|
+
import { createNotificationSender, sendFailureText } from "../../slices/notifications/send.js";
|
|
6
|
+
import { readNotificationUrl, saveNotificationUrl } from "../../slices/notifications/settings.js";
|
|
4
7
|
import { appearances, providerIds } from "../../slices/settings/model.js";
|
|
5
8
|
import { readSettings, saveSettings } from "../../slices/settings/playback.js";
|
|
6
9
|
import { addVoice, removeVoice, voiceIdMax, voiceNameMax, voices, } from "../../slices/settings/voices.js";
|
|
@@ -13,6 +16,9 @@ const voiceBody = z.object({
|
|
|
13
16
|
name: z.string(),
|
|
14
17
|
voiceId: z.string(),
|
|
15
18
|
});
|
|
19
|
+
// Length and scheme are the slice's rules, so their sentences reach the field; this bound only
|
|
20
|
+
// keeps a pasted novel off the parser.
|
|
21
|
+
const notificationBody = z.object({ url: z.string().max(notificationUrlMax * 2) });
|
|
16
22
|
const playbackBody = z.object({
|
|
17
23
|
silenceGapSeconds: z.number(),
|
|
18
24
|
appearance: z.enum(appearances),
|
|
@@ -22,6 +28,7 @@ const playbackBody = z.object({
|
|
|
22
28
|
export function settingsRoutes(deps) {
|
|
23
29
|
const playback = { db: deps.db, log: deps.log };
|
|
24
30
|
const voiceDeps = { db: deps.db, ids: deps.ids };
|
|
31
|
+
const send = deps.sendNotification ?? createNotificationSender(globalThis.fetch);
|
|
25
32
|
return new Hono()
|
|
26
33
|
.get("/", (c) => c.json(readSettings(playback)))
|
|
27
34
|
.put("/", zValidator("json", playbackBody, onInvalid), (c) => {
|
|
@@ -35,6 +42,43 @@ export function settingsRoutes(deps) {
|
|
|
35
42
|
});
|
|
36
43
|
}
|
|
37
44
|
return c.json(result.settings);
|
|
45
|
+
})
|
|
46
|
+
.get("/notifications", (c) => c.json({ url: readNotificationUrl(deps.db) }))
|
|
47
|
+
.put("/notifications", zValidator("json", notificationBody, onInvalid), (c) => {
|
|
48
|
+
const result = saveNotificationUrl(deps.db, c.req.valid("json").url);
|
|
49
|
+
if (!result.ok) {
|
|
50
|
+
return problem(c, {
|
|
51
|
+
status: 400,
|
|
52
|
+
title: titleOf(400),
|
|
53
|
+
detail: `The Notification URL wasn't saved: ${result.message} Fix it in Settings → Notifications → Notification URL, then press Save.`,
|
|
54
|
+
extensions: { fields: [{ field: "url", message: result.message }] },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return c.json({ url: result.url });
|
|
58
|
+
})
|
|
59
|
+
.post("/notifications/test", zValidator("json", notificationBody, onInvalid), async (c) => {
|
|
60
|
+
const url = c.req.valid("json").url.trim();
|
|
61
|
+
const invalid = url === ""
|
|
62
|
+
? "Enter a Notification URL first, for example https://ntfy.sh/your-topic."
|
|
63
|
+
: notificationUrlProblem(url);
|
|
64
|
+
if (invalid !== undefined) {
|
|
65
|
+
return problem(c, {
|
|
66
|
+
status: 400,
|
|
67
|
+
title: titleOf(400),
|
|
68
|
+
detail: `The test notification wasn't sent: ${invalid}`,
|
|
69
|
+
extensions: { fields: [{ field: "url", message: invalid }] },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
const result = await send(url, webhookBody(testNotice));
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
deps.log.write("warn", "notification.test", { detail: sendFailureText(result) });
|
|
75
|
+
return problem(c, {
|
|
76
|
+
status: 502,
|
|
77
|
+
title: titleOf(502),
|
|
78
|
+
detail: `The test notification wasn't delivered: ${sendFailureText(result)}. Check the address in Settings → Notifications → Notification URL (for ntfy, https://ntfy.sh/your-topic), then press Send test notification again.`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return c.json({ sent: true });
|
|
38
82
|
})
|
|
39
83
|
.get("/voices", (c) => c.json({ voices: voices(voiceDeps) }))
|
|
40
84
|
.post("/voices", zValidator("json", voiceBody, onInvalid), (c) => {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const requestLimits = { requestMs: 300_000, uploadIdleMs: 300_000 };
|
|
2
|
+
// Node's `requestTimeout` (5 minutes) is one limit for the whole server: every
|
|
3
|
+
// `connectionsCheckingInterval` it answers 408 and closes any connection whose current request
|
|
4
|
+
// started longer ago than that and has not fully arrived, however steadily its body is still
|
|
5
|
+
// coming in. A multi-GB backup over a slow link needs longer, so the server's limit is switched
|
|
6
|
+
// off and the same limit is kept here per request, with one exception: Import everything
|
|
7
|
+
// (PUT /api/storage/import) may take as long as it needs, and is only cut off when it sends
|
|
8
|
+
// nothing at all for `uploadIdleMs`. `headersTimeout` (1 minute) is Node's and still applies to
|
|
9
|
+
// every request.
|
|
10
|
+
export function limitRequestTimes(server, limits = requestLimits) {
|
|
11
|
+
if (!("requestTimeout" in server))
|
|
12
|
+
return;
|
|
13
|
+
server.requestTimeout = 0;
|
|
14
|
+
server.on("request", (request) => {
|
|
15
|
+
if (isBackupUpload(request)) {
|
|
16
|
+
// Idle only while the file is arriving; once it is in, the import may take its time.
|
|
17
|
+
request.setTimeout(limits.uploadIdleMs);
|
|
18
|
+
request.once("end", () => request.setTimeout(0));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
if (request.complete)
|
|
23
|
+
return;
|
|
24
|
+
// What Node itself does: a 408 if nothing has been answered on this connection yet.
|
|
25
|
+
const socket = request.socket;
|
|
26
|
+
if (socket.writable && socket.bytesWritten === 0)
|
|
27
|
+
socket.write("HTTP/1.1 408 Request Timeout\r\nConnection: close\r\n\r\n");
|
|
28
|
+
socket.destroy();
|
|
29
|
+
}, limits.requestMs);
|
|
30
|
+
timer.unref();
|
|
31
|
+
const clear = () => clearTimeout(timer);
|
|
32
|
+
request.once("end", clear);
|
|
33
|
+
request.once("close", clear);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function isBackupUpload(request) {
|
|
37
|
+
return request.method === "PUT" && (request.url ?? "").split("?")[0] === "/api/storage/import";
|
|
38
|
+
}
|
package/dist/host-cli/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { codexImage } from "../adapters/image/codex.js";
|
|
4
|
+
import { codexImage, codexImageTimeoutMs } from "../adapters/image/codex.js";
|
|
5
5
|
import { claudeCodeLlm } from "../adapters/llm/claude-code.js";
|
|
6
6
|
import { nodeClaudeCodeModels } from "../adapters/llm/claude-code-models.js";
|
|
7
7
|
import { codexLlm } from "../adapters/llm/codex.js";
|
|
@@ -75,7 +75,8 @@ export function createHostRuntime(deps) {
|
|
|
75
75
|
}),
|
|
76
76
|
image: {
|
|
77
77
|
id: "codex-image",
|
|
78
|
-
|
|
78
|
+
timeoutMs: codexImageTimeoutMs,
|
|
79
|
+
models: () => codexImage({ run: deps.run, readModels: () => models("codex") }).models(),
|
|
79
80
|
generate: async (request) => {
|
|
80
81
|
const binary = await command("codex-image");
|
|
81
82
|
return codexImage({ run: deps.run, binary, env: deps.env }).generate(request);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Library prompts of the new "shorts" kind: what the Video stage's Shorts step asks the text
|
|
2
|
+
-- model for when it picks the clips. SQLite cannot change a CHECK in place, so the table is
|
|
3
|
+
-- copied into one whose CHECK lists the new kind, like 0015 did for "description".
|
|
4
|
+
CREATE TABLE prompts_shorts_upgrade (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
kind TEXT NOT NULL CHECK (kind IN ('article','image','thumbnail','narration','description','shorts')),
|
|
7
|
+
name TEXT NOT NULL,
|
|
8
|
+
body TEXT NOT NULL,
|
|
9
|
+
slots TEXT NOT NULL,
|
|
10
|
+
updated_at TEXT NOT NULL
|
|
11
|
+
);
|
|
12
|
+
INSERT INTO prompts_shorts_upgrade (id,kind,name,body,slots,updated_at)
|
|
13
|
+
SELECT id,kind,name,body,slots,updated_at FROM prompts;
|
|
14
|
+
DROP TABLE prompts;
|
|
15
|
+
ALTER TABLE prompts_shorts_upgrade RENAME TO prompts;
|
|
16
|
+
CREATE UNIQUE INDEX prompts_name ON prompts(kind,lower(name));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
-- Before 2.4.0 every save made while a step waited for Resume minted a new held row for it,
|
|
2
|
+
-- even when the project already had a row for exactly that step (same key and fingerprint).
|
|
3
|
+
-- Save now reserves the existing one instead (existingWork in
|
|
4
|
+
-- slices/rebuild/transition-repo.ts); this removes the copies left behind by older saves.
|
|
5
|
+
--
|
|
6
|
+
-- A row counts for a step the way Save counts it: a finished row (work and piece done), or a
|
|
7
|
+
-- held row nothing ran for (pending, held, unsent, no planning context, no attempt). Per
|
|
8
|
+
-- project, key and fingerprint, the row Save would pick is kept: a finished one first,
|
|
9
|
+
-- otherwise the newest held one. Only held rows that are not that pick are removed, and only
|
|
10
|
+
-- when they carry one piece and no review checkpoint. A revision that reserved a removed
|
|
11
|
+
-- copy is pointed at the kept held row, which is the same unstarted step. A copy some
|
|
12
|
+
-- revision reserved while a finished row exists is left alone: pointing that revision at
|
|
13
|
+
-- the finished result would change what it shows. Finished, running, failed and admitted
|
|
14
|
+
-- work, and assets, are never touched.
|
|
15
|
+
CREATE TEMP TABLE m0022_eligible AS
|
|
16
|
+
SELECT w.id AS work_id, p.id AS piece_id, w.project_id, p.work_key, p.fingerprint,
|
|
17
|
+
w.rowid AS rid, w.state = 'done' AS done
|
|
18
|
+
FROM revision_work w JOIN revision_work_pieces p ON p.work_id = w.id
|
|
19
|
+
WHERE (w.state = 'done' AND p.state = 'done')
|
|
20
|
+
OR (w.state = 'pending' AND w.dispatch_state = 'held' AND p.state = 'held'
|
|
21
|
+
AND p.submitted_at IS NULL AND w.recipe_context IS NULL
|
|
22
|
+
AND NOT EXISTS (SELECT 1 FROM attempts a WHERE a.work_id = w.id));
|
|
23
|
+
|
|
24
|
+
CREATE TEMP TABLE m0022_kept AS
|
|
25
|
+
SELECT project_id, work_key, fingerprint, work_id, piece_id, done FROM (
|
|
26
|
+
SELECT e.*, row_number() OVER (
|
|
27
|
+
PARTITION BY project_id, work_key, fingerprint ORDER BY done DESC, rid DESC
|
|
28
|
+
) AS pick
|
|
29
|
+
FROM m0022_eligible e
|
|
30
|
+
) WHERE pick = 1;
|
|
31
|
+
|
|
32
|
+
CREATE TEMP TABLE m0022_copies AS
|
|
33
|
+
SELECT e.work_id, k.work_id AS kept_work_id, k.piece_id AS kept_piece_id
|
|
34
|
+
FROM m0022_eligible e
|
|
35
|
+
JOIN m0022_kept k ON k.project_id = e.project_id AND k.work_key = e.work_key
|
|
36
|
+
AND k.fingerprint = e.fingerprint
|
|
37
|
+
WHERE e.done = 0 AND e.work_id <> k.work_id
|
|
38
|
+
AND (SELECT count(*) FROM revision_work_pieces q WHERE q.work_id = e.work_id) = 1
|
|
39
|
+
AND NOT EXISTS (SELECT 1 FROM review_checkpoints c WHERE c.work_id = e.work_id)
|
|
40
|
+
AND (k.done = 0
|
|
41
|
+
OR NOT EXISTS (SELECT 1 FROM revision_work_reservations r WHERE r.work_id = e.work_id));
|
|
42
|
+
|
|
43
|
+
UPDATE revision_work_reservations
|
|
44
|
+
SET piece_id = CASE WHEN piece_id IS NULL THEN NULL ELSE (
|
|
45
|
+
SELECT c.kept_piece_id FROM m0022_copies c WHERE c.work_id = revision_work_reservations.work_id
|
|
46
|
+
) END,
|
|
47
|
+
work_id = (
|
|
48
|
+
SELECT c.kept_work_id FROM m0022_copies c WHERE c.work_id = revision_work_reservations.work_id
|
|
49
|
+
)
|
|
50
|
+
WHERE work_id IN (SELECT work_id FROM m0022_copies);
|
|
51
|
+
|
|
52
|
+
DELETE FROM revision_work WHERE id IN (SELECT work_id FROM m0022_copies);
|
|
53
|
+
|
|
54
|
+
DROP TABLE m0022_eligible;
|
|
55
|
+
DROP TABLE m0022_kept;
|
|
56
|
+
DROP TABLE m0022_copies;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
-- A Play draft can carry an uploaded establishing image (the Images stage's reference). SQLite
|
|
2
|
+
-- cannot widen a CHECK constraint in place, so the attachments table is rebuilt with the new
|
|
3
|
+
-- kind. Nothing references it, so enforcement stays on.
|
|
4
|
+
CREATE TABLE play_draft_attachments_reference_upgrade (
|
|
5
|
+
id TEXT PRIMARY KEY,
|
|
6
|
+
draft_id TEXT NOT NULL REFERENCES play_drafts(id) ON DELETE CASCADE,
|
|
7
|
+
staged_file_id TEXT REFERENCES staged_files(id) ON DELETE SET NULL,
|
|
8
|
+
kind TEXT NOT NULL CHECK(kind IN ('audio','images','thumbnail','reference')),
|
|
9
|
+
original_filename TEXT NOT NULL,
|
|
10
|
+
status TEXT NOT NULL CHECK(status IN ('pending','ready','reattach')),
|
|
11
|
+
error TEXT
|
|
12
|
+
);
|
|
13
|
+
INSERT INTO play_draft_attachments_reference_upgrade (id,draft_id,staged_file_id,kind,original_filename,status,error)
|
|
14
|
+
SELECT id,draft_id,staged_file_id,kind,original_filename,status,error FROM play_draft_attachments ORDER BY rowid;
|
|
15
|
+
DROP TABLE play_draft_attachments;
|
|
16
|
+
ALTER TABLE play_draft_attachments_reference_upgrade RENAME TO play_draft_attachments;
|
|
17
|
+
CREATE INDEX play_draft_attachment_file ON play_draft_attachments(staged_file_id);
|
|
18
|
+
CREATE INDEX play_draft_attachment_owner ON play_draft_attachments(draft_id);
|
|
@@ -13,6 +13,8 @@ export const bridgeLimits = {
|
|
|
13
13
|
frame: 4 * 1024 * 1024,
|
|
14
14
|
stream: 64 * 1024 * 1024,
|
|
15
15
|
image: 32 * 1024 * 1024,
|
|
16
|
+
// An establishing image sent along with an image request; base64 keeps it inside `request`.
|
|
17
|
+
reference: 10 * 1024 * 1024,
|
|
16
18
|
text: 2 * 1024 * 1024,
|
|
17
19
|
generation: 5,
|
|
18
20
|
metadata: 8,
|
|
@@ -39,9 +41,22 @@ export const hostLlmSchema = z
|
|
|
39
41
|
.strict();
|
|
40
42
|
export const hostImageSchema = z
|
|
41
43
|
.object({
|
|
42
|
-
|
|
44
|
+
// "codex-imagegen" is the Codex default; any other is one of the Codex CLI's models.
|
|
45
|
+
model: short.refine((v) => !v.startsWith("-") && !/\s/.test(v)),
|
|
43
46
|
prompt: z.string().min(1).max(bridgeLimits.text),
|
|
44
47
|
aspect: z.enum(["16:9", "9:16"]),
|
|
48
|
+
thinking: z.enum(thinkingModes).optional(),
|
|
49
|
+
// The establishing image, as base64 (at most `bridgeLimits.reference` bytes decoded).
|
|
50
|
+
reference: z
|
|
51
|
+
.object({
|
|
52
|
+
mime: z.enum(["image/png", "image/jpeg"]),
|
|
53
|
+
base64: z
|
|
54
|
+
.string()
|
|
55
|
+
.max(Math.ceil(bridgeLimits.reference / 3) * 4)
|
|
56
|
+
.regex(/^[A-Za-z0-9+/]*={0,2}$/),
|
|
57
|
+
})
|
|
58
|
+
.strict()
|
|
59
|
+
.optional(),
|
|
45
60
|
})
|
|
46
61
|
.strict();
|
|
47
62
|
export const hostOpenFolderSchema = z
|
|
@@ -73,7 +88,7 @@ export const hostModelsSchema = z
|
|
|
73
88
|
id: short,
|
|
74
89
|
name: short,
|
|
75
90
|
group: short.optional(),
|
|
76
|
-
thinkingModes: z.array(z.enum(thinkingModes)).max(
|
|
91
|
+
thinkingModes: z.array(z.enum(thinkingModes)).max(thinkingModes.length).optional(),
|
|
77
92
|
})
|
|
78
93
|
.strict())
|
|
79
94
|
.max(1000),
|
package/dist/kernel/ports/llm.js
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export const messageRoles = ["system", "user", "assistant"];
|
|
2
|
-
|
|
2
|
+
// "max" and "ultra" are the Codex CLI's two highest reasoning efforts (its model list reports
|
|
3
|
+
// them per model); the catalogue's models never offer them.
|
|
4
|
+
export const thinkingModes = ["off", "low", "medium", "high", "xhigh", "max", "ultra"];
|
|
@@ -5,11 +5,13 @@ import { isProviderError, providerError } from "../ports/model.js";
|
|
|
5
5
|
// Four attempts - the first plus 3 retries - waiting 2 s, 8 s, 30 s.
|
|
6
6
|
export const attemptLimit = 4;
|
|
7
7
|
export const backoffMs = [2000, 8000, 30_000];
|
|
8
|
-
// Each attempt times out at 120 s, image calls at 300 s
|
|
8
|
+
// Each attempt times out at 120 s, image calls at 300 s, and an image animated into a clip at
|
|
9
|
+
// 900 s: image-to-video models queue and take one to five minutes when they run.
|
|
9
10
|
export const timeoutMs = {
|
|
10
11
|
llm: 120_000,
|
|
11
12
|
tts: 120_000,
|
|
12
13
|
image: 300_000,
|
|
14
|
+
video: 900_000,
|
|
13
15
|
};
|
|
14
16
|
// A refusal and an unsupported capability are the provider's final answer; retrying only
|
|
15
17
|
// spends the user's money again. The third is a key removed mid-run, which leaves nothing
|
|
@@ -23,7 +25,7 @@ const terminalKinds = [
|
|
|
23
25
|
];
|
|
24
26
|
export async function attempt(ctx, call, opts) {
|
|
25
27
|
ctx.signal.throwIfAborted();
|
|
26
|
-
const limit = timeoutMs[opts.kind];
|
|
28
|
+
const limit = opts.timeoutMs ?? timeoutMs[opts.kind];
|
|
27
29
|
for (let n = 1;; n += 1) {
|
|
28
30
|
ctx.signal.throwIfAborted();
|
|
29
31
|
const retrieving = ctx.continuation?.read() !== undefined;
|
|
@@ -131,6 +133,7 @@ const callee = {
|
|
|
131
133
|
llm: "AI model",
|
|
132
134
|
tts: "narration provider",
|
|
133
135
|
image: "image provider",
|
|
136
|
+
video: "image-to-video model",
|
|
134
137
|
};
|
|
135
138
|
// Node's `fetch` rejects a request that never reached the server with this TypeError.
|
|
136
139
|
function unreachable(error) {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { providerError } from "../ports/model.js";
|
|
2
3
|
import { attempt } from "./attempt.js";
|
|
3
4
|
export function stageProviders(deps, context, pieceId) {
|
|
4
5
|
const ctx = {
|
|
@@ -140,14 +141,53 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
140
141
|
},
|
|
141
142
|
image: (call) => {
|
|
142
143
|
const port = deps.registry.image(call.provider);
|
|
144
|
+
const callId = randomUUID();
|
|
145
|
+
// A long job (the Codex CLI reviewing and redrawing its image) says how far it has got
|
|
146
|
+
// on the stage's live panel; the other providers report nothing and show no panel.
|
|
147
|
+
const onProgress = (text) => context.emit({
|
|
148
|
+
type: "llm.preview",
|
|
149
|
+
...(pieceId === undefined ? {} : { workPieceId: pieceId }),
|
|
150
|
+
projectId: context.stage.projectId,
|
|
151
|
+
stage: context.stage.kind,
|
|
152
|
+
callId,
|
|
153
|
+
label: call.previewLabel ?? "Image",
|
|
154
|
+
text,
|
|
155
|
+
reset: true,
|
|
156
|
+
});
|
|
143
157
|
return schedule(call.provider, () => attempt(ctx, (signal) => port.generate({
|
|
144
158
|
model: call.model,
|
|
145
159
|
prompt: call.prompt,
|
|
146
160
|
aspect: call.aspect,
|
|
161
|
+
...(call.thinking === undefined ? {} : { thinking: call.thinking }),
|
|
162
|
+
...(call.reference === undefined ? {} : { reference: call.reference }),
|
|
163
|
+
onProgress,
|
|
164
|
+
signal,
|
|
165
|
+
}),
|
|
166
|
+
// One request, one answer: the limit runs over the whole call - 300 s, or the
|
|
167
|
+
// provider's own when it needs longer.
|
|
168
|
+
{
|
|
169
|
+
kind: "image",
|
|
170
|
+
...(port.timeoutMs === undefined ? {} : { timeoutMs: port.timeoutMs }),
|
|
171
|
+
}));
|
|
172
|
+
},
|
|
173
|
+
animate: (call) => {
|
|
174
|
+
const port = deps.registry.image(call.provider);
|
|
175
|
+
const animate = port.animate;
|
|
176
|
+
if (animate === undefined)
|
|
177
|
+
return Promise.reject(providerError({
|
|
178
|
+
kind: "unsupported",
|
|
179
|
+
message: `${call.provider} can't turn images into video clips. Choose fal.ai or Replicate as the image provider in Edit project → Providers, or turn Animate images off in Edit project → Inputs → Look.`,
|
|
180
|
+
}));
|
|
181
|
+
return schedule(call.provider, () => attempt(ctx, (signal) => animate({
|
|
182
|
+
model: call.model,
|
|
183
|
+
prompt: call.prompt,
|
|
184
|
+
image: call.image,
|
|
185
|
+
aspect: call.aspect,
|
|
186
|
+
seconds: call.seconds,
|
|
147
187
|
signal,
|
|
148
188
|
}),
|
|
149
|
-
//
|
|
150
|
-
{ kind: "
|
|
189
|
+
// Queued and slow: the 900 s runs over the whole call, polling included.
|
|
190
|
+
{ kind: "video" }));
|
|
151
191
|
},
|
|
152
192
|
forPiece: (piece) => stageProviders(deps, context, piece),
|
|
153
193
|
};
|