@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
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
-- foreign-keys: off
|
|
2
|
+
-- SQLite cannot widen a CHECK constraint in place, so the two tables that list the stage
|
|
3
|
+
-- kinds are rebuilt. `stages` is referenced by attempts, stage_pieces and revision_work with
|
|
4
|
+
-- ON DELETE CASCADE: migrate.ts turns enforcement off for this file, or dropping the old
|
|
5
|
+
-- table would empty those three, and checks every reference before it commits.
|
|
6
|
+
CREATE TABLE stages_document_upgrade (id TEXT PRIMARY KEY, project_id TEXT NOT NULL REFERENCES projects(id) ON DELETE CASCADE, kind TEXT NOT NULL CHECK(kind IN ('research','article','audio','images','thumbnail','video','document')), source TEXT NOT NULL, state TEXT NOT NULL CHECK(state IN ('pending','running','done','failed','canceled','provided','skipped')), failure_reason TEXT, attempt_count INTEGER NOT NULL DEFAULT 0, progress_current INTEGER, progress_total INTEGER, started_at TEXT, finished_at TEXT, UNIQUE(project_id, kind));
|
|
7
|
+
INSERT INTO stages_document_upgrade (id,project_id,kind,source,state,failure_reason,attempt_count,progress_current,progress_total,started_at,finished_at)
|
|
8
|
+
SELECT id,project_id,kind,source,state,failure_reason,attempt_count,progress_current,progress_total,started_at,finished_at FROM stages;
|
|
9
|
+
DROP TABLE stages;
|
|
10
|
+
ALTER TABLE stages_document_upgrade RENAME TO stages;
|
|
11
|
+
CREATE UNIQUE INDEX stages_project_identity ON stages(project_id,id,kind);
|
|
12
|
+
|
|
13
|
+
-- Every existing project gets the new stage switched off, so nothing it already finished
|
|
14
|
+
-- shows as waiting.
|
|
15
|
+
INSERT INTO stages (id,project_id,kind,source,state)
|
|
16
|
+
SELECT id || '-document', id, 'document', 'off', 'skipped' FROM projects;
|
|
17
|
+
|
|
18
|
+
CREATE TABLE revision_pieces_document_upgrade (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
project_id TEXT NOT NULL,
|
|
21
|
+
revision_id TEXT NOT NULL,
|
|
22
|
+
piece_key TEXT NOT NULL,
|
|
23
|
+
stage_kind TEXT NOT NULL CHECK(stage_kind IN
|
|
24
|
+
('research','article','audio','images','thumbnail','video','document')),
|
|
25
|
+
asset_id TEXT,
|
|
26
|
+
fingerprint TEXT NOT NULL,
|
|
27
|
+
descriptor TEXT NOT NULL CHECK(json_valid(descriptor)),
|
|
28
|
+
publication_id TEXT,
|
|
29
|
+
selected INTEGER NOT NULL CHECK(selected IN (0,1)),
|
|
30
|
+
created_at TEXT NOT NULL,
|
|
31
|
+
FOREIGN KEY(project_id, revision_id)
|
|
32
|
+
REFERENCES project_revisions(project_id, id) ON DELETE CASCADE,
|
|
33
|
+
FOREIGN KEY(project_id, asset_id)
|
|
34
|
+
REFERENCES project_assets(project_id, id) ON DELETE CASCADE
|
|
35
|
+
);
|
|
36
|
+
INSERT INTO revision_pieces_document_upgrade (id,project_id,revision_id,piece_key,stage_kind,asset_id,fingerprint,descriptor,publication_id,selected,created_at)
|
|
37
|
+
SELECT id,project_id,revision_id,piece_key,stage_kind,asset_id,fingerprint,descriptor,publication_id,selected,created_at FROM revision_pieces;
|
|
38
|
+
DROP TABLE revision_pieces;
|
|
39
|
+
ALTER TABLE revision_pieces_document_upgrade RENAME TO revision_pieces;
|
|
40
|
+
CREATE UNIQUE INDEX revision_pieces_selected ON revision_pieces(revision_id, piece_key)
|
|
41
|
+
WHERE selected = 1;
|
|
42
|
+
CREATE INDEX revision_pieces_revision ON revision_pieces(revision_id, created_at, id);
|
|
43
|
+
CREATE UNIQUE INDEX revision_pieces_publication ON revision_pieces(revision_id, publication_id, piece_key)
|
|
44
|
+
WHERE publication_id IS NOT NULL;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE prompts_description_upgrade (
|
|
2
|
+
id TEXT PRIMARY KEY,
|
|
3
|
+
kind TEXT NOT NULL CHECK (kind IN ('article','image','thumbnail','narration','description')),
|
|
4
|
+
name TEXT NOT NULL,
|
|
5
|
+
body TEXT NOT NULL,
|
|
6
|
+
slots TEXT NOT NULL,
|
|
7
|
+
updated_at TEXT NOT NULL
|
|
8
|
+
);
|
|
9
|
+
INSERT INTO prompts_description_upgrade (id,kind,name,body,slots,updated_at)
|
|
10
|
+
SELECT id,kind,name,body,slots,updated_at FROM prompts;
|
|
11
|
+
DROP TABLE prompts;
|
|
12
|
+
ALTER TABLE prompts_description_upgrade RENAME TO prompts;
|
|
13
|
+
CREATE UNIQUE INDEX prompts_name ON prompts(kind,lower(name));
|
package/dist/kernel/lock.js
CHANGED
|
@@ -6,14 +6,15 @@ export function acquireInstanceLock(lockPath) {
|
|
|
6
6
|
if (!claim(lockPath, pid)) {
|
|
7
7
|
const holder = holderOf(lockPath);
|
|
8
8
|
if (holder !== undefined && holds(holder, pid)) {
|
|
9
|
-
throw new Error(`
|
|
10
|
-
|
|
9
|
+
throw new Error(`Slopify is already running on this data directory (process ${holder.pid}). ` +
|
|
10
|
+
"Use the copy that is already open, or stop it (Ctrl+C in its terminal) and start again. " +
|
|
11
|
+
`If no Slopify is running, delete ${lockPath} and try again.`);
|
|
11
12
|
}
|
|
12
13
|
// The recorded process is gone, or the file says nothing usable. Reclaiming it here
|
|
13
14
|
// is what keeps a crash from bricking the data directory until a file is deleted by hand.
|
|
14
15
|
discard(lockPath);
|
|
15
16
|
if (!claim(lockPath, pid)) {
|
|
16
|
-
throw new Error(`
|
|
17
|
+
throw new Error(`Another Slopify started on this data directory at the same moment (lock file ${lockPath}). Close the extra copy and start Slopify again.`);
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
return {
|
package/dist/kernel/pipeline.js
CHANGED
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
// runner may not import a slice to learn them.
|
|
4
4
|
// The frame a run is made in, and the aspect an image is asked for: one set, named once.
|
|
5
5
|
export const formats = ["16:9", "9:16"];
|
|
6
|
-
|
|
6
|
+
// Document is last so every screen that walks this list in order shows the six older
|
|
7
|
+
// stages where they always were.
|
|
8
|
+
export const stageKinds = [
|
|
9
|
+
"research",
|
|
10
|
+
"article",
|
|
11
|
+
"audio",
|
|
12
|
+
"images",
|
|
13
|
+
"thumbnail",
|
|
14
|
+
"video",
|
|
15
|
+
"document",
|
|
16
|
+
];
|
|
7
17
|
export const stageStates = [
|
|
8
18
|
"pending",
|
|
9
19
|
"running",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { messageRoles, thinkingModes } from "./llm.js";
|
|
3
4
|
import { llmDocumentsSchema } from "./llm-documents.js";
|
|
@@ -16,6 +17,9 @@ export const bridgeLimits = {
|
|
|
16
17
|
generation: 5,
|
|
17
18
|
metadata: 8,
|
|
18
19
|
};
|
|
20
|
+
/** A folder the helper refuses to open on purpose; the route answers it with a 403. */
|
|
21
|
+
export class HostFolderRefused extends Error {
|
|
22
|
+
}
|
|
19
23
|
const short = z
|
|
20
24
|
.string()
|
|
21
25
|
.min(1)
|
|
@@ -40,6 +44,16 @@ export const hostImageSchema = z
|
|
|
40
44
|
aspect: z.enum(["16:9", "9:16"]),
|
|
41
45
|
})
|
|
42
46
|
.strict();
|
|
47
|
+
export const hostOpenFolderSchema = z
|
|
48
|
+
.object({
|
|
49
|
+
path: z
|
|
50
|
+
.string()
|
|
51
|
+
.min(1)
|
|
52
|
+
.max(4096)
|
|
53
|
+
.refine((v) => isAbsolute(v) && !/[\p{Cc}]/u.test(v)),
|
|
54
|
+
})
|
|
55
|
+
.strict();
|
|
56
|
+
export const hostOpenedSchema = z.object({ opened: z.literal(true) }).strict();
|
|
43
57
|
export const hostStatusSchema = z
|
|
44
58
|
.object({
|
|
45
59
|
id: z.enum(hostCliIds),
|
|
@@ -1 +1,15 @@
|
|
|
1
|
-
|
|
1
|
+
// Where the audio stopped matching its transcript: seconds into the aligned file, the
|
|
2
|
+
// transcript words it expected there and what the model heard instead (either may be empty).
|
|
3
|
+
// The aligner throws it so the caller can say which part of the narration to redo.
|
|
4
|
+
export class SubtitleMismatch extends Error {
|
|
5
|
+
at;
|
|
6
|
+
expected;
|
|
7
|
+
heard;
|
|
8
|
+
constructor(message, at, expected, heard) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.at = at;
|
|
11
|
+
this.expected = expected;
|
|
12
|
+
this.heard = heard;
|
|
13
|
+
this.name = "SubtitleMismatch";
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -100,8 +100,8 @@ function classify(error, expired, opts, limit) {
|
|
|
100
100
|
return providerError({
|
|
101
101
|
kind: "timeout",
|
|
102
102
|
message: opts.streaming === true
|
|
103
|
-
? `
|
|
104
|
-
: `
|
|
103
|
+
? `The ${callee[opts.kind]} sent nothing for ${seconds} seconds, so Slopify stopped waiting. It may be overloaded: wait a few minutes, then use Retry stage.`
|
|
104
|
+
: `The ${callee[opts.kind]} did not answer within ${seconds} seconds. It may be overloaded: wait a few minutes, then use Retry stage, or choose another model in the Providers section of Edit project.`,
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
if (isProviderError(error)) {
|
|
@@ -113,11 +113,35 @@ function classify(error, expired, opts, limit) {
|
|
|
113
113
|
...(error.fault.retryAfterMs === undefined ? {} : { retryAfterMs: error.fault.retryAfterMs }),
|
|
114
114
|
});
|
|
115
115
|
}
|
|
116
|
+
if (unreachable(error)) {
|
|
117
|
+
// `fetch` names only itself; the cause's code (ENOTFOUND, ECONNREFUSED) is the clue.
|
|
118
|
+
const code = causeCode(error);
|
|
119
|
+
return providerError({
|
|
120
|
+
kind: "other",
|
|
121
|
+
message: `Slopify could not reach the ${callee[opts.kind]} over the internet${code === undefined ? "" : ` (${code})`}. Check your internet connection, firewall or VPN, then use Retry stage.`,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
116
124
|
return providerError({
|
|
117
125
|
kind: "other",
|
|
118
126
|
message: redact(error instanceof Error ? error.message : String(error)),
|
|
119
127
|
});
|
|
120
128
|
}
|
|
129
|
+
// What the user calls the other end of each kind of call.
|
|
130
|
+
const callee = {
|
|
131
|
+
llm: "AI model",
|
|
132
|
+
tts: "narration provider",
|
|
133
|
+
image: "image provider",
|
|
134
|
+
};
|
|
135
|
+
// Node's `fetch` rejects a request that never reached the server with this TypeError.
|
|
136
|
+
function unreachable(error) {
|
|
137
|
+
return error instanceof TypeError && error.message === "fetch failed";
|
|
138
|
+
}
|
|
139
|
+
function causeCode(error) {
|
|
140
|
+
const cause = error.cause;
|
|
141
|
+
if (typeof cause !== "object" || cause === null || !("code" in cause))
|
|
142
|
+
return undefined;
|
|
143
|
+
return typeof cause.code === "string" && /^[A-Z_]+$/.test(cause.code) ? cause.code : undefined;
|
|
144
|
+
}
|
|
121
145
|
// The same milliseconds, measured two ways. `idle` restarts the count on every chunk, so a
|
|
122
146
|
// stalled stream is cut off and a long answer is not. Cancel still wins.
|
|
123
147
|
function deadline(clock, parent, ms, idle) {
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// Images use saved prompts and can run immediately. A thumbnail's source and an
|
|
2
|
-
// audio-only export narrow these dependencies through dependenciesOf().
|
|
2
|
+
// audio-only export narrow these dependencies through dependenciesOf(). The document is
|
|
3
|
+
// laid out from the article alone, with the thumbnail as its cover when there is one, so it
|
|
4
|
+
// runs beside narration and images rather than after them.
|
|
3
5
|
export const deps = {
|
|
4
6
|
research: [],
|
|
5
7
|
article: ["research"],
|
|
@@ -7,6 +9,7 @@ export const deps = {
|
|
|
7
9
|
images: [],
|
|
8
10
|
thumbnail: ["article"],
|
|
9
11
|
video: ["article", "audio", "images", "thumbnail"],
|
|
12
|
+
document: ["article", "thumbnail"],
|
|
10
13
|
};
|
|
11
14
|
// `provided` and `skipped` release a dependency exactly as `done` does.
|
|
12
15
|
export function satisfied(state) {
|
|
@@ -37,6 +40,8 @@ export function dependenciesOf(kind, sources) {
|
|
|
37
40
|
return [];
|
|
38
41
|
if (kind === "video" && sources.video === "off")
|
|
39
42
|
return ["article", "audio"];
|
|
43
|
+
if (kind === "document" && sources.thumbnail === "off")
|
|
44
|
+
return ["article"];
|
|
40
45
|
return deps[kind];
|
|
41
46
|
}
|
|
42
47
|
// The thin meter under a running row on Projects, averaging stage progress. Every stage
|
package/dist/main.js
CHANGED
|
@@ -27,6 +27,7 @@ import { ulidIds } from "./kernel/ids.js";
|
|
|
27
27
|
import { acquireInstanceLock } from "./kernel/lock.js";
|
|
28
28
|
import { openLog } from "./kernel/log.js";
|
|
29
29
|
import { ensureDirs, layout, subtitleModelDir } from "./kernel/paths.js";
|
|
30
|
+
import { stageKinds } from "./kernel/pipeline.js";
|
|
30
31
|
import { sqliteAttempts } from "./kernel/runner/attempt-repo.js";
|
|
31
32
|
import { createCheckpointAuthority, } from "./kernel/runner/checkpoint-authority.js";
|
|
32
33
|
import { createRunner } from "./kernel/runner/index.js";
|
|
@@ -119,7 +120,7 @@ export async function boot(config, options = {}) {
|
|
|
119
120
|
if (dockerState !== undefined &&
|
|
120
121
|
(process.env.SLOPIFY_CONTAINER !== "1" ||
|
|
121
122
|
dockerState !== "/opt/slopify-install/activation.json"))
|
|
122
|
-
throw new Error("
|
|
123
|
+
throw new Error("This container was started with Slopify's Docker settings in the wrong place (SLOPIFY_DOCKER_INSTALL_STATE). Start it with the launcher instead: npx @gentbajko/slopify --docker");
|
|
123
124
|
const candidateToken = process.env.SLOPIFY_UPDATE_TOKEN ?? "";
|
|
124
125
|
const pendingActivation = isUpdateToken(candidateToken) && process.env.SLOPIFY_UPDATE_PENDING === "1";
|
|
125
126
|
ensureDirs(paths, { mode: 0o700 });
|
|
@@ -243,7 +244,7 @@ export async function boot(config, options = {}) {
|
|
|
243
244
|
: !existsSync(oldEntry) || !existsSync(workerEntry)
|
|
244
245
|
? "Run Slopify from its installed package to use in-app updates."
|
|
245
246
|
: npm === undefined
|
|
246
|
-
? "npm
|
|
247
|
+
? "npm was not found, so in-app updates are off. Install Node.js with npm (https://nodejs.org), or update from the terminal: npx @gentbajko/slopify@latest"
|
|
247
248
|
: undefined,
|
|
248
249
|
busy: () => updateDb.prepare("SELECT 1 FROM stages WHERE state = 'running' LIMIT 1").get() !==
|
|
249
250
|
undefined ||
|
|
@@ -323,7 +324,10 @@ export async function boot(config, options = {}) {
|
|
|
323
324
|
schedules: scheduleDeps,
|
|
324
325
|
...(rebuild.measureAudio === undefined ? {} : { measureAudio: rebuild.measureAudio }),
|
|
325
326
|
openFolder,
|
|
326
|
-
folderLocation:
|
|
327
|
+
folderLocation: {
|
|
328
|
+
...(await dockerFolderConfiguration(process.env, paths.projects)),
|
|
329
|
+
...(hostCli === undefined ? {} : { openOnHost: hostCli.openFolder }),
|
|
330
|
+
},
|
|
327
331
|
...(dockerState === undefined ? {} : { installationPending: () => updater.locked() }),
|
|
328
332
|
db,
|
|
329
333
|
paths,
|
|
@@ -484,7 +488,7 @@ export function wireRunner({ audioPreviews, db, paths, clock, ids, log, hub, tel
|
|
|
484
488
|
});
|
|
485
489
|
},
|
|
486
490
|
},
|
|
487
|
-
runs: Object.fromEntries(
|
|
491
|
+
runs: Object.fromEntries(stageKinds.map((kind) => [
|
|
488
492
|
kind,
|
|
489
493
|
(context) => runRevisionInvocation(execution, context, stageProviders(providers, context)),
|
|
490
494
|
])),
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
// The format is the kernel's: the image port asks for the same two aspects.
|
|
2
2
|
export { formats } from "../../kernel/pipeline.js";
|
|
3
3
|
// Generate, Provide or Off for most stages, plus the thumbnail's two Generate modes.
|
|
4
|
-
// Article stays Generate or Provide; Video can be Generate or Off.
|
|
4
|
+
// Article stays Generate or Provide; Video and Document can be Generate or Off.
|
|
5
5
|
export const stageSources = ["generate", "provide", "off", "from_prompt", "prompt_by_llm"];
|
|
6
|
+
export function sourceOf(sources, kind) {
|
|
7
|
+
return sources[kind] ?? "off";
|
|
8
|
+
}
|
|
9
|
+
// How each slideshow image moves while it is on screen: Zoom in and out, Pan across, a
|
|
10
|
+
// Mix of both taking turns, or Still. `video/motion.ts` has the rules.
|
|
11
|
+
export const motionStyles = ["zoom", "pan", "mixed", "still"];
|
|
6
12
|
export const entryModes = ["text", "llm"];
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { stageKinds } from "../../kernel/pipeline.js";
|
|
2
|
+
import { sourceOf } from "./model.js";
|
|
2
3
|
export const titleMax = 200;
|
|
3
4
|
export const valueMax = 200;
|
|
4
5
|
export const numberPerPromptMax = 20;
|
|
@@ -6,6 +7,41 @@ export const imagesPerRunMax = 60;
|
|
|
6
7
|
// ceiling: the default gap is 3 s and nothing fixes an upper or lower bound, so the range
|
|
7
8
|
// is this module's. A wider gap is a settings change, not a schema change.
|
|
8
9
|
export const silenceGapSecondsMax = 30;
|
|
10
|
+
// ceiling: the owner's numbers. A still held past ten minutes is a separate kind of video.
|
|
11
|
+
export const imageSecondsMin = 1;
|
|
12
|
+
export const imageSecondsMax = 600;
|
|
13
|
+
export const defaultImageSeconds = 15;
|
|
14
|
+
// ceiling: the same 30 s as the gap; half seconds are enough precision for a pause.
|
|
15
|
+
export const edgeSilenceSecondsMax = 30;
|
|
16
|
+
export const defaultEdgeSilenceSeconds = 2;
|
|
17
|
+
// ceiling: past 50% the crop loses more of the picture than it shows.
|
|
18
|
+
export const zoomPercentMax = 50;
|
|
19
|
+
export const defaultZoomPercent = 22.5;
|
|
20
|
+
export const defaultMotionStyle = "zoom";
|
|
21
|
+
// The words Play, Edit project and the change preview use for each motion style.
|
|
22
|
+
export const motionStyleLabels = {
|
|
23
|
+
zoom: "Zoom in and out",
|
|
24
|
+
pan: "Pan across",
|
|
25
|
+
mixed: "Mix of both",
|
|
26
|
+
still: "Still",
|
|
27
|
+
};
|
|
28
|
+
// Shared by admission, Play's draft conversion and Edit project, so all three say the same
|
|
29
|
+
// sentence about the same value.
|
|
30
|
+
export function imageSecondsProblem(value) {
|
|
31
|
+
return Number.isInteger(value) && value >= imageSecondsMin && value <= imageSecondsMax
|
|
32
|
+
? undefined
|
|
33
|
+
: `Enter a whole number of seconds between ${imageSecondsMin} and ${imageSecondsMax}.`;
|
|
34
|
+
}
|
|
35
|
+
export function zoomPercentProblem(value) {
|
|
36
|
+
return Number.isInteger(value * 2) && value >= 0 && value <= zoomPercentMax
|
|
37
|
+
? undefined
|
|
38
|
+
: `Enter a zoom between 0 and ${zoomPercentMax} percent, in steps of 0.5.`;
|
|
39
|
+
}
|
|
40
|
+
export function edgeSilenceSecondsProblem(value) {
|
|
41
|
+
return Number.isInteger(value * 2) && value >= 0 && value <= edgeSilenceSecondsMax
|
|
42
|
+
? undefined
|
|
43
|
+
: `Enter a number of seconds between 0 and ${edgeSilenceSecondsMax}, in steps of 0.5.`;
|
|
44
|
+
}
|
|
9
45
|
// The legal source for each stage. Exported because Play's
|
|
10
46
|
// source switches offer exactly these and nothing else, so the segmented control and the
|
|
11
47
|
// refusal below it are the same list and a stage whose legal set changes cannot leave a
|
|
@@ -18,22 +54,23 @@ export const allowedSources = {
|
|
|
18
54
|
images: ["off", "generate", "provide"],
|
|
19
55
|
thumbnail: ["off", "from_prompt", "prompt_by_llm", "provide"],
|
|
20
56
|
video: ["off", "generate"],
|
|
57
|
+
document: ["off", "generate"],
|
|
21
58
|
};
|
|
22
59
|
export function admit(input) {
|
|
23
60
|
const fields = [];
|
|
24
61
|
const draft = normaliseDraft(input.draft);
|
|
25
62
|
const { sources } = draft;
|
|
26
63
|
if (draft.title === "") {
|
|
27
|
-
fields.push({ field: "title", message: "
|
|
64
|
+
fields.push({ field: "title", message: "Enter a title for this video." });
|
|
28
65
|
}
|
|
29
66
|
else if (draft.title.length > titleMax) {
|
|
30
|
-
fields.push({ field: "title", message: `
|
|
67
|
+
fields.push({ field: "title", message: `Keep the title to ${titleMax} characters or fewer.` });
|
|
31
68
|
}
|
|
32
69
|
for (const kind of stageKinds) {
|
|
33
|
-
if (!allowedSources[kind].includes(sources
|
|
70
|
+
if (!allowedSources[kind].includes(sourceOf(sources, kind))) {
|
|
34
71
|
fields.push({
|
|
35
72
|
field: `sources.${kind}`,
|
|
36
|
-
message: `
|
|
73
|
+
message: `"${sourceOf(sources, kind)}" is not an option for ${kind}. Reload the page and choose again.`,
|
|
37
74
|
});
|
|
38
75
|
}
|
|
39
76
|
}
|
|
@@ -43,9 +80,10 @@ export function admit(input) {
|
|
|
43
80
|
sources.thumbnail === "prompt_by_llm" ||
|
|
44
81
|
draft.intro?.mode === "llm" ||
|
|
45
82
|
draft.outro?.mode === "llm" ||
|
|
46
|
-
usesNarrationPreparation(draft)
|
|
83
|
+
usesNarrationPreparation(draft) ||
|
|
84
|
+
usesYoutubeDescription(draft);
|
|
47
85
|
if (needsLlm && !chosen(draft.llm)) {
|
|
48
|
-
fields.push({ field: "llm", message: "
|
|
86
|
+
fields.push({ field: "llm", message: "Choose a text (LLM) provider and model." });
|
|
49
87
|
}
|
|
50
88
|
if (sources.article === "generate" && blank(draft.articlePrompt)) {
|
|
51
89
|
fields.push({ field: "articlePrompt", message: "Pick an article prompt." });
|
|
@@ -54,10 +92,10 @@ export function admit(input) {
|
|
|
54
92
|
fields.push(...narrationPreparationFields(draft));
|
|
55
93
|
if (sources.audio === "generate") {
|
|
56
94
|
if (voiced === undefined || !chosen(voiced)) {
|
|
57
|
-
fields.push({ field: "audio", message: "
|
|
95
|
+
fields.push({ field: "audio", message: "Choose a narration provider and model." });
|
|
58
96
|
}
|
|
59
97
|
else if (voiced.voice.trim() === "") {
|
|
60
|
-
fields.push({ field: "audio.voice", message: "
|
|
98
|
+
fields.push({ field: "audio.voice", message: "Choose a voice for the narration." });
|
|
61
99
|
}
|
|
62
100
|
}
|
|
63
101
|
if (sources.images === "generate") {
|
|
@@ -67,15 +105,19 @@ export function admit(input) {
|
|
|
67
105
|
sources.thumbnail === "from_prompt" ||
|
|
68
106
|
sources.thumbnail === "prompt_by_llm") &&
|
|
69
107
|
!chosen(draft.images)) {
|
|
70
|
-
fields.push({ field: "images", message: "
|
|
108
|
+
fields.push({ field: "images", message: "Choose an image provider and model." });
|
|
71
109
|
}
|
|
72
110
|
if ((sources.thumbnail === "from_prompt" || sources.thumbnail === "prompt_by_llm") &&
|
|
73
111
|
blank(draft.thumbnailPrompt)) {
|
|
74
112
|
fields.push({ field: "thumbnailPrompt", message: "Pick a thumbnail prompt." });
|
|
75
113
|
}
|
|
76
114
|
if (sources.audio === "off" && draft.subtitles !== undefined && draft.subtitles.mode !== "off") {
|
|
77
|
-
fields.push({
|
|
115
|
+
fields.push({
|
|
116
|
+
field: "subtitles.mode",
|
|
117
|
+
message: "Subtitles need narration. Turn narration on, or turn subtitles off.",
|
|
118
|
+
});
|
|
78
119
|
}
|
|
120
|
+
fields.push(...youtubeDescriptionFields(draft));
|
|
79
121
|
checkProvided(draft, input.staged, fields);
|
|
80
122
|
checkValues(draft, input.requiredSlots, fields);
|
|
81
123
|
if (!Number.isFinite(draft.silenceGapSeconds) ||
|
|
@@ -83,9 +125,19 @@ export function admit(input) {
|
|
|
83
125
|
draft.silenceGapSeconds > silenceGapSecondsMax) {
|
|
84
126
|
fields.push({
|
|
85
127
|
field: "silenceGapSeconds",
|
|
86
|
-
message: `
|
|
128
|
+
message: `Enter a silence gap between 0 and ${silenceGapSecondsMax} seconds.`,
|
|
87
129
|
});
|
|
88
130
|
}
|
|
131
|
+
// Checked only where they are used: a hidden control cannot hold up a run.
|
|
132
|
+
const imageProblem = sources.video === "generate" ? imageSecondsProblem(draft.imageSeconds) : undefined;
|
|
133
|
+
if (imageProblem !== undefined)
|
|
134
|
+
fields.push({ field: "imageSeconds", message: imageProblem });
|
|
135
|
+
const zoomProblem = sources.video === "generate" ? zoomPercentProblem(draft.zoomPercent) : undefined;
|
|
136
|
+
if (zoomProblem !== undefined)
|
|
137
|
+
fields.push({ field: "zoomPercent", message: zoomProblem });
|
|
138
|
+
const edgeProblem = sources.audio === "off" ? undefined : edgeSilenceSecondsProblem(draft.edgeSilenceSeconds);
|
|
139
|
+
if (edgeProblem !== undefined)
|
|
140
|
+
fields.push({ field: "edgeSilenceSeconds", message: edgeProblem });
|
|
89
141
|
return fields.length === 0 ? { ok: true, draft } : { ok: false, fields };
|
|
90
142
|
}
|
|
91
143
|
// Research only feeds article writing, so a provided article hides it.
|
|
@@ -121,7 +173,10 @@ export function normaliseDraft(draft) {
|
|
|
121
173
|
function checkImagePrompts(draft, fields) {
|
|
122
174
|
if (draft.imagePrompts.length === 0) {
|
|
123
175
|
// A run always has an image source.
|
|
124
|
-
fields.push({
|
|
176
|
+
fields.push({
|
|
177
|
+
field: "imagePrompts",
|
|
178
|
+
message: "Tick at least one image prompt, or turn images off.",
|
|
179
|
+
});
|
|
125
180
|
return;
|
|
126
181
|
}
|
|
127
182
|
let total = 0;
|
|
@@ -135,14 +190,14 @@ function checkImagePrompts(draft, fields) {
|
|
|
135
190
|
prompt.number > numberPerPromptMax) {
|
|
136
191
|
fields.push({
|
|
137
192
|
field: `imagePrompts.${index}.number`,
|
|
138
|
-
message: `
|
|
193
|
+
message: `Enter a whole number between 1 and ${numberPerPromptMax}.`,
|
|
139
194
|
});
|
|
140
195
|
}
|
|
141
196
|
}
|
|
142
197
|
if (total > imagesPerRunMax) {
|
|
143
198
|
fields.push({
|
|
144
199
|
field: "imagePrompts",
|
|
145
|
-
message: `A
|
|
200
|
+
message: `A video can have at most ${imagesPerRunMax} images; these prompts ask for ${total}. Lower the numbers.`,
|
|
146
201
|
});
|
|
147
202
|
}
|
|
148
203
|
}
|
|
@@ -168,11 +223,14 @@ function checkProvided(draft, staged, fields) {
|
|
|
168
223
|
else if (ids.length > imagesPerRunMax) {
|
|
169
224
|
fields.push({
|
|
170
225
|
field: "provided.images",
|
|
171
|
-
message: `A
|
|
226
|
+
message: `A video can have at most ${imagesPerRunMax} images. Remove some.`,
|
|
172
227
|
});
|
|
173
228
|
}
|
|
174
229
|
if (new Set(ids).size !== ids.length) {
|
|
175
|
-
fields.push({
|
|
230
|
+
fields.push({
|
|
231
|
+
field: "provided.images",
|
|
232
|
+
message: "The same image is picked twice. Remove the duplicate.",
|
|
233
|
+
});
|
|
176
234
|
}
|
|
177
235
|
for (const [index, id] of ids.entries()) {
|
|
178
236
|
checkFile(staged, id, "images", `provided.images.${index}`, "Pick an image.", fields);
|
|
@@ -187,28 +245,31 @@ function checkFile(staged, id, kind, field, missing, fields) {
|
|
|
187
245
|
}
|
|
188
246
|
const file = staged.find((candidate) => candidate.id === id);
|
|
189
247
|
if (file === undefined || file.stageKind !== kind) {
|
|
190
|
-
fields.push({ field, message: "That upload is no longer available
|
|
248
|
+
fields.push({ field, message: "That upload is no longer available. Choose the file again." });
|
|
191
249
|
return;
|
|
192
250
|
}
|
|
193
251
|
if (file.state !== "staged") {
|
|
194
|
-
fields.push({ field, message: "This
|
|
252
|
+
fields.push({ field, message: "This file is still uploading. Wait for it to finish." });
|
|
195
253
|
}
|
|
196
254
|
}
|
|
197
255
|
function checkValues(draft, requiredSlots, fields) {
|
|
198
256
|
for (const name of requiredSlots) {
|
|
199
257
|
const value = Object.hasOwn(draft.values, name) ? draft.values[name] : undefined;
|
|
200
258
|
if (value === undefined || value === "") {
|
|
201
|
-
fields.push({ field: `values.${name}`, message: "
|
|
259
|
+
fields.push({ field: `values.${name}`, message: "Fill in this field." });
|
|
202
260
|
continue;
|
|
203
261
|
}
|
|
204
262
|
if (value.length > valueMax) {
|
|
205
263
|
fields.push({
|
|
206
264
|
field: `values.${name}`,
|
|
207
|
-
message: `
|
|
265
|
+
message: `Keep this to ${valueMax} characters or fewer.`,
|
|
208
266
|
});
|
|
209
267
|
}
|
|
210
268
|
if (/[\n\r]/.test(value)) {
|
|
211
|
-
fields.push({
|
|
269
|
+
fields.push({
|
|
270
|
+
field: `values.${name}`,
|
|
271
|
+
message: "Keep this to a single line, without line breaks.",
|
|
272
|
+
});
|
|
212
273
|
}
|
|
213
274
|
}
|
|
214
275
|
}
|
|
@@ -224,7 +285,22 @@ export function narrationPreparationFields(draft) {
|
|
|
224
285
|
? [
|
|
225
286
|
{
|
|
226
287
|
field: "narrationPrompt",
|
|
227
|
-
message: "Narration
|
|
288
|
+
message: "Narration preparation only works with the Inworld TTS-2 model. Choose that model under Narration, or turn preparation Off.",
|
|
289
|
+
},
|
|
290
|
+
]
|
|
291
|
+
: [];
|
|
292
|
+
}
|
|
293
|
+
// The YouTube description is written from the narration's word timings, so it needs
|
|
294
|
+
// narration; it runs whether the video renders or only the WAV is exported.
|
|
295
|
+
export function usesYoutubeDescription(draft) {
|
|
296
|
+
return draft.youtubeDescription === true && draft.sources.audio !== "off";
|
|
297
|
+
}
|
|
298
|
+
export function youtubeDescriptionFields(draft) {
|
|
299
|
+
return draft.youtubeDescription === true && draft.sources.audio === "off"
|
|
300
|
+
? [
|
|
301
|
+
{
|
|
302
|
+
field: "youtubeDescription",
|
|
303
|
+
message: "The YouTube description is timed from the narration. Turn narration on, or turn the YouTube description off.",
|
|
228
304
|
},
|
|
229
305
|
]
|
|
230
306
|
: [];
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { thinkingModes } from "../../kernel/ports/llm.js";
|
|
3
3
|
import { checkpointStageSchema } from "../checkpoints/schema.js";
|
|
4
|
+
import { documentThemes } from "../document/model.js";
|
|
4
5
|
import { chunkModes } from "../narration/chunk.js";
|
|
5
6
|
import { subtitleConfigSchema } from "../subtitles/model.js";
|
|
6
|
-
import { entryModes, formats, stageSources } from "./model.js";
|
|
7
|
+
import { entryModes, formats, motionStyles, stageSources } from "./model.js";
|
|
8
|
+
import { defaultEdgeSilenceSeconds, defaultImageSeconds, defaultMotionStyle, defaultZoomPercent, } from "./rules.js";
|
|
7
9
|
const providerChoice = z.object({
|
|
8
10
|
provider: z.string(),
|
|
9
11
|
model: z.string(),
|
|
@@ -21,8 +23,9 @@ export const runDraftSchema = z.object({
|
|
|
21
23
|
.optional(),
|
|
22
24
|
title: z.string(),
|
|
23
25
|
format: z.enum(formats),
|
|
24
|
-
// Spelled out rather than built from stageKinds, so the inferred type carries the
|
|
25
|
-
// keys and this schema stays assignable to the domain type without a cast.
|
|
26
|
+
// Spelled out rather than built from stageKinds, so the inferred type carries the
|
|
27
|
+
// keys and this schema stays assignable to the domain type without a cast. Document is
|
|
28
|
+
// optional: everything saved before it existed has no such key, and absent means Off.
|
|
26
29
|
sources: z.object({
|
|
27
30
|
research: z.enum(stageSources),
|
|
28
31
|
article: z.enum(stageSources),
|
|
@@ -30,6 +33,7 @@ export const runDraftSchema = z.object({
|
|
|
30
33
|
images: z.enum(stageSources),
|
|
31
34
|
thumbnail: z.enum(stageSources),
|
|
32
35
|
video: z.enum(stageSources),
|
|
36
|
+
document: z.enum(stageSources).optional(),
|
|
33
37
|
}),
|
|
34
38
|
llm: providerChoice.optional(),
|
|
35
39
|
audio: providerChoice
|
|
@@ -60,7 +64,16 @@ export const runDraftSchema = z.object({
|
|
|
60
64
|
})
|
|
61
65
|
.optional(),
|
|
62
66
|
silenceGapSeconds: z.number(),
|
|
67
|
+
// Defaulted rather than required, so a config, revision, draft review or backup saved
|
|
68
|
+
// before these existed still parses. The range is `rules.ts`'s, not the schema's.
|
|
69
|
+
imageSeconds: z.number().default(defaultImageSeconds),
|
|
70
|
+
zoomPercent: z.number().default(defaultZoomPercent),
|
|
71
|
+
motionStyle: z.enum(motionStyles).default(defaultMotionStyle),
|
|
72
|
+
edgeSilenceSeconds: z.number().default(defaultEdgeSilenceSeconds),
|
|
63
73
|
subtitles: subtitleConfigSchema.optional(),
|
|
74
|
+
document: z.object({ theme: z.enum(documentThemes) }).optional(),
|
|
75
|
+
youtubeDescription: z.boolean().optional(),
|
|
76
|
+
descriptionPrompt: z.string().optional(),
|
|
64
77
|
});
|
|
65
78
|
export const runConfigSchema = runDraftSchema.extend({
|
|
66
79
|
rendered: z.record(z.string(), z.string()),
|
|
@@ -5,6 +5,7 @@ import { admitInitialRevision } from "../rebuild/runtime-admission.js";
|
|
|
5
5
|
import { adoptBaseline } from "../revisions/adopt.js";
|
|
6
6
|
import { attachStagedFile, storeText } from "../storage/staging.js";
|
|
7
7
|
import { releaseStagedFile } from "../storage/staging-refs.js";
|
|
8
|
+
import { sourceOf, } from "./model.js";
|
|
8
9
|
import { insertProject, insertStage } from "./repo.js";
|
|
9
10
|
// Provide → `provided` with its output attached; Off → `skipped`;
|
|
10
11
|
// everything else → `pending`.
|
|
@@ -32,10 +33,10 @@ export function startRun(deps, draft, rendered, retainStaged = false, templates)
|
|
|
32
33
|
id: deps.ids.next(),
|
|
33
34
|
projectId: id,
|
|
34
35
|
kind,
|
|
35
|
-
source: draft.sources
|
|
36
|
+
source: sourceOf(draft.sources, kind),
|
|
36
37
|
state: kind === "video" && draft.sources.video === "off" && draft.sources.audio !== "off"
|
|
37
38
|
? "pending"
|
|
38
|
-
: initialState(draft.sources
|
|
39
|
+
: initialState(sourceOf(draft.sources, kind)),
|
|
39
40
|
failureReason: null,
|
|
40
41
|
attemptCount: 0,
|
|
41
42
|
progressCurrent: null,
|
|
@@ -81,7 +81,9 @@ function truncated(answer) {
|
|
|
81
81
|
}
|
|
82
82
|
// Empty response → failed attempt.
|
|
83
83
|
function written(answer) {
|
|
84
|
-
return answer.text.trim() === ""
|
|
84
|
+
return answer.text.trim() === ""
|
|
85
|
+
? "The AI model returned an empty article. Retry stage; if it keeps happening, choose a different model in Edit project → Providers."
|
|
86
|
+
: undefined;
|
|
85
87
|
}
|
|
86
88
|
function finished(answer) {
|
|
87
89
|
const empty = written(answer);
|
|
@@ -89,6 +91,6 @@ function finished(answer) {
|
|
|
89
91
|
return empty;
|
|
90
92
|
}
|
|
91
93
|
return truncated(answer)
|
|
92
|
-
? `
|
|
94
|
+
? `The article was still unfinished after ${String(continuationLimit)} continuations because the AI model kept stopping at its length limit. Ask for a shorter article in Edit project → Prompts, or choose a model with a larger output limit in Edit project → Providers, then Retry stage.`
|
|
93
95
|
: undefined;
|
|
94
96
|
}
|
|
@@ -7,11 +7,11 @@ export function plainText(markdown) {
|
|
|
7
7
|
const paragraphs = tree.children
|
|
8
8
|
.map((node) => {
|
|
9
9
|
if (node.type !== "paragraph")
|
|
10
|
-
throw new Error("
|
|
10
|
+
throw new Error("Slopify hit an internal error (the article couldn't be turned into plain text). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
11
11
|
return node.children
|
|
12
12
|
.map((child) => {
|
|
13
13
|
if (child.type !== "text")
|
|
14
|
-
throw new Error("
|
|
14
|
+
throw new Error("Slopify hit an internal error (the article couldn't be turned into plain text). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
15
15
|
return child.value;
|
|
16
16
|
})
|
|
17
17
|
.join("");
|