@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// the picked templates, the slot names they ask for, and the rendered text stored on the
|
|
3
3
|
// project. The bodies are read at the click, not at selection, so an edit made in between
|
|
4
4
|
// is the one that runs.
|
|
5
|
-
import { normaliseDraft, usesNarrationPreparation } from "../admission/rules.js";
|
|
5
|
+
import { normaliseDraft, usesNarrationPreparation, usesYoutubeDescription, } from "../admission/rules.js";
|
|
6
6
|
import { collectFields, render } from "../admission/substitute.js";
|
|
7
7
|
import { snapshotEntry, snapshotPrompt } from "./snapshot.js";
|
|
8
8
|
export function pickTemplates(db, input, snapshot) {
|
|
@@ -22,6 +22,9 @@ export function pickTemplates(db, input, snapshot) {
|
|
|
22
22
|
const outro = pickEntry(db, "outro", draft.outro, missing, snapshot);
|
|
23
23
|
push(text, "intro", intro);
|
|
24
24
|
push(text, "outro", outro);
|
|
25
|
+
// None picked is the built-in prompt, which asks for no keyword.
|
|
26
|
+
if (usesYoutubeDescription(draft))
|
|
27
|
+
body(db, "description", draft.descriptionPrompt, "descriptionPrompt", missing, text, "description", snapshot);
|
|
25
28
|
if (sources.images === "generate") {
|
|
26
29
|
for (const [index, picked] of draft.imagePrompts.entries()) {
|
|
27
30
|
const field = `imagePrompts.${String(index)}.name`;
|
|
@@ -58,7 +61,7 @@ function body(db, kind, name, field, missing, into, key, snapshot) {
|
|
|
58
61
|
}
|
|
59
62
|
const prompt = snapshotPrompt(db, snapshot, kind, name);
|
|
60
63
|
if (prompt === undefined) {
|
|
61
|
-
missing.push({ field, message: `That ${kind} prompt
|
|
64
|
+
missing.push({ field, message: `That ${kind} prompt was deleted. Choose another.` });
|
|
62
65
|
return;
|
|
63
66
|
}
|
|
64
67
|
into.push({ key, body: prompt.body });
|
|
@@ -71,7 +74,7 @@ function pickEntry(db, category, choice, missing, snapshot) {
|
|
|
71
74
|
if (entry === undefined) {
|
|
72
75
|
missing.push({
|
|
73
76
|
field: category,
|
|
74
|
-
message: `That ${category} entry
|
|
77
|
+
message: `That ${category} entry was deleted. Choose another.`,
|
|
75
78
|
});
|
|
76
79
|
}
|
|
77
80
|
return entry;
|
|
@@ -46,7 +46,7 @@ export async function joinNarration(deps, input) {
|
|
|
46
46
|
const first = input.files[0];
|
|
47
47
|
if (first === undefined) {
|
|
48
48
|
// The stage refuses an empty narration before it gets here, so this is a bug.
|
|
49
|
-
throw new Error("there
|
|
49
|
+
throw new Error("Slopify hit an internal error (there is no narration audio to join). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
50
50
|
}
|
|
51
51
|
mkdirSync(dirname(input.output), { recursive: true, mode: 0o700 });
|
|
52
52
|
if (input.files.length === 1) {
|
|
@@ -18,7 +18,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
|
|
|
18
18
|
const chunks = chunkNarration(source, chunking).map((text) => {
|
|
19
19
|
const start = source.indexOf(text, offset);
|
|
20
20
|
if (start < 0)
|
|
21
|
-
throw new Error("
|
|
21
|
+
throw new Error("Slopify hit an internal error (a narration chunk isn't in the article text). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
22
22
|
offset = start + text.length;
|
|
23
23
|
return { key: nextKey(text), text, start, end: offset };
|
|
24
24
|
});
|
|
@@ -78,7 +78,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
|
|
|
78
78
|
}
|
|
79
79
|
const chunk = chunks[index];
|
|
80
80
|
if (chunk === undefined)
|
|
81
|
-
throw new Error("
|
|
81
|
+
throw new Error("Slopify hit an internal error (a narration chunk went missing). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
82
82
|
while ((matches[matchIndex]?.end ?? Infinity) <= chunk.end)
|
|
83
83
|
matchIndex++;
|
|
84
84
|
const match = matches[matchIndex];
|
|
@@ -90,7 +90,7 @@ export function pronunciationChunks(source, chunking, entries, overriddenKeys =
|
|
|
90
90
|
continue;
|
|
91
91
|
const first = chunks[from];
|
|
92
92
|
if (first === undefined)
|
|
93
|
-
throw new Error("
|
|
93
|
+
throw new Error("Slopify hit an internal error (a group of narration chunks is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
94
94
|
const text = source.slice(first.start, chunk.end);
|
|
95
95
|
result.push(from === index
|
|
96
96
|
? { key: chunk.key, text }
|
|
@@ -105,7 +105,7 @@ export function pronunciationMatches(source, entries) {
|
|
|
105
105
|
continue;
|
|
106
106
|
const entry = ordered.find((_, index) => match[index + 1] !== undefined);
|
|
107
107
|
if (entry === undefined)
|
|
108
|
-
throw new Error("
|
|
108
|
+
throw new Error("Slopify hit an internal error (a pronunciation glossary term has no pronunciation). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
109
109
|
matches.push({ start: match.index, end, entry });
|
|
110
110
|
}
|
|
111
111
|
return matches;
|
|
@@ -117,7 +117,7 @@ export function pronunciationSpans(source, entries) {
|
|
|
117
117
|
for (const [index, word] of Array.from(words).entries()) {
|
|
118
118
|
const ipa = match.entry.ipa[index];
|
|
119
119
|
if (ipa === undefined)
|
|
120
|
-
throw new Error("
|
|
120
|
+
throw new Error("Slopify hit an internal error (a pronunciation glossary term is missing a word). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
121
121
|
const start = match.start + word.index;
|
|
122
122
|
spans.push({ start, end: start + word[0].length, text: `/${ipa}/` });
|
|
123
123
|
}
|
|
@@ -14,7 +14,7 @@ import { planLegacyNarration } from "./legacy-plan.js";
|
|
|
14
14
|
import { observeNarration } from "./live.js";
|
|
15
15
|
// An empty narration source fails the stage immediately with no retries. Thrown from the
|
|
16
16
|
// slice rather than from a provider call, so the attempt wrapper never sees it.
|
|
17
|
-
export const nothingToNarrate = "nothing to narrate";
|
|
17
|
+
export const nothingToNarrate = "There is nothing to narrate because the article is empty. Add article text in Edit project → Article, then Retry stage.";
|
|
18
18
|
// What a chunk piece carries between runs: the text that was sent, and the file it came
|
|
19
19
|
// back as once the provider answered. That is the whole of the resume.
|
|
20
20
|
const chunkPayload = z.object({ text: z.string(), file: z.string().optional() });
|
|
@@ -35,13 +35,13 @@ export async function runNarration(deps, context, providers) {
|
|
|
35
35
|
deps.audioPreviews?.clear(projectId);
|
|
36
36
|
const project = projectById(deps.db, projectId);
|
|
37
37
|
if (project === undefined) {
|
|
38
|
-
throw new Error(
|
|
38
|
+
throw new Error("This project no longer exists; it may have been deleted while it was running.");
|
|
39
39
|
}
|
|
40
40
|
const choice = project.config.audio;
|
|
41
41
|
if (choice === undefined) {
|
|
42
42
|
// Admission refuses a run whose audio is Generate without a provider and a voice, so
|
|
43
43
|
// reaching here is a bug in admission rather than something the user did.
|
|
44
|
-
throw new Error("the
|
|
44
|
+
throw new Error("No voice is set for the narration. Choose a voice in Edit project → Providers, then Retry stage.");
|
|
45
45
|
}
|
|
46
46
|
// Read once, before anything is written: the segment steps below ask it what a previous
|
|
47
47
|
// run already stored, and the body step is the only thing that adds to it.
|
|
@@ -168,7 +168,7 @@ async function speakSegments(deps, context, providers, choice, outputs) {
|
|
|
168
168
|
if (text === "") {
|
|
169
169
|
// The same rule, applied to a segment: there is nothing to say, and silently
|
|
170
170
|
// dropping an intro the user picked would lose it without telling anyone.
|
|
171
|
-
throw new Error(`
|
|
171
|
+
throw new Error(`The ${segment.category} is empty, so there is nothing to narrate for it. Write the ${segment.category} or remove it in Edit project, then Retry stage.`);
|
|
172
172
|
}
|
|
173
173
|
const spoken = await providers.forPiece(piece.id).tts({
|
|
174
174
|
provider: choice.provider,
|
|
@@ -200,7 +200,7 @@ function segmentPieces(deps, projectId) {
|
|
|
200
200
|
if (article === undefined) {
|
|
201
201
|
// Admission writes all six stage rows with the project, so a project without an article
|
|
202
202
|
// stage is a bug rather than a run the user configured.
|
|
203
|
-
throw new Error(
|
|
203
|
+
throw new Error("Slopify hit an internal error (the project has no article stage). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
204
204
|
}
|
|
205
205
|
return piecesOf(deps.db, article.id, "segment");
|
|
206
206
|
}
|
|
@@ -257,13 +257,13 @@ function store(deps, projectId, role, path, durationMs, choice) {
|
|
|
257
257
|
}
|
|
258
258
|
function chunkOf(piece) {
|
|
259
259
|
if (piece.payload === null) {
|
|
260
|
-
throw new Error(
|
|
260
|
+
throw new Error("Slopify hit an internal error (a saved narration chunk is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
261
261
|
}
|
|
262
262
|
return chunkPayload.parse(JSON.parse(piece.payload));
|
|
263
263
|
}
|
|
264
264
|
function segmentOf(piece) {
|
|
265
265
|
if (piece.payload === null) {
|
|
266
|
-
throw new Error(
|
|
266
|
+
throw new Error("Slopify hit an internal error (a saved intro or outro is empty). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
267
267
|
}
|
|
268
268
|
return segmentPayload.parse(JSON.parse(piece.payload));
|
|
269
269
|
}
|
|
@@ -9,14 +9,14 @@ function atoms(source, from, to, spans) {
|
|
|
9
9
|
const span = spans.get(at);
|
|
10
10
|
if (span !== undefined) {
|
|
11
11
|
if (span.end > to || span.end <= at)
|
|
12
|
-
throw new Error("
|
|
12
|
+
throw new Error("Slopify hit an internal error (a pronunciation fix spans more than one word). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
13
13
|
result.push({ text: span.text, spokenText: source.slice(at, span.end) });
|
|
14
14
|
at = span.end;
|
|
15
15
|
}
|
|
16
16
|
else {
|
|
17
17
|
const point = source.codePointAt(at);
|
|
18
18
|
if (point === undefined)
|
|
19
|
-
throw new Error("
|
|
19
|
+
throw new Error("Slopify hit an internal error (a narration position is outside the article text). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
20
20
|
const text = String.fromCodePoint(point);
|
|
21
21
|
result.push({ text, spokenText: text });
|
|
22
22
|
at += text.length;
|
|
@@ -34,7 +34,7 @@ function checkedSpans(source, pronunciation) {
|
|
|
34
34
|
span.end <= span.start ||
|
|
35
35
|
span.end > source.length ||
|
|
36
36
|
!/^[^\s\uD800-\uDFFF]+$/u.test(source.slice(span.start, span.end)))
|
|
37
|
-
throw new Error("
|
|
37
|
+
throw new Error("Slopify hit an internal error (pronunciation fixes overlap or split a word). Retry stage; if it happens again, use Download diagnostics in Settings and report it.");
|
|
38
38
|
spans.set(span.start, span);
|
|
39
39
|
end = span.end;
|
|
40
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { numberPerPromptMax } from "../admission/rules.js";
|
|
2
|
+
import { defaultEdgeSilenceSeconds, defaultImageSeconds, defaultZoomPercent, edgeSilenceSecondsProblem, imageSecondsProblem, numberPerPromptMax, zoomPercentProblem, } from "../admission/rules.js";
|
|
3
3
|
import { runDraftSchema } from "../admission/schema.js";
|
|
4
|
+
import { documentThemeOf } from "../document/model.js";
|
|
4
5
|
import { defaultSubtitles } from "../subtitles/model.js";
|
|
5
6
|
export function toAdmissionDraft(input) {
|
|
6
7
|
const { form } = input.document;
|
|
@@ -22,6 +23,18 @@ export function toAdmissionDraft(input) {
|
|
|
22
23
|
fields.push({ field, message: `Enter a whole number between ${min} and ${max}.` });
|
|
23
24
|
return min;
|
|
24
25
|
};
|
|
26
|
+
// A value the run uses is refused in the rule's words; one it ignores (the control is
|
|
27
|
+
// hidden) falls back to the default rather than holding up the run.
|
|
28
|
+
const measure = (field, used, fallback, problem) => {
|
|
29
|
+
const raw = form[field].trim();
|
|
30
|
+
const value = raw === "" ? Number.NaN : Number(raw);
|
|
31
|
+
const refused = problem(value);
|
|
32
|
+
if (refused === undefined)
|
|
33
|
+
return value;
|
|
34
|
+
if (used)
|
|
35
|
+
fields.push({ field, message: refused });
|
|
36
|
+
return fallback;
|
|
37
|
+
};
|
|
25
38
|
const entry = (category) => {
|
|
26
39
|
const name = form[category];
|
|
27
40
|
if (sources.audio !== "generate" || name === "")
|
|
@@ -31,7 +44,7 @@ export function toAdmissionDraft(input) {
|
|
|
31
44
|
return { name: saved.name, mode: saved.mode };
|
|
32
45
|
fields.push({
|
|
33
46
|
field: category,
|
|
34
|
-
message: `That ${category} entry
|
|
47
|
+
message: `That ${category} entry was deleted. Choose another.`,
|
|
35
48
|
});
|
|
36
49
|
return undefined;
|
|
37
50
|
};
|
|
@@ -42,8 +55,8 @@ export function toAdmissionDraft(input) {
|
|
|
42
55
|
fields.push({
|
|
43
56
|
field,
|
|
44
57
|
message: attachment?.state === "copying" || attachment?.state === "pending"
|
|
45
|
-
? "This
|
|
46
|
-
: "That upload is no longer available
|
|
58
|
+
? "This file is still uploading. Wait for it to finish."
|
|
59
|
+
: "That upload is no longer available. Choose the file again.",
|
|
47
60
|
});
|
|
48
61
|
return undefined;
|
|
49
62
|
};
|
|
@@ -58,6 +71,9 @@ export function toAdmissionDraft(input) {
|
|
|
58
71
|
title: form.title,
|
|
59
72
|
format: form.format,
|
|
60
73
|
sources,
|
|
74
|
+
...(sources.document === "generate"
|
|
75
|
+
? { document: { theme: documentThemeOf(form.document) } }
|
|
76
|
+
: {}),
|
|
61
77
|
llm: form.llm,
|
|
62
78
|
audio: sources.audio === "generate" || form.audio.usePronunciationGlossary !== undefined
|
|
63
79
|
? form.audio
|
|
@@ -67,6 +83,13 @@ export function toAdmissionDraft(input) {
|
|
|
67
83
|
...(sources.audio === "generate" && form.narrationPrompt?.trim()
|
|
68
84
|
? { narrationPrompt: form.narrationPrompt }
|
|
69
85
|
: {}),
|
|
86
|
+
// Timed from the narration, so a switch left on with narration Off asks for nothing.
|
|
87
|
+
...(sources.audio !== "off" && form.youtubeDescription === true
|
|
88
|
+
? {
|
|
89
|
+
youtubeDescription: true,
|
|
90
|
+
...(form.descriptionPrompt?.trim() ? { descriptionPrompt: form.descriptionPrompt } : {}),
|
|
91
|
+
}
|
|
92
|
+
: {}),
|
|
70
93
|
imagePrompts: sources.images === "generate"
|
|
71
94
|
? form.imagePrompts.map((prompt, index) => ({
|
|
72
95
|
name: prompt.name,
|
|
@@ -112,12 +135,19 @@ export function toAdmissionDraft(input) {
|
|
|
112
135
|
fontSize: number(form.subtitles.fontSize, "subtitles.fontSize", 120, 16),
|
|
113
136
|
},
|
|
114
137
|
silenceGapSeconds: input.silenceGapSeconds,
|
|
138
|
+
imageSeconds: measure("imageSeconds", sources.video === "generate", defaultImageSeconds, imageSecondsProblem),
|
|
139
|
+
zoomPercent: measure("zoomPercent", sources.video === "generate", defaultZoomPercent, zoomPercentProblem),
|
|
140
|
+
// A choice from a list, so there is nothing to refuse; a run without a video keeps it
|
|
141
|
+
// for when the video is turned back on.
|
|
142
|
+
motionStyle: form.motionStyle,
|
|
143
|
+
edgeSilenceSeconds: measure("edgeSilenceSeconds", sources.audio !== "off", defaultEdgeSilenceSeconds, edgeSilenceSecondsProblem),
|
|
115
144
|
};
|
|
116
145
|
const parsed = runDraftSchema.safeParse(draft);
|
|
117
146
|
if (!parsed.success)
|
|
118
147
|
fields.push(...parsed.error.issues.map((issue) => ({
|
|
119
148
|
field: issue.path.join("."),
|
|
120
|
-
|
|
149
|
+
// The schema carries no sentences of its own; its defaults read like code.
|
|
150
|
+
message: "This setting is not valid. Reload the page and choose it again.",
|
|
121
151
|
})));
|
|
122
152
|
return fields.length || !parsed.success
|
|
123
153
|
? { ok: false, fields }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { checkRuntimeModel } from "../../catalog/runtime-models.js";
|
|
2
2
|
import { modelFields } from "../../catalog/validate.js";
|
|
3
3
|
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
4
|
-
import { usesNarrationPreparation } from "../admission/rules.js";
|
|
4
|
+
import { usesNarrationPreparation, usesYoutubeDescription, } from "../admission/rules.js";
|
|
5
5
|
import { cliPathChanged } from "../settings/cli-paths.js";
|
|
6
6
|
import { hasKey, listVoices } from "../settings/repo.js";
|
|
7
7
|
export function choices(runs) {
|
|
@@ -16,7 +16,8 @@ export function choices(runs) {
|
|
|
16
16
|
d.sources.thumbnail === "prompt_by_llm" ||
|
|
17
17
|
d.intro?.mode === "llm" ||
|
|
18
18
|
d.outro?.mode === "llm" ||
|
|
19
|
-
usesNarrationPreparation(d)
|
|
19
|
+
usesNarrationPreparation(d) ||
|
|
20
|
+
usesYoutubeDescription(d),
|
|
20
21
|
},
|
|
21
22
|
{
|
|
22
23
|
field: "audio",
|
|
@@ -48,7 +49,7 @@ export async function checkDraftReadiness(deps, runs) {
|
|
|
48
49
|
field: c.field,
|
|
49
50
|
message: provider?.readiness.kind === "cli" && provider.readiness.issue
|
|
50
51
|
? provider.readiness.issue
|
|
51
|
-
: "
|
|
52
|
+
: "Set up this provider in Settings → Providers before starting.",
|
|
52
53
|
});
|
|
53
54
|
continue;
|
|
54
55
|
}
|
|
@@ -56,12 +57,12 @@ export async function checkDraftReadiness(deps, runs) {
|
|
|
56
57
|
if (result === "missing")
|
|
57
58
|
fields.push({
|
|
58
59
|
field: `${c.field}.model`,
|
|
59
|
-
message: "
|
|
60
|
+
message: "This model is no longer available. Choose another model.",
|
|
60
61
|
});
|
|
61
62
|
else if (result === "thinking")
|
|
62
63
|
fields.push({
|
|
63
64
|
field: `${c.field}.thinking`,
|
|
64
|
-
message: "
|
|
65
|
+
message: "This model does not support that thinking setting. Choose another.",
|
|
65
66
|
});
|
|
66
67
|
}
|
|
67
68
|
return { fields: [...fields, ...localDraftReadiness(deps, runs, providers)], providers };
|
|
@@ -73,15 +74,21 @@ export function localDraftReadiness(deps, runs, providers) {
|
|
|
73
74
|
if (cliPathChanged(deps.db, provider))
|
|
74
75
|
fields.push({
|
|
75
76
|
field: `${c.field}.cliPath`,
|
|
76
|
-
message: "
|
|
77
|
+
message: "This provider's command-line tool changed in Settings → Providers. Choose Review and start again.",
|
|
77
78
|
});
|
|
78
79
|
if (provider?.readiness.kind === "keyed" && !hasKey(deps.db, provider.id))
|
|
79
|
-
fields.push({
|
|
80
|
+
fields.push({
|
|
81
|
+
field: c.field,
|
|
82
|
+
message: "Add this provider's API key in Settings → Providers before starting.",
|
|
83
|
+
});
|
|
80
84
|
}
|
|
81
85
|
for (const { draft } of runs)
|
|
82
86
|
if (draft.sources.audio === "generate" &&
|
|
83
87
|
draft.audio &&
|
|
84
88
|
!listVoices(deps.db).some((v) => v.provider === draft.audio?.provider && v.voiceId === draft.audio.voice))
|
|
85
|
-
fields.push({
|
|
89
|
+
fields.push({
|
|
90
|
+
field: "audio.voice",
|
|
91
|
+
message: "Choose one of your saved voices. Add voices in Settings → Voices.",
|
|
92
|
+
});
|
|
86
93
|
return fields;
|
|
87
94
|
}
|
|
@@ -47,7 +47,10 @@ export function resolveReviewInputs(deps, view, font) {
|
|
|
47
47
|
if (!words.success)
|
|
48
48
|
fields.push({ field: "expectedWords", message: "Enter a whole number between 1 and 100000." });
|
|
49
49
|
if (document.variants.length > 49)
|
|
50
|
-
fields.push({
|
|
50
|
+
fields.push({
|
|
51
|
+
field: "variants",
|
|
52
|
+
message: "You can review at most 50 videos at once, counting the first one. Remove some variations.",
|
|
53
|
+
});
|
|
51
54
|
const converted = toAdmissionDraft({
|
|
52
55
|
document,
|
|
53
56
|
attachments: fresh.value.attachments,
|
|
@@ -96,7 +99,7 @@ export function resolveReviewInputs(deps, view, font) {
|
|
|
96
99
|
field: index > 0 && (issue.field === "title" || issue.field.startsWith("values."))
|
|
97
100
|
? `variants.${index - 1}.${issue.field}`
|
|
98
101
|
: issue.field,
|
|
99
|
-
message: `${index > 0 ? `
|
|
102
|
+
message: `${index > 0 ? `Video ${index + 1}: ` : ""}${issue.message}`,
|
|
100
103
|
})));
|
|
101
104
|
runs.push({
|
|
102
105
|
draft: accepted,
|
|
@@ -118,7 +121,7 @@ export function resolveReviewInputs(deps, view, font) {
|
|
|
118
121
|
if (!stat?.isFile() || font.id !== converted.draft.subtitles?.fontId)
|
|
119
122
|
fields.push({
|
|
120
123
|
field: "subtitles.fontId",
|
|
121
|
-
message: "
|
|
124
|
+
message: "This font was deleted. Choose another font.",
|
|
122
125
|
});
|
|
123
126
|
else
|
|
124
127
|
fontHash = createHash("sha256").update(readFileSync(font.path)).digest("hex");
|
|
@@ -133,7 +136,7 @@ export function resolveReviewInputs(deps, view, font) {
|
|
|
133
136
|
: run.draft.sources[stage] !== "generate")
|
|
134
137
|
fields.push({
|
|
135
138
|
field: `checkpoints.${stage}`,
|
|
136
|
-
message: "
|
|
139
|
+
message: "Checkpoints can only go before a step Slopify generates, or before export. Remove this checkpoint or turn that step on.",
|
|
137
140
|
});
|
|
138
141
|
if (fields.length)
|
|
139
142
|
return reviewRefusal(view, fields);
|
|
@@ -27,7 +27,7 @@ export async function resolvePlayReview(deps, view) {
|
|
|
27
27
|
return reviewRefusal(view, [
|
|
28
28
|
{
|
|
29
29
|
field: "subtitles.fontId",
|
|
30
|
-
message: "
|
|
30
|
+
message: "This font was deleted. Choose another font.",
|
|
31
31
|
},
|
|
32
32
|
]);
|
|
33
33
|
}
|
|
@@ -41,12 +41,12 @@ export async function resolvePlayReview(deps, view) {
|
|
|
41
41
|
if (result === "missing")
|
|
42
42
|
fields.push({
|
|
43
43
|
field: `${choice.field}.model`,
|
|
44
|
-
message: "
|
|
44
|
+
message: "This model is no longer available. Choose another model.",
|
|
45
45
|
});
|
|
46
46
|
else if (result === "thinking")
|
|
47
47
|
fields.push({
|
|
48
48
|
field: `${choice.field}.thinking`,
|
|
49
|
-
message: "
|
|
49
|
+
message: "This model does not support that thinking setting. Choose another.",
|
|
50
50
|
});
|
|
51
51
|
}
|
|
52
52
|
if (fields.length)
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import { formats } from "../../kernel/pipeline.js";
|
|
2
|
+
import { formats, stageKinds } from "../../kernel/pipeline.js";
|
|
3
3
|
import { thinkingModes } from "../../kernel/ports/llm.js";
|
|
4
|
-
import { stageSources } from "../admission/model.js";
|
|
4
|
+
import { motionStyles, stageSources } from "../admission/model.js";
|
|
5
|
+
import { defaultEdgeSilenceSeconds, defaultImageSeconds, defaultMotionStyle, defaultZoomPercent, } from "../admission/rules.js";
|
|
5
6
|
import { runDraftSchema } from "../admission/schema.js";
|
|
6
7
|
import { checkpointRowSchema, checkpointStageSchema } from "../checkpoints/schema.js";
|
|
8
|
+
import { documentThemes } from "../document/model.js";
|
|
7
9
|
import { librarySnapshotSchema } from "../library/snapshot.js";
|
|
8
10
|
import { chunkModes } from "../narration/chunk.js";
|
|
9
11
|
import { subtitleModes, subtitlePositions } from "../subtitles/model.js";
|
|
@@ -27,9 +29,17 @@ export const playDraftFormSchema = z
|
|
|
27
29
|
images: z.enum(stageSources),
|
|
28
30
|
thumbnail: z.enum(stageSources),
|
|
29
31
|
video: z.enum(stageSources),
|
|
32
|
+
// Absent on drafts and templates saved before the Document stage: Off.
|
|
33
|
+
document: z.enum(stageSources).optional(),
|
|
30
34
|
})
|
|
31
35
|
.strict()
|
|
32
36
|
.readonly(),
|
|
37
|
+
// Absent until the theme is first chosen: the default theme.
|
|
38
|
+
document: z
|
|
39
|
+
.object({ theme: z.enum(documentThemes) })
|
|
40
|
+
.strict()
|
|
41
|
+
.readonly()
|
|
42
|
+
.optional(),
|
|
33
43
|
llm: provider.readonly(),
|
|
34
44
|
audio: provider
|
|
35
45
|
.extend({ voice: text, usePronunciationGlossary: z.boolean().optional() })
|
|
@@ -37,6 +47,9 @@ export const playDraftFormSchema = z
|
|
|
37
47
|
images: provider.readonly(),
|
|
38
48
|
articlePrompt: text,
|
|
39
49
|
narrationPrompt: text.optional(),
|
|
50
|
+
// Absent on drafts and templates saved before the YouTube description: off, built-in prompt.
|
|
51
|
+
youtubeDescription: z.boolean().optional(),
|
|
52
|
+
descriptionPrompt: text.optional(),
|
|
40
53
|
imagePrompts: z.array(z.object({ name: text, number: text }).strict().readonly()).readonly(),
|
|
41
54
|
thumbnailPrompt: text,
|
|
42
55
|
intro: text,
|
|
@@ -55,6 +68,12 @@ export const playDraftFormSchema = z
|
|
|
55
68
|
})
|
|
56
69
|
.strict()
|
|
57
70
|
.readonly(),
|
|
71
|
+
// Raw text, like every other number on Play, so what was typed survives a reload.
|
|
72
|
+
// Defaulted for drafts and templates saved before these controls existed.
|
|
73
|
+
imageSeconds: text.default(String(defaultImageSeconds)),
|
|
74
|
+
edgeSilenceSeconds: text.default(String(defaultEdgeSilenceSeconds)),
|
|
75
|
+
zoomPercent: text.default(String(defaultZoomPercent)),
|
|
76
|
+
motionStyle: z.enum(motionStyles).default(defaultMotionStyle),
|
|
58
77
|
values,
|
|
59
78
|
provided: z
|
|
60
79
|
.object({
|
|
@@ -183,9 +202,7 @@ export const playReviewSchema = z
|
|
|
183
202
|
stage: checkpointStageSchema,
|
|
184
203
|
fingerprint: text,
|
|
185
204
|
workKeys: z.array(text).readonly(),
|
|
186
|
-
dependents: z
|
|
187
|
-
.array(z.enum(["research", "article", "audio", "images", "thumbnail", "video"]))
|
|
188
|
-
.readonly(),
|
|
205
|
+
dependents: z.array(z.enum(stageKinds)).readonly(),
|
|
189
206
|
})
|
|
190
207
|
.strict()
|
|
191
208
|
.readonly())
|
|
@@ -68,7 +68,9 @@ function attachmentsOf(deps, id) {
|
|
|
68
68
|
state,
|
|
69
69
|
stagedFileId: invalid ? null : row.staged_file_id,
|
|
70
70
|
bytes: invalid ? 0 : (staged?.bytes ?? 0),
|
|
71
|
-
error: invalid
|
|
71
|
+
error: invalid
|
|
72
|
+
? "This file is missing or did not finish uploading. Attach it again."
|
|
73
|
+
: row.error,
|
|
72
74
|
};
|
|
73
75
|
});
|
|
74
76
|
}
|
|
@@ -43,7 +43,7 @@ export async function uploadDraftAttachment(deps, input) {
|
|
|
43
43
|
if (!result.ok) {
|
|
44
44
|
deps.db
|
|
45
45
|
.prepare("UPDATE play_draft_attachments SET status='reattach',error=? WHERE id=? AND draft_id=? AND staged_file_id IS NULL")
|
|
46
|
-
.run("
|
|
46
|
+
.run("The upload failed. Attach the file again.", input.attachmentId, input.draftId);
|
|
47
47
|
return refuse("invalid-edit");
|
|
48
48
|
}
|
|
49
49
|
const updated = deps.db
|
|
@@ -74,6 +74,7 @@ function documentFromProject(deps, revision) {
|
|
|
74
74
|
};
|
|
75
75
|
addPrompt("article", config.articlePrompt, "article");
|
|
76
76
|
addPrompt("narration", config.narrationPrompt, "narration");
|
|
77
|
+
addPrompt("description", config.descriptionPrompt, "description");
|
|
77
78
|
const definitions = revision.content.imageOrder.flatMap((key) => {
|
|
78
79
|
const definition = revision.content.imageDefinitions[key];
|
|
79
80
|
return definition === undefined ? [] : [definition];
|
|
@@ -127,12 +128,17 @@ function documentFromProject(deps, revision) {
|
|
|
127
128
|
title: config.title,
|
|
128
129
|
format: config.format,
|
|
129
130
|
sources: config.sources,
|
|
131
|
+
...(config.document === undefined ? {} : { document: config.document }),
|
|
130
132
|
checkpoints: listCheckpoints(deps.db, revision.projectId, revision.id).map((gate) => gate.stage),
|
|
131
133
|
llm: config.llm ?? { provider: "", model: "" },
|
|
132
134
|
audio: config.audio ?? { provider: "", model: "", voice: "" },
|
|
133
135
|
images: config.images ?? { provider: "", model: "" },
|
|
134
136
|
articlePrompt: config.articlePrompt ?? "",
|
|
135
137
|
...(config.narrationPrompt === undefined ? {} : { narrationPrompt: config.narrationPrompt }),
|
|
138
|
+
...(config.youtubeDescription === true ? { youtubeDescription: true } : {}),
|
|
139
|
+
...(config.descriptionPrompt === undefined
|
|
140
|
+
? {}
|
|
141
|
+
: { descriptionPrompt: config.descriptionPrompt }),
|
|
136
142
|
imagePrompts,
|
|
137
143
|
thumbnailPrompt: config.thumbnailPrompt ?? "",
|
|
138
144
|
intro: config.intro?.name ?? "",
|
|
@@ -149,6 +155,10 @@ function documentFromProject(deps, revision) {
|
|
|
149
155
|
fontSize: config.subtitles?.fontSize === undefined ? "48" : String(config.subtitles.fontSize),
|
|
150
156
|
position: config.subtitles?.position ?? "bottom",
|
|
151
157
|
},
|
|
158
|
+
imageSeconds: String(config.imageSeconds),
|
|
159
|
+
edgeSilenceSeconds: String(config.edgeSilenceSeconds),
|
|
160
|
+
zoomPercent: String(config.zoomPercent),
|
|
161
|
+
motionStyle: config.motionStyle,
|
|
152
162
|
values: config.values,
|
|
153
163
|
provided,
|
|
154
164
|
},
|
|
@@ -5,6 +5,7 @@ export function templateSetup(deps, input) {
|
|
|
5
5
|
const choices = [
|
|
6
6
|
{ kind: "article", name: input.form.articlePrompt },
|
|
7
7
|
{ kind: "narration", name: input.form.narrationPrompt ?? "" },
|
|
8
|
+
{ kind: "description", name: input.form.descriptionPrompt ?? "" },
|
|
8
9
|
...input.form.imagePrompts.map((row) => ({ kind: "image", name: row.name })),
|
|
9
10
|
{ kind: "thumbnail", name: input.form.thumbnailPrompt },
|
|
10
11
|
];
|
|
@@ -92,14 +92,14 @@ export function admitPreview(deps, input) {
|
|
|
92
92
|
continue;
|
|
93
93
|
}
|
|
94
94
|
if (existing?.state === "running")
|
|
95
|
-
throw new Error("
|
|
95
|
+
throw new Error("Slopify hit an internal error (a step is already running for a different version of the project). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
96
96
|
deps.db
|
|
97
97
|
.prepare("DELETE FROM revision_work_reservations WHERE revision_id=? AND work_key=?")
|
|
98
98
|
.run(view.revision.id, recipe.key);
|
|
99
99
|
const key = reservationKey(recipe, view);
|
|
100
100
|
const fingerprint = snapshot.anchors[key];
|
|
101
101
|
if (fingerprint === undefined)
|
|
102
|
-
throw new Error("
|
|
102
|
+
throw new Error("Slopify hit an internal error (a step has no saved starting point). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
103
103
|
const work = insertInvocation(deps, view, recipe, catalogue, { key, fingerprint }, false, view.revision.id, admissionId);
|
|
104
104
|
workIds.push(work.workId);
|
|
105
105
|
}
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { stageKinds } from "../../kernel/pipeline.js";
|
|
2
|
+
import { sourceOf } from "../admission/model.js";
|
|
3
|
+
import { motionStyleLabels } from "../admission/rules.js";
|
|
4
|
+
import { documentThemeLabels, documentThemeOf } from "../document/model.js";
|
|
2
5
|
import { getRevisionView } from "../revisions/view.js";
|
|
3
6
|
export function previewDetails(deps, view, allRecipes, selected) {
|
|
4
7
|
const parent = view.revision.parentId === null
|
|
@@ -111,7 +114,7 @@ function inputChanges(parent, view) {
|
|
|
111
114
|
add("Project title", before.title, after.title);
|
|
112
115
|
add("Aspect ratio", before.format, after.format);
|
|
113
116
|
for (const kind of stageKinds)
|
|
114
|
-
add(`${kind[0]?.toUpperCase()}${kind.slice(1)} source`, before.sources
|
|
117
|
+
add(`${kind[0]?.toUpperCase()}${kind.slice(1)} source`, sourceOf(before.sources, kind), sourceOf(after.sources, kind));
|
|
115
118
|
for (const [name, a, b] of [
|
|
116
119
|
["Text", before.llm, after.llm],
|
|
117
120
|
["Narration", before.audio, after.audio],
|
|
@@ -124,10 +127,17 @@ function inputChanges(parent, view) {
|
|
|
124
127
|
add("Narration voice", before.audio?.voice, after.audio?.voice);
|
|
125
128
|
add("Narration Preparation", before.narrationPrompt, after.narrationPrompt);
|
|
126
129
|
add("Narration chunking", before.chunking, after.chunking);
|
|
130
|
+
add("YouTube description", before.youtubeDescription === true ? "On" : "Off", after.youtubeDescription === true ? "On" : "Off");
|
|
131
|
+
add("Description prompt", before.descriptionPrompt, after.descriptionPrompt);
|
|
127
132
|
add("Silence gap (seconds)", before.silenceGapSeconds, after.silenceGapSeconds);
|
|
133
|
+
add("Silence at start and end (seconds)", before.edgeSilenceSeconds, after.edgeSilenceSeconds);
|
|
134
|
+
add("Seconds per image", before.imageSeconds, after.imageSeconds);
|
|
135
|
+
add("Zoom (%)", before.zoomPercent, after.zoomPercent);
|
|
136
|
+
add("Motion", motionStyleLabels[before.motionStyle], motionStyleLabels[after.motionStyle]);
|
|
128
137
|
add("Intro", before.intro, after.intro);
|
|
129
138
|
add("Outro", before.outro, after.outro);
|
|
130
139
|
add("Subtitle settings", before.subtitles, after.subtitles);
|
|
140
|
+
add("Document theme", documentThemeLabels[documentThemeOf(before.document)], documentThemeLabels[documentThemeOf(after.document)]);
|
|
131
141
|
add("Article text", parent.articleMarkdown, view.articleMarkdown);
|
|
132
142
|
add("Provided research", before.provided.research, after.provided.research);
|
|
133
143
|
for (const key of new Set([...Object.keys(before.values), ...Object.keys(after.values)]))
|
|
@@ -152,6 +152,9 @@ export function audioRecipes(context, text) {
|
|
|
152
152
|
"audio-media-v1",
|
|
153
153
|
ordered.map(({ value }) => resourceIdentity(context, value)),
|
|
154
154
|
config.silenceGapSeconds,
|
|
155
|
+
// The exported timeline, not a narration piece: the silence only reaches the
|
|
156
|
+
// exports that read this fingerprint.
|
|
157
|
+
config.edgeSilenceSeconds,
|
|
155
158
|
]),
|
|
156
159
|
timeline,
|
|
157
160
|
keys: ordered.map(({ value }) => value.key),
|
|
@@ -1,17 +1,22 @@
|
|
|
1
1
|
import { audioRecipes } from "./recipe-audio.js";
|
|
2
|
+
import { documentRecipes } from "./recipe-document.js";
|
|
2
3
|
import { exportRecipes } from "./recipe-exports.js";
|
|
3
4
|
import { textRecipes } from "./recipe-text.js";
|
|
4
5
|
import { thumbnailRecipes, visualAssets, visualRecipes } from "./recipe-visual.js";
|
|
6
|
+
import { youtubeRecipes } from "./recipe-youtube.js";
|
|
5
7
|
export function buildRecipes(context) {
|
|
6
8
|
const text = textRecipes(context);
|
|
7
9
|
const audio = audioRecipes(context, text);
|
|
8
10
|
const exports = exportRecipes(context, audio);
|
|
9
11
|
const captions = exports.find((value) => value.key === "subtitles:files");
|
|
12
|
+
const thumbnail = thumbnailRecipes(context, text.recipes);
|
|
10
13
|
return [
|
|
11
14
|
...text.recipes,
|
|
12
15
|
...audio.recipes,
|
|
13
16
|
...exports,
|
|
14
|
-
...
|
|
17
|
+
...youtubeRecipes(context, exports),
|
|
18
|
+
...thumbnail,
|
|
19
|
+
...documentRecipes(context, text, thumbnail),
|
|
15
20
|
...visualAssets(context, visualRecipes(context.config, context.content, audio.mediaFingerprint, captions?.fingerprint ?? null)),
|
|
16
21
|
];
|
|
17
22
|
}
|