@gentbajko/slopify 0.1.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/LICENSE +21 -0
- package/README.md +63 -0
- package/dist/adapter-registry.js +68 -0
- package/dist/adapters/image/bytes.js +64 -0
- package/dist/adapters/image/fal.js +164 -0
- package/dist/adapters/image/openai.js +155 -0
- package/dist/adapters/image/replicate.js +183 -0
- package/dist/adapters/llm/claude-code.js +158 -0
- package/dist/adapters/llm/codex.js +126 -0
- package/dist/adapters/llm/openrouter.js +193 -0
- package/dist/adapters/llm/run-cli.js +134 -0
- package/dist/adapters/llm/sse-lines.js +46 -0
- package/dist/adapters/retry-after.js +21 -0
- package/dist/adapters/tts/cartesia.js +105 -0
- package/dist/adapters/tts/elevenlabs.js +107 -0
- package/dist/adapters/tts/openai.js +95 -0
- package/dist/edge/cli.js +36 -0
- package/dist/edge/events/hub.js +76 -0
- package/dist/edge/http/actions.js +125 -0
- package/dist/edge/http/app.js +72 -0
- package/dist/edge/http/entries.js +41 -0
- package/dist/edge/http/files.js +67 -0
- package/dist/edge/http/problem.js +65 -0
- package/dist/edge/http/projects.js +128 -0
- package/dist/edge/http/prompts.js +73 -0
- package/dist/edge/http/providers.js +62 -0
- package/dist/edge/http/settings.js +90 -0
- package/dist/edge/http/staging.js +104 -0
- package/dist/edge/http/telemetry.js +29 -0
- package/dist/edge/http/usage.js +19 -0
- package/dist/edge/open-browser.js +20 -0
- package/dist/kernel/clock.js +18 -0
- package/dist/kernel/config/index.js +32 -0
- package/dist/kernel/db/index.js +24 -0
- package/dist/kernel/db/migrate.js +59 -0
- package/dist/kernel/db/migrations/0001-init.sql +17 -0
- package/dist/kernel/db/tx.js +53 -0
- package/dist/kernel/events.js +1 -0
- package/dist/kernel/ids.js +4 -0
- package/dist/kernel/lock.js +73 -0
- package/dist/kernel/log.js +34 -0
- package/dist/kernel/paths.js +21 -0
- package/dist/kernel/pipeline.js +22 -0
- package/dist/kernel/ports/image.js +1 -0
- package/dist/kernel/ports/llm.js +1 -0
- package/dist/kernel/ports/model.js +34 -0
- package/dist/kernel/ports/registry.js +1 -0
- package/dist/kernel/ports/tts.js +1 -0
- package/dist/kernel/runner/attempt-repo.js +50 -0
- package/dist/kernel/runner/attempt.js +146 -0
- package/dist/kernel/runner/graph.js +82 -0
- package/dist/kernel/runner/index.js +205 -0
- package/dist/kernel/runner/piece-repo.js +58 -0
- package/dist/kernel/runner/providers.js +92 -0
- package/dist/kernel/version.js +14 -0
- package/dist/main.js +212 -0
- package/dist/slices/admission/model.js +6 -0
- package/dist/slices/admission/repo.js +178 -0
- package/dist/slices/admission/rules.js +201 -0
- package/dist/slices/admission/start.js +103 -0
- package/dist/slices/admission/substitute.js +80 -0
- package/dist/slices/article/continuation.js +81 -0
- package/dist/slices/article/plain.js +14 -0
- package/dist/slices/article/run.js +166 -0
- package/dist/slices/article/split.js +51 -0
- package/dist/slices/article/store.js +21 -0
- package/dist/slices/cancel/index.js +61 -0
- package/dist/slices/images/run.js +221 -0
- package/dist/slices/library/lint.js +53 -0
- package/dist/slices/library/model.js +13 -0
- package/dist/slices/library/repo.js +111 -0
- package/dist/slices/library/save.js +87 -0
- package/dist/slices/library/slots.js +86 -0
- package/dist/slices/narration/chunk.js +71 -0
- package/dist/slices/narration/concat.js +79 -0
- package/dist/slices/narration/run.js +280 -0
- package/dist/slices/reruns/cascade.js +66 -0
- package/dist/slices/reruns/index.js +269 -0
- package/dist/slices/research/planner.js +81 -0
- package/dist/slices/research/run.js +192 -0
- package/dist/slices/research/synthesis.js +48 -0
- package/dist/slices/settings/cli-status.js +37 -0
- package/dist/slices/settings/keys.js +55 -0
- package/dist/slices/settings/model.js +54 -0
- package/dist/slices/settings/playback.js +60 -0
- package/dist/slices/settings/readiness.js +21 -0
- package/dist/slices/settings/repo.js +59 -0
- package/dist/slices/settings/voices.js +48 -0
- package/dist/slices/storage/asset-name.js +16 -0
- package/dist/slices/storage/delete-project.js +35 -0
- package/dist/slices/storage/downloads.js +100 -0
- package/dist/slices/storage/layout.js +59 -0
- package/dist/slices/storage/model.js +20 -0
- package/dist/slices/storage/reconcile.js +102 -0
- package/dist/slices/storage/repo.js +104 -0
- package/dist/slices/storage/staging.js +224 -0
- package/dist/slices/telemetry/collector-client.js +51 -0
- package/dist/slices/telemetry/flush.js +111 -0
- package/dist/slices/telemetry/machine.js +33 -0
- package/dist/slices/telemetry/model.js +39 -0
- package/dist/slices/telemetry/record.js +41 -0
- package/dist/slices/telemetry/repo.js +73 -0
- package/dist/slices/telemetry/usage.js +82 -0
- package/dist/slices/thumbnail/by-llm.js +30 -0
- package/dist/slices/thumbnail/run.js +200 -0
- package/dist/slices/video/ffmpeg.js +230 -0
- package/dist/slices/video/plan.js +71 -0
- package/dist/slices/video/run.js +179 -0
- package/dist/web/app-icon.svg +1 -0
- package/dist/web/assets/barlow-condensed-latin-600-normal-BFJEwTuo.woff +0 -0
- package/dist/web/assets/barlow-condensed-latin-600-normal-DepVgxBB.woff2 +0 -0
- package/dist/web/assets/barlow-condensed-latin-700-normal-Dmwat-ge.woff +0 -0
- package/dist/web/assets/barlow-condensed-latin-700-normal-v1xN8_Wq.woff2 +0 -0
- package/dist/web/assets/barlow-condensed-latin-ext-600-normal-18ESti3H.woff2 +0 -0
- package/dist/web/assets/barlow-condensed-latin-ext-600-normal-Clv9cIcR.woff +0 -0
- package/dist/web/assets/barlow-condensed-latin-ext-700-normal-BIHFfxf0.woff +0 -0
- package/dist/web/assets/barlow-condensed-latin-ext-700-normal-CwuXbfVR.woff2 +0 -0
- package/dist/web/assets/barlow-condensed-vietnamese-600-normal-A5AYRdjN.woff2 +0 -0
- package/dist/web/assets/barlow-condensed-vietnamese-600-normal-CNlPk46_.woff +0 -0
- package/dist/web/assets/barlow-condensed-vietnamese-700-normal-DYeBwlKR.woff2 +0 -0
- package/dist/web/assets/barlow-condensed-vietnamese-700-normal-DhIzd8Tb.woff +0 -0
- package/dist/web/assets/barlow-latin-400-normal-fsAxiSwU.woff +0 -0
- package/dist/web/assets/barlow-latin-400-normal-qiz4-Cze.woff2 +0 -0
- package/dist/web/assets/barlow-latin-500-normal-BPAOfeC8.woff2 +0 -0
- package/dist/web/assets/barlow-latin-500-normal-C1h8hMer.woff +0 -0
- package/dist/web/assets/barlow-latin-600-normal-CNwfPWQD.woff +0 -0
- package/dist/web/assets/barlow-latin-600-normal-DILqtrty.woff2 +0 -0
- package/dist/web/assets/barlow-latin-700-normal-A9pxMQ4z.woff2 +0 -0
- package/dist/web/assets/barlow-latin-700-normal-__SGTsZ1.woff +0 -0
- package/dist/web/assets/barlow-latin-800-normal-BdVooDN4.woff +0 -0
- package/dist/web/assets/barlow-latin-800-normal-s1sAMnoV.woff2 +0 -0
- package/dist/web/assets/barlow-latin-ext-400-normal-CvBsJvxq.woff +0 -0
- package/dist/web/assets/barlow-latin-ext-400-normal-HxX4XjxC.woff2 +0 -0
- package/dist/web/assets/barlow-latin-ext-500-normal-CJPcKP2Q.woff +0 -0
- package/dist/web/assets/barlow-latin-ext-500-normal-DOaysfXq.woff2 +0 -0
- package/dist/web/assets/barlow-latin-ext-600-normal-B8NK_A3D.woff2 +0 -0
- package/dist/web/assets/barlow-latin-ext-600-normal-DMVRjfRT.woff +0 -0
- package/dist/web/assets/barlow-latin-ext-700-normal-BLuWmldJ.woff2 +0 -0
- package/dist/web/assets/barlow-latin-ext-700-normal-CctuGmmz.woff +0 -0
- package/dist/web/assets/barlow-latin-ext-800-normal-BiucknKG.woff2 +0 -0
- package/dist/web/assets/barlow-latin-ext-800-normal-D7I3yvUw.woff +0 -0
- package/dist/web/assets/barlow-vietnamese-400-normal-BFeobeCK.woff +0 -0
- package/dist/web/assets/barlow-vietnamese-400-normal-Dpl4UHAZ.woff2 +0 -0
- package/dist/web/assets/barlow-vietnamese-500-normal-GNfB7rCE.woff +0 -0
- package/dist/web/assets/barlow-vietnamese-500-normal-zTViEIzf.woff2 +0 -0
- package/dist/web/assets/barlow-vietnamese-600-normal-CA_GiK2e.woff +0 -0
- package/dist/web/assets/barlow-vietnamese-600-normal-DcjprdFV.woff2 +0 -0
- package/dist/web/assets/barlow-vietnamese-700-normal-4Jt4k04K.woff +0 -0
- package/dist/web/assets/barlow-vietnamese-700-normal-D6euyNzi.woff2 +0 -0
- package/dist/web/assets/barlow-vietnamese-800-normal-Cl1Mc_Dv.woff2 +0 -0
- package/dist/web/assets/barlow-vietnamese-800-normal-D0VWpbij.woff +0 -0
- package/dist/web/assets/index-BPqQnrpy.css +1 -0
- package/dist/web/assets/index-D_sWbKQi.js +81 -0
- package/dist/web/favicon.svg +1 -0
- package/dist/web/index.html +15 -0
- package/package.json +41 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { zValidator } from "@hono/zod-validator";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { appearances, providerIds } from "../../slices/settings/model.js";
|
|
5
|
+
import { readSettings, saveSettings } from "../../slices/settings/playback.js";
|
|
6
|
+
import { addVoice, removeVoice, voiceIdMax, voiceNameMax, voices, } from "../../slices/settings/voices.js";
|
|
7
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
8
|
+
const idParam = z.object({ id: z.string().min(1).max(64) });
|
|
9
|
+
// Shape only. The rules - trimming, emptiness, length, the provider speaking at all -
|
|
10
|
+
// are the slice's, so one set of messages reaches the form (03-conventions).
|
|
11
|
+
const voiceBody = z.object({
|
|
12
|
+
provider: z.enum(providerIds),
|
|
13
|
+
name: z.string(),
|
|
14
|
+
voiceId: z.string(),
|
|
15
|
+
});
|
|
16
|
+
const playbackBody = z.object({
|
|
17
|
+
silenceGapSeconds: z.number(),
|
|
18
|
+
appearance: z.enum(appearances),
|
|
19
|
+
});
|
|
20
|
+
// The return type is inferred so Hono keeps the route types the SPA's client is
|
|
21
|
+
// generated from; see stagingRoutes.
|
|
22
|
+
export function settingsRoutes(deps) {
|
|
23
|
+
const playback = { db: deps.db, log: deps.log };
|
|
24
|
+
const voiceDeps = { db: deps.db, ids: deps.ids };
|
|
25
|
+
return new Hono()
|
|
26
|
+
.get("/", (c) => c.json(readSettings(playback)))
|
|
27
|
+
.put("/", zValidator("json", playbackBody, onInvalid), (c) => {
|
|
28
|
+
const result = saveSettings(playback, c.req.valid("json"));
|
|
29
|
+
if (!result.ok) {
|
|
30
|
+
return problem(c, {
|
|
31
|
+
status: 400,
|
|
32
|
+
title: titleOf(400),
|
|
33
|
+
detail: "These settings cannot be saved; the listed fields need attention.",
|
|
34
|
+
extensions: { fields: result.fields },
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
return c.json(result.settings);
|
|
38
|
+
})
|
|
39
|
+
.get("/voices", (c) => c.json({ voices: voices(voiceDeps) }))
|
|
40
|
+
.post("/voices", zValidator("json", voiceBody, onInvalid), (c) => {
|
|
41
|
+
const result = addVoice(voiceDeps, c.req.valid("json"));
|
|
42
|
+
if (!result.ok) {
|
|
43
|
+
// `logic/02` §Q18: a voice ID already listed for its provider is a conflict with
|
|
44
|
+
// a row that exists, which the form shows under the Voice ID input.
|
|
45
|
+
return result.reason === "duplicate-voice-id"
|
|
46
|
+
? problem(c, {
|
|
47
|
+
status: 409,
|
|
48
|
+
title: titleOf(409),
|
|
49
|
+
detail: "This voice ID is already listed for this provider.",
|
|
50
|
+
})
|
|
51
|
+
: problem(c, {
|
|
52
|
+
status: 400,
|
|
53
|
+
title: titleOf(400),
|
|
54
|
+
detail: "This voice cannot be added; the listed fields need attention.",
|
|
55
|
+
extensions: { fields: [fieldOf(result.reason)] },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return c.json(result.voice, 201);
|
|
59
|
+
})
|
|
60
|
+
.delete("/voices/:id", zValidator("param", idParam, onInvalid), (c) => {
|
|
61
|
+
if (!removeVoice(voiceDeps, c.req.valid("param").id).ok) {
|
|
62
|
+
return problem(c, {
|
|
63
|
+
status: 404,
|
|
64
|
+
title: titleOf(404),
|
|
65
|
+
detail: "No voice has that id.",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
return c.body(null, 204);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function fieldOf(reason) {
|
|
72
|
+
switch (reason) {
|
|
73
|
+
case "blank-name":
|
|
74
|
+
return { field: "name", message: "A voice name is required." };
|
|
75
|
+
case "name-too-long":
|
|
76
|
+
return {
|
|
77
|
+
field: "name",
|
|
78
|
+
message: `A voice name is at most ${String(voiceNameMax)} characters.`,
|
|
79
|
+
};
|
|
80
|
+
case "blank-voice-id":
|
|
81
|
+
return { field: "voiceId", message: "A voice ID is required." };
|
|
82
|
+
case "voice-id-too-long":
|
|
83
|
+
return {
|
|
84
|
+
field: "voiceId",
|
|
85
|
+
message: `A voice ID is at most ${String(voiceIdMax)} characters.`,
|
|
86
|
+
};
|
|
87
|
+
case "not-a-tts-provider":
|
|
88
|
+
return { field: "provider", message: "Pick a text-to-speech provider." };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Readable } from "node:stream";
|
|
2
|
+
import { Busboy } from "@fastify/busboy";
|
|
3
|
+
import { zValidator } from "@hono/zod-validator";
|
|
4
|
+
import { Hono } from "hono";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { uploadableStageKinds } from "../../slices/storage/model.js";
|
|
7
|
+
import { stagedFiles } from "../../slices/storage/repo.js";
|
|
8
|
+
import { discardStagedFile, stageUpload } from "../../slices/storage/staging.js";
|
|
9
|
+
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
10
|
+
const kindParam = z.object({ kind: z.enum(uploadableStageKinds) });
|
|
11
|
+
const idParam = z.object({ id: z.string().min(1).max(64) });
|
|
12
|
+
// The return type is inferred on purpose: annotating it would erase the route types the
|
|
13
|
+
// SPA's client is generated from (02-models §Q27).
|
|
14
|
+
export function stagingRoutes(deps) {
|
|
15
|
+
const storage = {
|
|
16
|
+
db: deps.db,
|
|
17
|
+
paths: deps.paths,
|
|
18
|
+
ids: deps.ids,
|
|
19
|
+
clock: deps.clock,
|
|
20
|
+
log: deps.log,
|
|
21
|
+
// The slice never reaches for the hub: progress is a function it is handed.
|
|
22
|
+
emit: (event) => {
|
|
23
|
+
deps.hub.emitGlobal(event);
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
return new Hono()
|
|
27
|
+
.get("/", (c) => c.json({ files: stagedFiles(deps.db) }))
|
|
28
|
+
.post("/:kind", zValidator("param", kindParam, onInvalid), async (c) => {
|
|
29
|
+
const contentType = c.req.header("content-type");
|
|
30
|
+
if (contentType === undefined || !contentType.startsWith("multipart/form-data")) {
|
|
31
|
+
return problem(c, {
|
|
32
|
+
status: 415,
|
|
33
|
+
title: titleOf(415),
|
|
34
|
+
detail: "An upload is sent as multipart/form-data with the file in one part.",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
const body = c.req.raw.body;
|
|
38
|
+
if (body === null) {
|
|
39
|
+
return problem(c, {
|
|
40
|
+
status: 400,
|
|
41
|
+
title: titleOf(400),
|
|
42
|
+
detail: "The request carried no body.",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const result = await readUpload(storage, c.req.valid("param").kind, contentType, body);
|
|
46
|
+
if (result === undefined) {
|
|
47
|
+
return problem(c, {
|
|
48
|
+
status: 400,
|
|
49
|
+
title: titleOf(400),
|
|
50
|
+
detail: "The request carried no file part.",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!result.ok) {
|
|
54
|
+
return problem(c, { status: 400, title: titleOf(400), detail: detailOf(result.reason) });
|
|
55
|
+
}
|
|
56
|
+
return c.json(result.file, 201);
|
|
57
|
+
})
|
|
58
|
+
.delete("/:id", zValidator("param", idParam, onInvalid), (c) => {
|
|
59
|
+
const result = discardStagedFile(storage, c.req.valid("param").id);
|
|
60
|
+
if (!result.ok) {
|
|
61
|
+
return problem(c, {
|
|
62
|
+
status: 404,
|
|
63
|
+
title: titleOf(404),
|
|
64
|
+
detail: "No staged file has that id.",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return c.body(null, 204);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// The bytes go from the socket to the parser to the disk. Nothing collects them: the
|
|
71
|
+
// platform's own formData() materialises a whole part in memory first, which an
|
|
72
|
+
// uncapped audio or video upload cannot afford.
|
|
73
|
+
function readUpload(storage, stageKind, contentType, body) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const parser = new Busboy({
|
|
76
|
+
headers: { "content-type": contentType },
|
|
77
|
+
limits: { files: 1, fields: 8 },
|
|
78
|
+
// The parser hands the name through untouched: silently renaming a file the user
|
|
79
|
+
// picked would record a name nobody chose, so the slice rejects instead.
|
|
80
|
+
preservePath: true,
|
|
81
|
+
});
|
|
82
|
+
let started = false;
|
|
83
|
+
parser.on("file", (_field, stream, filename) => {
|
|
84
|
+
started = true;
|
|
85
|
+
stageUpload(storage, {
|
|
86
|
+
stageKind,
|
|
87
|
+
originalFilename: typeof filename === "string" ? filename : "",
|
|
88
|
+
content: stream,
|
|
89
|
+
}).then(resolve, reject);
|
|
90
|
+
});
|
|
91
|
+
parser.on("error", reject);
|
|
92
|
+
parser.on("finish", () => {
|
|
93
|
+
if (!started) {
|
|
94
|
+
resolve(undefined);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
Readable.fromWeb(body).pipe(parser);
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
function detailOf(reason) {
|
|
101
|
+
return reason === "empty-file"
|
|
102
|
+
? "The uploaded file is empty."
|
|
103
|
+
: "The file name may not be empty or contain a path separator.";
|
|
104
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { dismissNotice, noticeSeen } from "../../slices/telemetry/machine.js";
|
|
3
|
+
// The return type is inferred so Hono keeps the route types the SPA's client is
|
|
4
|
+
// generated from; see stagingRoutes.
|
|
5
|
+
export function telemetryRoutes(deps) {
|
|
6
|
+
const telemetry = {
|
|
7
|
+
db: deps.db,
|
|
8
|
+
ids: deps.ids,
|
|
9
|
+
clock: deps.clock,
|
|
10
|
+
log: deps.log,
|
|
11
|
+
appVersion: deps.version,
|
|
12
|
+
};
|
|
13
|
+
return (new Hono()
|
|
14
|
+
// What the SPA asks before it shows the first-run notice
|
|
15
|
+
// (mockup/02-first-run-notice.md). The app version comes with it because the notice
|
|
16
|
+
// names the version that goes out in every report, and the promise has to be made
|
|
17
|
+
// with the number it is true of.
|
|
18
|
+
.get("/notice", (c) => c.json({ seen: noticeSeen(deps.db), appVersion: deps.version }))
|
|
19
|
+
// "Got it" is the only control on that modal, and pressing it is what creates the
|
|
20
|
+
// machine id (logic/16 step 1). Idempotent: a second press, a reload or a second tab
|
|
21
|
+
// finds the machine already there and mints nothing.
|
|
22
|
+
.post("/notice", (c) => {
|
|
23
|
+
dismissNotice(telemetry);
|
|
24
|
+
deps.flushSoon();
|
|
25
|
+
// The machine id stays on this machine. Nothing needs it here, so nothing sends
|
|
26
|
+
// it back out of the process that made it.
|
|
27
|
+
return c.json({ seen: true, appVersion: deps.version });
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { allTelemetryEvents, machineOf } from "../../slices/telemetry/repo.js";
|
|
3
|
+
import { usageOf } from "../../slices/telemetry/usage.js";
|
|
4
|
+
// The data behind `uiux/screens/10-usage.md`: the five counters, the "Tokens by stage"
|
|
5
|
+
// table, and the machine id with the app version. logic/16 step 6 computes all of it from
|
|
6
|
+
// the local event log, "independent of delivery", so this route never touches the
|
|
7
|
+
// collector and answers just as well offline.
|
|
8
|
+
// The return type is inferred so Hono keeps the route types the SPA's client is
|
|
9
|
+
// generated from; see stagingRoutes.
|
|
10
|
+
export function usageRoutes(deps) {
|
|
11
|
+
return new Hono().get("/", (c) => c.json(usageOf({
|
|
12
|
+
events: allTelemetryEvents(deps.db),
|
|
13
|
+
// The machine id is shown on this page and goes no further: the server is bound to
|
|
14
|
+
// loopback and the SPA reading it is this machine's own. Nothing puts it in a
|
|
15
|
+
// payload - the collector is told it once, in the envelope (logic/16 §Q129).
|
|
16
|
+
machineId: machineOf(deps.db)?.machineId ?? null,
|
|
17
|
+
appVersion: deps.version,
|
|
18
|
+
})));
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export function browserCommand(platform, url) {
|
|
3
|
+
if (platform === "darwin") {
|
|
4
|
+
return ["open", [url]];
|
|
5
|
+
}
|
|
6
|
+
if (platform === "win32") {
|
|
7
|
+
// `start` is a cmd builtin, and its first quoted argument is the window title.
|
|
8
|
+
return ["cmd", ["/c", "start", "", url]];
|
|
9
|
+
}
|
|
10
|
+
return ["xdg-open", [url]];
|
|
11
|
+
}
|
|
12
|
+
export function openBrowser(url, warn) {
|
|
13
|
+
const [command, args] = browserCommand(process.platform, url);
|
|
14
|
+
const child = spawn(command, [...args], { stdio: "ignore", detached: true });
|
|
15
|
+
// No browser is a nuisance, not a failure: the URL is already on the terminal.
|
|
16
|
+
child.on("error", (error) => {
|
|
17
|
+
warn(`Could not open a browser (${error.message}). Open ${url} yourself.`);
|
|
18
|
+
});
|
|
19
|
+
child.unref();
|
|
20
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2
|
+
export const systemClock = {
|
|
3
|
+
now: () => new Date(),
|
|
4
|
+
sleep: async (ms, signal) => {
|
|
5
|
+
if (signal === undefined) {
|
|
6
|
+
await delay(ms);
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
await delay(ms, undefined, { signal });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
// node:timers rejects with its own AbortError; the caller asked to be told why the
|
|
14
|
+
// wait was cut short, which is what every other abort path in the app carries.
|
|
15
|
+
throw signal.aborted ? signal.reason : error;
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
const defaultPort = 4242;
|
|
4
|
+
const defaultHost = "127.0.0.1";
|
|
5
|
+
export function configFrom(flags, env) {
|
|
6
|
+
const port = flags.port ?? env.SLOPIFY_PORT;
|
|
7
|
+
const host = flags.host ?? env.SLOPIFY_HOST ?? defaultHost;
|
|
8
|
+
const dataDir = flags["data-dir"] ?? env.SLOPIFY_DATA_DIR ?? join(homedir(), ".slopify");
|
|
9
|
+
if (host.trim() === "") {
|
|
10
|
+
throw new Error("host must not be empty");
|
|
11
|
+
}
|
|
12
|
+
return {
|
|
13
|
+
port: port === undefined ? defaultPort : parsePort(port),
|
|
14
|
+
host,
|
|
15
|
+
dataDir: resolve(dataDir),
|
|
16
|
+
open: flags["no-open"] === true ? false : !isTruthy(env.SLOPIFY_NO_OPEN),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function parsePort(value) {
|
|
20
|
+
const port = Number(value.trim());
|
|
21
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
22
|
+
throw new Error(`invalid port ${JSON.stringify(value)}: expected an integer between 1 and 65535`);
|
|
23
|
+
}
|
|
24
|
+
return port;
|
|
25
|
+
}
|
|
26
|
+
function isTruthy(value) {
|
|
27
|
+
if (value === undefined) {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
const normalised = value.trim().toLowerCase();
|
|
31
|
+
return normalised !== "" && normalised !== "0" && normalised !== "false";
|
|
32
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { chmodSync, existsSync } from "node:fs";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
// SQLITE_CONSTRAINT_UNIQUE. The extended result code node:sqlite puts on the error it
|
|
4
|
+
// throws when a UNIQUE index rejects a row.
|
|
5
|
+
const uniqueConstraint = 2067;
|
|
6
|
+
export function openDb(file) {
|
|
7
|
+
const db = new DatabaseSync(file, { enableForeignKeyConstraints: true, timeout: 5000 });
|
|
8
|
+
db.exec("PRAGMA journal_mode = WAL");
|
|
9
|
+
// Provider keys live in this file. The data directory is already 0700, but SQLite
|
|
10
|
+
// creates the database and its WAL sidecars at 0644, which would survive a copy out.
|
|
11
|
+
if (file !== ":memory:") {
|
|
12
|
+
for (const path of [file, `${file}-wal`, `${file}-shm`]) {
|
|
13
|
+
if (existsSync(path)) {
|
|
14
|
+
chmodSync(path, 0o600);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return db;
|
|
19
|
+
}
|
|
20
|
+
// Which uniqueness rule was broken is the caller's to know from the statement it ran;
|
|
21
|
+
// this only says that the schema, not the code, refused the row.
|
|
22
|
+
export function isUniqueConstraint(error) {
|
|
23
|
+
return error instanceof Error && "errcode" in error && error.errcode === uniqueConstraint;
|
|
24
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
// Resolved from this module rather than from the working directory, so it holds both
|
|
3
|
+
// under Vitest (src/kernel/db/migrations) and after the build (dist/kernel/db/migrations,
|
|
4
|
+
// filled by packages/app/scripts/copy-migrations.mjs).
|
|
5
|
+
const migrationsDir = new URL("./migrations/", import.meta.url);
|
|
6
|
+
export function migrate(db, clock) {
|
|
7
|
+
const files = readdirSync(migrationsDir)
|
|
8
|
+
.filter((file) => file.endsWith(".sql"))
|
|
9
|
+
.sort();
|
|
10
|
+
const known = files.map(versionOf);
|
|
11
|
+
const applied = appliedVersions(db);
|
|
12
|
+
const newest = Math.max(0, ...applied);
|
|
13
|
+
const newestKnown = Math.max(0, ...known);
|
|
14
|
+
if (newest > newestKnown) {
|
|
15
|
+
throw new Error(`database schema ${newest} is newer than this app knows (${newestKnown})`);
|
|
16
|
+
}
|
|
17
|
+
for (const [index, file] of files.entries()) {
|
|
18
|
+
const version = known[index];
|
|
19
|
+
if (version === undefined || applied.has(version)) {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
db.exec("BEGIN");
|
|
23
|
+
try {
|
|
24
|
+
db.exec(readFileSync(new URL(file, migrationsDir), "utf8"));
|
|
25
|
+
db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)").run(version, clock.now().toISOString());
|
|
26
|
+
db.exec("COMMIT");
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
db.exec("ROLLBACK");
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function versionOf(file) {
|
|
35
|
+
const version = Number(file.slice(0, 4));
|
|
36
|
+
if (!Number.isInteger(version) || version < 1) {
|
|
37
|
+
throw new Error(`migration ${file} does not start with a version number`);
|
|
38
|
+
}
|
|
39
|
+
return version;
|
|
40
|
+
}
|
|
41
|
+
// The first migration creates schema_migrations itself, so an absent table means
|
|
42
|
+
// nothing has been applied yet.
|
|
43
|
+
function appliedVersions(db) {
|
|
44
|
+
const table = db
|
|
45
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'schema_migrations'")
|
|
46
|
+
.get();
|
|
47
|
+
if (table === undefined) {
|
|
48
|
+
return new Set();
|
|
49
|
+
}
|
|
50
|
+
const versions = new Set();
|
|
51
|
+
for (const row of db.prepare("SELECT version FROM schema_migrations").all()) {
|
|
52
|
+
const version = row.version;
|
|
53
|
+
if (typeof version !== "number") {
|
|
54
|
+
throw new Error(`schema_migrations holds a non-numeric version: ${String(version)}`);
|
|
55
|
+
}
|
|
56
|
+
versions.add(version);
|
|
57
|
+
}
|
|
58
|
+
return versions;
|
|
59
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
CREATE TABLE projects (id TEXT PRIMARY KEY, title TEXT NOT NULL CHECK(length(title) <= 200), format TEXT NOT NULL CHECK(format IN ('16:9','9:16')), config TEXT NOT NULL, created_at TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
2
|
+
CREATE TABLE stages (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK(kind IN ('research','article','audio','images','thumbnail','video')), source TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('pending','running','done','failed','canceled','provided','skipped')), failure_reason TEXT, attempt_count INTEGER NOT NULL DEFAULT 0, progress_current INTEGER, progress_total INTEGER, started_at TEXT, finished_at TEXT, UNIQUE(project_id, kind));
|
|
3
|
+
CREATE TABLE attempts (id TEXT PRIMARY KEY, stage_id TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE, piece_id TEXT, n INTEGER NOT NULL, started_at TEXT NOT NULL, ended_at TEXT, outcome TEXT, error_text TEXT);
|
|
4
|
+
CREATE TABLE stage_pieces (id TEXT PRIMARY KEY, stage_id TEXT NOT NULL REFERENCES stages(id) ON DELETE CASCADE, kind TEXT NOT NULL, idx INTEGER NOT NULL, state TEXT NOT NULL, payload TEXT, UNIQUE(stage_id, kind, idx));
|
|
5
|
+
CREATE TABLE outputs (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, stage_kind TEXT NOT NULL, role TEXT NOT NULL, path TEXT NOT NULL, original_filename TEXT, bytes INTEGER NOT NULL, duration_ms INTEGER, meta TEXT, created_at TEXT NOT NULL);
|
|
6
|
+
CREATE INDEX outputs_project ON outputs(project_id, stage_kind);
|
|
7
|
+
CREATE TABLE prompts (id TEXT PRIMARY KEY, kind TEXT NOT NULL CHECK(kind IN ('article','image','thumbnail')), name TEXT NOT NULL, body TEXT NOT NULL, slots TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
8
|
+
CREATE UNIQUE INDEX prompts_name ON prompts(kind, lower(name));
|
|
9
|
+
CREATE TABLE entries (id TEXT PRIMARY KEY, category TEXT NOT NULL CHECK(category IN ('intro','outro')), mode TEXT NOT NULL CHECK(mode IN ('text','llm')), name TEXT NOT NULL, body TEXT NOT NULL, slots TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
10
|
+
CREATE UNIQUE INDEX entries_name ON entries(category, lower(name));
|
|
11
|
+
CREATE TABLE provider_keys (provider TEXT PRIMARY KEY, key TEXT NOT NULL, updated_at TEXT NOT NULL);
|
|
12
|
+
CREATE TABLE voices (id TEXT PRIMARY KEY, provider TEXT NOT NULL, name TEXT NOT NULL, voice_id TEXT NOT NULL, UNIQUE(provider, voice_id));
|
|
13
|
+
CREATE TABLE settings (key TEXT PRIMARY KEY, value TEXT NOT NULL);
|
|
14
|
+
CREATE TABLE staged_files (id TEXT PRIMARY KEY, stage_kind TEXT NOT NULL, path TEXT NOT NULL, original_filename TEXT NOT NULL, bytes INTEGER NOT NULL, state TEXT NOT NULL, created_at TEXT NOT NULL);
|
|
15
|
+
CREATE TABLE telemetry_events (id TEXT PRIMARY KEY, type TEXT NOT NULL, payload TEXT NOT NULL, created_at TEXT NOT NULL, delivered_at TEXT);
|
|
16
|
+
CREATE TABLE machine (machine_id TEXT PRIMARY KEY, notice_seen_at TEXT, app_version TEXT NOT NULL);
|
|
17
|
+
CREATE TABLE schema_migrations (version INTEGER PRIMARY KEY, applied_at TEXT NOT NULL);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// SQLite starts a transaction for a SAVEPOINT issued outside one and nests it inside one
|
|
2
|
+
// that is already open, so a slice can wrap its own writes without knowing whether its
|
|
3
|
+
// caller wrapped them too. The name is a constant rather than a parameter: SQLite matches
|
|
4
|
+
// RELEASE and ROLLBACK TO against the most recent savepoint of that name, which is
|
|
5
|
+
// exactly the nesting we want, and nothing a caller passes reaches the statement.
|
|
6
|
+
const savepoint = "slopify";
|
|
7
|
+
export function transact(db, run) {
|
|
8
|
+
db.exec(`SAVEPOINT ${savepoint}`);
|
|
9
|
+
try {
|
|
10
|
+
const result = run();
|
|
11
|
+
// node:sqlite is synchronous, so an async block would release this frame while its
|
|
12
|
+
// awaited writes were still to come - and they would then land outside any
|
|
13
|
+
// transaction. The frame is unwound below, which is the only safe answer.
|
|
14
|
+
if (isThenable(result)) {
|
|
15
|
+
// Refusing the block orphans the promise it already started. Node treats an
|
|
16
|
+
// unhandled rejection as fatal, so it would take the process down and bury the
|
|
17
|
+
// error thrown here, which is the one that names the mistake.
|
|
18
|
+
void Promise.resolve(result).catch(() => { });
|
|
19
|
+
throw new Error("transact runs synchronous work only: an awaited write would land after the savepoint is released");
|
|
20
|
+
}
|
|
21
|
+
// Inside the try: a RELEASE that fails leaves the frame open with the block's writes
|
|
22
|
+
// uncommitted, which is a rollback, not a success to return a value from.
|
|
23
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
24
|
+
return result;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
unwind(db, error);
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// An unguarded ROLLBACK TO would leave the frame open on failure, and the enclosing
|
|
32
|
+
// transact - which names the same savepoint - would then aim its own rollback at the
|
|
33
|
+
// leaked frame and commit part of what it meant to undo.
|
|
34
|
+
function unwind(db, error) {
|
|
35
|
+
try {
|
|
36
|
+
db.exec(`ROLLBACK TO ${savepoint}`);
|
|
37
|
+
db.exec(`RELEASE ${savepoint}`);
|
|
38
|
+
}
|
|
39
|
+
catch (cleanup) {
|
|
40
|
+
throw new Error(`the transaction could not be unwound: ${messageOf(cleanup)}`, {
|
|
41
|
+
cause: error,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isThenable(value) {
|
|
46
|
+
return (typeof value === "object" &&
|
|
47
|
+
value !== null &&
|
|
48
|
+
"then" in value &&
|
|
49
|
+
typeof value.then === "function");
|
|
50
|
+
}
|
|
51
|
+
function messageOf(error) {
|
|
52
|
+
return error instanceof Error ? error.message : String(error);
|
|
53
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
export function acquireInstanceLock(lockPath) {
|
|
3
|
+
const pid = process.pid;
|
|
4
|
+
if (!claim(lockPath, pid)) {
|
|
5
|
+
const holder = holderOf(lockPath);
|
|
6
|
+
if (holder !== undefined && isAlive(holder)) {
|
|
7
|
+
throw new Error(`another Slopify instance is already running on this data directory (pid ${holder}). ` +
|
|
8
|
+
`Stop it, or delete ${lockPath} if it is stale.`);
|
|
9
|
+
}
|
|
10
|
+
// The recorded process is gone, or the file says nothing usable. Reclaiming it here
|
|
11
|
+
// is what keeps a crash from bricking the data directory until a file is deleted by hand.
|
|
12
|
+
discard(lockPath);
|
|
13
|
+
if (!claim(lockPath, pid)) {
|
|
14
|
+
throw new Error(`could not take the instance lock at ${lockPath}: another process was faster`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
release: () => {
|
|
19
|
+
if (holderOf(lockPath) === pid) {
|
|
20
|
+
discard(lockPath);
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
function claim(lockPath, pid) {
|
|
26
|
+
try {
|
|
27
|
+
writeFileSync(lockPath, `${pid}\n`, { flag: "wx", mode: 0o600 });
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (errnoOf(error) === "EEXIST") {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function holderOf(lockPath) {
|
|
38
|
+
let contents;
|
|
39
|
+
try {
|
|
40
|
+
contents = readFileSync(lockPath, "utf8");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
const pid = Number(contents.trim());
|
|
46
|
+
return Number.isInteger(pid) && pid > 0 ? pid : undefined;
|
|
47
|
+
}
|
|
48
|
+
function discard(lockPath) {
|
|
49
|
+
try {
|
|
50
|
+
unlinkSync(lockPath);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (errnoOf(error) !== "ENOENT") {
|
|
54
|
+
throw error;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function isAlive(pid) {
|
|
59
|
+
try {
|
|
60
|
+
process.kill(pid, 0);
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
// EPERM means the process exists and belongs to someone else.
|
|
65
|
+
return errnoOf(error) === "EPERM";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function errnoOf(error) {
|
|
69
|
+
if (error instanceof Error && "code" in error && typeof error.code === "string") {
|
|
70
|
+
return error.code;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { appendFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
// ceiling: prefix matching, not a secret scanner. The prefixes cover the providers in
|
|
4
|
+
// `slices/settings/model.ts`; fal.ai's `<uuid>:<hex>` form has no prefix to match on and
|
|
5
|
+
// is the known gap. That is why LogFields is a closed set of three strings: no caller
|
|
6
|
+
// can hand this module an arbitrary payload to serialise.
|
|
7
|
+
const keyLike = /\b(?:sk|rk|pk|hf|gsk|xai|nvapi|r8)[-_][A-Za-z0-9_-]{16,}|\bAIza[A-Za-z0-9_-]{16,}/g;
|
|
8
|
+
const bearerLike = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
9
|
+
export function openLog(logsDir, clock) {
|
|
10
|
+
return {
|
|
11
|
+
write: (level, event, fields) => {
|
|
12
|
+
const at = clock.now().toISOString();
|
|
13
|
+
const record = { ts: at, level, event };
|
|
14
|
+
if (fields?.projectId !== undefined) {
|
|
15
|
+
record.projectId = fields.projectId;
|
|
16
|
+
}
|
|
17
|
+
if (fields?.stage !== undefined) {
|
|
18
|
+
record.stage = fields.stage;
|
|
19
|
+
}
|
|
20
|
+
if (fields?.detail !== undefined) {
|
|
21
|
+
record.detail = redact(fields.detail);
|
|
22
|
+
}
|
|
23
|
+
// ceiling: one open/append/close per line, and rotation by the date in the file
|
|
24
|
+
// name. A held handle is the upgrade if the runner ever logs per streamed chunk.
|
|
25
|
+
const file = join(logsDir, `slopify-${at.slice(0, 10)}.log`);
|
|
26
|
+
appendFileSync(file, `${JSON.stringify(record)}\n`, { mode: 0o600 });
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// Exported for the attempt wrapper: a provider's verbatim error is shown on the project
|
|
31
|
+
// page and stored on the attempt, so it goes through the same filter as a log line.
|
|
32
|
+
export function redact(text) {
|
|
33
|
+
return text.replace(bearerLike, "Bearer [redacted]").replace(keyLike, "[redacted]");
|
|
34
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { chmodSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
export function layout(dataDir) {
|
|
4
|
+
const root = resolve(dataDir);
|
|
5
|
+
return {
|
|
6
|
+
dataDir: root,
|
|
7
|
+
db: join(root, "slopify.db"),
|
|
8
|
+
projects: join(root, "projects"),
|
|
9
|
+
staging: join(root, "staging"),
|
|
10
|
+
logs: join(root, "logs"),
|
|
11
|
+
lock: join(root, ".lock"),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function ensureDirs(paths, options) {
|
|
15
|
+
for (const dir of [paths.dataDir, paths.projects, paths.staging, paths.logs]) {
|
|
16
|
+
mkdirSync(dir, { recursive: true, mode: options.mode });
|
|
17
|
+
// mkdir's mode is masked by the umask and ignored for a directory that already
|
|
18
|
+
// exists; provider keys sit in this tree, so the mode has to be forced.
|
|
19
|
+
chmodSync(dir, options.mode);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// The pipeline's vocabulary. It sits in the kernel because the runner, the slices that
|
|
2
|
+
// implement a stage, and the edge that reports one all name the same finite sets, and
|
|
3
|
+
// the runner may not import a slice to learn them (03-conventions Dependency injection).
|
|
4
|
+
// The frame a run is made in, and the aspect an image is asked for: one set, named once
|
|
5
|
+
// (`logic/04`, `logic/09` step 1).
|
|
6
|
+
export const formats = ["16:9", "9:16"];
|
|
7
|
+
export const stageKinds = ["research", "article", "audio", "images", "thumbnail", "video"];
|
|
8
|
+
export const stageStates = [
|
|
9
|
+
"pending",
|
|
10
|
+
"running",
|
|
11
|
+
"done",
|
|
12
|
+
"failed",
|
|
13
|
+
"canceled",
|
|
14
|
+
"provided",
|
|
15
|
+
"skipped",
|
|
16
|
+
];
|
|
17
|
+
// logic/01 §Q9 names four project states and derives them in order: running if any
|
|
18
|
+
// stage is running, else canceled, else failed, else done when video is done. A project
|
|
19
|
+
// whose stages are all still pending matches none of them, and that window is real
|
|
20
|
+
// between creating the project and the runner starting the first stage, so `pending` is
|
|
21
|
+
// the fallback the derivation returns. Nothing stores it: status is always derived.
|
|
22
|
+
export const projectStates = ["running", "canceled", "failed", "done", "pending"];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const messageRoles = ["system", "user", "assistant"];
|