@gentbajko/slopify 0.1.0 → 0.3.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/README.md +2 -2
- package/dist/adapter-registry.js +11 -12
- package/dist/adapters/image/bytes.js +1 -1
- package/dist/adapters/image/fal.js +55 -44
- package/dist/adapters/image/openai.js +19 -24
- package/dist/adapters/image/replicate.js +33 -43
- package/dist/adapters/llm/claude-code.js +16 -19
- package/dist/adapters/llm/codex.js +14 -15
- package/dist/adapters/llm/openrouter.js +13 -16
- package/dist/adapters/llm/run-cli.js +9 -11
- package/dist/adapters/llm/sse-lines.js +1 -2
- package/dist/adapters/retry-after.js +6 -6
- package/dist/adapters/tts/cartesia.js +5 -5
- package/dist/adapters/tts/elevenlabs.js +14 -19
- package/dist/adapters/tts/openai.js +9 -10
- package/dist/edge/cli.js +0 -0
- package/dist/edge/http/actions.js +8 -9
- package/dist/edge/http/app.js +3 -3
- package/dist/edge/http/entries.js +1 -1
- package/dist/edge/http/projects.js +7 -7
- package/dist/edge/http/prompts.js +6 -6
- package/dist/edge/http/providers.js +4 -4
- package/dist/edge/http/settings.js +2 -2
- package/dist/edge/http/staging.js +1 -1
- package/dist/edge/http/telemetry.js +6 -7
- package/dist/edge/http/usage.js +5 -5
- package/dist/kernel/config/index.js +1 -1
- package/dist/kernel/db/tx.js +9 -12
- package/dist/kernel/log.js +5 -5
- package/dist/kernel/pipeline.js +7 -9
- package/dist/kernel/ports/model.js +3 -4
- package/dist/kernel/runner/attempt-repo.js +5 -6
- package/dist/kernel/runner/attempt.js +16 -23
- package/dist/kernel/runner/graph.js +18 -29
- package/dist/kernel/runner/index.js +27 -33
- package/dist/kernel/runner/piece-repo.js +9 -11
- package/dist/kernel/runner/providers.js +5 -7
- package/dist/kernel/version.js +3 -3
- package/dist/main.js +10 -11
- package/dist/slices/admission/model.js +2 -2
- package/dist/slices/admission/repo.js +6 -7
- package/dist/slices/admission/rules.js +12 -15
- package/dist/slices/admission/start.js +7 -8
- package/dist/slices/admission/substitute.js +11 -11
- package/dist/slices/article/continuation.js +9 -9
- package/dist/slices/article/plain.js +5 -5
- package/dist/slices/article/run.js +25 -25
- package/dist/slices/article/split.js +1 -1
- package/dist/slices/article/store.js +5 -5
- package/dist/slices/cancel/index.js +8 -8
- package/dist/slices/images/run.js +32 -34
- package/dist/slices/library/lint.js +3 -3
- package/dist/slices/library/model.js +7 -7
- package/dist/slices/library/repo.js +3 -3
- package/dist/slices/library/save.js +7 -7
- package/dist/slices/library/slots.js +5 -5
- package/dist/slices/narration/chunk.js +12 -12
- package/dist/slices/narration/concat.js +12 -13
- package/dist/slices/narration/run.js +46 -49
- package/dist/slices/reruns/cascade.js +10 -11
- package/dist/slices/reruns/index.js +36 -37
- package/dist/slices/research/planner.js +4 -5
- package/dist/slices/research/run.js +22 -23
- package/dist/slices/research/synthesis.js +5 -5
- package/dist/slices/settings/cli-status.js +9 -10
- package/dist/slices/settings/keys.js +8 -9
- package/dist/slices/settings/model.js +5 -5
- package/dist/slices/settings/readiness.js +3 -4
- package/dist/slices/settings/repo.js +1 -1
- package/dist/slices/settings/voices.js +3 -4
- package/dist/slices/storage/asset-name.js +2 -2
- package/dist/slices/storage/delete-project.js +3 -5
- package/dist/slices/storage/downloads.js +4 -4
- package/dist/slices/storage/layout.js +5 -6
- package/dist/slices/storage/model.js +1 -1
- package/dist/slices/storage/reconcile.js +6 -7
- package/dist/slices/storage/repo.js +1 -1
- package/dist/slices/storage/staging.js +12 -15
- package/dist/slices/telemetry/collector-client.js +2 -2
- package/dist/slices/telemetry/flush.js +8 -8
- package/dist/slices/telemetry/machine.js +6 -7
- package/dist/slices/telemetry/model.js +11 -13
- package/dist/slices/telemetry/record.js +7 -7
- package/dist/slices/telemetry/repo.js +4 -4
- package/dist/slices/telemetry/usage.js +5 -5
- package/dist/slices/thumbnail/by-llm.js +2 -2
- package/dist/slices/thumbnail/run.js +30 -32
- package/dist/slices/video/ffmpeg.js +6 -6
- package/dist/slices/video/plan.js +9 -9
- package/dist/slices/video/run.js +19 -20
- package/dist/web/assets/index-DTqh2zoJ.js +81 -0
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- package/dist/web/assets/index-D_sWbKQi.js +0 -81
|
@@ -5,7 +5,7 @@ import { cliEvent, cliShaped, endedWithout, promptOf } from "./run-cli.js";
|
|
|
5
5
|
import { lines } from "./sse-lines.js";
|
|
6
6
|
// The local-agent adapter for the Codex CLI. Same shape as Claude Code's and a different
|
|
7
7
|
// vocabulary: Codex writes a JSONL thread of `thread.started`, `item.*` and `turn.*`
|
|
8
|
-
// events. No key here either - the CLI's own login authenticates it
|
|
8
|
+
// events. No key here either - the CLI's own login authenticates it.
|
|
9
9
|
export const codexBinary = "codex";
|
|
10
10
|
// ceiling: a fixed list. `codex` has no offline command that prints the models an account
|
|
11
11
|
// may use - `codex doctor` reports the install, not the catalogue, and the model refresh
|
|
@@ -16,13 +16,13 @@ export const codexModels = [
|
|
|
16
16
|
{ id: "gpt-5.1-codex-max", name: "GPT-5.1 Codex Max" },
|
|
17
17
|
{ id: "gpt-5.6-sol", name: "GPT-5.6 Sol" },
|
|
18
18
|
];
|
|
19
|
-
// `codex exec --help` (0.149.1) for the flags. `-c web_search=<mode>` is a TOML override,
|
|
20
|
-
//
|
|
21
|
-
// `
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
// the
|
|
25
|
-
//
|
|
19
|
+
// `codex exec --help` (0.149.1) for the flags. `-c web_search=<mode>` is a TOML override, and
|
|
20
|
+
// the binary's own error names the modes: "unknown variant `bogus`, expected one of `disabled`,
|
|
21
|
+
// `cached`, `indexed`, `live`". `live` is the grounded mode research asks for and `disabled` is
|
|
22
|
+
// what every other stage runs under, so none grounds itself by accident. `--ephemeral` keeps no
|
|
23
|
+
// session file, `--skip-git-repo-check` lets it run in the data directory, and the quotes in
|
|
24
|
+
// the value are part of the argv element because the override is parsed as TOML, where a bare
|
|
25
|
+
// `live` is not a string.
|
|
26
26
|
export function codexArgs(req) {
|
|
27
27
|
return [
|
|
28
28
|
"exec",
|
|
@@ -72,10 +72,9 @@ export function codexLlm(deps) {
|
|
|
72
72
|
}
|
|
73
73
|
if (event.type === "turn.completed") {
|
|
74
74
|
const { usage } = cliShaped(binary, turnCompleted, event.value);
|
|
75
|
-
// ceiling: Codex reports no stop reason, so the continuation loop
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
// constraining every stage's answer.
|
|
75
|
+
// ceiling: Codex reports no stop reason, so the continuation loop cannot tell a
|
|
76
|
+
// finished answer from one cut at the output limit. A `--output-schema` run
|
|
77
|
+
// would, at the cost of constraining every stage's answer.
|
|
79
78
|
yield { type: "done", usage: usageOf(usage), finishReason: null };
|
|
80
79
|
return;
|
|
81
80
|
}
|
|
@@ -101,8 +100,8 @@ export function codexLlm(deps) {
|
|
|
101
100
|
run.kill();
|
|
102
101
|
}
|
|
103
102
|
// A cancelled run ends its stream the same way an exhausted one does: the child was
|
|
104
|
-
// killed, so stdout simply stopped.
|
|
105
|
-
//
|
|
103
|
+
// killed, so stdout simply stopped. An aborted call counts as nothing, so it must not
|
|
104
|
+
// be reported as the provider failing.
|
|
106
105
|
req.signal.throwIfAborted();
|
|
107
106
|
// The stream ended with neither a completed turn nor a failure.
|
|
108
107
|
throw providerError({
|
|
@@ -113,7 +112,7 @@ export function codexLlm(deps) {
|
|
|
113
112
|
return {
|
|
114
113
|
id: "codex",
|
|
115
114
|
// ceiling: one `item.completed` per whole message rather than per token, same as the
|
|
116
|
-
// other CLI. Enough for the idle timeout
|
|
115
|
+
// other CLI. Enough for the idle timeout to see life on the stream.
|
|
117
116
|
capabilities: { streams: true, reportsUsage: true, webSearch: true },
|
|
118
117
|
models: () => Promise.resolve(codexModels),
|
|
119
118
|
complete,
|
|
@@ -3,9 +3,8 @@ import { redact } from "../../kernel/log.js";
|
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
4
|
import { retryAfter } from "../retry-after.js";
|
|
5
5
|
import { sseData } from "./sse-lines.js";
|
|
6
|
-
// The HTTP gateway adapter
|
|
7
|
-
//
|
|
8
|
-
// the reader at rung 6, so nothing here is worth a dependency.
|
|
6
|
+
// The HTTP gateway adapter: the platform's own `fetch` plus the line reader beside this
|
|
7
|
+
// file, no SDK. Neither is worth a dependency.
|
|
9
8
|
export const openRouterBase = "https://openrouter.ai/api/v1";
|
|
10
9
|
// OpenRouter ranks apps by these two headers; they carry no user data.
|
|
11
10
|
const appHeaders = {
|
|
@@ -13,7 +12,7 @@ const appHeaders = {
|
|
|
13
12
|
"X-Title": "Slopify",
|
|
14
13
|
};
|
|
15
14
|
// A wire payload is narrowed, never cast: everything unlisted is dropped at the seam so
|
|
16
|
-
// no vendor shape can leak past this file
|
|
15
|
+
// no vendor shape can leak past this file.
|
|
17
16
|
const modelList = z.object({
|
|
18
17
|
data: z.array(z.object({ id: z.string(), name: z.string().optional() })),
|
|
19
18
|
});
|
|
@@ -46,9 +45,9 @@ export function openRouterLlm(deps) {
|
|
|
46
45
|
})),
|
|
47
46
|
stream: true,
|
|
48
47
|
// The usage-accounting flag: without it the final chunk carries no token counts
|
|
49
|
-
// and the Usage page would have nothing to count
|
|
48
|
+
// and the Usage page would have nothing to count.
|
|
50
49
|
usage: { include: true },
|
|
51
|
-
//
|
|
50
|
+
// Web grounding is asked for explicitly. OpenRouter runs the
|
|
52
51
|
// search itself, so the plugin works whatever model was picked.
|
|
53
52
|
...(req.webSearch === true ? { plugins: [{ id: "web" }] } : {}),
|
|
54
53
|
}),
|
|
@@ -86,8 +85,8 @@ export function openRouterLlm(deps) {
|
|
|
86
85
|
}
|
|
87
86
|
if (choice?.finish_reason !== undefined && choice.finish_reason !== null) {
|
|
88
87
|
// It arrives on the last choice chunk, one frame before the usage frame, so it is
|
|
89
|
-
// held rather than read off whichever chunk happens to be last
|
|
90
|
-
// reads it to tell a finished article from a truncated one
|
|
88
|
+
// held rather than read off whichever chunk happens to be last - the continuation
|
|
89
|
+
// loop reads it to tell a finished article from a truncated one.
|
|
91
90
|
finishReason = choice.finish_reason;
|
|
92
91
|
}
|
|
93
92
|
if (chunk.usage !== undefined && chunk.usage !== null) {
|
|
@@ -131,7 +130,7 @@ export function openRouterLlm(deps) {
|
|
|
131
130
|
};
|
|
132
131
|
}
|
|
133
132
|
function headers(key) {
|
|
134
|
-
//
|
|
133
|
+
// An attempt that finds no key fails rather than calling anonymously
|
|
135
134
|
// and being told off by the provider in words the user cannot act on. `missing_key`
|
|
136
135
|
// rather than `auth` because the same rule makes it terminal: there is nothing to
|
|
137
136
|
// retry until the user saves a key, and the wrapper is where that is decided.
|
|
@@ -140,8 +139,7 @@ function headers(key) {
|
|
|
140
139
|
}
|
|
141
140
|
return { Authorization: `Bearer ${key}`, ...appHeaders };
|
|
142
141
|
}
|
|
143
|
-
// Only the adapter
|
|
144
|
-
// the attempt wrapper maps it and nothing downstream classifies again (03-conventions).
|
|
142
|
+
// Only the adapter sees the vendor's status code, so only the adapter names the kind.
|
|
145
143
|
function kindOf(status) {
|
|
146
144
|
if (status === 401 || status === 403) {
|
|
147
145
|
return "auth";
|
|
@@ -149,16 +147,15 @@ function kindOf(status) {
|
|
|
149
147
|
if (status === 429) {
|
|
150
148
|
return "rate_limit";
|
|
151
149
|
}
|
|
152
|
-
// ceiling: everything else is `other` and is retried
|
|
153
|
-
// way four times over
|
|
154
|
-
//
|
|
150
|
+
// ceiling: everything else is `other` and is retried, so a 400 or a 402 fails the same
|
|
151
|
+
// way four times over. A terminal "this will never work" kind has to reach the port's
|
|
152
|
+
// error contract first, which is not this adapter's to widen.
|
|
155
153
|
return "other";
|
|
156
154
|
}
|
|
157
155
|
async function failure(response) {
|
|
158
156
|
const text = await response.text().catch(() => "");
|
|
159
157
|
const parsed = errorBody.safeParse(safeJson(text));
|
|
160
|
-
// The provider's own words,
|
|
161
|
-
// error body is free to quote the key back and this is the first place it is held.
|
|
158
|
+
// The provider's own words, through the redactor - an error body may quote the key back.
|
|
162
159
|
const message = redact(parsed.success ? parsed.data.error.message : text.trim() || response.statusText);
|
|
163
160
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
164
161
|
return providerError({
|
|
@@ -8,12 +8,11 @@ import { providerError } from "../../kernel/ports/model.js";
|
|
|
8
8
|
// buries its reason in the first line of a long report.
|
|
9
9
|
export const stderrMax = 8192;
|
|
10
10
|
export function nodeRunCli(binary, args, signal) {
|
|
11
|
-
// An argument array, never a shell string: a prompt carrying backticks, `$(...)`,
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// no agent session outlives the run that started it (`logic/13`).
|
|
11
|
+
// An argument array, never a shell string: a prompt carrying backticks, `$(...)`, quotes or
|
|
12
|
+
// newlines is one argv element and nothing in it can become a command. stdin is /dev/null
|
|
13
|
+
// because `codex exec` appends piped stdin to the prompt, and a pipe nobody closes would
|
|
14
|
+
// leave it waiting for an EOF that never comes. `signal` is how the child dies: Node sends it
|
|
15
|
+
// SIGTERM when the stage is cancelled, so no agent session outlives the run that started it.
|
|
17
16
|
const child = spawn(binary, [...args], {
|
|
18
17
|
signal,
|
|
19
18
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -60,16 +59,15 @@ export function nodeRunCli(binary, args, signal) {
|
|
|
60
59
|
};
|
|
61
60
|
}
|
|
62
61
|
// ceiling: both CLIs take one prompt string, so a system or assistant turn is flattened
|
|
63
|
-
// into it under a header
|
|
64
|
-
//
|
|
65
|
-
// `logic/06` and `logic/07` each compose exactly one user message.
|
|
62
|
+
// into it under a header. Claude Code's `--append-system-prompt` is the upgrade if a stage
|
|
63
|
+
// ever needs a real system turn; today research and the article each compose one message.
|
|
66
64
|
export function promptOf(messages) {
|
|
67
65
|
return messages
|
|
68
66
|
.map((message) => message.role === "user" ? message.content : `[${message.role}]\n${message.content}`)
|
|
69
67
|
.join("\n\n");
|
|
70
68
|
}
|
|
71
|
-
// A bare non-zero exit tells the user nothing, so whatever the CLI put on
|
|
72
|
-
//
|
|
69
|
+
// A bare non-zero exit tells the user nothing, so the stage shows whatever the CLI put on
|
|
70
|
+
// stderr - the provider's error text, verbatim.
|
|
73
71
|
export function endedWithout(binary, ended, stderr) {
|
|
74
72
|
const said = redact(stderr.trim());
|
|
75
73
|
const how = ended.error === null
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// Bytes off a socket or off a child's stdout, turned into the units above them. One file
|
|
2
2
|
// for both because OpenRouter's SSE and the two CLIs' JSONL differ only in what a line
|
|
3
|
-
// means, and
|
|
4
|
-
// at rung 6, no SDK for any of the three providers.
|
|
3
|
+
// means, and none of the three providers is worth an SDK.
|
|
5
4
|
// A chunk boundary lands wherever the network or the pipe put it: between two lines, in
|
|
6
5
|
// the middle of one, or inside a multi-byte character. TextDecoder's streaming mode holds
|
|
7
6
|
// back the tail of a split UTF-8 sequence instead of emitting a replacement character,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
1
|
+
// A 429 naming a Retry-After replaces the fixed backoff for that wait. RFC 9110 allows seconds
|
|
2
|
+
// or an HTTP date; `Date.parse` is the platform's own reader for the second form, so no date
|
|
3
|
+
// parsing is written here.
|
|
4
4
|
//
|
|
5
|
-
// It sits beside the adapters rather than inside one because the rule is the retry
|
|
6
|
-
//
|
|
7
|
-
//
|
|
5
|
+
// It sits beside the adapters rather than inside one because the rule is the retry policy's,
|
|
6
|
+
// not any provider's: four adapters read the same header off the same kind of response. Nothing
|
|
7
|
+
// else is shared between them.
|
|
8
8
|
export function retryAfter(header) {
|
|
9
9
|
if (header === null) {
|
|
10
10
|
return undefined;
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
4
|
import { retryAfter } from "../retry-after.js";
|
|
5
|
-
// The HTTP gateway adapter for Cartesia
|
|
5
|
+
// The HTTP gateway adapter for Cartesia. `fetch` and
|
|
6
6
|
// nothing else; `/tts/bytes` streams the audio it renders, so the response body is
|
|
7
7
|
// already the stream the port asks for.
|
|
8
8
|
export const cartesiaBase = "https://api.cartesia.ai";
|
|
@@ -12,7 +12,7 @@ export const cartesiaBase = "https://api.cartesia.ai";
|
|
|
12
12
|
export const cartesiaVersion = "2026-03-01";
|
|
13
13
|
export const cartesiaModel = "sonic-3.5";
|
|
14
14
|
// mp3 is the port's container; `bit_rate` is required for it and `sample_rate` fixes the
|
|
15
|
-
// rate the concatenation
|
|
15
|
+
// rate the concatenation then keeps.
|
|
16
16
|
const outputFormat = { container: "mp3", bit_rate: 128_000, sample_rate: 44_100 };
|
|
17
17
|
// The structured error of API version 2026-03-01 and newer.
|
|
18
18
|
const errorBody = z.object({
|
|
@@ -33,7 +33,7 @@ export function cartesiaTts(deps) {
|
|
|
33
33
|
"Cartesia-Version": cartesiaVersion,
|
|
34
34
|
"Content-Type": "application/json",
|
|
35
35
|
},
|
|
36
|
-
//
|
|
36
|
+
// No pre-check on length; Cartesia's own limit surfaces as its
|
|
37
37
|
// error. `language` is left out so the model reads it off the transcript.
|
|
38
38
|
body: JSON.stringify({
|
|
39
39
|
model_id: cartesiaModel,
|
|
@@ -54,7 +54,7 @@ export function cartesiaTts(deps) {
|
|
|
54
54
|
}
|
|
55
55
|
function keyOf(deps) {
|
|
56
56
|
const key = deps.key();
|
|
57
|
-
//
|
|
57
|
+
// An absent key is terminal, so it never becomes a request.
|
|
58
58
|
if (key === undefined || key === "") {
|
|
59
59
|
throw providerError({ kind: "missing_key", message: "no Cartesia key is stored" });
|
|
60
60
|
}
|
|
@@ -78,7 +78,7 @@ async function failure(response, voiceId) {
|
|
|
78
78
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
79
79
|
return providerError({
|
|
80
80
|
kind: kindOf(response.status),
|
|
81
|
-
// `
|
|
81
|
+
// `voice_not_found` has to say which voice was asked for; Cartesia's
|
|
82
82
|
// own message does not repeat the id.
|
|
83
83
|
message: `Cartesia answered ${response.status} for voice ${voiceId}: ${message}`,
|
|
84
84
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
@@ -2,17 +2,16 @@ import { z } from "zod";
|
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
4
|
import { retryAfter } from "../retry-after.js";
|
|
5
|
-
// The HTTP gateway adapter for ElevenLabs
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// nothing but a dependency.
|
|
5
|
+
// The HTTP gateway adapter for ElevenLabs: the platform's own `fetch` and nothing else. The
|
|
6
|
+
// response body already is the `ReadableStream<Uint8Array>` the port asks for, so an SDK
|
|
7
|
+
// would buy nothing but a dependency.
|
|
9
8
|
export const elevenLabsBase = "https://api.elevenlabs.io/v1";
|
|
10
9
|
// The /stream endpoint rather than the plain one: `capabilities.streams` is true, and the
|
|
11
10
|
// attempt wrapper measures its 120 s as an idle timeout between chunks only when bytes
|
|
12
|
-
// keep arriving
|
|
11
|
+
// keep arriving.
|
|
13
12
|
export const elevenLabsModel = "eleven_multilingual_v2";
|
|
14
|
-
// mp3 at the port's container, 44.1 kHz, 128 kbps: `kernel/ports/tts.ts` fixes mp3 and
|
|
15
|
-
//
|
|
13
|
+
// mp3 at the port's container, 44.1 kHz, 128 kbps: `kernel/ports/tts.ts` fixes mp3 and the
|
|
14
|
+
// concatenation keeps the provider's own sample rate.
|
|
16
15
|
export const elevenLabsFormat = "mp3_44100_128";
|
|
17
16
|
// A wire payload is narrowed, never cast. `detail` is an object on a handled failure and
|
|
18
17
|
// a string on the framework's own; anything else falls back to the raw text.
|
|
@@ -32,7 +31,7 @@ export function elevenLabsTts(deps) {
|
|
|
32
31
|
method: "POST",
|
|
33
32
|
signal: req.signal,
|
|
34
33
|
headers: { "xi-api-key": keyOf(deps), "Content-Type": "application/json" },
|
|
35
|
-
//
|
|
34
|
+
// No pre-check on length. A text past the model's limit comes
|
|
36
35
|
// back as the provider's own 400 and that is what the stage shows.
|
|
37
36
|
body: JSON.stringify({ text: req.text, model_id: elevenLabsModel }),
|
|
38
37
|
});
|
|
@@ -48,15 +47,13 @@ export function elevenLabsTts(deps) {
|
|
|
48
47
|
}
|
|
49
48
|
function keyOf(deps) {
|
|
50
49
|
const key = deps.key();
|
|
51
|
-
// `
|
|
52
|
-
// `missing_key` rather than `auth` because the same rule makes it terminal.
|
|
50
|
+
// `missing_key`, not `auth`, because that rule makes it terminal.
|
|
53
51
|
if (key === undefined || key === "") {
|
|
54
52
|
throw providerError({ kind: "missing_key", message: "no ElevenLabs key is stored" });
|
|
55
53
|
}
|
|
56
54
|
return key;
|
|
57
55
|
}
|
|
58
|
-
// Only the adapter
|
|
59
|
-
// the attempt wrapper maps it and nothing downstream classifies again (03-conventions).
|
|
56
|
+
// Only the adapter sees the vendor's status code, so only the adapter names the kind.
|
|
60
57
|
function kindOf(status) {
|
|
61
58
|
if (status === 401 || status === 403) {
|
|
62
59
|
return "auth";
|
|
@@ -64,21 +61,19 @@ function kindOf(status) {
|
|
|
64
61
|
if (status === 429) {
|
|
65
62
|
return "rate_limit";
|
|
66
63
|
}
|
|
67
|
-
// ceiling: everything else is `other` and is retried, so a text past the character
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
// this adapter's to widen.
|
|
64
|
+
// ceiling: everything else is `other` and is retried, so a text past the character limit
|
|
65
|
+
// fails the same way four times over. A terminal "this will never work" kind has to reach the
|
|
66
|
+
// port's error contract first, which is not this adapter's to widen.
|
|
71
67
|
return "other";
|
|
72
68
|
}
|
|
73
69
|
async function failure(response, voiceId) {
|
|
74
70
|
const text = await response.text().catch(() => "");
|
|
75
|
-
// The provider's own words,
|
|
76
|
-
// error body is free to quote the key back and this is the first place it is held.
|
|
71
|
+
// The provider's own words, through the redactor - an error body may quote the key back.
|
|
77
72
|
const message = redact(detailOf(text) || response.statusText);
|
|
78
73
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
79
74
|
return providerError({
|
|
80
75
|
kind: kindOf(response.status),
|
|
81
|
-
//
|
|
76
|
+
// A rejected voice ID has to be named, and it is the one part of the
|
|
82
77
|
// request the user chose. It is not secret, unlike everything else on the wire.
|
|
83
78
|
message: `ElevenLabs answered ${response.status} for voice ${voiceId}: ${message}`,
|
|
84
79
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
@@ -2,12 +2,11 @@ import { z } from "zod";
|
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
4
|
import { retryAfter } from "../retry-after.js";
|
|
5
|
-
// The HTTP gateway adapter for OpenAI's speech endpoint
|
|
6
|
-
//
|
|
7
|
-
// asks for.
|
|
5
|
+
// The HTTP gateway adapter for OpenAI's speech endpoint. `fetch` and nothing else; the response
|
|
6
|
+
// body is already the stream the port asks for.
|
|
8
7
|
export const openAiAudioBase = "https://api.openai.com/v1";
|
|
9
|
-
//
|
|
10
|
-
//
|
|
8
|
+
// gpt-4o-mini-tts, tts-1 and tts-1-hd; the first is the current one and the cheapest of the
|
|
9
|
+
// three per character.
|
|
11
10
|
export const openAiTtsModel = "gpt-4o-mini-tts";
|
|
12
11
|
const errorBody = z.object({
|
|
13
12
|
error: z.object({ message: z.string(), code: z.string().nullish() }),
|
|
@@ -28,9 +27,9 @@ export function openAiTts(deps) {
|
|
|
28
27
|
Authorization: `Bearer ${keyOf(deps)}`,
|
|
29
28
|
"Content-Type": "application/json",
|
|
30
29
|
},
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
30
|
+
// The 4096-character cap is not checked here. A longer text comes back as OpenAI's own
|
|
31
|
+
// 400 and that is what the stage shows, so a user who chose Whole text learns the limit
|
|
32
|
+
// from the provider that set it.
|
|
34
33
|
body: JSON.stringify({
|
|
35
34
|
model: openAiTtsModel,
|
|
36
35
|
input: req.text,
|
|
@@ -53,7 +52,7 @@ function voiceOf(voiceId) {
|
|
|
53
52
|
}
|
|
54
53
|
function keyOf(deps) {
|
|
55
54
|
const key = deps.key();
|
|
56
|
-
//
|
|
55
|
+
// An absent key is terminal, so it never becomes a request.
|
|
57
56
|
if (key === undefined || key === "") {
|
|
58
57
|
throw providerError({ kind: "missing_key", message: "no OpenAI key is stored" });
|
|
59
58
|
}
|
|
@@ -79,7 +78,7 @@ async function failure(response, voiceId) {
|
|
|
79
78
|
const retryAfterMs = retryAfter(response.headers.get("retry-after"));
|
|
80
79
|
return providerError({
|
|
81
80
|
kind: kindOf(response.status),
|
|
82
|
-
//
|
|
81
|
+
// The voice ID is named, so a rejected voice reads differently from
|
|
83
82
|
// a rejected key.
|
|
84
83
|
message: `OpenAI answered ${response.status} for voice ${voiceId}: ${message}`,
|
|
85
84
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
package/dist/edge/cli.js
CHANGED
|
File without changes
|
|
@@ -8,10 +8,9 @@ import { cancelProject } from "../../slices/cancel/index.js";
|
|
|
8
8
|
import { deleteImage, editArticle, regenerateImage, rerunStage, retryStage, } from "../../slices/reruns/index.js";
|
|
9
9
|
import { outputsOf } from "../../slices/storage/repo.js";
|
|
10
10
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
11
|
-
// The actions
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// decides nothing (`logic/12`, `logic/13`).
|
|
11
|
+
// The actions on the project header and on each stage: Cancel, Retry, Re-run, Save &
|
|
12
|
+
// re-run, Regenerate one image, Delete one image. Every one changes rows and files through
|
|
13
|
+
// a slice and then ticks the runner; the route itself decides nothing.
|
|
15
14
|
const idParam = z.object({
|
|
16
15
|
id: z
|
|
17
16
|
.string()
|
|
@@ -44,12 +43,12 @@ const details = {
|
|
|
44
43
|
"no-project": "No project has that id.",
|
|
45
44
|
"unknown-image": "This project has no image with that id.",
|
|
46
45
|
"empty-article": "An article cannot be saved empty.",
|
|
47
|
-
//
|
|
46
|
+
// The precondition on every re-run: no stage of the project is `running`.
|
|
48
47
|
running: "This project is still running. Cancel it or wait for it to finish.",
|
|
49
48
|
"not-rerunnable": "Only a stage that has finished, failed, or been canceled can be re-run.",
|
|
50
49
|
"not-retryable": "Only a failed or canceled stage can be retried.",
|
|
51
50
|
"no-article": "This project has no article to edit yet.",
|
|
52
|
-
//
|
|
51
|
+
// At least one image always remains.
|
|
53
52
|
"last-image": "At least one image must remain, so the last one cannot be deleted.",
|
|
54
53
|
};
|
|
55
54
|
// The return type is inferred so Hono keeps the route types the SPA's client is
|
|
@@ -79,8 +78,8 @@ export function actionRoutes(deps) {
|
|
|
79
78
|
outputs: outputsOf(deps.db, projectId),
|
|
80
79
|
};
|
|
81
80
|
};
|
|
82
|
-
// Every re-run action ends the same way: the rows are written, then the runner is
|
|
83
|
-
//
|
|
81
|
+
// Every re-run action ends the same way: the rows are written, then the runner is asked
|
|
82
|
+
// to look at the project. The cascade does the rest by itself.
|
|
84
83
|
const started = (c, projectId, result) => {
|
|
85
84
|
if (!result.ok) {
|
|
86
85
|
return problem(c, {
|
|
@@ -99,7 +98,7 @@ export function actionRoutes(deps) {
|
|
|
99
98
|
if (!result.ok) {
|
|
100
99
|
return problem(c, { status: 404, title: titleOf(404), detail: details["no-project"] });
|
|
101
100
|
}
|
|
102
|
-
// No tick:
|
|
101
|
+
// No tick: a canceled project sits until the user retries a stage.
|
|
103
102
|
return c.json({ ...view(id), canceled: result.canceled });
|
|
104
103
|
})
|
|
105
104
|
.post("/:id/stages/:kind/retry", zValidator("param", stageParam, onInvalid), (c) => {
|
package/dist/edge/http/app.js
CHANGED
|
@@ -24,8 +24,8 @@ function apiRoutes(deps, startedAt) {
|
|
|
24
24
|
}))
|
|
25
25
|
.route("/staging", stagingRoutes(deps))
|
|
26
26
|
.route("/projects", projectRoutes(deps))
|
|
27
|
-
// The
|
|
28
|
-
//
|
|
27
|
+
// The re-run and cancel actions sit on the same prefix as the project itself; they
|
|
28
|
+
// are their own router because they are their own concern.
|
|
29
29
|
.route("/projects", actionRoutes(deps))
|
|
30
30
|
.route("/prompts", promptRoutes(deps))
|
|
31
31
|
.route("/entries", entryRoutes(deps))
|
|
@@ -46,7 +46,7 @@ export function createApp(deps) {
|
|
|
46
46
|
.route("/api", apiRoutes(deps, startedAt))
|
|
47
47
|
.get("/api/events/global", (c) => streamSSE(c, (stream) => deps.hub.subscribeGlobal(stream, c.req.raw.signal)))
|
|
48
48
|
.get("/api/events/projects/:id", (c) => streamSSE(c, (stream) => deps.hub.subscribe(c.req.param("id"), stream, c.req.raw.signal)))
|
|
49
|
-
// Files are served by URL, not through the API
|
|
49
|
+
// Files are served by URL, not through the API.
|
|
50
50
|
.route("/", fileRoutes(deps))
|
|
51
51
|
// The API answers for its whole prefix, so an unknown endpoint is a problem+json 404
|
|
52
52
|
// rather than the SPA's index.html with a 200.
|
|
@@ -5,7 +5,7 @@ import { entryCategories, entryModes } from "../../slices/library/model.js";
|
|
|
5
5
|
import { listEntries } from "../../slices/library/repo.js";
|
|
6
6
|
import { createEntry, removeEntry, updateEntry } from "../../slices/library/save.js";
|
|
7
7
|
import { onInvalid } from "./problem.js";
|
|
8
|
-
//
|
|
8
|
+
// One rule set for prompts and entries, so one refusal mapping too.
|
|
9
9
|
import { refused } from "./prompts.js";
|
|
10
10
|
const idParam = z.object({
|
|
11
11
|
id: z
|
|
@@ -44,7 +44,7 @@ export function projectRoutes(deps) {
|
|
|
44
44
|
});
|
|
45
45
|
return (new Hono()
|
|
46
46
|
.post("/", zValidator("json", runDraftSchema, onInvalid), (c) => {
|
|
47
|
-
//
|
|
47
|
+
// The bodies are read here, at the click, so an edit made since the
|
|
48
48
|
// prompt was selected is the one that runs.
|
|
49
49
|
const picked = pickTemplates(deps.db, c.req.valid("json"));
|
|
50
50
|
const admitted = admit({
|
|
@@ -63,13 +63,13 @@ export function projectRoutes(deps) {
|
|
|
63
63
|
});
|
|
64
64
|
}
|
|
65
65
|
// Rendered from the values admit() has trimmed, so the stored text carries no
|
|
66
|
-
// padding the user did not intend
|
|
66
|
+
// padding the user did not intend.
|
|
67
67
|
const { project } = startRun(storage, admitted.draft, renderPicked(picked, admitted.draft.values));
|
|
68
|
-
//
|
|
68
|
+
// One event per project created. record() swallows its own
|
|
69
69
|
// failures, so a broken telemetry write cannot cost the user the run.
|
|
70
70
|
record(telemetry, "project.created", {});
|
|
71
71
|
deps.flushSoon();
|
|
72
|
-
//
|
|
72
|
+
// The run starts only once the project is committed.
|
|
73
73
|
deps.runner.tick(project.id);
|
|
74
74
|
// Read back after the tick, not from the rows startRun built: the runner has
|
|
75
75
|
// already claimed every eligible stage, and a body that paired status "running"
|
|
@@ -101,8 +101,8 @@ export function projectRoutes(deps) {
|
|
|
101
101
|
outputs: outputsOf(deps.db, project.id),
|
|
102
102
|
});
|
|
103
103
|
})
|
|
104
|
-
//
|
|
105
|
-
//
|
|
104
|
+
// Irreversible, and only from the app: the Projects screen puts a confirmation
|
|
105
|
+
// dialog in front of it.
|
|
106
106
|
.delete("/:id", zValidator("param", idParam, onInvalid), (c) => {
|
|
107
107
|
const result = deleteProject(storageForDelete, c.req.valid("param").id);
|
|
108
108
|
if (result.ok) {
|
|
@@ -122,7 +122,7 @@ const deleteStatus = {
|
|
|
122
122
|
};
|
|
123
123
|
const deleteDetails = {
|
|
124
124
|
"no-project": "No project has that id.",
|
|
125
|
-
//
|
|
125
|
+
// The run has to be stopped before its files can go.
|
|
126
126
|
running: "This project is still running. Cancel the run first.",
|
|
127
127
|
files: "Some of this project's files could not be removed.",
|
|
128
128
|
};
|
|
@@ -13,7 +13,7 @@ const idParam = z.object({
|
|
|
13
13
|
.regex(/^[0-9A-Za-z_-]+$/),
|
|
14
14
|
});
|
|
15
15
|
// Shape only. The rules - trimming, emptiness, length, the slot lint - are the slice's,
|
|
16
|
-
// so one set of messages reaches the editor
|
|
16
|
+
// so one set of messages reaches the editor.
|
|
17
17
|
const promptBody = z.object({
|
|
18
18
|
kind: z.enum(promptKinds),
|
|
19
19
|
name: z.string(),
|
|
@@ -25,7 +25,7 @@ export function promptRoutes(deps) {
|
|
|
25
25
|
const library = { db: deps.db, ids: deps.ids, clock: deps.clock };
|
|
26
26
|
return (new Hono()
|
|
27
27
|
// Every kind in one list: 04 Prompts filters by tab, and Duplicate needs the body it
|
|
28
|
-
// is copying
|
|
28
|
+
// is copying.
|
|
29
29
|
.get("/", (c) => c.json({ prompts: listPrompts(deps.db) }))
|
|
30
30
|
.post("/", zValidator("json", promptBody, onInvalid), (c) => {
|
|
31
31
|
const result = createPrompt(library, c.req.valid("json"));
|
|
@@ -35,15 +35,15 @@ export function promptRoutes(deps) {
|
|
|
35
35
|
const result = updatePrompt(library, c.req.valid("param").id, c.req.valid("json"));
|
|
36
36
|
return result.ok ? c.json(result.value) : refused(c, result, "prompt");
|
|
37
37
|
})
|
|
38
|
-
//
|
|
38
|
+
// A project holds its own rendered text, so nothing cascades and a
|
|
39
39
|
// template used by past projects is deleted like any other.
|
|
40
40
|
.delete("/:id", zValidator("param", idParam, onInvalid), (c) => {
|
|
41
41
|
const result = removePrompt(library, c.req.valid("param").id);
|
|
42
42
|
return result.ok ? c.body(null, 204) : refused(c, result, "prompt");
|
|
43
43
|
}));
|
|
44
44
|
}
|
|
45
|
-
// One refusal mapping for both libraries:
|
|
46
|
-
//
|
|
45
|
+
// One refusal mapping for both libraries: prompts and entries share one rule set, so
|
|
46
|
+
// entryRoutes reuses this rather than mirroring it.
|
|
47
47
|
export function refused(c, failure, noun) {
|
|
48
48
|
switch (failure.reason) {
|
|
49
49
|
case "invalid":
|
|
@@ -53,7 +53,7 @@ export function refused(c, failure, noun) {
|
|
|
53
53
|
detail: `This ${noun} cannot be saved; the listed fields need attention.`,
|
|
54
54
|
extensions: { fields: failure.fields },
|
|
55
55
|
});
|
|
56
|
-
//
|
|
56
|
+
// The name is refused against a row that exists, and the form marks the field.
|
|
57
57
|
case "duplicate-name":
|
|
58
58
|
return problem(c, {
|
|
59
59
|
status: 409,
|
|
@@ -6,9 +6,9 @@ import { providerById, providerIds } from "../../slices/settings/model.js";
|
|
|
6
6
|
import { providerStatuses } from "../../slices/settings/readiness.js";
|
|
7
7
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
8
8
|
const providerParam = z.object({ id: z.enum(providerIds) });
|
|
9
|
-
// ceiling:
|
|
10
|
-
//
|
|
11
|
-
//
|
|
9
|
+
// ceiling: no format check is allowed on a key, so the only thing said about the value is
|
|
10
|
+
// that it is a string of a length a key could plausibly have. Raise the bound if a provider
|
|
11
|
+
// ever issues something longer.
|
|
12
12
|
const keyBody = z.object({ key: z.string().min(1).max(4096) });
|
|
13
13
|
// The return type is inferred so Hono keeps the route types the SPA's client is
|
|
14
14
|
// generated from; see stagingRoutes.
|
|
@@ -17,7 +17,7 @@ export function providerRoutes(deps) {
|
|
|
17
17
|
const readiness = { db: deps.db, probe: deps.probe };
|
|
18
18
|
return (new Hono()
|
|
19
19
|
// What Settings draws its rails from and Play its dropdowns: every provider, with
|
|
20
|
-
// the one fact that decides whether it is selectable
|
|
20
|
+
// the one fact that decides whether it is selectable.
|
|
21
21
|
.get("/", async (c) => c.json({ providers: await providerStatuses(readiness) }))
|
|
22
22
|
.put("/:id/key", zValidator("param", providerParam, onInvalid), zValidator("json", keyBody, onInvalid), (c) => {
|
|
23
23
|
const { id } = c.req.valid("param");
|
|
@@ -7,7 +7,7 @@ import { addVoice, removeVoice, voiceIdMax, voiceNameMax, voices, } from "../../
|
|
|
7
7
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
8
8
|
const idParam = z.object({ id: z.string().min(1).max(64) });
|
|
9
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
|
|
10
|
+
// are the slice's, so one set of messages reaches the form.
|
|
11
11
|
const voiceBody = z.object({
|
|
12
12
|
provider: z.enum(providerIds),
|
|
13
13
|
name: z.string(),
|
|
@@ -40,7 +40,7 @@ export function settingsRoutes(deps) {
|
|
|
40
40
|
.post("/voices", zValidator("json", voiceBody, onInvalid), (c) => {
|
|
41
41
|
const result = addVoice(voiceDeps, c.req.valid("json"));
|
|
42
42
|
if (!result.ok) {
|
|
43
|
-
//
|
|
43
|
+
// A voice ID already listed for its provider is a conflict with
|
|
44
44
|
// a row that exists, which the form shows under the Voice ID input.
|
|
45
45
|
return result.reason === "duplicate-voice-id"
|
|
46
46
|
? problem(c, {
|
|
@@ -10,7 +10,7 @@ import { onInvalid, problem, titleOf } from "./problem.js";
|
|
|
10
10
|
const kindParam = z.object({ kind: z.enum(uploadableStageKinds) });
|
|
11
11
|
const idParam = z.object({ id: z.string().min(1).max(64) });
|
|
12
12
|
// The return type is inferred on purpose: annotating it would erase the route types the
|
|
13
|
-
// SPA's client is generated from
|
|
13
|
+
// SPA's client is generated from.
|
|
14
14
|
export function stagingRoutes(deps) {
|
|
15
15
|
const storage = {
|
|
16
16
|
db: deps.db,
|