@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
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(),
|
|
@@ -95,6 +96,14 @@ export const catalogueSchema = z
|
|
|
95
96
|
export function isVideoModel(model) {
|
|
96
97
|
return model.keywords.includes("video");
|
|
97
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.";
|
|
98
107
|
// The image-to-video models a provider offers, as Animate images lists them.
|
|
99
108
|
export function videoModelsOf(catalogue, provider) {
|
|
100
109
|
return catalogue.image.filter((m) => m.provider === provider && m.enabled && !m.deprecated && isVideoModel(m));
|
package/dist/catalog/validate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { usesNarrationPreparation, usesShorts, 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
3
|
import { usesAnimation } from "../slices/video/edit-settings.js";
|
|
4
|
-
import { videoModelsOf } from "./schema.js";
|
|
4
|
+
import { referenceRefusal, takesReferenceImage, videoModelsOf } from "./schema.js";
|
|
5
5
|
export function modelFields(draft, catalogue) {
|
|
6
6
|
if (!catalogue)
|
|
7
7
|
return [];
|
|
@@ -45,6 +45,11 @@ export function modelFields(draft, catalogue) {
|
|
|
45
45
|
field,
|
|
46
46
|
message: "This model does not support that thinking setting. Choose another.",
|
|
47
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
|
+
});
|
|
48
53
|
else if ("llm" in model && draft.sources.research === "generate" && !model.llm.webSearch)
|
|
49
54
|
fields.push({
|
|
50
55
|
field,
|
|
@@ -65,6 +70,16 @@ export function modelFields(draft, catalogue) {
|
|
|
65
70
|
message: "This image model cannot make the vertical (9:16) images Shorts need. Choose another image model, or turn Shorts off.",
|
|
66
71
|
});
|
|
67
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
|
+
}
|
|
68
83
|
// Animate images runs on the image provider's image-to-video models.
|
|
69
84
|
if (usesAnimation(draft) && draft.images !== undefined) {
|
|
70
85
|
const model = draft.videoEdit?.animateModel ?? "";
|
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)
|
|
@@ -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) => {
|
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,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"];
|
|
@@ -25,7 +25,7 @@ const terminalKinds = [
|
|
|
25
25
|
];
|
|
26
26
|
export async function attempt(ctx, call, opts) {
|
|
27
27
|
ctx.signal.throwIfAborted();
|
|
28
|
-
const limit = timeoutMs[opts.kind];
|
|
28
|
+
const limit = opts.timeoutMs ?? timeoutMs[opts.kind];
|
|
29
29
|
for (let n = 1;; n += 1) {
|
|
30
30
|
ctx.signal.throwIfAborted();
|
|
31
31
|
const retrieving = ctx.continuation?.read() !== undefined;
|
|
@@ -141,14 +141,34 @@ export function stageProviders(deps, context, pieceId) {
|
|
|
141
141
|
},
|
|
142
142
|
image: (call) => {
|
|
143
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
|
+
});
|
|
144
157
|
return schedule(call.provider, () => attempt(ctx, (signal) => port.generate({
|
|
145
158
|
model: call.model,
|
|
146
159
|
prompt: call.prompt,
|
|
147
160
|
aspect: call.aspect,
|
|
161
|
+
...(call.thinking === undefined ? {} : { thinking: call.thinking }),
|
|
162
|
+
...(call.reference === undefined ? {} : { reference: call.reference }),
|
|
163
|
+
onProgress,
|
|
148
164
|
signal,
|
|
149
165
|
}),
|
|
150
|
-
// One request, one answer: the
|
|
151
|
-
|
|
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
|
+
}));
|
|
152
172
|
},
|
|
153
173
|
animate: (call) => {
|
|
154
174
|
const port = deps.registry.image(call.provider);
|