@gentbajko/slopify 2.0.2 → 2.0.3
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/adapters/alignment/protocol.js +11 -1
- package/dist/adapters/alignment/runner.js +4 -1
- package/dist/adapters/alignment/worker.js +28 -5
- package/dist/adapters/explain.js +93 -0
- package/dist/adapters/ffmpeg.js +3 -3
- package/dist/adapters/host-cli/index.js +9 -2
- package/dist/adapters/host-cli/transport.js +4 -4
- package/dist/adapters/image/bytes.js +2 -2
- package/dist/adapters/image/codex-output.js +12 -12
- package/dist/adapters/image/codex.js +24 -8
- package/dist/adapters/image/fal.js +12 -6
- package/dist/adapters/image/google.js +18 -8
- package/dist/adapters/image/models.js +8 -2
- package/dist/adapters/image/openai.js +15 -6
- package/dist/adapters/image/replicate.js +16 -7
- package/dist/adapters/llm/claude-code-models.js +1 -1
- package/dist/adapters/llm/claude-code.js +16 -7
- package/dist/adapters/llm/cli-login-error.js +2 -1
- package/dist/adapters/llm/codex-models.js +1 -1
- package/dist/adapters/llm/codex.js +5 -6
- package/dist/adapters/llm/document-workspace.js +2 -2
- package/dist/adapters/llm/gemini-models.js +1 -1
- package/dist/adapters/llm/gemini-workspace.js +1 -1
- package/dist/adapters/llm/gemini.js +15 -11
- package/dist/adapters/llm/openrouter.js +21 -9
- package/dist/adapters/llm/run-cli.js +14 -11
- package/dist/adapters/tts/cartesia.js +10 -3
- package/dist/adapters/tts/elevenlabs.js +18 -6
- package/dist/adapters/tts/inworld-async.js +47 -16
- package/dist/adapters/tts/inworld.js +25 -15
- package/dist/adapters/tts/openai.js +18 -6
- package/dist/catalog/registry.js +13 -10
- package/dist/catalog/store.js +1 -1
- package/dist/catalog/validate.js +13 -4
- package/dist/edge/cli.js +33 -7
- package/dist/edge/docker-launch.js +6 -6
- package/dist/edge/docker-projects/engine.js +34 -13
- package/dist/edge/docker-projects/install.js +8 -8
- package/dist/edge/docker-projects/state.js +6 -6
- package/dist/edge/docker-projects/tree.js +6 -6
- package/dist/edge/docker.js +13 -13
- package/dist/edge/http/actions.js +27 -25
- package/dist/edge/http/app.js +3 -3
- package/dist/edge/http/audio-preview.js +1 -1
- package/dist/edge/http/checkpoints.js +5 -1
- package/dist/edge/http/diagnostics.js +3 -1
- package/dist/edge/http/draft-files.js +2 -2
- package/dist/edge/http/draft-problem.js +11 -1
- package/dist/edge/http/files.js +4 -4
- package/dist/edge/http/folder-location.js +3 -3
- package/dist/edge/http/fonts.js +22 -10
- package/dist/edge/http/host-cli.js +34 -12
- package/dist/edge/http/multipart.js +7 -5
- package/dist/edge/http/open-folder.js +2 -2
- package/dist/edge/http/planning.js +4 -4
- package/dist/edge/http/problem.js +2 -2
- package/dist/edge/http/project-templates.js +7 -3
- package/dist/edge/http/projects.js +8 -8
- package/dist/edge/http/prompts.js +9 -4
- package/dist/edge/http/providers.js +15 -7
- package/dist/edge/http/revision-files.js +3 -3
- package/dist/edge/http/revisions.js +30 -4
- package/dist/edge/http/schedules.js +9 -9
- package/dist/edge/http/settings.js +15 -9
- package/dist/edge/http/staging.js +5 -5
- package/dist/edge/http/storage.js +4 -4
- package/dist/edge/http/subtitles.js +1 -1
- package/dist/edge/http/tutorial.js +2 -2
- package/dist/edge/http/update.js +3 -3
- package/dist/host-cli/install.js +28 -6
- package/dist/host-cli/paths.js +6 -6
- package/dist/host-cli/server.js +1 -1
- package/dist/host-cli/service.js +11 -11
- package/dist/host-cli/status.js +7 -3
- package/dist/kernel/config/index.js +6 -4
- package/dist/kernel/db/migrate.js +2 -1
- package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
- package/dist/kernel/lock.js +4 -3
- package/dist/kernel/ports/subtitles.js +15 -1
- package/dist/kernel/runner/attempt.js +26 -2
- package/dist/main.js +2 -2
- package/dist/slices/admission/rules.js +32 -20
- package/dist/slices/article/continuation.js +4 -2
- package/dist/slices/article/plain.js +2 -2
- package/dist/slices/article/provided-entries.js +3 -3
- package/dist/slices/article/run.js +2 -2
- package/dist/slices/article/segments.js +5 -3
- package/dist/slices/control/providers.js +20 -7
- package/dist/slices/fonts/catalog.js +1 -1
- package/dist/slices/fonts/upload.js +1 -1
- package/dist/slices/images/run.js +5 -5
- package/dist/slices/library/lint.js +13 -7
- package/dist/slices/library/slots.js +2 -2
- package/dist/slices/narration/concat.js +1 -1
- package/dist/slices/narration/pronunciation-chunks.js +3 -3
- package/dist/slices/narration/pronunciation.js +2 -2
- package/dist/slices/narration/run.js +7 -7
- package/dist/slices/narration/steering.js +3 -3
- package/dist/slices/play-drafts/convert.js +5 -4
- package/dist/slices/play-drafts/readiness.js +12 -6
- package/dist/slices/play-drafts/review-inputs.js +7 -4
- package/dist/slices/play-drafts/review.js +3 -3
- package/dist/slices/play-drafts/service.js +3 -1
- package/dist/slices/play-drafts/uploads.js +1 -1
- package/dist/slices/rebuild/admission-repo.js +2 -2
- package/dist/slices/rebuild/recipe-legacy.js +1 -1
- package/dist/slices/rebuild/recipe-narration-text.js +1 -1
- package/dist/slices/rebuild/recipe-visual.js +1 -1
- package/dist/slices/rebuild/recovery.js +2 -2
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/runtime-admission.js +4 -4
- package/dist/slices/rebuild/runtime-article.js +5 -5
- package/dist/slices/rebuild/runtime-checkpoints.js +4 -4
- package/dist/slices/rebuild/runtime-export-inputs.js +6 -6
- package/dist/slices/rebuild/runtime-export.js +4 -4
- package/dist/slices/rebuild/runtime-local.js +6 -6
- package/dist/slices/rebuild/runtime-narration-text.js +46 -7
- package/dist/slices/rebuild/runtime-piece-label.js +62 -0
- package/dist/slices/rebuild/runtime-provider.js +6 -4
- package/dist/slices/rebuild/runtime-publication.js +3 -3
- package/dist/slices/rebuild/runtime-run.js +11 -1
- package/dist/slices/rebuild/runtime-subtitles.js +78 -15
- package/dist/slices/rebuild/transition-repo.js +6 -6
- package/dist/slices/reruns/index.js +1 -1
- package/dist/slices/research/run.js +9 -7
- package/dist/slices/research/synthesis.js +4 -2
- package/dist/slices/revisions/mutation-assets.js +26 -8
- package/dist/slices/revisions/mutation-cues.js +1 -1
- package/dist/slices/revisions/rules.js +18 -9
- package/dist/slices/revisions/schema.js +1 -1
- package/dist/slices/schedules/repo.js +55 -11
- package/dist/slices/schedules/scheduler.js +25 -8
- package/dist/slices/schedules/schema.js +19 -4
- package/dist/slices/schedules/service.js +2 -0
- package/dist/slices/settings/cli-paths.js +7 -4
- package/dist/slices/settings/models.js +4 -4
- package/dist/slices/settings/playback.js +2 -2
- package/dist/slices/storage/assets.js +5 -5
- package/dist/slices/storage/layout.js +1 -1
- package/dist/slices/subtitles/captions.js +1 -1
- package/dist/slices/subtitles/prepare.js +5 -5
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/thumbnail/run.js +8 -8
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/ffmpeg.js +6 -4
- package/dist/slices/video/plan.js +1 -1
- package/dist/slices/video/run.js +2 -2
- package/dist/updater/plan.js +1 -1
- package/dist/updater/service.js +4 -3
- package/dist/updater/worker.js +6 -4
- package/dist/web/assets/{index-CZNiHm8P.css → index-Bwtmu9ei.css} +1 -1
- package/dist/web/assets/index-DjhMJqMN.js +141 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-DPgGLX5m.js +0 -137
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { httpFailure, missingKey, noAudio, refreshList, unreadable, voiceFix } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
// The HTTP gateway adapter for ElevenLabs: the platform's own `fetch` and nothing else. The
|
|
6
7
|
// response body already is the `ReadableStream<Uint8Array>` the port asks for, so an SDK
|
|
@@ -44,12 +45,12 @@ export function elevenLabsTts(deps) {
|
|
|
44
45
|
signal: AbortSignal.timeout(10_000),
|
|
45
46
|
});
|
|
46
47
|
if (!response.ok)
|
|
47
|
-
throw await failure(response);
|
|
48
|
+
throw await failure(response, undefined, refreshList);
|
|
48
49
|
const parsed = modelList.safeParse(safeJson(await response.text()));
|
|
49
50
|
if (!parsed.success) {
|
|
50
51
|
throw providerError({
|
|
51
52
|
kind: "other",
|
|
52
|
-
message: "ElevenLabs
|
|
53
|
+
message: unreadable("ElevenLabs"),
|
|
53
54
|
});
|
|
54
55
|
}
|
|
55
56
|
return parsed.data
|
|
@@ -70,7 +71,7 @@ export function elevenLabsTts(deps) {
|
|
|
70
71
|
throw await failure(response, req.voiceId);
|
|
71
72
|
}
|
|
72
73
|
if (response.body === null) {
|
|
73
|
-
throw providerError({ kind: "other", message: "ElevenLabs
|
|
74
|
+
throw providerError({ kind: "other", message: noAudio("ElevenLabs") });
|
|
74
75
|
}
|
|
75
76
|
return { audio: response.body, container: "mp3" };
|
|
76
77
|
},
|
|
@@ -80,7 +81,7 @@ function keyOf(deps) {
|
|
|
80
81
|
const key = deps.key();
|
|
81
82
|
// `missing_key`, not `auth`, because that rule makes it terminal.
|
|
82
83
|
if (key === undefined || key === "") {
|
|
83
|
-
throw providerError({ kind: "missing_key", message: "
|
|
84
|
+
throw providerError({ kind: "missing_key", message: missingKey("ElevenLabs") });
|
|
84
85
|
}
|
|
85
86
|
return key;
|
|
86
87
|
}
|
|
@@ -97,7 +98,7 @@ function kindOf(status) {
|
|
|
97
98
|
// port's error contract first, which is not this adapter's to widen.
|
|
98
99
|
return "other";
|
|
99
100
|
}
|
|
100
|
-
async function failure(response, voiceId) {
|
|
101
|
+
async function failure(response, voiceId, next) {
|
|
101
102
|
const text = await response.text().catch(() => "");
|
|
102
103
|
// The provider's own words, through the redactor - an error body may quote the key back.
|
|
103
104
|
const message = redact(detailOf(text) || response.statusText);
|
|
@@ -106,7 +107,18 @@ async function failure(response, voiceId) {
|
|
|
106
107
|
kind: kindOf(response.status),
|
|
107
108
|
// A rejected voice ID has to be named, and it is the one part of the
|
|
108
109
|
// request the user chose. It is not secret, unlike everything else on the wire.
|
|
109
|
-
message:
|
|
110
|
+
message: httpFailure({
|
|
111
|
+
provider: "ElevenLabs",
|
|
112
|
+
status: response.status,
|
|
113
|
+
detail: message,
|
|
114
|
+
next,
|
|
115
|
+
...(voiceId === undefined
|
|
116
|
+
? {}
|
|
117
|
+
: {
|
|
118
|
+
subject: `narration request for voice "${voiceId}"`,
|
|
119
|
+
fix: voiceFix(voiceId),
|
|
120
|
+
}),
|
|
121
|
+
}),
|
|
110
122
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
111
123
|
});
|
|
112
124
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { httpFailure, internalError, noAudio, providerSaid, unreadable, voiceFix, } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
const operationName = /^workspaces\/[A-Za-z0-9_-]+\/ttsAsyncJobs\/[A-Za-z0-9_-]+\/operations\/[A-Za-z0-9_-]+$/;
|
|
6
7
|
const operationSchema = z.object({
|
|
@@ -15,11 +16,14 @@ const operationSchema = z.object({
|
|
|
15
16
|
export async function* inworldAsync(deps, request, key) {
|
|
16
17
|
const saved = request.continuation?.read();
|
|
17
18
|
if (saved !== undefined && !operationName.test(saved))
|
|
18
|
-
throw
|
|
19
|
+
throw providerError({
|
|
20
|
+
kind: "other",
|
|
21
|
+
message: internalError("the saved Inworld job reference is damaged"),
|
|
22
|
+
});
|
|
19
23
|
if (saved === undefined && request.text.length > 100_000) {
|
|
20
24
|
throw providerError({
|
|
21
25
|
kind: "unsupported",
|
|
22
|
-
message: "
|
|
26
|
+
message: "This narration part is longer than the 100,000 characters Inworld takes at once (10,000 on On-Demand accounts). Set Chunking to Paragraph in the Providers section of Edit project, then use Retry stage.",
|
|
23
27
|
});
|
|
24
28
|
}
|
|
25
29
|
const headers = { Authorization: `Basic ${key}`, "Content-Type": "application/json" };
|
|
@@ -45,10 +49,10 @@ export async function* inworldAsync(deps, request, key) {
|
|
|
45
49
|
let expected = saved;
|
|
46
50
|
for (;;) {
|
|
47
51
|
request.signal.throwIfAborted();
|
|
48
|
-
await check(response, key);
|
|
52
|
+
await check(response, key, request.voiceId);
|
|
49
53
|
const parsed = operationSchema.safeParse(await response.json().catch(() => null));
|
|
50
54
|
if (!parsed.success || (expected && parsed.data.name !== expected))
|
|
51
|
-
throw
|
|
55
|
+
throw unreadableAnswer();
|
|
52
56
|
const operation = parsed.data;
|
|
53
57
|
expected = operation.name;
|
|
54
58
|
request.continuation?.write(operation.name);
|
|
@@ -65,11 +69,11 @@ export async function* inworldAsync(deps, request, key) {
|
|
|
65
69
|
: code === 3 || code === 5 || code === 9
|
|
66
70
|
? "unsupported"
|
|
67
71
|
: "other",
|
|
68
|
-
message:
|
|
72
|
+
message: streamFailure(code, clean(message ?? "", key), request.voiceId),
|
|
69
73
|
});
|
|
70
74
|
}
|
|
71
75
|
if (!operation.response)
|
|
72
|
-
throw
|
|
76
|
+
throw providerError({ kind: "other", message: noAudio("Inworld") });
|
|
73
77
|
yield* download(deps.fetch, operation.response.audioUri, request.signal);
|
|
74
78
|
return;
|
|
75
79
|
}
|
|
@@ -85,7 +89,7 @@ export async function* inworldAsync(deps, request, key) {
|
|
|
85
89
|
async function* download(fetch, uri, signal) {
|
|
86
90
|
const url = URL.parse(uri);
|
|
87
91
|
if (url?.protocol !== "https:" || url.username || url.password)
|
|
88
|
-
throw
|
|
92
|
+
throw unreadableAnswer();
|
|
89
93
|
// This is a signed storage URL, not an Inworld API endpoint. Never forward Basic auth.
|
|
90
94
|
let response;
|
|
91
95
|
try {
|
|
@@ -94,12 +98,12 @@ async function* download(fetch, uri, signal) {
|
|
|
94
98
|
catch {
|
|
95
99
|
signal.throwIfAborted();
|
|
96
100
|
// Signed query parameters must not become a persisted error or log entry.
|
|
97
|
-
throw
|
|
101
|
+
throw downloadFailed("");
|
|
98
102
|
}
|
|
99
103
|
if (!response.ok)
|
|
100
|
-
throw
|
|
104
|
+
throw downloadFailed(` (error ${String(response.status)})`);
|
|
101
105
|
if (!response.body)
|
|
102
|
-
throw
|
|
106
|
+
throw providerError({ kind: "other", message: noAudio("Inworld") });
|
|
103
107
|
const reader = response.body.getReader();
|
|
104
108
|
let heard = false;
|
|
105
109
|
try {
|
|
@@ -116,16 +120,16 @@ async function* download(fetch, uri, signal) {
|
|
|
116
120
|
}
|
|
117
121
|
catch {
|
|
118
122
|
signal.throwIfAborted();
|
|
119
|
-
throw
|
|
123
|
+
throw downloadFailed("");
|
|
120
124
|
}
|
|
121
125
|
finally {
|
|
122
126
|
await reader.cancel().catch(() => { });
|
|
123
127
|
reader.releaseLock();
|
|
124
128
|
}
|
|
125
129
|
if (!heard)
|
|
126
|
-
throw
|
|
130
|
+
throw providerError({ kind: "other", message: noAudio("Inworld") });
|
|
127
131
|
}
|
|
128
|
-
async function check(response, key) {
|
|
132
|
+
async function check(response, key, voiceId) {
|
|
129
133
|
if (response.ok)
|
|
130
134
|
return;
|
|
131
135
|
const detail = clean(await response.text().catch(() => ""), key);
|
|
@@ -138,13 +142,40 @@ async function check(response, key) {
|
|
|
138
142
|
: response.status === 400 || response.status === 404
|
|
139
143
|
? "unsupported"
|
|
140
144
|
: "other",
|
|
141
|
-
message:
|
|
145
|
+
message: httpFailure({
|
|
146
|
+
provider: "Inworld",
|
|
147
|
+
status: response.status,
|
|
148
|
+
detail: detail || response.statusText,
|
|
149
|
+
subject: `narration request for voice "${voiceId}"`,
|
|
150
|
+
fix: voiceFix(voiceId),
|
|
151
|
+
}),
|
|
142
152
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
143
153
|
});
|
|
144
154
|
}
|
|
145
155
|
function clean(message, key) {
|
|
146
156
|
return redact(message.replaceAll(key, "[redacted]"));
|
|
147
157
|
}
|
|
148
|
-
|
|
149
|
-
|
|
158
|
+
// Signed query parameters must not become a persisted error or log entry, so the link is
|
|
159
|
+
// never quoted.
|
|
160
|
+
function downloadFailed(status) {
|
|
161
|
+
return providerError({
|
|
162
|
+
kind: "other",
|
|
163
|
+
message: `Inworld made the narration, but Slopify could not download it${status}. Check your internet connection, then use Retry stage.`,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function unreadableAnswer() {
|
|
167
|
+
return providerError({ kind: "other", message: unreadable("Inworld") });
|
|
168
|
+
}
|
|
169
|
+
// Inworld's streamed and async errors carry a gRPC code instead of an HTTP status.
|
|
170
|
+
export function streamFailure(code, detail, voiceId) {
|
|
171
|
+
const label = `code ${String(code)}`;
|
|
172
|
+
if (code === 16)
|
|
173
|
+
return httpFailure({ provider: "Inworld", status: 401, detail, label });
|
|
174
|
+
if (code === 7)
|
|
175
|
+
return httpFailure({ provider: "Inworld", status: 403, detail, label });
|
|
176
|
+
if (code === 8)
|
|
177
|
+
return httpFailure({ provider: "Inworld", status: 429, detail, label });
|
|
178
|
+
if (code === 3 || code === 5 || code === 9)
|
|
179
|
+
return providerSaid("Inworld", "could not narrate this text", detail, voiceFix(voiceId));
|
|
180
|
+
return providerSaid("Inworld", "stopped before the narration was finished", detail, "This is usually temporary: wait a few minutes, then use Retry stage.");
|
|
150
181
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { httpFailure, missingKey, noAudio, unreadable, voiceFix } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
|
-
import { inworldAsync } from "./inworld-async.js";
|
|
6
|
+
import { inworldAsync, streamFailure } from "./inworld-async.js";
|
|
6
7
|
export const inworldModels = [
|
|
7
8
|
{ id: "inworld-tts-2", name: "Realtime TTS-2" },
|
|
8
9
|
{ id: "inworld-tts-2-flash", name: "Realtime TTS-2 Flash" },
|
|
@@ -22,7 +23,7 @@ export function inworldTts(deps) {
|
|
|
22
23
|
?.trim()
|
|
23
24
|
.replace(/^Basic\s+/i, "");
|
|
24
25
|
if (!key)
|
|
25
|
-
throw providerError({ kind: "missing_key", message: "
|
|
26
|
+
throw providerError({ kind: "missing_key", message: missingKey("Inworld") });
|
|
26
27
|
const cancel = new AbortController();
|
|
27
28
|
const signal = AbortSignal.any([request.signal, cancel.signal]);
|
|
28
29
|
const iterator = synthesizeRequest(deps, { ...request, signal }, key);
|
|
@@ -56,7 +57,10 @@ async function* synthesizeRequest(deps, request, key) {
|
|
|
56
57
|
return;
|
|
57
58
|
}
|
|
58
59
|
if (request.text.trim().length === 0)
|
|
59
|
-
throw providerError({
|
|
60
|
+
throw providerError({
|
|
61
|
+
kind: "unsupported",
|
|
62
|
+
message: "Inworld was sent an empty piece of narration. Check the article has text in the Article section of Edit project, then use Retry stage.",
|
|
63
|
+
});
|
|
60
64
|
if ((request.model ?? "inworld-tts-2") === "inworld-tts-2" && request.text.length > 4000) {
|
|
61
65
|
yield* inworldAsync(deps, request, key);
|
|
62
66
|
return;
|
|
@@ -64,7 +68,7 @@ async function* synthesizeRequest(deps, request, key) {
|
|
|
64
68
|
if (request.text.length > 4000)
|
|
65
69
|
throw providerError({
|
|
66
70
|
kind: "unsupported",
|
|
67
|
-
message: "
|
|
71
|
+
message: "This narration part is longer than the 4,000 characters Inworld's Flash model takes at once. In the Providers section of Edit project, set Chunking to Paragraph or a smaller character count, or switch to the Realtime TTS-2 model.",
|
|
68
72
|
});
|
|
69
73
|
request.signal.throwIfAborted();
|
|
70
74
|
const response = await deps.fetch("https://api.inworld.ai/tts/v1/voice:stream", {
|
|
@@ -90,12 +94,18 @@ async function* synthesizeRequest(deps, request, key) {
|
|
|
90
94
|
: response.status === 400 || response.status === 404
|
|
91
95
|
? "unsupported"
|
|
92
96
|
: "other",
|
|
93
|
-
message: clean(
|
|
97
|
+
message: clean(httpFailure({
|
|
98
|
+
provider: "Inworld",
|
|
99
|
+
status: response.status,
|
|
100
|
+
detail: detail || response.statusText,
|
|
101
|
+
subject: `narration request for voice "${request.voiceId}"`,
|
|
102
|
+
fix: voiceFix(request.voiceId),
|
|
103
|
+
}), key),
|
|
94
104
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
95
105
|
});
|
|
96
106
|
}
|
|
97
107
|
if (response.body === null)
|
|
98
|
-
throw
|
|
108
|
+
throw providerError({ kind: "other", message: noAudio("Inworld") });
|
|
99
109
|
let heard = false;
|
|
100
110
|
for await (const line of lines(response.body)) {
|
|
101
111
|
request.signal.throwIfAborted();
|
|
@@ -104,11 +114,11 @@ async function* synthesizeRequest(deps, request, key) {
|
|
|
104
114
|
raw = JSON.parse(line);
|
|
105
115
|
}
|
|
106
116
|
catch {
|
|
107
|
-
throw
|
|
117
|
+
throw unreadableAudio();
|
|
108
118
|
}
|
|
109
119
|
const parsed = envelope.safeParse(raw);
|
|
110
120
|
if (!parsed.success || (!parsed.data.result && !parsed.data.error))
|
|
111
|
-
throw
|
|
121
|
+
throw unreadableAudio();
|
|
112
122
|
if (parsed.data.error) {
|
|
113
123
|
const { code, message } = parsed.data.error;
|
|
114
124
|
throw providerError({
|
|
@@ -119,14 +129,14 @@ async function* synthesizeRequest(deps, request, key) {
|
|
|
119
129
|
: code === 3 || code === 5
|
|
120
130
|
? "unsupported"
|
|
121
131
|
: "other",
|
|
122
|
-
message:
|
|
132
|
+
message: streamFailure(code, clean(message ?? "", key), request.voiceId),
|
|
123
133
|
});
|
|
124
134
|
}
|
|
125
135
|
const encoded = parsed.data.result?.audioContent;
|
|
126
136
|
if (!encoded)
|
|
127
137
|
continue;
|
|
128
138
|
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(encoded) || encoded.length % 4 === 1)
|
|
129
|
-
throw
|
|
139
|
+
throw unreadableAudio();
|
|
130
140
|
const audio = Buffer.from(encoded, "base64");
|
|
131
141
|
if (audio.length === 0)
|
|
132
142
|
continue;
|
|
@@ -134,7 +144,7 @@ async function* synthesizeRequest(deps, request, key) {
|
|
|
134
144
|
yield audio;
|
|
135
145
|
}
|
|
136
146
|
if (!heard)
|
|
137
|
-
throw
|
|
147
|
+
throw providerError({ kind: "other", message: noAudio("Inworld") });
|
|
138
148
|
}
|
|
139
149
|
async function* lines(body) {
|
|
140
150
|
const reader = body.getReader();
|
|
@@ -147,7 +157,7 @@ async function* lines(body) {
|
|
|
147
157
|
let end = pending.indexOf("\n");
|
|
148
158
|
while (end !== -1) {
|
|
149
159
|
if (end > 8 * 1024 * 1024)
|
|
150
|
-
throw
|
|
160
|
+
throw unreadableAudio();
|
|
151
161
|
const line = pending.slice(0, end).trim();
|
|
152
162
|
pending = pending.slice(end + 1);
|
|
153
163
|
if (line)
|
|
@@ -155,7 +165,7 @@ async function* lines(body) {
|
|
|
155
165
|
end = pending.indexOf("\n");
|
|
156
166
|
}
|
|
157
167
|
if (pending.length > 8 * 1024 * 1024)
|
|
158
|
-
throw
|
|
168
|
+
throw unreadableAudio();
|
|
159
169
|
if (done)
|
|
160
170
|
break;
|
|
161
171
|
}
|
|
@@ -170,6 +180,6 @@ async function* lines(body) {
|
|
|
170
180
|
function clean(message, key) {
|
|
171
181
|
return redact(message.replaceAll(key, "[redacted]"));
|
|
172
182
|
}
|
|
173
|
-
function
|
|
174
|
-
return providerError({ kind: "other", message:
|
|
183
|
+
function unreadableAudio() {
|
|
184
|
+
return providerError({ kind: "other", message: unreadable("Inworld") });
|
|
175
185
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { redact } from "../../kernel/log.js";
|
|
3
3
|
import { providerError } from "../../kernel/ports/model.js";
|
|
4
|
+
import { httpFailure, missingKey, noAudio, refreshList, unreadable, voiceFix } from "../explain.js";
|
|
4
5
|
import { retryAfter } from "../retry-after.js";
|
|
5
6
|
// The HTTP gateway adapter for OpenAI's speech endpoint. `fetch` and nothing else; the response
|
|
6
7
|
// body is already the stream the port asks for.
|
|
@@ -34,12 +35,12 @@ export function openAiTts(deps) {
|
|
|
34
35
|
signal: AbortSignal.timeout(10_000),
|
|
35
36
|
});
|
|
36
37
|
if (!response.ok)
|
|
37
|
-
throw await failure(response);
|
|
38
|
+
throw await failure(response, undefined, refreshList);
|
|
38
39
|
const parsed = modelList.safeParse(safeJson(await response.text()));
|
|
39
40
|
if (!parsed.success) {
|
|
40
41
|
throw providerError({
|
|
41
42
|
kind: "other",
|
|
42
|
-
message: "OpenAI
|
|
43
|
+
message: unreadable("OpenAI"),
|
|
43
44
|
});
|
|
44
45
|
}
|
|
45
46
|
return parsed.data.data
|
|
@@ -68,7 +69,7 @@ export function openAiTts(deps) {
|
|
|
68
69
|
throw await failure(response, req.voiceId);
|
|
69
70
|
}
|
|
70
71
|
if (response.body === null) {
|
|
71
|
-
throw providerError({ kind: "other", message: "OpenAI
|
|
72
|
+
throw providerError({ kind: "other", message: noAudio("OpenAI") });
|
|
72
73
|
}
|
|
73
74
|
return { audio: response.body, container: "mp3" };
|
|
74
75
|
},
|
|
@@ -81,7 +82,7 @@ function keyOf(deps) {
|
|
|
81
82
|
const key = deps.key();
|
|
82
83
|
// An absent key is terminal, so it never becomes a request.
|
|
83
84
|
if (key === undefined || key === "") {
|
|
84
|
-
throw providerError({ kind: "missing_key", message: "
|
|
85
|
+
throw providerError({ kind: "missing_key", message: missingKey("OpenAI") });
|
|
85
86
|
}
|
|
86
87
|
return key;
|
|
87
88
|
}
|
|
@@ -96,7 +97,7 @@ function kindOf(status) {
|
|
|
96
97
|
// port's error contract has no terminal kind for a request that cannot be repaired.
|
|
97
98
|
return "other";
|
|
98
99
|
}
|
|
99
|
-
async function failure(response, voiceId) {
|
|
100
|
+
async function failure(response, voiceId, next) {
|
|
100
101
|
const text = await response.text().catch(() => "");
|
|
101
102
|
const parsed = errorBody.safeParse(safeJson(text));
|
|
102
103
|
// Verbatim, through the same redactor the wrapper uses: OpenAI's own 401 quotes the
|
|
@@ -107,7 +108,18 @@ async function failure(response, voiceId) {
|
|
|
107
108
|
kind: kindOf(response.status),
|
|
108
109
|
// The voice ID is named, so a rejected voice reads differently from
|
|
109
110
|
// a rejected key.
|
|
110
|
-
message:
|
|
111
|
+
message: httpFailure({
|
|
112
|
+
provider: "OpenAI",
|
|
113
|
+
status: response.status,
|
|
114
|
+
detail: message,
|
|
115
|
+
next,
|
|
116
|
+
...(voiceId === undefined
|
|
117
|
+
? {}
|
|
118
|
+
: {
|
|
119
|
+
subject: `narration request for voice "${voiceId}"`,
|
|
120
|
+
fix: voiceFix(voiceId),
|
|
121
|
+
}),
|
|
122
|
+
}),
|
|
111
123
|
...(retryAfterMs === undefined ? {} : { retryAfterMs }),
|
|
112
124
|
});
|
|
113
125
|
}
|
package/dist/catalog/registry.js
CHANGED
|
@@ -7,7 +7,7 @@ export function curateRegistry(registry, catalogue) {
|
|
|
7
7
|
if (!model)
|
|
8
8
|
throw providerError({
|
|
9
9
|
kind: "unsupported",
|
|
10
|
-
message:
|
|
10
|
+
message: `The chosen ${provider} model is no longer in Slopify's model list. In the Edit tab, choose Edit project and change it under Providers, then use Resume.`,
|
|
11
11
|
});
|
|
12
12
|
return model;
|
|
13
13
|
};
|
|
@@ -21,7 +21,10 @@ export function curateRegistry(registry, catalogue) {
|
|
|
21
21
|
models: () => port.models(),
|
|
22
22
|
complete: async function* (request) {
|
|
23
23
|
if (request.model.trim() === "")
|
|
24
|
-
throw providerError({
|
|
24
|
+
throw providerError({
|
|
25
|
+
kind: "unsupported",
|
|
26
|
+
message: "No model is chosen for this step. In the Edit tab, choose Edit project and change it under Providers, then use Resume.",
|
|
27
|
+
});
|
|
25
28
|
let models;
|
|
26
29
|
try {
|
|
27
30
|
models = await port.models();
|
|
@@ -34,19 +37,19 @@ export function curateRegistry(registry, catalogue) {
|
|
|
34
37
|
if (models !== undefined && selected === undefined)
|
|
35
38
|
throw providerError({
|
|
36
39
|
kind: "unsupported",
|
|
37
|
-
message: "
|
|
40
|
+
message: "The chosen model is not available in the AI command-line tool on your computer. Update the tool, or pick another model: in the Edit tab, choose Edit project and change it under Providers, then use Resume.",
|
|
38
41
|
});
|
|
39
42
|
if (request.thinking !== undefined &&
|
|
40
43
|
selected !== undefined &&
|
|
41
44
|
!selected.thinkingModes?.includes(request.thinking))
|
|
42
45
|
throw providerError({
|
|
43
46
|
kind: "unsupported",
|
|
44
|
-
message: "
|
|
47
|
+
message: "The chosen model does not support this thinking setting. In the Edit tab, choose Edit project and change it under Providers, then use Resume.",
|
|
45
48
|
});
|
|
46
49
|
if (request.webSearch && !port.capabilities.webSearch)
|
|
47
50
|
throw providerError({
|
|
48
51
|
kind: "unsupported",
|
|
49
|
-
message: "This
|
|
52
|
+
message: "This command-line tool cannot search the web for research. Choose another text provider or turn Research off in the Edit tab under Edit project, then use Resume.",
|
|
50
53
|
});
|
|
51
54
|
const thinkingConfig = id === "codex" && request.thinking !== undefined
|
|
52
55
|
? { effort: request.thinking === "off" ? "none" : request.thinking }
|
|
@@ -75,13 +78,13 @@ export function curateRegistry(registry, catalogue) {
|
|
|
75
78
|
if (request.webSearch && !model.llm.webSearch)
|
|
76
79
|
throw providerError({
|
|
77
80
|
kind: "unsupported",
|
|
78
|
-
message: "
|
|
81
|
+
message: "The chosen model cannot search the web for research. Choose another model or turn Research off in the Edit tab under Edit project, then use Resume.",
|
|
79
82
|
});
|
|
80
83
|
const thinkingConfig = request.thinking === undefined ? undefined : model.llm.thinking?.[request.thinking];
|
|
81
84
|
if (request.thinking !== undefined && !thinkingConfig)
|
|
82
85
|
throw providerError({
|
|
83
86
|
kind: "unsupported",
|
|
84
|
-
message: "
|
|
87
|
+
message: "The chosen model does not support this thinking setting. In the Edit tab, choose Edit project and change it under Providers, then use Resume.",
|
|
85
88
|
});
|
|
86
89
|
yield* port.complete({
|
|
87
90
|
...request,
|
|
@@ -100,7 +103,7 @@ export function curateRegistry(registry, catalogue) {
|
|
|
100
103
|
if (!(await port.models()).some((model) => model.id === request.model))
|
|
101
104
|
throw providerError({
|
|
102
105
|
kind: "unsupported",
|
|
103
|
-
message: "
|
|
106
|
+
message: "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.",
|
|
104
107
|
});
|
|
105
108
|
return port.generate(request);
|
|
106
109
|
},
|
|
@@ -114,7 +117,7 @@ export function curateRegistry(registry, catalogue) {
|
|
|
114
117
|
if (!("image" in model) || !model.image.aspectRatios.includes(request.aspect))
|
|
115
118
|
throw providerError({
|
|
116
119
|
kind: "unsupported",
|
|
117
|
-
message: "
|
|
120
|
+
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.",
|
|
118
121
|
});
|
|
119
122
|
return port.generate(request);
|
|
120
123
|
},
|
|
@@ -135,7 +138,7 @@ export function curateRegistry(registry, catalogue) {
|
|
|
135
138
|
if (request.text.length > model.tts.maxCharacters)
|
|
136
139
|
throw providerError({
|
|
137
140
|
kind: "unsupported",
|
|
138
|
-
message:
|
|
141
|
+
message: `A piece of narration is longer than this voice model's ${model.tts.maxCharacters}-character limit. Use Re-run section on Audio so Slopify splits it into shorter pieces.`,
|
|
139
142
|
});
|
|
140
143
|
return port.synthesize({ ...request, model: model.id });
|
|
141
144
|
},
|
package/dist/catalog/store.js
CHANGED
|
@@ -40,7 +40,7 @@ export function createCatalogueStore(deps) {
|
|
|
40
40
|
}
|
|
41
41
|
catch {
|
|
42
42
|
warning =
|
|
43
|
-
"Your models.yaml is
|
|
43
|
+
"Your models.yaml file is missing or has a mistake, so Slopify is still using the last working model list. Fix the file, or use Refresh published catalogue in Settings → Models to replace it.";
|
|
44
44
|
}
|
|
45
45
|
return current;
|
|
46
46
|
}
|
package/dist/catalog/validate.js
CHANGED
|
@@ -31,16 +31,25 @@ export function modelFields(draft, catalogue) {
|
|
|
31
31
|
continue;
|
|
32
32
|
const model = catalogue.models(choice.provider, family).find((m) => m.id === choice.model);
|
|
33
33
|
if (!model)
|
|
34
|
-
fields.push({
|
|
34
|
+
fields.push({
|
|
35
|
+
field,
|
|
36
|
+
message: "This model is no longer in Slopify's model list. Choose another model.",
|
|
37
|
+
});
|
|
35
38
|
else if ("llm" in model && choice.thinking && !model.llm.thinking?.[choice.thinking])
|
|
36
|
-
fields.push({
|
|
39
|
+
fields.push({
|
|
40
|
+
field,
|
|
41
|
+
message: "This model does not support that thinking setting. Choose another.",
|
|
42
|
+
});
|
|
37
43
|
else if ("llm" in model && draft.sources.research === "generate" && !model.llm.webSearch)
|
|
38
44
|
fields.push({
|
|
39
45
|
field,
|
|
40
|
-
message: "This model cannot
|
|
46
|
+
message: "This model cannot search the web for research. Choose another model or turn Research off.",
|
|
41
47
|
});
|
|
42
48
|
else if ("image" in model && !model.image.aspectRatios.includes(draft.format))
|
|
43
|
-
fields.push({
|
|
49
|
+
fields.push({
|
|
50
|
+
field,
|
|
51
|
+
message: "This image model cannot make images in this video's shape. Choose another model.",
|
|
52
|
+
});
|
|
44
53
|
}
|
|
45
54
|
return fields;
|
|
46
55
|
}
|
package/dist/edge/cli.js
CHANGED
|
@@ -23,17 +23,18 @@ const { values } = parseArgs({
|
|
|
23
23
|
"accept-host-cli": { type: "boolean" },
|
|
24
24
|
},
|
|
25
25
|
});
|
|
26
|
+
let config;
|
|
26
27
|
try {
|
|
27
28
|
if (values["projects-dir"] !== undefined && !values.docker)
|
|
28
|
-
throw new Error("--projects-dir
|
|
29
|
+
throw new Error("--projects-dir only works together with --docker. Without Docker, choose where Slopify keeps its files with --data-dir <folder>.");
|
|
29
30
|
if ((values["host-cli"] !== undefined || values["accept-host-cli"] !== undefined) &&
|
|
30
31
|
!values.docker)
|
|
31
|
-
throw new Error("
|
|
32
|
+
throw new Error("--host-cli and --accept-host-cli only work together with --docker. Add --docker, or remove those options.");
|
|
32
33
|
if (values["host-cli"] !== undefined && values["host-cli"] !== "off")
|
|
33
|
-
throw new Error("The --host-cli
|
|
34
|
+
throw new Error(`--host-cli=${values["host-cli"]} is not supported. The only value is --host-cli=off, which starts Docker using API keys only; leave the option out to use the AI CLIs installed on this machine.`);
|
|
34
35
|
if (values.docker) {
|
|
35
36
|
if (values.host !== undefined || values["data-dir"] !== undefined)
|
|
36
|
-
throw new Error("Docker keeps
|
|
37
|
+
throw new Error("--host and --data-dir can't be used with --docker: the Docker version always listens on localhost and keeps its data in a Docker volume. Use --projects-dir <folder> for project files and --port <number> for the port.");
|
|
37
38
|
const docker = await import("./docker.js");
|
|
38
39
|
docker.assertManagedDockerHost(process.platform, process.getuid?.(), process.getgid?.());
|
|
39
40
|
const { prepareDockerHostCli } = docker;
|
|
@@ -73,10 +74,10 @@ try {
|
|
|
73
74
|
},
|
|
74
75
|
});
|
|
75
76
|
if (result.error)
|
|
76
|
-
throw result.error;
|
|
77
|
+
throw new Error(`Could not start the Docker launcher script (${result.error.message}). Make sure bash is installed and on your PATH, then try again.`);
|
|
77
78
|
process.exit(result.status ?? 1);
|
|
78
79
|
}
|
|
79
|
-
|
|
80
|
+
config = configFrom(values, process.env);
|
|
80
81
|
const forwarded = await forwardManagedUpdate(config.dataDir, readVersion(), process.argv.slice(2));
|
|
81
82
|
if (forwarded !== undefined)
|
|
82
83
|
process.exit(forwarded);
|
|
@@ -103,6 +104,31 @@ try {
|
|
|
103
104
|
});
|
|
104
105
|
}
|
|
105
106
|
catch (error) {
|
|
106
|
-
console.error(error
|
|
107
|
+
console.error(explainStartupError(error, config));
|
|
107
108
|
process.exit(1);
|
|
108
109
|
}
|
|
110
|
+
// Node's own wording for a failed listen or file operation ("listen EADDRINUSE: address already
|
|
111
|
+
// in use") names the symptom but not the fix, so the common startup failures are restated here.
|
|
112
|
+
function explainStartupError(error, config) {
|
|
113
|
+
if (!(error instanceof Error))
|
|
114
|
+
return String(error);
|
|
115
|
+
const code = "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
116
|
+
const syscall = "syscall" in error && typeof error.syscall === "string" ? error.syscall : "";
|
|
117
|
+
if (config !== undefined && syscall === "listen") {
|
|
118
|
+
const other = config.port === 7070 ? 7071 : 7070;
|
|
119
|
+
if (code === "EADDRINUSE")
|
|
120
|
+
return `Port ${config.port} is already in use by another program (maybe another Slopify). Stop that program, or start Slopify on another port: npx @gentbajko/slopify --port ${other}`;
|
|
121
|
+
if (code === "EACCES" || code === "EPERM")
|
|
122
|
+
return `Slopify is not allowed to use port ${config.port}${config.port < 1024 ? " (ports below 1024 need administrator rights)" : ""}. Start it on another port: npx @gentbajko/slopify --port ${other}`;
|
|
123
|
+
if (code === "EADDRNOTAVAIL" || code === "ENOTFOUND" || code === "EAI_AGAIN")
|
|
124
|
+
return `Slopify can't listen on ${config.host} because that address doesn't belong to this machine. Leave out --host (and SLOPIFY_HOST) to use 127.0.0.1, or use an address this machine owns.`;
|
|
125
|
+
}
|
|
126
|
+
const path = "path" in error && typeof error.path === "string" ? error.path : undefined;
|
|
127
|
+
if (config !== undefined &&
|
|
128
|
+
path?.startsWith(config.dataDir) &&
|
|
129
|
+
(code === "EACCES" || code === "EPERM" || code === "EROFS"))
|
|
130
|
+
return `Slopify can't write to its data folder ${config.dataDir} (${error.message}). Make sure your user owns that folder and can write to it, or choose another one: npx @gentbajko/slopify --data-dir <folder>`;
|
|
131
|
+
if (code === "ENOSPC")
|
|
132
|
+
return `The disk is full (${error.message}). Free up some space and start Slopify again.`;
|
|
133
|
+
return error.message;
|
|
134
|
+
}
|