@gentbajko/slopify 2.0.2 → 2.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/SUBTITLES.md +1 -1
- package/dist/adapters/alignment/protocol.js +11 -1
- package/dist/adapters/alignment/runner.js +4 -1
- package/dist/adapters/alignment/threads.js +11 -0
- package/dist/adapters/alignment/worker.js +66 -9
- package/dist/adapters/explain.js +93 -0
- package/dist/adapters/ffmpeg.js +3 -3
- package/dist/adapters/host-cli/index.js +34 -3
- 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/assets/document/SOURCE.txt +7 -0
- package/dist/assets/document/background.jpg +0 -0
- package/dist/assets/document/fonts/Cinzel-Black.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Bold.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Medium.ttf +0 -0
- package/dist/assets/document/fonts/Cinzel-Regular.ttf +0 -0
- package/dist/assets/document/fonts/OFL.txt +93 -0
- package/dist/assets/document/fonts/SOURCE.txt +11 -0
- package/dist/catalog/registry.js +13 -10
- package/dist/catalog/store.js +1 -1
- package/dist/catalog/validate.js +16 -6
- 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 +11 -7
- package/dist/edge/docker-projects/tree.js +6 -6
- package/dist/edge/docker.js +13 -13
- package/dist/edge/host-cli.js +14 -0
- 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-schema.js +5 -8
- package/dist/edge/http/folder-location.js +13 -8
- package/dist/edge/http/fonts.js +22 -10
- package/dist/edge/http/host-cli.js +61 -14
- 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 +8 -4
- 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/open-folder.js +95 -0
- package/dist/host-cli/paths.js +6 -6
- package/dist/host-cli/runtime.js +2 -0
- 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 +27 -2
- package/dist/kernel/db/migrations/0013-schedule-topic-queue.sql +4 -0
- package/dist/kernel/db/migrations/0014-document-stage.sql +44 -0
- package/dist/kernel/db/migrations/0015-description-prompts.sql +13 -0
- package/dist/kernel/lock.js +4 -3
- package/dist/kernel/pipeline.js +11 -1
- package/dist/kernel/ports/host-cli.js +14 -0
- package/dist/kernel/ports/subtitles.js +15 -1
- package/dist/kernel/runner/attempt.js +26 -2
- package/dist/kernel/runner/graph.js +6 -1
- package/dist/main.js +8 -4
- package/dist/slices/admission/model.js +7 -1
- package/dist/slices/admission/rules.js +98 -22
- package/dist/slices/admission/schema.js +16 -3
- package/dist/slices/admission/start.js +3 -2
- 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/document/blocks.js +129 -0
- package/dist/slices/document/flow.js +117 -0
- package/dist/slices/document/fonts.js +54 -0
- package/dist/slices/document/model.js +11 -0
- package/dist/slices/document/pages.js +198 -0
- package/dist/slices/document/render.js +242 -0
- package/dist/slices/document/sources.js +35 -0
- package/dist/slices/document/theme.js +158 -0
- package/dist/slices/document/writer.js +66 -0
- package/dist/slices/estimate/index.js +14 -2
- 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/model.js +1 -1
- package/dist/slices/library/slots.js +6 -3
- 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 +35 -5
- package/dist/slices/play-drafts/readiness.js +15 -8
- package/dist/slices/play-drafts/review-inputs.js +7 -4
- package/dist/slices/play-drafts/review.js +3 -3
- package/dist/slices/play-drafts/schema.js +22 -5
- package/dist/slices/play-drafts/service.js +3 -1
- package/dist/slices/play-drafts/uploads.js +1 -1
- package/dist/slices/project-templates/from-project.js +10 -0
- package/dist/slices/project-templates/setup.js +1 -0
- package/dist/slices/rebuild/admission-repo.js +2 -2
- package/dist/slices/rebuild/preview-details.js +11 -1
- package/dist/slices/rebuild/recipe-audio.js +3 -0
- package/dist/slices/rebuild/recipe-build.js +6 -1
- package/dist/slices/rebuild/recipe-document.js +36 -0
- package/dist/slices/rebuild/recipe-exports.js +14 -2
- package/dist/slices/rebuild/recipe-fixture.js +4 -0
- package/dist/slices/rebuild/recipe-legacy.js +6 -1
- package/dist/slices/rebuild/recipe-model.js +2 -0
- package/dist/slices/rebuild/recipe-narration-text.js +1 -1
- package/dist/slices/rebuild/recipe-provider-choice.js +3 -0
- package/dist/slices/rebuild/recipe-save.js +15 -3
- package/dist/slices/rebuild/recipe-validation.js +15 -2
- package/dist/slices/rebuild/recipe-visual.js +11 -2
- package/dist/slices/rebuild/recipe-work.js +15 -0
- package/dist/slices/rebuild/recipe-youtube.js +33 -0
- package/dist/slices/rebuild/recovery-selection.js +2 -0
- package/dist/slices/rebuild/recovery.js +12 -7
- package/dist/slices/rebuild/repo.js +3 -3
- package/dist/slices/rebuild/runtime-actions.js +2 -0
- 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-document.js +85 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +7 -6
- package/dist/slices/rebuild/runtime-export.js +39 -23
- 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 +66 -0
- package/dist/slices/rebuild/runtime-provider.js +6 -4
- package/dist/slices/rebuild/runtime-publication.js +4 -3
- package/dist/slices/rebuild/runtime-run.js +22 -4
- package/dist/slices/rebuild/runtime-store.js +7 -2
- package/dist/slices/rebuild/runtime-subtitles.js +82 -16
- package/dist/slices/rebuild/runtime-youtube.js +84 -0
- package/dist/slices/rebuild/transition-repo.js +8 -6
- package/dist/slices/reruns/cascade.js +6 -4
- 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 +4 -2
- package/dist/slices/revisions/projection.js +2 -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/downloads.js +1 -0
- package/dist/slices/storage/layout.js +7 -1
- package/dist/slices/storage/model.js +3 -0
- package/dist/slices/subtitles/captions.js +1 -1
- package/dist/slices/subtitles/prepare.js +7 -6
- package/dist/slices/subtitles/transcript.js +1 -1
- package/dist/slices/telemetry/model.js +1 -1
- package/dist/slices/thumbnail/run.js +8 -8
- package/dist/slices/video/audio-export.js +11 -3
- package/dist/slices/video/audio-inputs.js +2 -2
- package/dist/slices/video/edit-list.js +73 -0
- package/dist/slices/video/ffmpeg.js +129 -61
- package/dist/slices/video/motion.js +51 -0
- package/dist/slices/video/plan.js +62 -28
- package/dist/slices/video/reuse-audio.js +2 -0
- package/dist/slices/video/run.js +23 -12
- package/dist/slices/video/slideshow.js +48 -0
- package/dist/slices/video/write-export.js +17 -25
- package/dist/slices/youtube/answer.js +199 -0
- package/dist/slices/youtube/model.js +27 -0
- package/dist/slices/youtube/timestamps.js +24 -0
- package/dist/slices/youtube/transcript.js +47 -0
- 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-B6NbhpjD.css +1 -0
- package/dist/web/assets/index-ComF5gwP.js +142 -0
- package/dist/web/index.html +2 -2
- package/package.json +4 -2
- package/dist/web/assets/index-CZNiHm8P.css +0 -1
- package/dist/web/assets/index-DPgGLX5m.js +0 -137
package/SUBTITLES.md
CHANGED
|
@@ -16,7 +16,7 @@ Fonts are copied into the completed project's caption assets, so an existing exp
|
|
|
16
16
|
|
|
17
17
|
## Local model and licenses
|
|
18
18
|
|
|
19
|
-
Speech inference uses MIT-licensed `onnxruntime-
|
|
19
|
+
Speech inference uses MIT-licensed `onnxruntime-node@1.30.0` (native CPU) in a separate process, with `min(8, CPUs − 1)` threads; set `SLOPIFY_SUBTITLE_THREADS` to override. The npm package bundles CPU builds for Linux x64/arm64, Windows x64/arm64 and Apple silicon macOS (about 220 MB unpacked); Intel Macs have no build, so subtitle timing falls back to the slower single-threaded `onnxruntime-web` there. Its postinstall script only fetches optional CUDA files on Linux x64 and is not needed (the Docker image installs with `--ignore-scripts`); model weights are downloaded lazily into `<data-dir>/models/english-subtitles/`.
|
|
20
20
|
|
|
21
21
|
Model: [Xenova/wav2vec2-base-960h](https://huggingface.co/Xenova/wav2vec2-base-960h/tree/a19f851b3d42865797e410752b4c570c871e4825), an ONNX conversion of [facebook/wav2vec2-base-960h](https://huggingface.co/facebook/wav2vec2-base-960h), licensed Apache-2.0. Pinned revision: `a19f851b3d42865797e410752b4c570c871e4825`. The quantized model is 95,286,046 bytes, SHA256 `cd5040c147381580ed73258143dd8e0c28e800a09e74ee42ee2b3e8cb4d760a3`; every cached/downloaded model is verified before use.
|
|
22
22
|
|
|
@@ -24,5 +24,15 @@ export const workerMessage = z.discriminatedUnion("type", [
|
|
|
24
24
|
})),
|
|
25
25
|
}),
|
|
26
26
|
omissionSchema.extend({ type: z.literal("omission") }),
|
|
27
|
-
z.object({
|
|
27
|
+
z.object({
|
|
28
|
+
type: z.literal("error"),
|
|
29
|
+
message: z.string(),
|
|
30
|
+
mismatch: z
|
|
31
|
+
.object({
|
|
32
|
+
at: z.number().finite().nonnegative(),
|
|
33
|
+
expected: z.string().max(2000),
|
|
34
|
+
heard: z.string().max(2000),
|
|
35
|
+
})
|
|
36
|
+
.optional(),
|
|
37
|
+
}),
|
|
28
38
|
]);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { fork } from "node:child_process";
|
|
2
|
+
import { SubtitleMismatch, } from "../../kernel/ports/subtitles.js";
|
|
2
3
|
import { workerMessage } from "./protocol.js";
|
|
3
4
|
export async function runAlignmentWorker(input, signal, onProgress, worker = new URL("./worker.js", import.meta.url), onOmission) {
|
|
4
5
|
signal.throwIfAborted();
|
|
@@ -65,7 +66,9 @@ export async function runAlignmentWorker(input, signal, onProgress, worker = new
|
|
|
65
66
|
}
|
|
66
67
|
}
|
|
67
68
|
else if (message.type === "error")
|
|
68
|
-
finish(
|
|
69
|
+
finish(message.mismatch === undefined
|
|
70
|
+
? new Error(message.message)
|
|
71
|
+
: new SubtitleMismatch(message.message, message.mismatch.at, message.mismatch.expected, message.mismatch.heard));
|
|
69
72
|
else
|
|
70
73
|
finish(undefined, message.words.map(({ confidence, ...word }) => ({
|
|
71
74
|
...word,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { availableParallelism } from "node:os";
|
|
2
|
+
// Measured on a 32-thread machine, 5 min of narration in 12 s windows: one native CPU session with
|
|
3
|
+
// 8 intra-op threads ran 37× realtime; 16 threads 31×, 32 threads 20×, 1 thread 20×, and the old
|
|
4
|
+
// single-threaded WASM runtime 4.8×. More threads in one session gets slower, so 8 is the ceiling.
|
|
5
|
+
const maxThreads = 8;
|
|
6
|
+
export function subtitleThreads(env = process.env, cpus = availableParallelism()) {
|
|
7
|
+
const override = env.SLOPIFY_SUBTITLE_THREADS?.trim();
|
|
8
|
+
if (override !== undefined && /^[1-9]\d*$/.test(override))
|
|
9
|
+
return Number(override);
|
|
10
|
+
return Math.min(maxThreads, Math.max(1, cpus - 1));
|
|
11
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { open, readFile, stat } from "node:fs/promises";
|
|
2
|
-
import
|
|
2
|
+
import { SubtitleMismatch } from "../../kernel/ports/subtitles.js";
|
|
3
3
|
import { mismatch } from "./ctc.js";
|
|
4
4
|
import { workerInput } from "./protocol.js";
|
|
5
5
|
import { speechWords } from "./text.js";
|
|
6
|
+
import { subtitleThreads } from "./threads.js";
|
|
6
7
|
import { alignSpeechWindow, greedy } from "./window.js";
|
|
7
8
|
const sampleRate = 16000;
|
|
8
9
|
const windowSeconds = 12;
|
|
@@ -13,15 +14,19 @@ process.once("message", (raw) => {
|
|
|
13
14
|
send({ type: "error", message: "The subtitle worker received an invalid request." });
|
|
14
15
|
return;
|
|
15
16
|
}
|
|
16
|
-
run(input.data).then((words) => send({ type: "done", words }), (error) => send({
|
|
17
|
+
run(input.data).then((words) => send({ type: "done", words }), (error) => send({
|
|
18
|
+
type: "error",
|
|
19
|
+
message: error instanceof Error ? error.message : String(error),
|
|
20
|
+
...(error instanceof SubtitleMismatch
|
|
21
|
+
? { mismatch: { at: error.at, expected: error.expected, heard: error.heard } }
|
|
22
|
+
: {}),
|
|
23
|
+
}));
|
|
17
24
|
});
|
|
18
25
|
function send(message) {
|
|
19
26
|
process.send?.(message);
|
|
20
27
|
}
|
|
21
28
|
async function run(input) {
|
|
22
|
-
ort
|
|
23
|
-
const model = new Uint8Array(await readFile(input.modelPath));
|
|
24
|
-
const session = await ort.InferenceSession.create(model, { executionProviders: ["wasm"] });
|
|
29
|
+
const { ort, session } = await openSession(input.modelPath);
|
|
25
30
|
const file = await open(input.pcmPath, "r");
|
|
26
31
|
try {
|
|
27
32
|
const totalSamples = (await stat(input.pcmPath)).size / 4;
|
|
@@ -64,11 +69,20 @@ async function run(input) {
|
|
|
64
69
|
const finalWindow = sampleAt + count >= totalSamples;
|
|
65
70
|
const complete = finalWindow && cursor + candidate.length === source.length;
|
|
66
71
|
const cutoff = finalWindow ? count / sampleRate : count / sampleRate - overlapSeconds;
|
|
67
|
-
const
|
|
72
|
+
const stuck = () => new SubtitleMismatch(mismatch, sampleAt / sampleRate, snippet(candidate.map((word) => word.text)), snippet(observed.split(/\s+/)));
|
|
73
|
+
let recovered;
|
|
74
|
+
try {
|
|
75
|
+
recovered = alignSpeechWindow(logits.data, frames, candidate, complete, cutoff, cursor === 0 ? 0 : omissionBudget - omitted);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
if (error instanceof Error && error.message === mismatch)
|
|
79
|
+
throw stuck();
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
68
82
|
const accepted = recovered.words;
|
|
69
83
|
const last = accepted.at(-1);
|
|
70
84
|
if (last === undefined)
|
|
71
|
-
throw
|
|
85
|
+
throw stuck();
|
|
72
86
|
if (recovered.skipped > 0) {
|
|
73
87
|
omitted += recovered.skipped;
|
|
74
88
|
send({
|
|
@@ -95,7 +109,7 @@ async function run(input) {
|
|
|
95
109
|
tensor.dispose();
|
|
96
110
|
}
|
|
97
111
|
if (cursor !== source.length || output.length === 0)
|
|
98
|
-
throw new
|
|
112
|
+
throw new SubtitleMismatch(mismatch, sampleAt / sampleRate, snippet(source.slice(cursor).map((word) => word.text)), "");
|
|
99
113
|
// A substantial spoken tail absent from the transcript is a mismatch too.
|
|
100
114
|
while (sampleAt + sampleRate < totalSamples) {
|
|
101
115
|
const count = Math.min(windowSeconds * sampleRate, totalSamples - sampleAt);
|
|
@@ -110,7 +124,7 @@ async function run(input) {
|
|
|
110
124
|
if (logits !== undefined &&
|
|
111
125
|
logits.data instanceof Float32Array &&
|
|
112
126
|
greedy(logits.data, logits.dims[1] ?? 0).replace(/[^A-Z]/g, "").length > 8)
|
|
113
|
-
throw new
|
|
127
|
+
throw new SubtitleMismatch(mismatch, sampleAt / sampleRate, "", snippet(greedy(logits.data, logits.dims[1] ?? 0).split(/\s+/)));
|
|
114
128
|
for (const tensor of Object.values(result))
|
|
115
129
|
tensor.dispose();
|
|
116
130
|
}
|
|
@@ -123,6 +137,42 @@ async function run(input) {
|
|
|
123
137
|
await session.release();
|
|
124
138
|
}
|
|
125
139
|
}
|
|
140
|
+
// One native CPU session per job, as benchmarked; threads.ts records the measured speeds. The
|
|
141
|
+
// native runtime loads its binary on import, so a missing or foreign binary fails there; that
|
|
142
|
+
// system (an Intel Mac has no 1.24 build) falls back to the single-threaded WebAssembly runtime,
|
|
143
|
+
// about 7× slower but the same model, rather than losing captions.
|
|
144
|
+
async function openSession(modelPath) {
|
|
145
|
+
let native;
|
|
146
|
+
try {
|
|
147
|
+
native = await import("onnxruntime-node");
|
|
148
|
+
}
|
|
149
|
+
catch (nativeError) {
|
|
150
|
+
try {
|
|
151
|
+
const web = await import("onnxruntime-web/wasm");
|
|
152
|
+
web.env.wasm.numThreads = 1;
|
|
153
|
+
const session = await web.InferenceSession.create(new Uint8Array(await readFile(modelPath)), {
|
|
154
|
+
executionProviders: ["wasm"],
|
|
155
|
+
});
|
|
156
|
+
return {
|
|
157
|
+
ort: web,
|
|
158
|
+
session: session,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
const system = `${process.platform} ${process.arch}`;
|
|
163
|
+
const reason = nativeError instanceof Error
|
|
164
|
+
? (nativeError.message.split("\n")[0] ?? "")
|
|
165
|
+
: String(nativeError);
|
|
166
|
+
throw new Error(`Subtitle timing could not start: neither of its speech engines loaded on this computer (${system}): ${reason}. Reinstall Slopify so npm installs the engine for this system, then try again; if it keeps failing, turn subtitles off for this project.`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const session = await native.InferenceSession.create(modelPath, {
|
|
170
|
+
executionProviders: ["cpu"],
|
|
171
|
+
intraOpNumThreads: subtitleThreads(),
|
|
172
|
+
interOpNumThreads: 1,
|
|
173
|
+
});
|
|
174
|
+
return { ort: native, session };
|
|
175
|
+
}
|
|
126
176
|
function candidates(source, cursor, observed) {
|
|
127
177
|
const selected = [];
|
|
128
178
|
let length = 0;
|
|
@@ -140,6 +190,13 @@ function candidates(source, cursor, observed) {
|
|
|
140
190
|
}
|
|
141
191
|
return selected;
|
|
142
192
|
}
|
|
193
|
+
// A dozen words: enough to find the place in the text, short enough for an error row.
|
|
194
|
+
function snippet(words) {
|
|
195
|
+
return words
|
|
196
|
+
.filter((word) => word !== "")
|
|
197
|
+
.slice(0, 12)
|
|
198
|
+
.join(" ");
|
|
199
|
+
}
|
|
143
200
|
function normalize(audio) {
|
|
144
201
|
let mean = 0;
|
|
145
202
|
for (const value of audio)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// The sentences a failed provider call shows. Whatever an adapter throws becomes the stage's
|
|
2
|
+
// failure reason on the project page, read by someone who only wants the video, so each one
|
|
3
|
+
// says what failed, the likely cause and the next step, using the names on screen. The
|
|
4
|
+
// provider's own words stay in, quoted after the plain sentence, because they are often the
|
|
5
|
+
// only clue to a problem Slopify cannot see. Keys never reach here: callers redact first.
|
|
6
|
+
// A provider's error body can be a whole HTML page; the first few hundred characters say why.
|
|
7
|
+
const detailMax = 300;
|
|
8
|
+
export function missingKey(provider) {
|
|
9
|
+
return `No ${provider} API key is saved. Add one in Settings → Providers, then use Retry stage.`;
|
|
10
|
+
}
|
|
11
|
+
export const refreshList = "refresh the list";
|
|
12
|
+
export const checkChoice = "Check the provider and model chosen in the Providers section of Edit project, then use Retry stage.";
|
|
13
|
+
export function httpFailure(failure) {
|
|
14
|
+
const { provider, status } = failure;
|
|
15
|
+
const said = quoted(failure.label ?? `error ${String(status)}`, failure.detail);
|
|
16
|
+
const next = failure.next ?? "use Retry stage";
|
|
17
|
+
if (status === 401) {
|
|
18
|
+
return `${provider} did not accept the API key${said}. The key may be wrong, expired or revoked: paste a current key in Settings → Providers, then ${next}.`;
|
|
19
|
+
}
|
|
20
|
+
if (status === 403) {
|
|
21
|
+
return `${provider} refused access${said}. The API key may not be allowed to use this model, or the account needs attention: check your ${provider} account and the key in Settings → Providers, then ${next}.`;
|
|
22
|
+
}
|
|
23
|
+
if (status === 402) {
|
|
24
|
+
return `${provider} says the account is out of credits${said}. Add credits on the ${provider} website, then ${next}.`;
|
|
25
|
+
}
|
|
26
|
+
if (status === 429) {
|
|
27
|
+
return `${provider} is limiting requests${said}. Too many requests were sent at once or the plan's quota is used up: wait a few minutes or check your ${provider} usage and billing, then ${next}.`;
|
|
28
|
+
}
|
|
29
|
+
if (status >= 400 && status < 500) {
|
|
30
|
+
return `${provider} rejected the ${failure.subject ?? "request"}${said}. ${failure.fix ?? (failure.next === undefined ? checkChoice : `Check the key in Settings → Providers, then ${next}.`)}`;
|
|
31
|
+
}
|
|
32
|
+
if (status >= 500) {
|
|
33
|
+
return `${provider} had a problem on its side${said}. This is usually temporary: wait a few minutes, then ${next}.`;
|
|
34
|
+
}
|
|
35
|
+
return `${provider} answered with an unexpected error${said}. ${failure.next === undefined ? "Use Retry stage" : "Try again"}; if it keeps failing, use Download diagnostics in Settings and report it.`;
|
|
36
|
+
}
|
|
37
|
+
// A failure the provider reported inside an otherwise successful answer, with no status.
|
|
38
|
+
export function providerSaid(provider, what, detail, next) {
|
|
39
|
+
return `${provider} ${what}${quoted("", detail)}. ${next}`;
|
|
40
|
+
}
|
|
41
|
+
export function unreadable(provider) {
|
|
42
|
+
return `${provider} sent back an answer Slopify could not read. This is usually temporary: use Retry stage; if it keeps happening, use Download diagnostics in Settings and report it.`;
|
|
43
|
+
}
|
|
44
|
+
export function droppedStream(provider) {
|
|
45
|
+
return `The connection to ${provider} dropped before the answer was complete. Check your internet connection, then use Retry stage.`;
|
|
46
|
+
}
|
|
47
|
+
export function noAudio(provider) {
|
|
48
|
+
return `${provider} finished without sending any audio. Use Retry stage; if it keeps happening, check the voice in Settings → Voices.`;
|
|
49
|
+
}
|
|
50
|
+
export function noImage(provider) {
|
|
51
|
+
return `${provider} finished without sending an image. Use Retry stage; if it keeps happening, reword the image prompt in the Images section of Edit project or choose another image model in its Providers section.`;
|
|
52
|
+
}
|
|
53
|
+
export function refusedImage(provider, detail) {
|
|
54
|
+
return `${provider} refused to make this image under its content rules${quoted("", detail)}. Reword the image prompt in the Images section of Edit project, or choose another image provider in its Providers section.`;
|
|
55
|
+
}
|
|
56
|
+
// A narration request the provider turned down is most often a voice that no longer exists
|
|
57
|
+
// or a part longer than the model takes.
|
|
58
|
+
export function voiceFix(voiceId) {
|
|
59
|
+
return `Check that voice "${voiceId}" still exists in Settings → Voices and that each narration part fits the model's length limit (Chunking, in the Providers section of Edit project), then use Retry stage.`;
|
|
60
|
+
}
|
|
61
|
+
export function internalError(what) {
|
|
62
|
+
return `Slopify hit an internal error (${what}). Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.`;
|
|
63
|
+
}
|
|
64
|
+
const cliNames = {
|
|
65
|
+
claude: "Claude Code CLI",
|
|
66
|
+
"claude-code": "Claude Code CLI",
|
|
67
|
+
codex: "Codex CLI",
|
|
68
|
+
gemini: "Gemini CLI",
|
|
69
|
+
};
|
|
70
|
+
// A configured path (`/opt/bin/claude`, `C:\\tools\\codex.cmd`) still names the CLI the user chose.
|
|
71
|
+
export function cliName(binary) {
|
|
72
|
+
const base = commandOf(binary);
|
|
73
|
+
return cliNames[base] ?? `"${base}" CLI`;
|
|
74
|
+
}
|
|
75
|
+
// What to type in a terminal to run it.
|
|
76
|
+
export function commandOf(binary) {
|
|
77
|
+
return (binary.split(/[\\/]/).at(-1) ?? binary).replace(/\.(?:cmd|exe|bat|js|mjs)$/i, "");
|
|
78
|
+
}
|
|
79
|
+
// An error the CLI itself reported, in its own words, with the step that most often fixes it.
|
|
80
|
+
export function cliReported(binary, detail, next) {
|
|
81
|
+
return `The ${cliName(binary)} reported an error${quoted("", detail)}. ${next}`;
|
|
82
|
+
}
|
|
83
|
+
export function cliCheck(binary) {
|
|
84
|
+
return `Use Retry stage; if it keeps failing, run ${commandOf(binary)} in a terminal to check it works and is signed in, or choose another model in the Providers section of Edit project.`;
|
|
85
|
+
}
|
|
86
|
+
// ` (error 401: "Unauthorized")`, ` (error 401)`, ` ("Unauthorized")`, or nothing.
|
|
87
|
+
export function quoted(label, detail) {
|
|
88
|
+
const text = detail.trim();
|
|
89
|
+
const clipped = text.length > detailMax ? `${text.slice(0, detailMax)}…` : text;
|
|
90
|
+
if (clipped === "")
|
|
91
|
+
return label === "" ? "" : ` (${label})`;
|
|
92
|
+
return label === "" ? ` ("${clipped}")` : ` (${label}: "${clipped}")`;
|
|
93
|
+
}
|
package/dist/adapters/ffmpeg.js
CHANGED
|
@@ -15,7 +15,7 @@ export async function prepareFfmpeg(deps) {
|
|
|
15
15
|
return override;
|
|
16
16
|
}
|
|
17
17
|
if (typeof deps.bundled !== "string" || deps.bundled === "") {
|
|
18
|
-
throw new Error(`
|
|
18
|
+
throw new Error(`Slopify has no built-in copy of ffmpeg (the tool it uses to make video) for this kind of computer. Install ffmpeg, then restart Slopify. ${remedy}`);
|
|
19
19
|
}
|
|
20
20
|
if (existsSync(deps.bundled)) {
|
|
21
21
|
await verify(deps.bundled);
|
|
@@ -58,7 +58,7 @@ export async function prepareFfmpeg(deps) {
|
|
|
58
58
|
rmSync(staging, { recursive: true, force: true });
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
-
const remedy = "You can also set SLOPIFY_FFMPEG (or FFMPEG_BIN) to a working ffmpeg
|
|
61
|
+
const remedy = "You can also set the SLOPIFY_FFMPEG (or FFMPEG_BIN) environment variable to the full path of a working ffmpeg.";
|
|
62
62
|
async function verify(bin) {
|
|
63
63
|
try {
|
|
64
64
|
const { stdout } = await promisify(execFile)(bin, ["-version"], {
|
|
@@ -70,7 +70,7 @@ async function verify(bin) {
|
|
|
70
70
|
throw new Error("the executable did not identify itself as ffmpeg");
|
|
71
71
|
}
|
|
72
72
|
catch (error) {
|
|
73
|
-
throw new Error(`ffmpeg at ${bin} could not be started. ${remedy} ${detail(error)}`);
|
|
73
|
+
throw new Error(`ffmpeg at ${bin} could not be started. Reinstall ffmpeg or check your antivirus has not quarantined it, then restart Slopify. ${remedy} ${detail(error)}`);
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
function detail(error) {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { redact } from "../../kernel/log.js";
|
|
2
|
-
import { bridgeLimits, hostFaultSchema, hostFrameSchema, hostHealthSchema, hostImageSchema, hostLlmSchema, hostModelsSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
|
|
2
|
+
import { bridgeLimits, hostFaultSchema, hostFrameSchema, hostHealthSchema, hostImageSchema, hostLlmSchema, hostModelsSchema, hostOpenedSchema, hostOpenFolderSchema, hostStatusSchema, } from "../../kernel/ports/host-cli.js";
|
|
3
3
|
import { isProviderError, providerError } from "../../kernel/ports/model.js";
|
|
4
4
|
import { sniffImage } from "../image/bytes.js";
|
|
5
5
|
import { hostRequest, hostUnavailable, readHostBytes, } from "./transport.js";
|
|
6
|
+
const tooBigForHost = "Slopify could not send this request to the host helper because it is larger than, or shaped differently from, what the helper accepts. Make the inputs shorter in Edit project, then use Retry stage; if it happens again, use Download diagnostics in Settings and report it.";
|
|
6
7
|
export function createHostCliClient(options) {
|
|
7
8
|
async function request(input) {
|
|
8
9
|
if (!options.directory)
|
|
@@ -49,6 +50,30 @@ export function createHostCliClient(options) {
|
|
|
49
50
|
...(model.thinkingModes === undefined ? {} : { thinkingModes: model.thinkingModes }),
|
|
50
51
|
}));
|
|
51
52
|
return {
|
|
53
|
+
openFolder: async (path, signal) => {
|
|
54
|
+
const parsed = hostOpenFolderSchema.safeParse({ path });
|
|
55
|
+
if (!parsed.success || !options.directory)
|
|
56
|
+
return false;
|
|
57
|
+
// An older helper answers 404 and a refused folder 403; either way the caller shows the path.
|
|
58
|
+
try {
|
|
59
|
+
const response = await hostRequest({
|
|
60
|
+
directory: options.directory,
|
|
61
|
+
path: "/v1/open-folder",
|
|
62
|
+
method: "POST",
|
|
63
|
+
kind: "metadata",
|
|
64
|
+
body: Buffer.from(JSON.stringify(parsed.data)),
|
|
65
|
+
signal,
|
|
66
|
+
});
|
|
67
|
+
if (response.statusCode !== 200) {
|
|
68
|
+
response.destroy();
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
return hostOpenedSchema.safeParse(JSON.parse((await readHostBytes(response, bridgeLimits.status)).toString("utf8"))).success;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
52
77
|
status: async (id) => {
|
|
53
78
|
try {
|
|
54
79
|
const status = await metadata(`/v1/status/${id}`, hostStatusSchema);
|
|
@@ -88,7 +113,10 @@ export function createHostCliClient(options) {
|
|
|
88
113
|
...(req.webSearch === undefined ? {} : { webSearch: req.webSearch }),
|
|
89
114
|
});
|
|
90
115
|
if (!parsed.success)
|
|
91
|
-
throw providerError({
|
|
116
|
+
throw providerError({
|
|
117
|
+
kind: "unsupported",
|
|
118
|
+
message: tooBigForHost,
|
|
119
|
+
});
|
|
92
120
|
await ready(req.signal);
|
|
93
121
|
const response = await request({
|
|
94
122
|
path: `/v1/llm/${id}`,
|
|
@@ -122,7 +150,10 @@ export function createHostCliClient(options) {
|
|
|
122
150
|
aspect: req.aspect,
|
|
123
151
|
});
|
|
124
152
|
if (!parsed.success)
|
|
125
|
-
throw providerError({
|
|
153
|
+
throw providerError({
|
|
154
|
+
kind: "unsupported",
|
|
155
|
+
message: tooBigForHost,
|
|
156
|
+
});
|
|
126
157
|
await ready(req.signal);
|
|
127
158
|
const response = await request({
|
|
128
159
|
path: "/v1/image",
|
|
@@ -8,8 +8,8 @@ export function hostUnavailable(submitted = false) {
|
|
|
8
8
|
return providerError({
|
|
9
9
|
kind: "unavailable",
|
|
10
10
|
message: submitted
|
|
11
|
-
? "The host CLI
|
|
12
|
-
: "
|
|
11
|
+
? "The connection to the host helper dropped before the CLI's answer arrived, so Slopify cannot tell whether it finished (it may already have used your quota). Check the host helper is still running, then use Retry stage."
|
|
12
|
+
: "Slopify cannot reach its host helper, which runs the Claude Code, Codex and Gemini CLIs outside Docker. On the host computer, run npx @gentbajko/slopify@latest --docker to set it up or restart it, then use Retry stage.",
|
|
13
13
|
});
|
|
14
14
|
}
|
|
15
15
|
async function tokenAt(directory) {
|
|
@@ -33,7 +33,7 @@ export async function hostRequest(options) {
|
|
|
33
33
|
if (options.body && options.body.byteLength > bridgeLimits.request)
|
|
34
34
|
throw providerError({
|
|
35
35
|
kind: "unsupported",
|
|
36
|
-
message: "
|
|
36
|
+
message: "This request is larger than the host helper accepts (16 MB), so nothing was sent. Make the inputs shorter in Edit project, or choose an API provider such as OpenRouter in its Providers section, then use Retry stage.",
|
|
37
37
|
});
|
|
38
38
|
let token;
|
|
39
39
|
try {
|
|
@@ -121,7 +121,7 @@ export async function readHostBytes(response, maximum) {
|
|
|
121
121
|
if (length > maximum)
|
|
122
122
|
throw providerError({
|
|
123
123
|
kind: "unavailable",
|
|
124
|
-
message: "
|
|
124
|
+
message: "The host helper sent back more data than Slopify accepts, so the answer was dropped. Use Retry stage; if it happens again, use Download diagnostics in Settings and report it.",
|
|
125
125
|
});
|
|
126
126
|
chunks.push(value);
|
|
127
127
|
}
|
|
@@ -40,7 +40,7 @@ export async function downloadImage(download) {
|
|
|
40
40
|
// quoted back - a fal or Replicate delivery link carries its own signature.
|
|
41
41
|
throw providerError({
|
|
42
42
|
kind: "other",
|
|
43
|
-
message: `${download.provider}
|
|
43
|
+
message: `${download.provider} made the image, but Slopify could not download it (error ${String(response.status)}). Check your internet connection, then use Retry stage.`,
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
46
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
@@ -48,7 +48,7 @@ export async function downloadImage(download) {
|
|
|
48
48
|
if (mime === undefined) {
|
|
49
49
|
throw providerError({
|
|
50
50
|
kind: "other",
|
|
51
|
-
message: `${download.provider}
|
|
51
|
+
message: `${download.provider} sent back something that is not a PNG or JPEG image (${describeBytes(bytes)}). Use Retry stage; if it keeps happening, choose another image model in the Providers section of Edit project.`,
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
return { bytes, mime };
|
|
@@ -8,23 +8,23 @@ const maxOutputBytes = 32 * 1024 * 1024;
|
|
|
8
8
|
function unavailable(detail) {
|
|
9
9
|
return providerError({
|
|
10
10
|
kind: "unavailable",
|
|
11
|
-
message: `Codex finished, but ${detail}.
|
|
11
|
+
message: `The Codex CLI finished, but Slopify could not collect the image: ${detail}. Use Retry stage to make it again (this uses your Codex quota again).`,
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
export function codexGeneratedImage(env, threadId, startedAt) {
|
|
15
15
|
if (!z.uuid().safeParse(threadId).success || threadId === undefined)
|
|
16
|
-
throw unavailable("
|
|
16
|
+
throw unavailable("the image job could not be identified");
|
|
17
17
|
const root = join(resolve(env.CODEX_HOME || join(env.HOME || homedir(), ".codex")), "generated_images");
|
|
18
18
|
const directory = join(root, threadId);
|
|
19
19
|
try {
|
|
20
20
|
for (const path of [root, directory]) {
|
|
21
21
|
const entry = lstatSync(path);
|
|
22
22
|
if (!entry.isDirectory() || entry.isSymbolicLink())
|
|
23
|
-
throw unavailable("
|
|
23
|
+
throw unavailable("Codex's image folder is not a plain folder");
|
|
24
24
|
}
|
|
25
25
|
const canonical = realpathSync(directory);
|
|
26
26
|
if (canonical !== join(realpathSync(root), threadId))
|
|
27
|
-
throw unavailable("
|
|
27
|
+
throw unavailable("Codex's image folder moved while it was being read");
|
|
28
28
|
// Codex 0.155.1 omits image items from exec JSONL. Its artifact contract is
|
|
29
29
|
// generated_images/<thread.started ID>/<sanitized tool call ID>.png.
|
|
30
30
|
const dir = opendirSync(directory);
|
|
@@ -32,11 +32,11 @@ export function codexGeneratedImage(env, threadId, startedAt) {
|
|
|
32
32
|
try {
|
|
33
33
|
const entry = dir.readSync();
|
|
34
34
|
if (entry === null)
|
|
35
|
-
throw unavailable("no image was saved
|
|
35
|
+
throw unavailable("no image was saved");
|
|
36
36
|
if (dir.readSync() !== null)
|
|
37
|
-
throw unavailable("more than one
|
|
37
|
+
throw unavailable("more than one file was saved");
|
|
38
38
|
if (!entry.isFile() || !/^[a-zA-Z0-9_-]+\.png$/.test(entry.name))
|
|
39
|
-
throw unavailable("
|
|
39
|
+
throw unavailable("the saved file is not a plain image file");
|
|
40
40
|
name = entry.name;
|
|
41
41
|
}
|
|
42
42
|
finally {
|
|
@@ -45,7 +45,7 @@ export function codexGeneratedImage(env, threadId, startedAt) {
|
|
|
45
45
|
const path = join(directory, name);
|
|
46
46
|
const before = lstatSync(path);
|
|
47
47
|
if (!before.isFile() || before.isSymbolicLink() || realpathSync(path) !== join(canonical, name))
|
|
48
|
-
throw unavailable("
|
|
48
|
+
throw unavailable("the saved file is not a plain image file");
|
|
49
49
|
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
50
50
|
try {
|
|
51
51
|
const stat = fstatSync(fd);
|
|
@@ -56,7 +56,7 @@ export function codexGeneratedImage(env, threadId, startedAt) {
|
|
|
56
56
|
stat.size === 0 ||
|
|
57
57
|
stat.size > maxOutputBytes ||
|
|
58
58
|
stat.mtimeMs < Math.floor(startedAt / 1000) * 1000)
|
|
59
|
-
throw unavailable("
|
|
59
|
+
throw unavailable("the saved file is empty, too large, older than this job, or not a plain file");
|
|
60
60
|
const bytes = Buffer.alloc(stat.size + 1);
|
|
61
61
|
let length = 0;
|
|
62
62
|
while (length < bytes.length) {
|
|
@@ -71,11 +71,11 @@ export function codexGeneratedImage(env, threadId, startedAt) {
|
|
|
71
71
|
after.mtimeMs !== stat.mtimeMs ||
|
|
72
72
|
after.ctimeMs !== stat.ctimeMs ||
|
|
73
73
|
realpathSync(directory) !== canonical)
|
|
74
|
-
throw unavailable("
|
|
74
|
+
throw unavailable("the file changed while it was being read");
|
|
75
75
|
const image = bytes.subarray(0, length);
|
|
76
76
|
const mime = sniffImage(image);
|
|
77
77
|
if (mime === undefined)
|
|
78
|
-
throw unavailable("
|
|
78
|
+
throw unavailable("the saved file is not a PNG or JPEG image");
|
|
79
79
|
return { bytes: image, mime };
|
|
80
80
|
}
|
|
81
81
|
finally {
|
|
@@ -85,6 +85,6 @@ export function codexGeneratedImage(env, threadId, startedAt) {
|
|
|
85
85
|
catch (error) {
|
|
86
86
|
if (isProviderError(error))
|
|
87
87
|
throw error;
|
|
88
|
-
throw unavailable("
|
|
88
|
+
throw unavailable("the saved file could not be read");
|
|
89
89
|
}
|
|
90
90
|
}
|
|
@@ -4,6 +4,7 @@ import { join } from "node:path";
|
|
|
4
4
|
import { z } from "zod";
|
|
5
5
|
import { redact } from "../../kernel/log.js";
|
|
6
6
|
import { providerError } from "../../kernel/ports/model.js";
|
|
7
|
+
import { cliReported, quoted, refusedImage } from "../explain.js";
|
|
7
8
|
import { cliLoginError } from "../llm/cli-login-error.js";
|
|
8
9
|
import { cliEvent, cliShaped, endedWithout, stopCliRun } from "../llm/run-cli.js";
|
|
9
10
|
import { lines } from "../llm/sse-lines.js";
|
|
@@ -87,7 +88,10 @@ export function codexImage(deps) {
|
|
|
87
88
|
models: async () => [codexImageModel],
|
|
88
89
|
generate: async (req) => {
|
|
89
90
|
if (req.model !== codexImageModel.id)
|
|
90
|
-
throw providerError({
|
|
91
|
+
throw providerError({
|
|
92
|
+
kind: "unsupported",
|
|
93
|
+
message: "The Codex CLI makes images only with its own image model. Choose that model for images in the Providers section of Edit project, then use Retry stage.",
|
|
94
|
+
});
|
|
91
95
|
req.signal.throwIfAborted();
|
|
92
96
|
const directory = mkdtempSync(join(tmpdir(), "slopify-codex-image-"));
|
|
93
97
|
const env = imageEnvironment(directory, deps.env ?? process.env);
|
|
@@ -102,7 +106,10 @@ export function codexImage(deps) {
|
|
|
102
106
|
});
|
|
103
107
|
}
|
|
104
108
|
catch {
|
|
105
|
-
throw providerError({
|
|
109
|
+
throw providerError({
|
|
110
|
+
kind: "unsupported",
|
|
111
|
+
message: "The Codex CLI could not be started. Check it is installed and set up in Settings → Providers, then use Retry stage.",
|
|
112
|
+
});
|
|
106
113
|
}
|
|
107
114
|
let completed = false;
|
|
108
115
|
let unavailable = false;
|
|
@@ -115,7 +122,7 @@ export function codexImage(deps) {
|
|
|
115
122
|
if (threadId !== undefined)
|
|
116
123
|
throw providerError({
|
|
117
124
|
kind: "unavailable",
|
|
118
|
-
message: "Codex
|
|
125
|
+
message: "The Codex CLI started more than one image job for one image, so Slopify cannot tell which result is right (both may have used your quota). Use Retry stage to make the image again.",
|
|
119
126
|
});
|
|
120
127
|
threadId = cliShaped(binary, z.object({ thread_id: z.string() }), event.value).thread_id;
|
|
121
128
|
}
|
|
@@ -128,7 +135,9 @@ export function codexImage(deps) {
|
|
|
128
135
|
throw login;
|
|
129
136
|
throw providerError({
|
|
130
137
|
kind: /refus|content.policy|safety/i.test(message) ? "refusal" : "other",
|
|
131
|
-
message:
|
|
138
|
+
message: /refus|content.policy|safety/i.test(message)
|
|
139
|
+
? refusedImage("The Codex CLI", redact(message))
|
|
140
|
+
: cliReported(binary, redact(message), imageNext),
|
|
132
141
|
});
|
|
133
142
|
}
|
|
134
143
|
else if (event.type === "error") {
|
|
@@ -140,7 +149,9 @@ export function codexImage(deps) {
|
|
|
140
149
|
kind: /image.generation|image tool|feature.*unavailable/i.test(message)
|
|
141
150
|
? "unsupported"
|
|
142
151
|
: "other",
|
|
143
|
-
message:
|
|
152
|
+
message: /image.generation|image tool|feature.*unavailable/i.test(message)
|
|
153
|
+
? `The Codex CLI cannot make images${quoted("", redact(message))}. ${cannotDraw}`
|
|
154
|
+
: cliReported(binary, redact(message), imageNext),
|
|
144
155
|
});
|
|
145
156
|
}
|
|
146
157
|
else if (event.type === "item.completed") {
|
|
@@ -154,7 +165,10 @@ export function codexImage(deps) {
|
|
|
154
165
|
const ended = await run.ended;
|
|
155
166
|
req.signal.throwIfAborted();
|
|
156
167
|
if (ended.error !== null)
|
|
157
|
-
throw providerError({
|
|
168
|
+
throw providerError({
|
|
169
|
+
kind: "unsupported",
|
|
170
|
+
message: "The Codex CLI could not be started. Check it is installed and set up in Settings → Providers, then use Retry stage.",
|
|
171
|
+
});
|
|
158
172
|
if (ended.code !== 0 || !completed)
|
|
159
173
|
throw (cliLoginError("codex", run.stderr()) ??
|
|
160
174
|
providerError({
|
|
@@ -164,7 +178,7 @@ export function codexImage(deps) {
|
|
|
164
178
|
if (unavailable)
|
|
165
179
|
throw providerError({
|
|
166
180
|
kind: "unsupported",
|
|
167
|
-
message:
|
|
181
|
+
message: `The Codex CLI cannot make images. ${cannotDraw}`,
|
|
168
182
|
});
|
|
169
183
|
return codexGeneratedImage(env, threadId, startedAt);
|
|
170
184
|
}
|
|
@@ -191,11 +205,13 @@ async function* bounded(source, maximum) {
|
|
|
191
205
|
if (size > maximum)
|
|
192
206
|
throw providerError({
|
|
193
207
|
kind: "other",
|
|
194
|
-
message: "Codex
|
|
208
|
+
message: "The Codex CLI sent far more output than an image job should, so Slopify stopped it. Use Retry stage; if it keeps happening, update the Codex CLI to the latest version.",
|
|
195
209
|
});
|
|
196
210
|
yield chunk;
|
|
197
211
|
}
|
|
198
212
|
}
|
|
213
|
+
const imageNext = "Use Retry stage; if it keeps failing, run codex in a terminal to check it works and is signed in, or choose another image provider in the Providers section of Edit project.";
|
|
214
|
+
const cannotDraw = "Update the Codex CLI and check your ChatGPT plan includes image generation, or choose another image provider in the Providers section of Edit project.";
|
|
199
215
|
function imageEnvironment(directory, source) {
|
|
200
216
|
const env = { PWD: directory };
|
|
201
217
|
const allowed = [
|