@gentbajko/slopify 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -4
- package/dist/adapter-registry.js +28 -0
- package/dist/adapters/key-probes.js +57 -8
- package/dist/adapters/tts/gemini.js +206 -0
- package/dist/adapters/tts/pcm-mp3.js +62 -0
- package/dist/adapters/tts/system.js +147 -0
- package/dist/assets/models.yaml +38 -0
- package/dist/assets/style-preview/narration.mp3 +0 -0
- package/dist/assets/style-preview/tall-1.jpg +0 -0
- package/dist/assets/style-preview/tall-2.jpg +0 -0
- package/dist/assets/style-preview/tall-3.jpg +0 -0
- package/dist/assets/style-preview/wide-1.jpg +0 -0
- package/dist/assets/style-preview/wide-2.jpg +0 -0
- package/dist/assets/style-preview/wide-3.jpg +0 -0
- package/dist/catalog/registry.js +4 -1
- package/dist/catalog/validate.js +2 -2
- package/dist/edge/autostart/docker-record.js +2 -2
- package/dist/edge/autostart/docker.js +68 -0
- package/dist/edge/autostart/prompt.js +7 -1
- package/dist/edge/autostart/service.js +18 -9
- package/dist/edge/cli-args.js +71 -0
- package/dist/edge/cli.js +11 -16
- package/dist/edge/docker-install/run.js +7 -1
- package/dist/edge/events/hub.js +3 -1
- package/dist/edge/http/actions.js +6 -1
- package/dist/edge/http/app.js +2 -0
- package/dist/edge/http/byte-range.js +42 -0
- package/dist/edge/http/diagnostics.js +1 -0
- package/dist/edge/http/draft-files.js +2 -4
- package/dist/edge/http/files.js +10 -6
- package/dist/edge/http/onboarding.js +64 -4
- package/dist/edge/http/project-templates.js +19 -2
- package/dist/edge/http/projects.js +6 -1
- package/dist/edge/http/providers.js +39 -9
- package/dist/edge/http/reviews.js +18 -8
- package/dist/edge/http/revision-files.js +2 -4
- package/dist/edge/http/schedules.js +9 -5
- package/dist/edge/http/storage.js +16 -2
- package/dist/edge/http/studio.js +111 -31
- package/dist/edge/http/style-preview.js +3 -42
- package/dist/edge/http/tutorials.js +59 -0
- package/dist/edge/http/youtube-edits.js +11 -2
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/host-cli/gemini-login.js +83 -0
- package/dist/host-cli/status.js +4 -2
- package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
- package/dist/kernel/ports/model.js +5 -3
- package/dist/kernel/ports/system-speech.js +203 -0
- package/dist/kernel/runner/graph.js +17 -1
- package/dist/kernel/runner/index.js +4 -4
- package/dist/kernel/runner/providers.js +1 -1
- package/dist/main.js +5 -0
- package/dist/model-catalog.js +2 -0
- package/dist/patch-notes/3.0.1.md +105 -0
- package/dist/patch-notes/index.json +6 -0
- package/dist/slices/admission/rules.js +3 -3
- package/dist/slices/admission/start.js +1 -1
- package/dist/slices/channels/cast-match.js +1 -1
- package/dist/slices/channels/repo.js +3 -0
- package/dist/slices/channels/schema.js +23 -0
- package/dist/slices/channels/service.js +6 -6
- package/dist/slices/control/index.js +3 -1
- package/dist/slices/control/providers.js +4 -1
- package/dist/slices/episodes/related.js +2 -2
- package/dist/slices/estimate/requests.js +9 -1
- package/dist/slices/eta/history.js +56 -0
- package/dist/slices/eta/model.js +63 -0
- package/dist/slices/eta/view.js +25 -0
- package/dist/slices/fixes/rules.js +1 -1
- package/dist/slices/fonts/coverage.js +1 -1
- package/dist/slices/loudness/model.js +1 -1
- package/dist/slices/model-upkeep/usage.js +4 -2
- package/dist/slices/narration/pauses-model.js +1 -1
- package/dist/slices/narration/pronunciation.js +2 -2
- package/dist/slices/notifications/notifier.js +28 -2
- package/dist/slices/notifications/rules.js +26 -2
- package/dist/slices/onboarding/first-run.js +40 -14
- package/dist/slices/onboarding/full-video.js +81 -0
- package/dist/slices/onboarding/model.js +21 -0
- package/dist/slices/onboarding/quick-short.js +25 -10
- package/dist/slices/play-drafts/convert.js +3 -3
- package/dist/slices/play-drafts/readiness.js +1 -1
- package/dist/slices/project-templates/from-project.js +10 -3
- package/dist/slices/project-templates/next-chapter.js +94 -0
- package/dist/slices/project-templates/one-off.js +1 -1
- package/dist/slices/project-templates/schema.js +7 -1
- package/dist/slices/rebuild/recipe-exports.js +5 -0
- package/dist/slices/rebuild/recipe-voices.js +1 -1
- package/dist/slices/rebuild/review-redo.js +15 -0
- package/dist/slices/rebuild/runtime-export-bed.js +1 -1
- package/dist/slices/rebuild/runtime-review.js +35 -5
- package/dist/slices/rebuild/runtime-store.js +23 -1
- package/dist/slices/rebuild/runtime-voices.js +2 -2
- package/dist/slices/rebuild/service-readiness.js +3 -3
- package/dist/slices/reviews/repo.js +16 -4
- package/dist/slices/run-cost/panel.js +30 -8
- package/dist/slices/run-cost/pricing.js +4 -1
- package/dist/slices/schedules/agenda.js +66 -2
- package/dist/slices/schedules/repo.js +1 -1
- package/dist/slices/schedules/scheduler.js +1 -1
- package/dist/slices/schedules/schema.js +43 -2
- package/dist/slices/schedules/similar.js +41 -3
- package/dist/slices/schedules/topic-list.js +2 -2
- package/dist/slices/schedules/topics.js +118 -17
- package/dist/slices/settings/first-run.js +5 -1
- package/dist/slices/settings/health.js +64 -7
- package/dist/slices/settings/key-guides.js +17 -7
- package/dist/slices/settings/key-test.js +12 -4
- package/dist/slices/settings/keys.js +10 -5
- package/dist/slices/settings/model-reach.js +52 -0
- package/dist/slices/settings/model.js +49 -0
- package/dist/slices/settings/models.js +5 -1
- package/dist/slices/settings/readiness.js +14 -2
- package/dist/slices/settings/tutorial-schema.js +5 -0
- package/dist/slices/shorts/render.js +18 -17
- package/dist/slices/storage/documents.js +11 -3
- package/dist/slices/storage/downloads.js +33 -1
- package/dist/slices/storage/portable.js +4 -2
- package/dist/slices/studio/model.js +2 -0
- package/dist/slices/studio/pack.js +6 -4
- package/dist/slices/studio/queue.js +69 -0
- package/dist/slices/studio/settings.js +42 -5
- package/dist/slices/style-preview/narration.js +33 -0
- package/dist/slices/style-preview/render.js +43 -56
- package/dist/slices/style-preview/schema.js +17 -3
- package/dist/slices/style-preview/service.js +3 -4
- package/dist/slices/style-preview/settings.js +75 -25
- package/dist/slices/tutorials/anchors.js +57 -0
- package/dist/slices/tutorials/library.js +185 -0
- package/dist/slices/voices/audio-files.js +7 -2
- package/dist/slices/voices/cast.js +26 -8
- package/dist/slices/voices/grouping.js +10 -0
- package/dist/slices/voices/model.js +49 -10
- package/dist/slices/voices/script.js +1 -1
- package/dist/slices/youtube/edits-repo.js +19 -4
- package/dist/slices/youtube/placeholders.js +2 -2
- package/dist/tutorials/AI-CLIs.md +146 -0
- package/dist/tutorials/Backups.md +121 -0
- package/dist/tutorials/Calendar.md +103 -0
- package/dist/tutorials/Cast-Library.md +112 -0
- package/dist/tutorials/Channels.md +148 -0
- package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
- package/dist/tutorials/Docker.md +171 -0
- package/dist/tutorials/Document-Themes.md +207 -0
- package/dist/tutorials/Editing-a-Project.md +204 -0
- package/dist/tutorials/FAQ.md +134 -0
- package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
- package/dist/tutorials/Home-and-Projects.md +174 -0
- package/dist/tutorials/Home.md +119 -0
- package/dist/tutorials/Install.md +154 -0
- package/dist/tutorials/Intros-and-Outros.md +86 -0
- package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
- package/dist/tutorials/Library-Overview.md +75 -0
- package/dist/tutorials/Models.md +133 -0
- package/dist/tutorials/Multiple-Voices.md +161 -0
- package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
- package/dist/tutorials/Notifications.md +86 -0
- package/dist/tutorials/Other-Languages.md +110 -0
- package/dist/tutorials/PDF-Documents.md +100 -0
- package/dist/tutorials/Play-Images.md +135 -0
- package/dist/tutorials/Play-Narration.md +167 -0
- package/dist/tutorials/Play-Outputs.md +109 -0
- package/dist/tutorials/Play-Overview.md +216 -0
- package/dist/tutorials/Play-Title-and-Article.md +135 -0
- package/dist/tutorials/Play-Video-and-Style.md +143 -0
- package/dist/tutorials/Project-Page.md +228 -0
- package/dist/tutorials/Prompts.md +136 -0
- package/dist/tutorials/Providers-and-Keys.md +290 -0
- package/dist/tutorials/Publishing-to-YouTube.md +135 -0
- package/dist/tutorials/Recovery-and-Retries.md +141 -0
- package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
- package/dist/tutorials/Schedules.md +154 -0
- package/dist/tutorials/Settings-Reference.md +258 -0
- package/dist/tutorials/Shorts.md +116 -0
- package/dist/tutorials/Start-at-Login.md +126 -0
- package/dist/tutorials/Studio-Extension.md +146 -0
- package/dist/tutorials/Templates.md +146 -0
- package/dist/tutorials/Trash.md +87 -0
- package/dist/tutorials/Troubleshooting.md +149 -0
- package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
- package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
- package/dist/tutorials/Video-Editing.md +135 -0
- package/dist/tutorials/Where-Your-Files-Live.md +149 -0
- package/dist/tutorials/YouTube-Description.md +132 -0
- package/dist/tutorials/Your-First-Short.md +125 -0
- package/dist/tutorials/_Footer.md +1 -0
- package/dist/tutorials/_Sidebar.md +69 -0
- package/dist/web/assets/index-DBLSfqA-.css +1 -0
- package/dist/web/assets/index-DFdIhmm0.js +307 -0
- package/dist/web/assets/{pdf-BG8n_uc5.js → pdf-B9OY1ioX.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/web/assets/index-CUA2sUxU.css +0 -1
- package/dist/web/assets/index-CX590kE-.js +0 -303
|
@@ -18,6 +18,23 @@ export function noticeOf(previous, next) {
|
|
|
18
18
|
return undefined;
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
const reviewItemNames = {
|
|
22
|
+
article: "the article",
|
|
23
|
+
images: "an image",
|
|
24
|
+
narration: "the narration",
|
|
25
|
+
thumbnail: "the thumbnail",
|
|
26
|
+
shorts: "a short",
|
|
27
|
+
};
|
|
28
|
+
export function reviewNoticeText(subject) {
|
|
29
|
+
const title = subject.title.trim() === "" ? "Untitled project" : subject.title.trim();
|
|
30
|
+
const reason = shortReason(subject.reason);
|
|
31
|
+
const item = reviewItemNames[subject.stage] ?? "an item";
|
|
32
|
+
const headline = `Review needs a decision: ${title}`;
|
|
33
|
+
return {
|
|
34
|
+
headline: reason === undefined ? headline : `${headline} — ${reason}`,
|
|
35
|
+
detail: `The automatic review flagged ${item} and kept it. Open the project and press Overrule to keep it or Redo to make it again.`,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
21
38
|
const reasonMax = 140;
|
|
22
39
|
// A provider's failure can run to paragraphs; a notification has a line.
|
|
23
40
|
export function shortReason(reason) {
|
|
@@ -71,12 +88,19 @@ export function topicsNoticeText(subject) {
|
|
|
71
88
|
}
|
|
72
89
|
export const testNotice = {
|
|
73
90
|
headline: "Slopify test notification",
|
|
74
|
-
detail: "Notifications work. You'll get one when a run finishes, fails or waits for you.",
|
|
91
|
+
detail: "Notifications work. You'll get one when a run finishes, fails or waits for you, and when a review needs your decision.",
|
|
75
92
|
};
|
|
76
93
|
// The body a Notification URL receives: plain text, which ntfy shows as the message. Nothing
|
|
77
94
|
// but the project's title, what happened and the provider's own short reason goes in it.
|
|
78
95
|
export function webhookBody(text) {
|
|
79
|
-
return `${text.headline}\n${text.detail}\n`;
|
|
96
|
+
return `${text.headline}\n${text.detail}\n${text.link === undefined ? "" : `${text.link}\n`}`;
|
|
97
|
+
}
|
|
98
|
+
// Where a project opens in the browser, from the address the server listens on. A wildcard
|
|
99
|
+
// (0.0.0.0 in Docker) or loopback address is reached as localhost from the same machine.
|
|
100
|
+
export function projectLink(host, port, projectId) {
|
|
101
|
+
const wildcard = host === "0.0.0.0" || host === "::" || host === "" || host === "127.0.0.1";
|
|
102
|
+
const authority = wildcard ? "localhost" : host.includes(":") ? `[${host}]` : host;
|
|
103
|
+
return `http://${authority}:${String(port)}/projects/${encodeURIComponent(projectId)}`;
|
|
80
104
|
}
|
|
81
105
|
export const notificationUrlMax = 2048;
|
|
82
106
|
// Empty clears the setting. Otherwise one absolute http(s) URL with no user name or password:
|
|
@@ -1,29 +1,55 @@
|
|
|
1
|
+
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
2
|
+
import { templateById } from "../project-templates/repo.js";
|
|
1
3
|
import { starterPacks } from "./packs.js";
|
|
2
4
|
import { sampleProjectIds } from "./sample.js";
|
|
3
|
-
import {
|
|
5
|
+
import { firstRunDismissed, readPackRecords } from "./state.js";
|
|
4
6
|
// The first-run screen shows on a fresh install and never again once it is skipped or a real
|
|
5
|
-
// project (anything but the samples) exists.
|
|
6
|
-
//
|
|
7
|
-
|
|
7
|
+
// project (anything but the samples) exists. Reading it writes nothing: a real project that
|
|
8
|
+
// exists before the screen was recorded as done is reported as `settle`, and the client
|
|
9
|
+
// records it with POST /dismiss, so deleting every project later does not bring it back.
|
|
10
|
+
export function firstRunView(db, statuses) {
|
|
8
11
|
const samples = sampleProjectIds(db);
|
|
9
12
|
const real = db
|
|
10
13
|
.prepare("SELECT 1 FROM projects WHERE id IS NOT ? AND id IS NOT ? AND id IS NOT ? LIMIT 1")
|
|
11
14
|
.get(samples.library, samples.audiobook, samples.podcast) !== undefined;
|
|
12
|
-
|
|
13
|
-
dismissFirstRun(db, at);
|
|
15
|
+
const dismissed = firstRunDismissed(db);
|
|
14
16
|
const packs = readPackRecords(db);
|
|
15
17
|
return {
|
|
16
|
-
show: !real && !
|
|
18
|
+
show: !real && !dismissed,
|
|
19
|
+
settle: real && !dismissed,
|
|
20
|
+
voice: firstVoice(statuses),
|
|
17
21
|
sampleProjectId: samples.library,
|
|
18
22
|
samples,
|
|
19
23
|
clis: detectedClis(statuses),
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// A pack counts as added only with its template: the first short adds a pack's prompts and
|
|
25
|
+
// voice but no template, and Add pack must still be able to add that template later (or
|
|
26
|
+
// again, after it was deleted).
|
|
27
|
+
packs: starterPacks.map((pack) => {
|
|
28
|
+
const template = packs[pack.id]?.template;
|
|
29
|
+
const kept = template !== undefined && templateById(db, template) !== undefined;
|
|
30
|
+
return {
|
|
31
|
+
id: pack.id,
|
|
32
|
+
name: pack.name,
|
|
33
|
+
summary: pack.summary,
|
|
34
|
+
installed: kept,
|
|
35
|
+
templateId: kept ? template : null,
|
|
36
|
+
};
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
// Who would narrate a first short: a keyed voice provider when one has a key, else the
|
|
41
|
+
// computer's own voice when a speech program was found.
|
|
42
|
+
export function firstVoice(statuses) {
|
|
43
|
+
const keyed = statuses.find((row) => row.family === "tts" && row.readiness.kind === "keyed" && readinessIsUsable(row.readiness));
|
|
44
|
+
const system = statuses.find((row) => row.readiness.kind === "local")?.readiness;
|
|
45
|
+
const local = system?.kind === "local" ? system : undefined;
|
|
46
|
+
return {
|
|
47
|
+
keyed: keyed?.displayName ?? null,
|
|
48
|
+
system: {
|
|
49
|
+
available: local?.available === true,
|
|
50
|
+
engine: local?.engine ?? null,
|
|
51
|
+
issue: local?.issue ?? null,
|
|
52
|
+
},
|
|
27
53
|
};
|
|
28
54
|
}
|
|
29
55
|
// What this machine can make without a key: each agent CLI Slopify found, and whether it
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { projectById } from "../admission/repo.js";
|
|
2
|
+
import { usesShortMode } from "../admission/short-mode.js";
|
|
3
|
+
import { promptById } from "../library/repo.js";
|
|
4
|
+
import { createDraft } from "../play-drafts/service.js";
|
|
5
|
+
import { templateById } from "../project-templates/repo.js";
|
|
6
|
+
import { freshTemplateDraft } from "../project-templates/setup.js";
|
|
7
|
+
import { installPack } from "./install.js";
|
|
8
|
+
import { starterPacks, starterSet } from "./packs.js";
|
|
9
|
+
import { readPackRecords } from "./state.js";
|
|
10
|
+
import { packTemplate } from "./template.js";
|
|
11
|
+
// The pack whose short script the short was written from. A short made without a pack, or
|
|
12
|
+
// whose pack's prompts were since renamed or removed, is the starter set's.
|
|
13
|
+
export function shortPack(deps, articlePrompt) {
|
|
14
|
+
const records = readPackRecords(deps.db);
|
|
15
|
+
for (const pack of starterPacks) {
|
|
16
|
+
const id = records[pack.id]?.prompts.shortScript;
|
|
17
|
+
if (id !== undefined && promptById(deps.db, id)?.name === articlePrompt)
|
|
18
|
+
return pack;
|
|
19
|
+
}
|
|
20
|
+
return starterSet;
|
|
21
|
+
}
|
|
22
|
+
export function fullVideoDraft(deps, input) {
|
|
23
|
+
const project = projectById(deps.db, input.projectId);
|
|
24
|
+
if (project === undefined)
|
|
25
|
+
return { ok: false, reason: "not-found" };
|
|
26
|
+
const config = project.config;
|
|
27
|
+
if (!usesShortMode(config))
|
|
28
|
+
return { ok: false, reason: "not-short" };
|
|
29
|
+
const topic = config.values.topic?.trim() || project.title;
|
|
30
|
+
const pack = shortPack(deps, config.articlePrompt ?? "");
|
|
31
|
+
const document = packDocument(deps, pack);
|
|
32
|
+
if (document === undefined)
|
|
33
|
+
return { ok: false, reason: "not-found" };
|
|
34
|
+
const provider = (choice, fallback) => choice === undefined || fallback.provider !== ""
|
|
35
|
+
? fallback
|
|
36
|
+
: {
|
|
37
|
+
provider: choice.provider,
|
|
38
|
+
model: choice.model,
|
|
39
|
+
...(choice.thinking === undefined ? {} : { thinking: choice.thinking }),
|
|
40
|
+
};
|
|
41
|
+
const form = document.form;
|
|
42
|
+
return createDraft(deps, {
|
|
43
|
+
id: input.draftId,
|
|
44
|
+
document: {
|
|
45
|
+
...document,
|
|
46
|
+
form: {
|
|
47
|
+
...form,
|
|
48
|
+
title: project.title,
|
|
49
|
+
values: { ...form.values, topic },
|
|
50
|
+
llm: provider(config.llm, form.llm),
|
|
51
|
+
images: provider(config.images, form.images),
|
|
52
|
+
audio: config.audio === undefined
|
|
53
|
+
? form.audio
|
|
54
|
+
: {
|
|
55
|
+
...form.audio,
|
|
56
|
+
provider: config.audio.provider,
|
|
57
|
+
model: config.audio.model,
|
|
58
|
+
voice: config.audio.voice,
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
// A pack's own template, as Templates → Apply to Play opens it. The starter set has no long
|
|
65
|
+
// video prompt of its own and no template, so it is its settings without one saved.
|
|
66
|
+
function packDocument(deps, pack) {
|
|
67
|
+
const withTemplate = pack.id !== starterSet.id;
|
|
68
|
+
const installed = installPack(deps, pack.id, { template: withTemplate });
|
|
69
|
+
if (!installed.ok)
|
|
70
|
+
return undefined;
|
|
71
|
+
const { templateId } = installed.value;
|
|
72
|
+
if (!withTemplate || templateId === null)
|
|
73
|
+
return packTemplate(pack, installed.value.prompts, installed.value.voice ?? pack.voice.voiceId);
|
|
74
|
+
const template = templateById(deps.db, templateId);
|
|
75
|
+
if (template === undefined)
|
|
76
|
+
return undefined;
|
|
77
|
+
return freshTemplateDraft(deps, template.document, {
|
|
78
|
+
id: template.id,
|
|
79
|
+
version: template.version,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
@@ -13,6 +13,22 @@ export const sampleProjectsSchema = z.object({
|
|
|
13
13
|
export const firstRunViewSchema = z
|
|
14
14
|
.object({
|
|
15
15
|
show: z.boolean(),
|
|
16
|
+
// A real project exists but the screen was never recorded as done: the client records it
|
|
17
|
+
// with POST /dismiss, so deleting every project later does not bring the screen back.
|
|
18
|
+
// Reading this view writes nothing.
|
|
19
|
+
settle: z.boolean(),
|
|
20
|
+
// Who narrates a first short: a voice provider with a saved key, else the computer's own.
|
|
21
|
+
voice: z.object({
|
|
22
|
+
// The keyed voice provider that would be used, by name ("OpenAI"), or null.
|
|
23
|
+
keyed: z.string().nullable(),
|
|
24
|
+
system: z.object({
|
|
25
|
+
available: z.boolean(),
|
|
26
|
+
// The speech program found ("eSpeak NG", "macOS voices").
|
|
27
|
+
engine: z.string().nullable(),
|
|
28
|
+
// Why none can be used, with the fix.
|
|
29
|
+
issue: z.string().nullable(),
|
|
30
|
+
}),
|
|
31
|
+
}),
|
|
16
32
|
// The Library of Alexandria's project, as before the demos; `samples` has all three.
|
|
17
33
|
sampleProjectId: z.string().nullable(),
|
|
18
34
|
samples: sampleProjectsSchema,
|
|
@@ -42,6 +58,11 @@ export const quickShortInputSchema = z
|
|
|
42
58
|
requestId: z.uuid(),
|
|
43
59
|
})
|
|
44
60
|
.strict();
|
|
61
|
+
// A finished short's "Make the full video on this topic": the draft id is the browser's, so a
|
|
62
|
+
// repeated press opens the same Play draft instead of making another.
|
|
63
|
+
export const fullVideoInputSchema = z
|
|
64
|
+
.object({ projectId: z.string().min(1).max(64), draftId: z.uuid() })
|
|
65
|
+
.strict();
|
|
45
66
|
export const quickShortResultSchema = z.object({
|
|
46
67
|
projectId: z.string(),
|
|
47
68
|
replayed: z.boolean(),
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { readinessIsUsable } from "../../kernel/ports/model.js";
|
|
1
2
|
import { titleMax } from "../admission/rules.js";
|
|
2
3
|
import { defaultSentencePauseSeconds } from "../narration/pauses-model.js";
|
|
4
|
+
import { systemVoiceProvider } from "../settings/model.js";
|
|
3
5
|
const textClis = ["claude-code", "codex", "gemini"];
|
|
4
6
|
const keyedImages = ["openai-image", "google-image", "fal", "replicate"];
|
|
5
7
|
const keyedVoices = ["openai-tts", "elevenlabs", "cartesia", "inworld"];
|
|
@@ -10,12 +12,7 @@ const preferred = {
|
|
|
10
12
|
"openai-tts": /^gpt-4o-mini-tts$/,
|
|
11
13
|
};
|
|
12
14
|
function ready(status) {
|
|
13
|
-
|
|
14
|
-
return false;
|
|
15
|
-
const readiness = status.readiness;
|
|
16
|
-
return readiness.kind === "cli"
|
|
17
|
-
? readiness.installed && readiness.issue === undefined
|
|
18
|
-
: readiness.hasKey;
|
|
15
|
+
return status !== undefined && readinessIsUsable(status.readiness);
|
|
19
16
|
}
|
|
20
17
|
async function firstModel(modelsFor, provider, family) {
|
|
21
18
|
let models;
|
|
@@ -62,8 +59,11 @@ export async function planShortProviders(input) {
|
|
|
62
59
|
need: "images",
|
|
63
60
|
message: "No image model is ready. Install Codex CLI and sign in to it, or add an OpenAI, Google, fal.ai or Replicate key in Settings → Providers.",
|
|
64
61
|
});
|
|
62
|
+
// A keyed voice first (a saved one, else the pack's suggestion), then the computer's own: a
|
|
63
|
+
// saved system voice, else the one found. Keys are the better voice, never a requirement.
|
|
65
64
|
let audio;
|
|
66
|
-
const
|
|
65
|
+
const keyed = (provider) => provider !== systemVoiceProvider;
|
|
66
|
+
const saved = input.voices.find((voice) => keyed(voice.provider) && ready(status(voice.provider)));
|
|
67
67
|
const provider = saved?.provider ??
|
|
68
68
|
(ready(status(input.suggested.provider)) ? input.suggested.provider : undefined);
|
|
69
69
|
if (provider !== undefined) {
|
|
@@ -71,16 +71,31 @@ export async function planShortProviders(input) {
|
|
|
71
71
|
if (model !== undefined)
|
|
72
72
|
audio = { provider, model, voice: saved?.voiceId ?? input.suggested.voiceId };
|
|
73
73
|
}
|
|
74
|
-
|
|
74
|
+
let systemVoice = false;
|
|
75
|
+
const system = status(systemVoiceProvider);
|
|
76
|
+
if (audio === undefined && ready(system) && input.systemVoice !== undefined) {
|
|
77
|
+
const savedSystem = input.voices.find((voice) => voice.provider === systemVoiceProvider);
|
|
78
|
+
audio = {
|
|
79
|
+
provider: systemVoiceProvider,
|
|
80
|
+
model: input.systemVoice.model,
|
|
81
|
+
voice: savedSystem?.voiceId ?? input.systemVoice.voiceId,
|
|
82
|
+
};
|
|
83
|
+
systemVoice = true;
|
|
84
|
+
}
|
|
85
|
+
if (audio === undefined) {
|
|
86
|
+
const systemIssue = system?.readiness.kind === "local" && system.readiness.issue !== undefined
|
|
87
|
+
? ` ${system.readiness.issue}`
|
|
88
|
+
: "";
|
|
75
89
|
gaps.push({
|
|
76
90
|
need: "voice",
|
|
77
91
|
message: keyedVoices.some((id) => ready(status(id)))
|
|
78
92
|
? "Your voice provider has no saved voice yet. Add one in Settings → Voices, then try again."
|
|
79
|
-
:
|
|
93
|
+
: `No voice is ready to narrate the short: no voice key is saved and this computer's built-in voice can't be used.${systemIssue || " Install espeak-ng, or add an OpenAI, ElevenLabs, Cartesia or Inworld key in Settings → Providers (with ElevenLabs, Cartesia or Inworld, also save a voice in Settings → Voices)."}`,
|
|
80
94
|
});
|
|
95
|
+
}
|
|
81
96
|
return llm === undefined || images === undefined || audio === undefined
|
|
82
97
|
? { ok: false, gaps }
|
|
83
|
-
: { ok: true, providers: { llm, audio, images } };
|
|
98
|
+
: { ok: true, providers: { llm, audio, images }, systemVoice };
|
|
84
99
|
}
|
|
85
100
|
// The short-mode draft for a topic, using the pack's (or starter set's) script and scene
|
|
86
101
|
// prompts under the names they have in the library.
|
|
@@ -12,13 +12,13 @@ import { defaultSubtitles } from "../subtitles/model.js";
|
|
|
12
12
|
import { ambientBedOfForm, ambientBedProblems } from "../video/ambient-bed.js";
|
|
13
13
|
// Said where the control is, since the music sits in a closed disclosure on the Export rail.
|
|
14
14
|
const musicMessages = {
|
|
15
|
-
uploading: "The shorts' background music is still uploading, so the run can't start yet. Wait for it to finish, or remove it under Outputs →
|
|
16
|
-
missing: "The shorts' background music file is no longer available (it was not uploaded again after the draft was copied, or the upload failed). Choose the file again under Outputs →
|
|
15
|
+
uploading: "The shorts' background music is still uploading, so the run can't start yet. Wait for it to finish, or remove it under Outputs → More shorts options → Background music.",
|
|
16
|
+
missing: "The shorts' background music file is no longer available (it was not uploaded again after the draft was copied, or the upload failed). Choose the file again under Outputs → More shorts options → Background music, or remove it.",
|
|
17
17
|
};
|
|
18
18
|
function pickMusic(id) {
|
|
19
19
|
return id === undefined ? {} : { shortsMusic: id };
|
|
20
20
|
}
|
|
21
|
-
const bedWhere = "under
|
|
21
|
+
const bedWhere = "under Video and style → Ambient sound";
|
|
22
22
|
const bedMessages = {
|
|
23
23
|
uploading: `The ambient sound's audio file is still uploading, so the run can't start yet. Wait for it to finish ${bedWhere}.`,
|
|
24
24
|
missing: `The ambient sound's audio file is missing (it was never chosen, was not uploaded again after the draft was copied, or the upload failed). Choose the file ${bedWhere}, or pick Rain, Fireplace or Wind instead.`,
|
|
@@ -54,7 +54,7 @@ export async function checkDraftReadiness(deps, runs) {
|
|
|
54
54
|
if (!provider || !readinessIsUsable(provider.readiness)) {
|
|
55
55
|
fields.push({
|
|
56
56
|
field: c.field,
|
|
57
|
-
message: provider
|
|
57
|
+
message: provider !== undefined && provider.readiness.kind !== "keyed" && provider.readiness.issue
|
|
58
58
|
? provider.readiness.issue
|
|
59
59
|
: "Set up this provider in Settings → Providers before starting.",
|
|
60
60
|
});
|
|
@@ -13,11 +13,12 @@ import { currentRevisionId, revisionById } from "../revisions/repo.js";
|
|
|
13
13
|
import { shortsExtrasForm } from "../shorts/model.js";
|
|
14
14
|
import { ambientBedFormOf } from "../video/ambient-bed.js";
|
|
15
15
|
import { usesScriptPrompt } from "../voices/model.js";
|
|
16
|
+
import { templateNameMax } from "./schema.js";
|
|
16
17
|
import { createTemplate, readTemplate } from "./service.js";
|
|
17
18
|
export const projectTemplateCreateSchema = z
|
|
18
19
|
.object({
|
|
19
20
|
id: z.uuid(),
|
|
20
|
-
name: z.string().trim().min(1).max(
|
|
21
|
+
name: z.string().trim().min(1).max(templateNameMax),
|
|
21
22
|
projectId: z.string().min(1).max(64),
|
|
22
23
|
revisionId: z.string().min(1).max(64),
|
|
23
24
|
})
|
|
@@ -65,7 +66,7 @@ export function createTemplateFromProject(deps, input) {
|
|
|
65
66
|
return result;
|
|
66
67
|
});
|
|
67
68
|
}
|
|
68
|
-
function documentFromProject(deps, revision) {
|
|
69
|
+
export function documentFromProject(deps, revision) {
|
|
69
70
|
const config = revision.config;
|
|
70
71
|
const prompts = [];
|
|
71
72
|
const entries = [];
|
|
@@ -211,7 +212,9 @@ function documentFromProject(deps, revision) {
|
|
|
211
212
|
// it cuts every N seconds like the project did.
|
|
212
213
|
...(config.videoEdit === undefined ? {} : { videoEdit: config.videoEdit }),
|
|
213
214
|
...(config.reviews === undefined ? {} : { reviews: reviewSettingsForm(config.reviews) }),
|
|
214
|
-
|
|
215
|
+
// Without the book: every run of a template would otherwise be the same chapter. "Make the
|
|
216
|
+
// next chapter" (`next-chapter.ts`) sets it itself.
|
|
217
|
+
...(config.voices === undefined ? {} : { voices: withoutBook(config.voices) }),
|
|
215
218
|
...(config.ambientBed === undefined
|
|
216
219
|
? {}
|
|
217
220
|
: { ambientBed: ambientBedFormOf(config.ambientBed) }),
|
|
@@ -265,3 +268,7 @@ function documentFromProject(deps, revision) {
|
|
|
265
268
|
fontUpload: null,
|
|
266
269
|
};
|
|
267
270
|
}
|
|
271
|
+
function withoutBook(voices) {
|
|
272
|
+
const { book: _book, ...rest } = voices;
|
|
273
|
+
return rest;
|
|
274
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { projectById } from "../admission/repo.js";
|
|
4
|
+
import { projectChannelId } from "../channels/repo.js";
|
|
5
|
+
import { draftRow } from "../play-drafts/repo.js";
|
|
6
|
+
import { createDraft, readDraft } from "../play-drafts/service.js";
|
|
7
|
+
import { currentRevisionId, revisionById } from "../revisions/repo.js";
|
|
8
|
+
import { bookChapterMax, bookLabel, bookTitleMax } from "../voices/model.js";
|
|
9
|
+
import { documentFromProject } from "./from-project.js";
|
|
10
|
+
// Books: an audiobook is written chapter by chapter, one project each. "Make the next chapter"
|
|
11
|
+
// on a chapter opens a new Play draft with everything the book shares - format, speakers and
|
|
12
|
+
// their voices, channel (and so its cast), prompts and settings - and the chapter number one
|
|
13
|
+
// higher. The chapter's own words are left empty: the new chapter is written or pasted fresh,
|
|
14
|
+
// and files the last one was given are listed to attach again, as a template's are.
|
|
15
|
+
export const nextChapterSchema = z
|
|
16
|
+
.object({ id: z.uuid(), projectId: z.string().min(1).max(64) })
|
|
17
|
+
.strict()
|
|
18
|
+
.readonly();
|
|
19
|
+
// The book a project's next chapter belongs to: its own book one chapter on, or, for an
|
|
20
|
+
// audiobook made on its own, a book named after it whose second chapter comes next.
|
|
21
|
+
export function nextBook(title, book) {
|
|
22
|
+
return book === undefined
|
|
23
|
+
? { title: title.trim().slice(0, bookTitleMax) || "Untitled book", chapter: 2 }
|
|
24
|
+
: { title: book.title, chapter: Math.min(bookChapterMax, book.chapter + 1) };
|
|
25
|
+
}
|
|
26
|
+
export function makeNextChapter(deps, input) {
|
|
27
|
+
const parsed = nextChapterSchema.safeParse(input);
|
|
28
|
+
if (!parsed.success)
|
|
29
|
+
return {
|
|
30
|
+
ok: false,
|
|
31
|
+
reason: "not-found",
|
|
32
|
+
message: "That project could not be found. Open it again from Projects, then try again.",
|
|
33
|
+
};
|
|
34
|
+
const { id, projectId } = parsed.data;
|
|
35
|
+
return transact(deps.db, () => {
|
|
36
|
+
// Pressed twice (a retry after a dropped answer): the draft it made the first time.
|
|
37
|
+
if (draftRow(deps.db, id) !== undefined) {
|
|
38
|
+
const made = readDraft(deps, id);
|
|
39
|
+
if (made.ok)
|
|
40
|
+
return { ok: true, value: made.value };
|
|
41
|
+
}
|
|
42
|
+
const project = projectById(deps.db, projectId);
|
|
43
|
+
const current = project === undefined ? undefined : currentRevisionId(deps.db, project.id);
|
|
44
|
+
const revision = project === undefined || current === undefined
|
|
45
|
+
? undefined
|
|
46
|
+
: revisionById(deps.db, project.id, current);
|
|
47
|
+
if (project === undefined || revision === undefined)
|
|
48
|
+
return {
|
|
49
|
+
ok: false,
|
|
50
|
+
reason: "not-found",
|
|
51
|
+
message: "That project could not be found; it may have been deleted. Open Projects and pick another chapter.",
|
|
52
|
+
};
|
|
53
|
+
const voices = revision.config.voices;
|
|
54
|
+
if (voices?.format !== "audiobook")
|
|
55
|
+
return {
|
|
56
|
+
ok: false,
|
|
57
|
+
reason: "not-an-audiobook",
|
|
58
|
+
message: "Only an audiobook has chapters. Pick Audiobook under Speakers (Play → Narration) to start a book.",
|
|
59
|
+
};
|
|
60
|
+
const book = nextBook(revision.config.title, voices.book);
|
|
61
|
+
const document = documentFromProject(deps, revision);
|
|
62
|
+
const form = document.form;
|
|
63
|
+
const result = createDraft(deps, {
|
|
64
|
+
id,
|
|
65
|
+
document: {
|
|
66
|
+
...document,
|
|
67
|
+
channelId: projectChannelId(deps.db, project.id),
|
|
68
|
+
form: {
|
|
69
|
+
...form,
|
|
70
|
+
title: bookLabel(book),
|
|
71
|
+
voices: { ...voices, book },
|
|
72
|
+
provided: {
|
|
73
|
+
...form.provided,
|
|
74
|
+
// The new chapter's own words, written or pasted fresh.
|
|
75
|
+
research: "",
|
|
76
|
+
article: "",
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
if (!result.ok)
|
|
82
|
+
return {
|
|
83
|
+
ok: false,
|
|
84
|
+
reason: "conflict",
|
|
85
|
+
message: "The next chapter's draft could not be made. Press Make the next chapter again; if it keeps failing, use Download diagnostics in Settings and report it.",
|
|
86
|
+
};
|
|
87
|
+
// Files the last chapter was given are attached again on Play, as with a template.
|
|
88
|
+
deps.db.prepare("UPDATE play_draft_attachments SET status='reattach' WHERE draft_id=?").run(id);
|
|
89
|
+
const made = readDraft(deps, id);
|
|
90
|
+
if (!made.ok)
|
|
91
|
+
throw new Error("The next chapter's draft could not be read back");
|
|
92
|
+
return { ok: true, value: made.value };
|
|
93
|
+
});
|
|
94
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { detectSlots } from "../admission/substitute.js";
|
|
2
2
|
// A template keeps the settings, never what was typed for one video. A keyword the project
|
|
3
|
-
// title names ({{Topic}} in "
|
|
3
|
+
// title names ({{Topic}} in "History: {{Topic}}") is the video's topic: it changes with every
|
|
4
4
|
// video, so a template stores it empty. Every other keyword ({{minWords}}, {{style}}) is a
|
|
5
5
|
// setting and keeps its value.
|
|
6
6
|
export function topicKeywords(title) {
|
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { playDraftDocumentSchema } from "../play-drafts/schema.js";
|
|
3
|
+
// The longest template name, in characters. Play's Save as template and Library → Templates
|
|
4
|
+
// (save and rename) cap their name fields at it, and a new template is refused past it.
|
|
5
|
+
export const templateNameMax = 120;
|
|
6
|
+
// A template named before the cap (Play once allowed 200) can still be saved under its name.
|
|
7
|
+
const templateNameStoredMax = 200;
|
|
3
8
|
export const templateCreateSchema = z
|
|
4
9
|
.object({
|
|
5
10
|
id: z.uuid(),
|
|
6
|
-
name: z.string().trim().min(1).max(
|
|
11
|
+
name: z.string().trim().min(1).max(templateNameMax),
|
|
7
12
|
document: playDraftDocumentSchema,
|
|
8
13
|
})
|
|
9
14
|
.strict()
|
|
@@ -11,6 +16,7 @@ export const templateCreateSchema = z
|
|
|
11
16
|
export const templateUpdateSchema = templateCreateSchema
|
|
12
17
|
.unwrap()
|
|
13
18
|
.extend({
|
|
19
|
+
name: z.string().trim().min(1).max(templateNameStoredMax),
|
|
14
20
|
baseVersion: z.number().int().positive(),
|
|
15
21
|
mutationId: z.uuid(),
|
|
16
22
|
})
|
|
@@ -67,6 +67,11 @@ export function exportRecipes(context, audio) {
|
|
|
67
67
|
(audio.sections ?? []).map((section) => [section.title, section.firstTurn]),
|
|
68
68
|
config.title,
|
|
69
69
|
...master.values,
|
|
70
|
+
// The book's title and chapter become the files' album and track tags; a project
|
|
71
|
+
// that is no chapter of a book keeps the values it always had.
|
|
72
|
+
...(voices.book === undefined
|
|
73
|
+
? []
|
|
74
|
+
: [["book-v1", voices.book.title, voices.book.chapter]]),
|
|
70
75
|
],
|
|
71
76
|
}, [...audio.keys, timing.key, ...master.keys], { unresolved: audio.sections === undefined }));
|
|
72
77
|
if (config.subtitles === undefined || config.subtitles.mode === "off")
|
|
@@ -249,7 +249,7 @@ function speakerGlossary(context, speaker, glossary) {
|
|
|
249
249
|
if (!own.ok)
|
|
250
250
|
return {
|
|
251
251
|
ok: false,
|
|
252
|
-
reason: `${speaker.name.trim()}'s pronunciations can't be used: ${own.reason.replace("Edit the glossary or turn off Use Pronunciation Glossary.", "Fix them under Speakers (Play →
|
|
252
|
+
reason: `${speaker.name.trim()}'s pronunciations can't be used: ${own.reason.replace("Edit the glossary or turn off Use Pronunciation Glossary.", "Fix them under Speakers (Play → Narration, or Edit project → Providers).")}`,
|
|
253
253
|
};
|
|
254
254
|
const run = context.config.audio?.usePronunciationGlossary === true && glossary?.ok === true
|
|
255
255
|
? glossary.entries
|
|
@@ -71,7 +71,22 @@ export function createReviewRedos() {
|
|
|
71
71
|
kick,
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
+
// The verdicts whose redo is being started right now. Overrule may call off a redo only while
|
|
75
|
+
// it waits, not once Slopify has begun making the item again.
|
|
76
|
+
const starting = new Set();
|
|
77
|
+
export function redoStarting(verdictId) {
|
|
78
|
+
return starting.has(verdictId);
|
|
79
|
+
}
|
|
74
80
|
export async function startRedo(deps, verdict) {
|
|
81
|
+
starting.add(verdict.id);
|
|
82
|
+
try {
|
|
83
|
+
await makeAgain(deps, verdict);
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
starting.delete(verdict.id);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function makeAgain(deps, verdict) {
|
|
75
90
|
const base = currentRevisionId(deps.db, verdict.projectId);
|
|
76
91
|
if (base === undefined) {
|
|
77
92
|
setRedoState(deps.db, verdict.id, "failed", "The project no longer exists.");
|
|
@@ -17,7 +17,7 @@ export async function exportBed(deps, context, view) {
|
|
|
17
17
|
};
|
|
18
18
|
if (bed.source !== "upload")
|
|
19
19
|
return { source: { kind: "noise", preset: bed.source }, ...settings };
|
|
20
|
-
const fix = "Make the video again from Play with another file under
|
|
20
|
+
const fix = "Make the video again from Play with another file under Video and style → Ambient sound, or pick Rain, Fireplace or Wind there instead.";
|
|
21
21
|
const assetId = view.revision.content.ambientBed;
|
|
22
22
|
const row = z
|
|
23
23
|
.object({ path: z.string() })
|
|
@@ -8,7 +8,7 @@ import { latestVerdict, saveVerdict } from "../reviews/repo.js";
|
|
|
8
8
|
import { parseVerdict, reviewMessages } from "../reviews/verdict.js";
|
|
9
9
|
import { pickedShortsOf } from "../shorts/clips.js";
|
|
10
10
|
import { outputPath } from "../storage/layout.js";
|
|
11
|
-
import { exportSnapshot } from "./runtime-export-inputs.js";
|
|
11
|
+
import { exportSnapshot, revisionTranscript, } from "./runtime-export-inputs.js";
|
|
12
12
|
import { savedCatalogue } from "./runtime-plan.js";
|
|
13
13
|
import { publishResult } from "./runtime-publication.js";
|
|
14
14
|
import { wordsSchema } from "./runtime-subtitles.js";
|
|
@@ -46,8 +46,8 @@ export async function executeReviewRecipe(deps, context, providers, piece) {
|
|
|
46
46
|
if (values === undefined || !values.success)
|
|
47
47
|
throw new Error("Slopify hit an internal error (a review step was set up wrongly). Try again; if it happens again, use Download diagnostics in Settings and report it.");
|
|
48
48
|
const [stage, itemKey, , provider, model, thinking, prompt, mode, retries] = values.data;
|
|
49
|
-
const
|
|
50
|
-
const material = reviewMaterial(deps, context,
|
|
49
|
+
const snapshot = exportSnapshot(deps, context, piece);
|
|
50
|
+
const material = reviewMaterial(deps, context, snapshot, stage, itemKey, prompt);
|
|
51
51
|
const previous = latestVerdict(deps.db, context.work.projectId, itemKey);
|
|
52
52
|
const attempt = nextAttempt(previous, material.itemFingerprint);
|
|
53
53
|
const row = deps.db
|
|
@@ -102,6 +102,16 @@ export async function executeReviewRecipe(deps, context, providers, piece) {
|
|
|
102
102
|
outcome: saved.outcome,
|
|
103
103
|
attempt: saved.attempt,
|
|
104
104
|
});
|
|
105
|
+
// Kept and flagged: nothing more happens to the item until someone decides.
|
|
106
|
+
if (saved.outcome === "flagged")
|
|
107
|
+
context.emit({
|
|
108
|
+
type: "review.flagged",
|
|
109
|
+
projectId: context.work.projectId,
|
|
110
|
+
verdictId: saved.id,
|
|
111
|
+
stage: saved.stage,
|
|
112
|
+
itemKey: saved.itemKey,
|
|
113
|
+
...(saved.reasons[0] === undefined ? {} : { reason: saved.reasons[0] }),
|
|
114
|
+
});
|
|
105
115
|
deps.count?.("stage.completed", {
|
|
106
116
|
provider,
|
|
107
117
|
model,
|
|
@@ -110,7 +120,27 @@ export async function executeReviewRecipe(deps, context, providers, piece) {
|
|
|
110
120
|
});
|
|
111
121
|
return "done";
|
|
112
122
|
}
|
|
113
|
-
|
|
123
|
+
// What the voice was asked to say, segment by segment, as the word timing matched it: the intro
|
|
124
|
+
// and outro, and the prepared or hand-edited spoken text rather than the article as written.
|
|
125
|
+
// Falls back to the article when those texts can't be read.
|
|
126
|
+
function spokenText(deps, snapshot) {
|
|
127
|
+
const present = ["intro", "body", "outro"].filter((kind) => snapshot.view.outputs.some((one) => one.selected &&
|
|
128
|
+
one.available &&
|
|
129
|
+
one.state === "ready" &&
|
|
130
|
+
one.output.role === `audio_${kind}`));
|
|
131
|
+
try {
|
|
132
|
+
const text = present.map((kind) => revisionTranscript(deps, snapshot, kind)).join("\n\n");
|
|
133
|
+
if (text.trim() !== "")
|
|
134
|
+
return text;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// The article below is still a fair reference.
|
|
138
|
+
}
|
|
139
|
+
return plainText(snapshot.view.articleMarkdown ?? "");
|
|
140
|
+
}
|
|
141
|
+
function reviewMaterial(deps, context, snapshot, stage, itemKey, prompt) {
|
|
142
|
+
const { view } = snapshot;
|
|
143
|
+
const recipes = snapshot.plan.recipes;
|
|
114
144
|
const config = view.revision.config;
|
|
115
145
|
const output = (workKey, role) => view.outputs.find((row) => row.workKey === workKey &&
|
|
116
146
|
row.selected &&
|
|
@@ -186,7 +216,7 @@ function reviewMaterial(deps, context, view, recipes, stage, itemKey, prompt) {
|
|
|
186
216
|
request: request([
|
|
187
217
|
{
|
|
188
218
|
label: "Text the narration was read from",
|
|
189
|
-
text: clip(
|
|
219
|
+
text: clip(spokenText(deps, snapshot)),
|
|
190
220
|
},
|
|
191
221
|
{ label: "What the timing heard", text: clip(words.map((word) => word.text).join(" ")) },
|
|
192
222
|
{
|