@gentbajko/slopify 0.8.3 → 1.0.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/README.md +28 -6
- package/SUBTITLES.md +2 -0
- package/dist/adapters/alignment/window.js +46 -5
- package/dist/adapters/alignment/worker.js +2 -2
- package/dist/adapters/llm/claude-code.js +6 -3
- package/dist/adapters/llm/codex.js +102 -12
- package/dist/adapters/llm/gemini.js +11 -8
- package/dist/adapters/llm/run-cli.js +61 -6
- package/dist/adapters/tts/inworld-async.js +4 -4
- package/dist/adapters/tts/inworld.js +73 -68
- package/dist/catalog/registry.js +13 -73
- package/dist/edge/cli.js +5 -2
- package/dist/edge/events/hub.js +7 -2
- package/dist/edge/events/preview-cache.js +7 -4
- package/dist/edge/events/visibility.js +33 -0
- package/dist/edge/http/actions.js +74 -79
- package/dist/edge/http/app.js +39 -6
- package/dist/edge/http/audio-preview.js +9 -1
- package/dist/edge/http/checkpoints.js +95 -0
- package/dist/edge/http/diagnostics.js +31 -0
- package/dist/edge/http/draft-files.js +69 -0
- package/dist/edge/http/draft-problem.js +15 -0
- package/dist/edge/http/drafts.js +95 -0
- package/dist/edge/http/multipart.js +86 -0
- package/dist/edge/http/mutations.js +49 -0
- package/dist/edge/http/planning.js +12 -1
- package/dist/edge/http/project-templates.js +74 -0
- package/dist/edge/http/projects.js +19 -2
- package/dist/edge/http/revision-files.js +84 -0
- package/dist/edge/http/revisions.js +103 -0
- package/dist/edge/http/schedules.js +100 -0
- package/dist/edge/http/staging.js +11 -52
- package/dist/edge/http/storage.js +110 -0
- package/dist/edge/http/subtitles.js +9 -79
- package/dist/edge/http/tutorial.js +26 -0
- package/dist/edge/signal-shutdown.js +23 -0
- package/dist/kernel/audio-preview.js +11 -5
- package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
- package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
- package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
- package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
- package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
- package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
- package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
- package/dist/kernel/ports/model.js +5 -0
- package/dist/kernel/runner/attempt-repo.js +15 -0
- package/dist/kernel/runner/attempt.js +14 -2
- package/dist/kernel/runner/checkpoint-authority.js +23 -0
- package/dist/kernel/runner/index.js +35 -11
- package/dist/kernel/runner/providers.js +18 -8
- package/dist/kernel/runner/work-attempt.js +47 -0
- package/dist/kernel/runner/work-authority.js +37 -0
- package/dist/kernel/runner/work.js +23 -0
- package/dist/main.js +227 -69
- package/dist/slices/admission/repo.js +5 -56
- package/dist/slices/admission/schema.js +64 -0
- package/dist/slices/admission/start.js +13 -4
- package/dist/slices/article/continuation.js +9 -3
- package/dist/slices/article/provided-entries.js +7 -3
- package/dist/slices/article/run.js +20 -7
- package/dist/slices/article/segments.js +13 -4
- package/dist/slices/batch/index.js +8 -11
- package/dist/slices/cancel/index.js +78 -34
- package/dist/slices/checkpoints/change.js +250 -0
- package/dist/slices/checkpoints/fingerprint.js +33 -0
- package/dist/slices/checkpoints/index.js +2 -0
- package/dist/slices/checkpoints/model.js +1 -0
- package/dist/slices/checkpoints/recovery.js +116 -0
- package/dist/slices/checkpoints/repo.js +132 -0
- package/dist/slices/checkpoints/rules.js +30 -0
- package/dist/slices/checkpoints/schema.js +59 -0
- package/dist/slices/control/index.js +88 -22
- package/dist/slices/control/providers.js +3 -2
- package/dist/slices/control/revision-control-schema.js +7 -0
- package/dist/slices/control/revision-control.js +59 -0
- package/dist/slices/estimate/index.js +77 -59
- package/dist/slices/estimate/requests.js +102 -0
- package/dist/slices/images/run.js +14 -3
- package/dist/slices/library/slots.js +11 -11
- package/dist/slices/library/snapshot.js +33 -0
- package/dist/slices/narration/legacy-plan.js +58 -0
- package/dist/slices/narration/live.js +2 -2
- package/dist/slices/narration/plan.js +48 -54
- package/dist/slices/narration/run.js +21 -8
- package/dist/slices/play-drafts/convert.js +120 -0
- package/dist/slices/play-drafts/model.js +1 -0
- package/dist/slices/play-drafts/readiness.js +80 -0
- package/dist/slices/play-drafts/repo.js +76 -0
- package/dist/slices/play-drafts/review-inputs.js +150 -0
- package/dist/slices/play-drafts/review.js +109 -0
- package/dist/slices/play-drafts/schema.js +218 -0
- package/dist/slices/play-drafts/service.js +242 -0
- package/dist/slices/play-drafts/start-repo.js +113 -0
- package/dist/slices/play-drafts/start.js +148 -0
- package/dist/slices/play-drafts/uploads.js +67 -0
- package/dist/slices/project-templates/from-project.js +159 -0
- package/dist/slices/project-templates/model.js +1 -0
- package/dist/slices/project-templates/repo.js +41 -0
- package/dist/slices/project-templates/schema.js +37 -0
- package/dist/slices/project-templates/service.js +136 -0
- package/dist/slices/project-templates/setup.js +55 -0
- package/dist/slices/rebuild/admission-repo.js +120 -0
- package/dist/slices/rebuild/dependencies.js +33 -0
- package/dist/slices/rebuild/model.js +94 -0
- package/dist/slices/rebuild/narration-history.js +43 -0
- package/dist/slices/rebuild/narration-reuse.js +20 -0
- package/dist/slices/rebuild/preview-details.js +144 -0
- package/dist/slices/rebuild/preview-plan.js +223 -0
- package/dist/slices/rebuild/preview-retained.js +128 -0
- package/dist/slices/rebuild/provided-review.js +32 -0
- package/dist/slices/rebuild/recipe-audio.js +172 -0
- package/dist/slices/rebuild/recipe-build.js +17 -0
- package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
- package/dist/slices/rebuild/recipe-exports.js +65 -0
- package/dist/slices/rebuild/recipe-fixture.js +150 -0
- package/dist/slices/rebuild/recipe-input-schema.js +82 -0
- package/dist/slices/rebuild/recipe-legacy.js +135 -0
- package/dist/slices/rebuild/recipe-model.js +77 -0
- package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
- package/dist/slices/rebuild/recipe-save.js +221 -0
- package/dist/slices/rebuild/recipe-text.js +170 -0
- package/dist/slices/rebuild/recipe-validation.js +86 -0
- package/dist/slices/rebuild/recipe-visual.js +111 -0
- package/dist/slices/rebuild/recipe-work.js +212 -0
- package/dist/slices/rebuild/recipes.js +4 -0
- package/dist/slices/rebuild/repo.js +64 -0
- package/dist/slices/rebuild/runtime-actions.js +76 -0
- package/dist/slices/rebuild/runtime-admission.js +71 -0
- package/dist/slices/rebuild/runtime-article.js +117 -0
- package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
- package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
- package/dist/slices/rebuild/runtime-export.js +139 -0
- package/dist/slices/rebuild/runtime-instructions.js +51 -0
- package/dist/slices/rebuild/runtime-local.js +137 -0
- package/dist/slices/rebuild/runtime-materialize.js +105 -0
- package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
- package/dist/slices/rebuild/runtime-plan.js +103 -0
- package/dist/slices/rebuild/runtime-provider.js +184 -0
- package/dist/slices/rebuild/runtime-publication.js +172 -0
- package/dist/slices/rebuild/runtime-run.js +35 -0
- package/dist/slices/rebuild/runtime-store.js +166 -0
- package/dist/slices/rebuild/runtime-subtitles.js +202 -0
- package/dist/slices/rebuild/service-readiness.js +110 -0
- package/dist/slices/rebuild/service.js +149 -0
- package/dist/slices/rebuild/transition-repo.js +108 -0
- package/dist/slices/rebuild/work-records.js +44 -0
- package/dist/slices/reruns/index.js +5 -0
- package/dist/slices/research/run.js +33 -12
- package/dist/slices/revisions/adopt-assets.js +31 -0
- package/dist/slices/revisions/adopt-content.js +112 -0
- package/dist/slices/revisions/adopt.js +100 -0
- package/dist/slices/revisions/downloads.js +80 -0
- package/dist/slices/revisions/index.js +5 -0
- package/dist/slices/revisions/manifest-repo.js +125 -0
- package/dist/slices/revisions/model.js +1 -0
- package/dist/slices/revisions/mutation-assets.js +143 -0
- package/dist/slices/revisions/mutation-cues.js +95 -0
- package/dist/slices/revisions/mutation-prepare.js +182 -0
- package/dist/slices/revisions/mutation-request.js +58 -0
- package/dist/slices/revisions/mutation-work.js +47 -0
- package/dist/slices/revisions/mutations.js +194 -0
- package/dist/slices/revisions/projection.js +82 -0
- package/dist/slices/revisions/publication-model.js +1 -0
- package/dist/slices/revisions/publication-rules.js +166 -0
- package/dist/slices/revisions/publish.js +115 -0
- package/dist/slices/revisions/repo.js +83 -0
- package/dist/slices/revisions/restore.js +60 -0
- package/dist/slices/revisions/rules.js +99 -0
- package/dist/slices/revisions/schema.js +160 -0
- package/dist/slices/revisions/view.js +44 -0
- package/dist/slices/schedules/calendar.js +47 -0
- package/dist/slices/schedules/model.js +1 -0
- package/dist/slices/schedules/repo.js +281 -0
- package/dist/slices/schedules/scheduler.js +144 -0
- package/dist/slices/schedules/schema.js +80 -0
- package/dist/slices/schedules/service.js +178 -0
- package/dist/slices/settings/cli-paths.js +1 -1
- package/dist/slices/settings/cli-status.js +45 -5
- package/dist/slices/settings/tutorial-schema.js +39 -0
- package/dist/slices/settings/tutorial.js +66 -0
- package/dist/slices/storage/assets.js +108 -0
- package/dist/slices/storage/delete-project.js +2 -1
- package/dist/slices/storage/downloads.js +4 -1
- package/dist/slices/storage/portable.js +871 -0
- package/dist/slices/storage/prepare.js +67 -0
- package/dist/slices/storage/reconcile.js +25 -5
- package/dist/slices/storage/repo.js +2 -15
- package/dist/slices/storage/schema.js +35 -0
- package/dist/slices/storage/staging-refs.js +26 -0
- package/dist/slices/storage/staging.js +16 -11
- package/dist/slices/thumbnail/run.js +26 -9
- package/dist/slices/video/audio-export-args.js +33 -0
- package/dist/slices/video/audio-export.js +13 -36
- package/dist/slices/video/audio-inputs.js +4 -3
- package/dist/slices/video/ffmpeg.js +6 -0
- package/dist/slices/video/reuse-audio.js +11 -4
- package/dist/updater/install-flow.js +27 -2
- package/dist/updater/plan.js +28 -2
- package/dist/updater/service.js +27 -2
- package/dist/updater/worker.js +2 -1
- package/dist/web/assets/index-CO7DnVX9.js +137 -0
- package/dist/web/assets/index-DX1_chKs.css +1 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/adapters/tts/inworld-text.js +0 -32
- package/dist/web/assets/index-Ce2HApus.css +0 -1
- package/dist/web/assets/index-xbD0MZOY.js +0 -130
|
@@ -1,81 +1,99 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { estimateRequests, groupEstimateRows } from "./requests.js";
|
|
3
|
+
export { estimateRequests } from "./requests.js";
|
|
1
4
|
export function estimateRun(draft, rendered, expectedWords, catalogue) {
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
:
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
z.number().finite().nonnegative().parse(expectedWords);
|
|
6
|
+
const data = catalogue?.read() ?? {
|
|
7
|
+
schemaVersion: 1,
|
|
8
|
+
updatedAt: "",
|
|
9
|
+
providers: {},
|
|
10
|
+
llm: [],
|
|
11
|
+
tts: [],
|
|
12
|
+
image: [],
|
|
13
|
+
};
|
|
14
|
+
const requests = [];
|
|
15
|
+
const llm = draft.llm ?? { provider: "", model: "" };
|
|
16
|
+
const tts = draft.audio ?? { provider: "", model: "" };
|
|
17
|
+
const image = draft.images ?? { provider: "", model: "" };
|
|
18
|
+
const textNote = data.llm.find((model) => model.provider === llm.provider &&
|
|
19
|
+
model.id === llm.model &&
|
|
20
|
+
model.enabled &&
|
|
21
|
+
!model.deprecated)?.pricing.note ??
|
|
22
|
+
"CLI subscription or model pricing is unavailable; usage may be billed by your account.";
|
|
23
|
+
const imageNote = data.image.find((model) => model.provider === image.provider &&
|
|
24
|
+
model.id === image.model &&
|
|
25
|
+
model.enabled &&
|
|
26
|
+
!model.deprecated)?.pricing.note ?? "Model or account pricing is unknown.";
|
|
9
27
|
const generatedArticle = draft.sources.article === "generate";
|
|
10
28
|
const articleChars = generatedArticle ? expectedWords * 6 : (draft.provided.article?.length ?? 0);
|
|
11
29
|
const promptChars = Object.values(rendered).reduce((sum, value) => sum + value.length, 0);
|
|
12
|
-
const
|
|
13
|
-
|
|
30
|
+
const local = (stage, detail) => {
|
|
31
|
+
requests.push({ kind: "local", stage, detail });
|
|
32
|
+
};
|
|
33
|
+
const text = (stage, inputCharacters, outputCharacters, detail) => {
|
|
34
|
+
requests.push({
|
|
35
|
+
kind: "llm",
|
|
14
36
|
stage,
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
37
|
+
provider: llm.provider,
|
|
38
|
+
model: llm.model,
|
|
39
|
+
inputCharacters,
|
|
40
|
+
outputCharacters,
|
|
41
|
+
detail: detail === undefined ? textNote : `${detail} ${textNote}`,
|
|
18
42
|
});
|
|
19
43
|
};
|
|
20
|
-
const textCost = (input, output) => {
|
|
21
|
-
const p = llm?.pricing;
|
|
22
|
-
return p?.inputPerMillionTokens === undefined || p.outputPerMillionTokens === undefined
|
|
23
|
-
? undefined
|
|
24
|
-
: ((input / 4) * p.inputPerMillionTokens + (output / 4) * p.outputPerMillionTokens) / 1000000;
|
|
25
|
-
};
|
|
26
|
-
const textNote = llm?.pricing.note ??
|
|
27
|
-
"CLI subscription or model pricing is unavailable; usage may be billed by your account.";
|
|
28
44
|
if (draft.sources.research === "generate")
|
|
29
|
-
|
|
45
|
+
text("Research", promptChars + 6000, 12000, "Assumes about 2,000 words of notes; web search fees are excluded.");
|
|
30
46
|
else
|
|
31
|
-
|
|
47
|
+
local("Research", "Provided or off; no generation charge.");
|
|
32
48
|
if (generatedArticle)
|
|
33
|
-
|
|
49
|
+
text("Article", promptChars + (draft.sources.research === "generate" ? 12000 : 0), articleChars, `${expectedWords.toLocaleString()} expected words.`);
|
|
34
50
|
else
|
|
35
|
-
|
|
51
|
+
local("Article", "Provided article; no generation charge.");
|
|
36
52
|
if (draft.intro?.mode === "llm" || draft.outro?.mode === "llm")
|
|
37
|
-
|
|
53
|
+
text("Intro / outro text", promptChars + articleChars, 2400);
|
|
38
54
|
if (draft.sources.audio === "generate") {
|
|
39
55
|
const extras = ["intro", "outro"].reduce((n, key) => n + (rendered[key]?.length ?? 0), 0);
|
|
40
56
|
const llmExtras = (draft.intro?.mode === "llm" ? 1200 : 0) + (draft.outro?.mode === "llm" ? 1200 : 0);
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
:
|
|
47
|
-
|
|
48
|
-
|
|
57
|
+
const choice = { stage: "Narration", provider: tts.provider, model: tts.model };
|
|
58
|
+
requests.push(generatedArticle || llmExtras > 0
|
|
59
|
+
? { ...choice, kind: "tts-estimate", characters: articleChars + extras + llmExtras }
|
|
60
|
+
: {
|
|
61
|
+
...choice,
|
|
62
|
+
kind: "tts",
|
|
63
|
+
text: (draft.provided.article ?? "") + (rendered.intro ?? "") + (rendered.outro ?? ""),
|
|
64
|
+
});
|
|
49
65
|
}
|
|
50
66
|
else
|
|
51
|
-
|
|
52
|
-
const images = draft.sources.images === "generate"
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
67
|
+
local("Narration", "Provided or off; no generation charge.");
|
|
68
|
+
const images = draft.sources.images === "generate"
|
|
69
|
+
? draft.imagePrompts.reduce((n, prompt) => n + prompt.number, 0)
|
|
70
|
+
: 0;
|
|
71
|
+
z.number().int().nonnegative().parse(images);
|
|
72
|
+
for (let index = 0; index < images; index++)
|
|
73
|
+
requests.push({
|
|
74
|
+
kind: "image",
|
|
75
|
+
stage: "Images",
|
|
76
|
+
provider: image.provider,
|
|
77
|
+
model: image.model,
|
|
78
|
+
detail: `${images} images. ${imageNote}`,
|
|
79
|
+
});
|
|
80
|
+
if (images === 0)
|
|
81
|
+
local("Images", "Provided or off.");
|
|
82
|
+
if (["from_prompt", "prompt_by_llm"].includes(draft.sources.thumbnail))
|
|
83
|
+
requests.push({
|
|
84
|
+
kind: "image",
|
|
85
|
+
stage: "Thumbnail",
|
|
86
|
+
provider: image.provider,
|
|
87
|
+
model: image.model,
|
|
88
|
+
});
|
|
89
|
+
else
|
|
90
|
+
local("Thumbnail", "Provided or off.");
|
|
64
91
|
if (draft.sources.thumbnail === "prompt_by_llm")
|
|
65
|
-
|
|
66
|
-
|
|
92
|
+
text("Thumbnail prompt", promptChars + articleChars, 1200);
|
|
93
|
+
local("Export / subtitles", "Local processing; no API fee.");
|
|
67
94
|
return {
|
|
68
|
-
|
|
69
|
-
rows,
|
|
95
|
+
...groupEstimateRows(estimateRequests(requests, data)),
|
|
70
96
|
expectedWords,
|
|
71
|
-
catalogueDate: catalogue
|
|
72
|
-
low: rows.reduce((n, r) => n + (r.low ?? 0), 0),
|
|
73
|
-
high: rows.reduce((n, r) => n + (r.high ?? 0), 0),
|
|
74
|
-
unknown: rows.filter((r) => r.low === null).length,
|
|
75
|
-
assumptions: [
|
|
76
|
-
"Planning estimate, not a spending limit. Generated lengths use a ±50% range; actual output can exceed it.",
|
|
77
|
-
"Assumes roughly 6 characters per word, 4 characters per token, and 150 spoken words per minute.",
|
|
78
|
-
"Retries, extra reasoning tokens, web tools, taxes, discounts and included credits are excluded. Unknown charges are not counted as zero.",
|
|
79
|
-
],
|
|
97
|
+
catalogueDate: catalogue === undefined ? null : data.updatedAt,
|
|
80
98
|
};
|
|
81
99
|
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const quantity = z.number().finite().nonnegative();
|
|
3
|
+
const common = z.object({
|
|
4
|
+
stage: z.string(),
|
|
5
|
+
detail: z.string().optional(),
|
|
6
|
+
variable: z.boolean().optional(),
|
|
7
|
+
expectedWords: quantity.optional(),
|
|
8
|
+
});
|
|
9
|
+
const provider = common.extend({ provider: z.string(), model: z.string() });
|
|
10
|
+
const requestSchema = z.discriminatedUnion("kind", [
|
|
11
|
+
provider
|
|
12
|
+
.extend({ kind: z.literal("llm"), inputCharacters: quantity, outputCharacters: quantity })
|
|
13
|
+
.strict(),
|
|
14
|
+
provider.extend({ kind: z.literal("tts"), text: z.string() }).strict(),
|
|
15
|
+
provider.extend({ kind: z.literal("tts-estimate"), characters: quantity }).strict(),
|
|
16
|
+
provider.extend({ kind: z.literal("image") }).strict(),
|
|
17
|
+
common.extend({ kind: z.literal("local") }).strict(),
|
|
18
|
+
]);
|
|
19
|
+
export function estimateRequests(requests, catalogue) {
|
|
20
|
+
const parsed = z.array(requestSchema).parse(requests);
|
|
21
|
+
const rows = parsed.map((request) => price(request, catalogue));
|
|
22
|
+
return {
|
|
23
|
+
currency: "USD",
|
|
24
|
+
rows,
|
|
25
|
+
low: rows.reduce((total, row) => total + (row.low ?? 0), 0),
|
|
26
|
+
high: rows.reduce((total, row) => total + (row.high ?? 0), 0),
|
|
27
|
+
unknown: rows.filter((row) => row.low === null).length,
|
|
28
|
+
expectedWords: parsed.reduce((total, request) => total + (request.expectedWords ?? 0), 0),
|
|
29
|
+
catalogueDate: catalogue.updatedAt,
|
|
30
|
+
assumptions: [
|
|
31
|
+
"Planning estimate, not a spending limit. Generated lengths use a ±50% range; actual output can exceed it.",
|
|
32
|
+
"Assumes roughly 6 characters per word, 4 characters per token, and 150 spoken words per minute.",
|
|
33
|
+
"Retries, extra reasoning tokens, web tools, taxes, discounts and included credits are excluded. Unknown charges are not counted as zero.",
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function groupEstimateRows(estimate) {
|
|
38
|
+
const grouped = new Map();
|
|
39
|
+
for (const row of estimate.rows) {
|
|
40
|
+
const previous = grouped.get(row.stage);
|
|
41
|
+
grouped.set(row.stage, previous === undefined
|
|
42
|
+
? row
|
|
43
|
+
: {
|
|
44
|
+
...previous,
|
|
45
|
+
low: previous.low === null || row.low === null ? null : previous.low + row.low,
|
|
46
|
+
high: previous.high === null || row.high === null ? null : previous.high + row.high,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const rows = [...grouped.values()];
|
|
50
|
+
return { ...estimate, rows, unknown: rows.filter((row) => row.low === null).length };
|
|
51
|
+
}
|
|
52
|
+
function price(request, catalogue) {
|
|
53
|
+
if (request.kind === "local")
|
|
54
|
+
return {
|
|
55
|
+
stage: request.stage,
|
|
56
|
+
low: 0,
|
|
57
|
+
high: 0,
|
|
58
|
+
detail: request.detail ?? "Local processing or retained output; no API fee.",
|
|
59
|
+
};
|
|
60
|
+
const family = request.kind === "tts-estimate" ? "tts" : request.kind;
|
|
61
|
+
const model = catalogue[family].find((entry) => entry.provider === request.provider &&
|
|
62
|
+
entry.id === request.model &&
|
|
63
|
+
entry.enabled &&
|
|
64
|
+
!entry.deprecated);
|
|
65
|
+
const rates = model?.pricing;
|
|
66
|
+
let amount;
|
|
67
|
+
let variable = request.variable === true;
|
|
68
|
+
let detail = rates?.note ?? "Model or account pricing is unknown.";
|
|
69
|
+
switch (request.kind) {
|
|
70
|
+
case "llm":
|
|
71
|
+
if (rates?.inputPerMillionTokens !== undefined && rates.outputPerMillionTokens !== undefined)
|
|
72
|
+
amount =
|
|
73
|
+
((request.inputCharacters / 4) * rates.inputPerMillionTokens +
|
|
74
|
+
(request.outputCharacters / 4) * rates.outputPerMillionTokens) /
|
|
75
|
+
1000000;
|
|
76
|
+
variable = true;
|
|
77
|
+
break;
|
|
78
|
+
case "tts":
|
|
79
|
+
case "tts-estimate": {
|
|
80
|
+
const characters = request.kind === "tts" ? request.text.length : request.characters;
|
|
81
|
+
const perMinute = rates?.perMillionCharacters === undefined && rates?.perMinute !== undefined;
|
|
82
|
+
if (rates?.perMillionCharacters !== undefined)
|
|
83
|
+
amount = (characters / 1000000) * rates.perMillionCharacters;
|
|
84
|
+
else if (rates?.perMinute !== undefined)
|
|
85
|
+
amount = (characters / 6 / 150) * rates.perMinute;
|
|
86
|
+
variable ||= request.kind === "tts-estimate" || perMinute;
|
|
87
|
+
detail = `About ${characters.toLocaleString()} characters. ${detail}`;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "image":
|
|
91
|
+
amount = rates?.perImage;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
if (amount !== undefined)
|
|
95
|
+
quantity.parse(amount);
|
|
96
|
+
return {
|
|
97
|
+
stage: request.stage,
|
|
98
|
+
low: amount === undefined ? null : amount * (variable ? 0.5 : 1),
|
|
99
|
+
high: amount === undefined ? null : amount * (variable ? 1.5 : 1),
|
|
100
|
+
detail: request.detail ?? detail,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
@@ -38,7 +38,10 @@ export async function runImages(deps, context, providers) {
|
|
|
38
38
|
}
|
|
39
39
|
// The run's own frame. The adapter turns it into whatever its provider spells the
|
|
40
40
|
// closest supported size, and the render crops whatever is left over.
|
|
41
|
-
const
|
|
41
|
+
const result = await sendAll(deps, context, providers, choice, pieces, project.format);
|
|
42
|
+
if (!result.ok)
|
|
43
|
+
return "held";
|
|
44
|
+
const made = result.value;
|
|
42
45
|
// Images made means images stored, and only the ones this run stored: a resume does not
|
|
43
46
|
// count an image the previous run made, and a regenerated image counts again.
|
|
44
47
|
//
|
|
@@ -56,6 +59,7 @@ export async function runImages(deps, context, providers) {
|
|
|
56
59
|
stage: "images",
|
|
57
60
|
detail: `${String(pieces.length)} images from ${String(project.config.imagePrompts.length)} prompts`,
|
|
58
61
|
});
|
|
62
|
+
return "done";
|
|
59
63
|
}
|
|
60
64
|
// For each ticked image prompt, its rendered text is sent Number times as independent
|
|
61
65
|
// parallel calls. The plan is made once and kept, so a retry finds the rows the first run
|
|
@@ -117,12 +121,17 @@ async function sendAll(deps, context, providers, choice, pieces, format) {
|
|
|
117
121
|
}
|
|
118
122
|
setPiece(deps.db, piece.id, "running", piece.payload);
|
|
119
123
|
try {
|
|
120
|
-
const
|
|
124
|
+
const result = await providers.forPiece(piece.id).image({
|
|
121
125
|
provider: choice.provider,
|
|
122
126
|
model: choice.model,
|
|
123
127
|
prompt: asked.prompt,
|
|
124
128
|
aspect: format,
|
|
125
129
|
});
|
|
130
|
+
if (!result.ok) {
|
|
131
|
+
setPiece(deps.db, piece.id, "pending", piece.payload);
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
const made = result.value;
|
|
126
135
|
const file = keep(deps, projectId, piece.idx, made);
|
|
127
136
|
// The prompt text, the prompt name, the index within the prompt, the
|
|
128
137
|
// provider and the model travel with the image.
|
|
@@ -146,11 +155,13 @@ async function sendAll(deps, context, providers, choice, pieces, format) {
|
|
|
146
155
|
let made = 0;
|
|
147
156
|
for (const outcome of outcomes) {
|
|
148
157
|
if (!outcome.ok) {
|
|
158
|
+
if ("reason" in outcome)
|
|
159
|
+
return outcome;
|
|
149
160
|
throw outcome.error;
|
|
150
161
|
}
|
|
151
162
|
made += outcome.made ? 1 : 0;
|
|
152
163
|
}
|
|
153
|
-
return made;
|
|
164
|
+
return { ok: true, value: made };
|
|
154
165
|
}
|
|
155
166
|
// An image counts as landed only when its row and its file are both still there: they are
|
|
156
167
|
// written one after the other, the boot reconcile can remove a file whose row survived,
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// is the one that runs.
|
|
5
5
|
import { normaliseDraft } from "../admission/rules.js";
|
|
6
6
|
import { collectFields, render } from "../admission/substitute.js";
|
|
7
|
-
import {
|
|
8
|
-
export function pickTemplates(db, input) {
|
|
7
|
+
import { snapshotEntry, snapshotPrompt } from "./snapshot.js";
|
|
8
|
+
export function pickTemplates(db, input, snapshot) {
|
|
9
9
|
const draft = normaliseDraft(input);
|
|
10
10
|
const { sources } = draft;
|
|
11
11
|
const missing = [];
|
|
@@ -14,22 +14,22 @@ export function pickTemplates(db, input) {
|
|
|
14
14
|
const text = [];
|
|
15
15
|
const image = [];
|
|
16
16
|
if (sources.article === "generate") {
|
|
17
|
-
body(db, "article", draft.articlePrompt, "articlePrompt", missing, text, "article");
|
|
17
|
+
body(db, "article", draft.articlePrompt, "articlePrompt", missing, text, "article", snapshot);
|
|
18
18
|
}
|
|
19
|
-
const intro = pickEntry(db, "intro", draft.intro, missing);
|
|
20
|
-
const outro = pickEntry(db, "outro", draft.outro, missing);
|
|
19
|
+
const intro = pickEntry(db, "intro", draft.intro, missing, snapshot);
|
|
20
|
+
const outro = pickEntry(db, "outro", draft.outro, missing, snapshot);
|
|
21
21
|
push(text, "intro", intro);
|
|
22
22
|
push(text, "outro", outro);
|
|
23
23
|
if (sources.images === "generate") {
|
|
24
24
|
for (const [index, picked] of draft.imagePrompts.entries()) {
|
|
25
25
|
const field = `imagePrompts.${String(index)}.name`;
|
|
26
|
-
body(db, "image", picked.name, field, missing, image, `imagePrompts.${String(index)}
|
|
26
|
+
body(db, "image", picked.name, field, missing, image, `imagePrompts.${String(index)}`, snapshot);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
// Both Generate modes read the same thumbnail template; Prompt by LLM sends it as the
|
|
30
30
|
// instruction rather than to the image provider.
|
|
31
31
|
if (sources.thumbnail === "from_prompt" || sources.thumbnail === "prompt_by_llm") {
|
|
32
|
-
body(db, "thumbnail", draft.thumbnailPrompt, "thumbnailPrompt", missing, image, "thumbnailPrompt");
|
|
32
|
+
body(db, "thumbnail", draft.thumbnailPrompt, "thumbnailPrompt", missing, image, "thumbnailPrompt", snapshot);
|
|
33
33
|
}
|
|
34
34
|
const fields = collectFields(text.map((picked) => picked.body), image.map((picked) => picked.body));
|
|
35
35
|
return {
|
|
@@ -48,24 +48,24 @@ export function renderPicked(picked, values) {
|
|
|
48
48
|
}
|
|
49
49
|
return rendered;
|
|
50
50
|
}
|
|
51
|
-
function body(db, kind, name, field, missing, into, key) {
|
|
51
|
+
function body(db, kind, name, field, missing, into, key, snapshot) {
|
|
52
52
|
// An unpicked prompt is admission's rule to state ("Pick an article prompt."), so this
|
|
53
53
|
// says nothing about it and the form marks the field once.
|
|
54
54
|
if (name === undefined || name.trim() === "") {
|
|
55
55
|
return;
|
|
56
56
|
}
|
|
57
|
-
const prompt =
|
|
57
|
+
const prompt = snapshotPrompt(db, snapshot, kind, name);
|
|
58
58
|
if (prompt === undefined) {
|
|
59
59
|
missing.push({ field, message: `That ${kind} prompt no longer exists; pick another.` });
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
into.push({ key, body: prompt.body });
|
|
63
63
|
}
|
|
64
|
-
function pickEntry(db, category, choice, missing) {
|
|
64
|
+
function pickEntry(db, category, choice, missing, snapshot) {
|
|
65
65
|
if (choice === undefined) {
|
|
66
66
|
return undefined;
|
|
67
67
|
}
|
|
68
|
-
const entry =
|
|
68
|
+
const entry = snapshotEntry(db, snapshot, category, choice.name);
|
|
69
69
|
if (entry === undefined) {
|
|
70
70
|
missing.push({
|
|
71
71
|
field: category,
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { entryCategories, entryModes, promptKinds } from "./model.js";
|
|
3
|
+
import { entryByName, promptByName } from "./repo.js";
|
|
4
|
+
const base = z.object({
|
|
5
|
+
id: z.string(),
|
|
6
|
+
name: z.string(),
|
|
7
|
+
body: z.string(),
|
|
8
|
+
slots: z.array(z.string()).readonly(),
|
|
9
|
+
updatedAt: z.string(),
|
|
10
|
+
});
|
|
11
|
+
export const librarySnapshotSchema = z
|
|
12
|
+
.object({
|
|
13
|
+
prompts: z
|
|
14
|
+
.array(base
|
|
15
|
+
.extend({ kind: z.enum(promptKinds) })
|
|
16
|
+
.strict()
|
|
17
|
+
.readonly())
|
|
18
|
+
.readonly(),
|
|
19
|
+
entries: z
|
|
20
|
+
.array(base
|
|
21
|
+
.extend({ category: z.enum(entryCategories), mode: z.enum(entryModes) })
|
|
22
|
+
.strict()
|
|
23
|
+
.readonly())
|
|
24
|
+
.readonly(),
|
|
25
|
+
})
|
|
26
|
+
.strict()
|
|
27
|
+
.readonly();
|
|
28
|
+
export function snapshotPrompt(db, snapshot, kind, name) {
|
|
29
|
+
return (snapshot?.prompts.find((row) => row.kind === kind && row.name.toLowerCase() === name.toLowerCase()) ?? promptByName(db, kind, name));
|
|
30
|
+
}
|
|
31
|
+
export function snapshotEntry(db, snapshot, category, name) {
|
|
32
|
+
return (snapshot?.entries.find((row) => row.category === category && row.name.toLowerCase() === name.toLowerCase()) ?? entryByName(db, category, name));
|
|
33
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { transact } from "../../kernel/db/tx.js";
|
|
3
|
+
import { splitText } from "../../kernel/ports/text.js";
|
|
4
|
+
import { insertPiece, piecesOf } from "../../kernel/runner/piece-repo.js";
|
|
5
|
+
const payload = z.object({ text: z.string(), file: z.string().optional() });
|
|
6
|
+
// Old whole-text plans can exceed a newly known account limit. Preserve finished
|
|
7
|
+
// pieces and their paths, and split only unfinished work before any request starts.
|
|
8
|
+
export function planLegacyNarration(deps, stageId, texts, maxCharacters, finished) {
|
|
9
|
+
const old = piecesOf(deps.db, stageId, "chunk");
|
|
10
|
+
const source = old.length
|
|
11
|
+
? old
|
|
12
|
+
: texts.map((text, idx) => ({
|
|
13
|
+
id: deps.ids.next(),
|
|
14
|
+
stageId,
|
|
15
|
+
kind: "chunk",
|
|
16
|
+
idx: idx + 1,
|
|
17
|
+
state: "pending",
|
|
18
|
+
payload: JSON.stringify({ text }),
|
|
19
|
+
}));
|
|
20
|
+
const planned = [];
|
|
21
|
+
for (const piece of source) {
|
|
22
|
+
const data = payload.parse(JSON.parse(piece.payload ?? "null"));
|
|
23
|
+
if (finished(piece)) {
|
|
24
|
+
planned.push({ ...piece, idx: planned.length + 1 });
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
for (const [part, text] of splitText(data.text, maxCharacters).entries())
|
|
28
|
+
planned.push({
|
|
29
|
+
...piece,
|
|
30
|
+
id: part === 0 ? piece.id : deps.ids.next(),
|
|
31
|
+
idx: planned.length + 1,
|
|
32
|
+
state: "pending",
|
|
33
|
+
payload: JSON.stringify({ text }),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (old.length &&
|
|
37
|
+
planned.length === old.length &&
|
|
38
|
+
planned.every((p, i) => p.id === old[i]?.id &&
|
|
39
|
+
payload.parse(JSON.parse(p.payload ?? "null")).text ===
|
|
40
|
+
payload.parse(JSON.parse(old[i]?.payload ?? "null")).text))
|
|
41
|
+
return old;
|
|
42
|
+
transact(deps.db, () => {
|
|
43
|
+
// Negative temporary indices avoid colliding with a retained completed piece.
|
|
44
|
+
deps.db
|
|
45
|
+
.prepare("UPDATE stage_pieces SET idx=-idx WHERE stage_id=? AND kind='chunk'")
|
|
46
|
+
.run(stageId);
|
|
47
|
+
const known = new Set(old.map((p) => p.id));
|
|
48
|
+
for (const piece of planned) {
|
|
49
|
+
if (known.has(piece.id))
|
|
50
|
+
deps.db
|
|
51
|
+
.prepare("UPDATE stage_pieces SET idx=?,state=?,payload=? WHERE id=?")
|
|
52
|
+
.run(piece.idx, piece.state, piece.payload, piece.id);
|
|
53
|
+
else
|
|
54
|
+
insertPiece(deps.db, piece);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
return planned;
|
|
58
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export function observeNarration(store, projectId, key, label) {
|
|
1
|
+
export function observeNarration(store, projectId, key, label, origin) {
|
|
2
2
|
if (store === undefined)
|
|
3
3
|
return undefined;
|
|
4
4
|
let sink;
|
|
5
5
|
return (event) => {
|
|
6
6
|
switch (event.type) {
|
|
7
7
|
case "start":
|
|
8
|
-
sink = store.begin(projectId, key, label);
|
|
8
|
+
sink = store.begin(projectId, key, label, origin);
|
|
9
9
|
break;
|
|
10
10
|
case "chunk":
|
|
11
11
|
sink?.append(event.bytes);
|
|
@@ -1,58 +1,52 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import { transact } from "../../kernel/db/tx.js";
|
|
3
1
|
import { splitText } from "../../kernel/ports/text.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
export function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
...piece,
|
|
30
|
-
id: part === 0 ? piece.id : deps.ids.next(),
|
|
31
|
-
idx: planned.length + 1,
|
|
32
|
-
state: "pending",
|
|
33
|
-
payload: JSON.stringify({ text }),
|
|
2
|
+
import { fingerprint } from "../../kernel/runner/work.js";
|
|
3
|
+
export function normalizeNarrationText(text) {
|
|
4
|
+
return text.replace(/\r\n?/g, "\n").trim();
|
|
5
|
+
}
|
|
6
|
+
export function narrationRequestFingerprint(input) {
|
|
7
|
+
return fingerprint([
|
|
8
|
+
"narration-request-v1",
|
|
9
|
+
input.provider,
|
|
10
|
+
input.model,
|
|
11
|
+
input.voice,
|
|
12
|
+
input.text,
|
|
13
|
+
null,
|
|
14
|
+
input.segment,
|
|
15
|
+
input.wholeText === null ? null : fingerprint(["whole-v1", input.wholeText]),
|
|
16
|
+
]);
|
|
17
|
+
}
|
|
18
|
+
export function planNarration(input) {
|
|
19
|
+
return input.groups.flatMap((group) => {
|
|
20
|
+
const text = normalizeNarrationText(group.text);
|
|
21
|
+
return splitText(text, input.maxCharacters).map((part, index) => {
|
|
22
|
+
const requestFingerprint = narrationRequestFingerprint({
|
|
23
|
+
...input,
|
|
24
|
+
text: part,
|
|
25
|
+
segment: group.segment,
|
|
26
|
+
wholeText: group.wholeRequest ? text : null,
|
|
34
27
|
});
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
deps.db
|
|
51
|
-
.prepare("UPDATE stage_pieces SET idx=?,state=?,payload=? WHERE id=?")
|
|
52
|
-
.run(piece.idx, piece.state, piece.payload, piece.id);
|
|
53
|
-
else
|
|
54
|
-
insertPiece(deps.db, piece);
|
|
55
|
-
}
|
|
28
|
+
const workFingerprint = fingerprint([requestFingerprint, group.regenerationToken]);
|
|
29
|
+
const retained = group.regenerationToken === null
|
|
30
|
+
? input.retained.find((one) => one.available && one.requestFingerprint === requestFingerprint)
|
|
31
|
+
: input.retained.find((one) => one.available && one.fingerprint === workFingerprint);
|
|
32
|
+
return {
|
|
33
|
+
key: `${group.key}:${index + 1}`,
|
|
34
|
+
logicalKey: group.key,
|
|
35
|
+
logicalText: text,
|
|
36
|
+
segment: group.segment,
|
|
37
|
+
text: part,
|
|
38
|
+
requestFingerprint,
|
|
39
|
+
fingerprint: workFingerprint,
|
|
40
|
+
assetId: retained?.assetId ?? null,
|
|
41
|
+
};
|
|
42
|
+
});
|
|
56
43
|
});
|
|
57
|
-
|
|
44
|
+
}
|
|
45
|
+
export function narrationRegenerationToken(tokens, logicalKey, segment) {
|
|
46
|
+
return tokens[logicalKey] ?? tokens[`audio:${segment}:future`] ?? null;
|
|
47
|
+
}
|
|
48
|
+
export function narrationRegenerationKey(key) {
|
|
49
|
+
return /^audio:(body:.+|intro|outro):[0-9]+$/.test(key)
|
|
50
|
+
? key.slice(0, key.lastIndexOf(":"))
|
|
51
|
+
: key;
|
|
58
52
|
}
|