@gentbajko/slopify 0.8.4 → 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.
Files changed (204) hide show
  1. package/README.md +28 -6
  2. package/dist/adapters/llm/claude-code.js +6 -3
  3. package/dist/adapters/llm/codex.js +102 -12
  4. package/dist/adapters/llm/gemini.js +11 -8
  5. package/dist/adapters/llm/run-cli.js +61 -6
  6. package/dist/adapters/tts/inworld-async.js +4 -4
  7. package/dist/adapters/tts/inworld.js +73 -68
  8. package/dist/catalog/registry.js +13 -73
  9. package/dist/edge/cli.js +5 -2
  10. package/dist/edge/events/hub.js +7 -2
  11. package/dist/edge/events/preview-cache.js +7 -4
  12. package/dist/edge/events/visibility.js +33 -0
  13. package/dist/edge/http/actions.js +74 -79
  14. package/dist/edge/http/app.js +39 -6
  15. package/dist/edge/http/audio-preview.js +9 -1
  16. package/dist/edge/http/checkpoints.js +95 -0
  17. package/dist/edge/http/diagnostics.js +31 -0
  18. package/dist/edge/http/draft-files.js +69 -0
  19. package/dist/edge/http/draft-problem.js +15 -0
  20. package/dist/edge/http/drafts.js +95 -0
  21. package/dist/edge/http/multipart.js +86 -0
  22. package/dist/edge/http/mutations.js +49 -0
  23. package/dist/edge/http/planning.js +12 -1
  24. package/dist/edge/http/project-templates.js +74 -0
  25. package/dist/edge/http/projects.js +19 -2
  26. package/dist/edge/http/revision-files.js +84 -0
  27. package/dist/edge/http/revisions.js +103 -0
  28. package/dist/edge/http/schedules.js +100 -0
  29. package/dist/edge/http/staging.js +11 -52
  30. package/dist/edge/http/storage.js +110 -0
  31. package/dist/edge/http/subtitles.js +9 -79
  32. package/dist/edge/http/tutorial.js +26 -0
  33. package/dist/edge/signal-shutdown.js +23 -0
  34. package/dist/kernel/audio-preview.js +11 -5
  35. package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
  36. package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
  37. package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
  38. package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
  39. package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
  40. package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
  41. package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
  42. package/dist/kernel/ports/model.js +5 -0
  43. package/dist/kernel/runner/attempt-repo.js +15 -0
  44. package/dist/kernel/runner/attempt.js +14 -2
  45. package/dist/kernel/runner/checkpoint-authority.js +23 -0
  46. package/dist/kernel/runner/index.js +35 -11
  47. package/dist/kernel/runner/providers.js +18 -8
  48. package/dist/kernel/runner/work-attempt.js +47 -0
  49. package/dist/kernel/runner/work-authority.js +37 -0
  50. package/dist/kernel/runner/work.js +23 -0
  51. package/dist/main.js +227 -69
  52. package/dist/slices/admission/repo.js +5 -56
  53. package/dist/slices/admission/schema.js +64 -0
  54. package/dist/slices/admission/start.js +13 -4
  55. package/dist/slices/article/continuation.js +9 -3
  56. package/dist/slices/article/provided-entries.js +7 -3
  57. package/dist/slices/article/run.js +20 -7
  58. package/dist/slices/article/segments.js +13 -4
  59. package/dist/slices/batch/index.js +8 -11
  60. package/dist/slices/cancel/index.js +78 -34
  61. package/dist/slices/checkpoints/change.js +250 -0
  62. package/dist/slices/checkpoints/fingerprint.js +33 -0
  63. package/dist/slices/checkpoints/index.js +2 -0
  64. package/dist/slices/checkpoints/model.js +1 -0
  65. package/dist/slices/checkpoints/recovery.js +116 -0
  66. package/dist/slices/checkpoints/repo.js +132 -0
  67. package/dist/slices/checkpoints/rules.js +30 -0
  68. package/dist/slices/checkpoints/schema.js +59 -0
  69. package/dist/slices/control/index.js +88 -22
  70. package/dist/slices/control/providers.js +3 -2
  71. package/dist/slices/control/revision-control-schema.js +7 -0
  72. package/dist/slices/control/revision-control.js +59 -0
  73. package/dist/slices/estimate/index.js +77 -59
  74. package/dist/slices/estimate/requests.js +102 -0
  75. package/dist/slices/images/run.js +14 -3
  76. package/dist/slices/library/slots.js +11 -11
  77. package/dist/slices/library/snapshot.js +33 -0
  78. package/dist/slices/narration/legacy-plan.js +58 -0
  79. package/dist/slices/narration/live.js +2 -2
  80. package/dist/slices/narration/plan.js +48 -54
  81. package/dist/slices/narration/run.js +21 -8
  82. package/dist/slices/play-drafts/convert.js +120 -0
  83. package/dist/slices/play-drafts/model.js +1 -0
  84. package/dist/slices/play-drafts/readiness.js +80 -0
  85. package/dist/slices/play-drafts/repo.js +76 -0
  86. package/dist/slices/play-drafts/review-inputs.js +150 -0
  87. package/dist/slices/play-drafts/review.js +109 -0
  88. package/dist/slices/play-drafts/schema.js +218 -0
  89. package/dist/slices/play-drafts/service.js +242 -0
  90. package/dist/slices/play-drafts/start-repo.js +113 -0
  91. package/dist/slices/play-drafts/start.js +148 -0
  92. package/dist/slices/play-drafts/uploads.js +67 -0
  93. package/dist/slices/project-templates/from-project.js +159 -0
  94. package/dist/slices/project-templates/model.js +1 -0
  95. package/dist/slices/project-templates/repo.js +41 -0
  96. package/dist/slices/project-templates/schema.js +37 -0
  97. package/dist/slices/project-templates/service.js +136 -0
  98. package/dist/slices/project-templates/setup.js +55 -0
  99. package/dist/slices/rebuild/admission-repo.js +120 -0
  100. package/dist/slices/rebuild/dependencies.js +33 -0
  101. package/dist/slices/rebuild/model.js +94 -0
  102. package/dist/slices/rebuild/narration-history.js +43 -0
  103. package/dist/slices/rebuild/narration-reuse.js +20 -0
  104. package/dist/slices/rebuild/preview-details.js +144 -0
  105. package/dist/slices/rebuild/preview-plan.js +223 -0
  106. package/dist/slices/rebuild/preview-retained.js +128 -0
  107. package/dist/slices/rebuild/provided-review.js +32 -0
  108. package/dist/slices/rebuild/recipe-audio.js +172 -0
  109. package/dist/slices/rebuild/recipe-build.js +17 -0
  110. package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
  111. package/dist/slices/rebuild/recipe-exports.js +65 -0
  112. package/dist/slices/rebuild/recipe-fixture.js +150 -0
  113. package/dist/slices/rebuild/recipe-input-schema.js +82 -0
  114. package/dist/slices/rebuild/recipe-legacy.js +135 -0
  115. package/dist/slices/rebuild/recipe-model.js +77 -0
  116. package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
  117. package/dist/slices/rebuild/recipe-save.js +221 -0
  118. package/dist/slices/rebuild/recipe-text.js +170 -0
  119. package/dist/slices/rebuild/recipe-validation.js +86 -0
  120. package/dist/slices/rebuild/recipe-visual.js +111 -0
  121. package/dist/slices/rebuild/recipe-work.js +212 -0
  122. package/dist/slices/rebuild/recipes.js +4 -0
  123. package/dist/slices/rebuild/repo.js +64 -0
  124. package/dist/slices/rebuild/runtime-actions.js +76 -0
  125. package/dist/slices/rebuild/runtime-admission.js +71 -0
  126. package/dist/slices/rebuild/runtime-article.js +117 -0
  127. package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
  128. package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
  129. package/dist/slices/rebuild/runtime-export.js +139 -0
  130. package/dist/slices/rebuild/runtime-instructions.js +51 -0
  131. package/dist/slices/rebuild/runtime-local.js +137 -0
  132. package/dist/slices/rebuild/runtime-materialize.js +105 -0
  133. package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
  134. package/dist/slices/rebuild/runtime-plan.js +103 -0
  135. package/dist/slices/rebuild/runtime-provider.js +184 -0
  136. package/dist/slices/rebuild/runtime-publication.js +172 -0
  137. package/dist/slices/rebuild/runtime-run.js +35 -0
  138. package/dist/slices/rebuild/runtime-store.js +166 -0
  139. package/dist/slices/rebuild/runtime-subtitles.js +202 -0
  140. package/dist/slices/rebuild/service-readiness.js +110 -0
  141. package/dist/slices/rebuild/service.js +149 -0
  142. package/dist/slices/rebuild/transition-repo.js +108 -0
  143. package/dist/slices/rebuild/work-records.js +44 -0
  144. package/dist/slices/reruns/index.js +5 -0
  145. package/dist/slices/research/run.js +33 -12
  146. package/dist/slices/revisions/adopt-assets.js +31 -0
  147. package/dist/slices/revisions/adopt-content.js +112 -0
  148. package/dist/slices/revisions/adopt.js +100 -0
  149. package/dist/slices/revisions/downloads.js +80 -0
  150. package/dist/slices/revisions/index.js +5 -0
  151. package/dist/slices/revisions/manifest-repo.js +125 -0
  152. package/dist/slices/revisions/model.js +1 -0
  153. package/dist/slices/revisions/mutation-assets.js +143 -0
  154. package/dist/slices/revisions/mutation-cues.js +95 -0
  155. package/dist/slices/revisions/mutation-prepare.js +182 -0
  156. package/dist/slices/revisions/mutation-request.js +58 -0
  157. package/dist/slices/revisions/mutation-work.js +47 -0
  158. package/dist/slices/revisions/mutations.js +194 -0
  159. package/dist/slices/revisions/projection.js +82 -0
  160. package/dist/slices/revisions/publication-model.js +1 -0
  161. package/dist/slices/revisions/publication-rules.js +166 -0
  162. package/dist/slices/revisions/publish.js +115 -0
  163. package/dist/slices/revisions/repo.js +83 -0
  164. package/dist/slices/revisions/restore.js +60 -0
  165. package/dist/slices/revisions/rules.js +99 -0
  166. package/dist/slices/revisions/schema.js +160 -0
  167. package/dist/slices/revisions/view.js +44 -0
  168. package/dist/slices/schedules/calendar.js +47 -0
  169. package/dist/slices/schedules/model.js +1 -0
  170. package/dist/slices/schedules/repo.js +281 -0
  171. package/dist/slices/schedules/scheduler.js +144 -0
  172. package/dist/slices/schedules/schema.js +80 -0
  173. package/dist/slices/schedules/service.js +178 -0
  174. package/dist/slices/settings/cli-paths.js +1 -1
  175. package/dist/slices/settings/cli-status.js +45 -5
  176. package/dist/slices/settings/tutorial-schema.js +39 -0
  177. package/dist/slices/settings/tutorial.js +66 -0
  178. package/dist/slices/storage/assets.js +108 -0
  179. package/dist/slices/storage/delete-project.js +2 -1
  180. package/dist/slices/storage/downloads.js +4 -1
  181. package/dist/slices/storage/portable.js +871 -0
  182. package/dist/slices/storage/prepare.js +67 -0
  183. package/dist/slices/storage/reconcile.js +25 -5
  184. package/dist/slices/storage/repo.js +2 -15
  185. package/dist/slices/storage/schema.js +35 -0
  186. package/dist/slices/storage/staging-refs.js +26 -0
  187. package/dist/slices/storage/staging.js +16 -11
  188. package/dist/slices/thumbnail/run.js +26 -9
  189. package/dist/slices/video/audio-export-args.js +33 -0
  190. package/dist/slices/video/audio-export.js +13 -36
  191. package/dist/slices/video/audio-inputs.js +4 -3
  192. package/dist/slices/video/ffmpeg.js +6 -0
  193. package/dist/slices/video/reuse-audio.js +11 -4
  194. package/dist/updater/install-flow.js +27 -2
  195. package/dist/updater/plan.js +28 -2
  196. package/dist/updater/service.js +27 -2
  197. package/dist/updater/worker.js +2 -1
  198. package/dist/web/assets/index-CO7DnVX9.js +137 -0
  199. package/dist/web/assets/index-DX1_chKs.css +1 -0
  200. package/dist/web/index.html +2 -2
  201. package/package.json +2 -2
  202. package/dist/adapters/tts/inworld-text.js +0 -32
  203. package/dist/web/assets/index-Ce2HApus.css +0 -1
  204. package/dist/web/assets/index-xbD0MZOY.js +0 -130
@@ -0,0 +1,32 @@
1
+ import { requestHash } from "../revisions/mutation-request.js";
2
+ export function providedDependencyFingerprint(plan, key) {
3
+ const recipe = plan.recipes.find((one) => one.key === key);
4
+ const timing = plan.recipes.find((one) => one.key === "subtitles:timing");
5
+ return requestHash("provided-review-v1", key, {
6
+ recipe,
7
+ dependencies: recipe?.dependsOn.map((dependency) => plan.recipes.find((one) => one.key === dependency)),
8
+ timing: key === "subtitles:cues"
9
+ ? timing?.input.kind === "local" && Array.isArray(timing.input.values)
10
+ ? timing.input.values.slice(1)
11
+ : null
12
+ : null,
13
+ });
14
+ }
15
+ export function applyProvidedReviews(db, revisionId, plan) {
16
+ const approved = new Set(db
17
+ .prepare("SELECT work_key,dependency_fingerprint FROM revision_provided_reviews WHERE revision_id=?")
18
+ .all(revisionId)
19
+ .filter((row) => row.dependency_fingerprint === providedDependencyFingerprint(plan, String(row.work_key)))
20
+ .map((row) => String(row.work_key)));
21
+ return {
22
+ ...plan,
23
+ work: plan.work.map((row) => approved.has(row.key) && row.disposition === "review"
24
+ ? {
25
+ ...row,
26
+ disposition: row.kind === "provided" ? "reuse" : "local",
27
+ reason: "Provided content was reviewed against these exact inputs.",
28
+ }
29
+ : row),
30
+ providedReuseRequired: plan.providedReuseRequired.filter((key) => !approved.has(key)),
31
+ };
32
+ }
@@ -0,0 +1,172 @@
1
+ import { fingerprint } from "../../kernel/runner/work.js";
2
+ import { chunkNarration, defaultChunking } from "../narration/chunk.js";
3
+ import { concatArgs } from "../narration/concat.js";
4
+ import { narrationRegenerationToken, normalizeNarrationText, planNarration, } from "../narration/plan.js";
5
+ import { recipe, resourceIdentity, selectedReference, } from "./recipe-model.js";
6
+ export function audioRecipes(context, text) {
7
+ const { config, content } = context;
8
+ const recipes = [];
9
+ if (config.sources.audio === "off")
10
+ return { recipes, mediaFingerprint: null, timeline: [], keys: [] };
11
+ let body;
12
+ if (config.sources.audio === "provide") {
13
+ body = recipe(context, "audio:provided", "audio", {
14
+ kind: "provided",
15
+ version: 1,
16
+ assetId: content.provided.audio ?? null,
17
+ semantic: text.articleText ?? text.article.fingerprint,
18
+ }, [text.article.key], { unresolved: content.provided.audio === undefined });
19
+ recipes.push(body);
20
+ }
21
+ else {
22
+ const groups = text.articleText === null
23
+ ? []
24
+ : chunkNarration(normalizeNarrationText(text.articleText), config.chunking ?? defaultChunking);
25
+ const occurrences = new Map();
26
+ const parts = [];
27
+ for (const logicalText of groups) {
28
+ const hash = fingerprint(logicalText).slice(0, 20);
29
+ const occurrence = (occurrences.get(hash) ?? 0) + 1;
30
+ occurrences.set(hash, occurrence);
31
+ const logicalKey = `audio:body:${hash}-${occurrence}`;
32
+ parts.push(...narrationParts(context, logicalKey, logicalText, "body", [text.article.key]));
33
+ }
34
+ if (text.articleText === null)
35
+ parts.push(recipe(context, "audio:body:future", "audio", {
36
+ kind: "deferred",
37
+ version: 1,
38
+ operation: "body-narration",
39
+ template: [text.article.fingerprint, voiceValues(context), chunkingValues(context)],
40
+ }, [text.article.key]));
41
+ recipes.push(...parts);
42
+ body = recipe(context, "audio:body:concat", "audio", {
43
+ kind: "local",
44
+ version: 1,
45
+ operation: "concat-narration",
46
+ values: [
47
+ parts.map((part) => resourceIdentity(context, part)),
48
+ parts.length === 1 ? "copy-single-file" : concatArgs("$parts", "$output"),
49
+ ],
50
+ }, parts.map((part) => part.key), { unresolved: text.articleText !== null && groups.length === 0 });
51
+ recipes.push(body);
52
+ }
53
+ const ordered = [];
54
+ for (const category of ["intro", "body", "outro"]) {
55
+ if (category === "body") {
56
+ ordered.push({ value: body, transcript: text.articleText ?? text.article.fingerprint });
57
+ continue;
58
+ }
59
+ const entry = text.entries[category];
60
+ if (entry === undefined)
61
+ continue;
62
+ const parts = entry.text === null
63
+ ? [
64
+ recipe(context, `audio:${category}:future`, "audio", {
65
+ kind: "deferred",
66
+ version: 1,
67
+ operation: `${category}-narration`,
68
+ template: [entry.recipe.fingerprint, voiceValues(context)],
69
+ }, [entry.recipe.key]),
70
+ ]
71
+ : narrationParts(context, `audio:${category}`, entry.text, category, [entry.recipe.key]);
72
+ recipes.push(...parts);
73
+ const audio = recipe(context, `audio:${category}`, "audio", {
74
+ kind: "local",
75
+ version: 1,
76
+ operation: "concat-narration",
77
+ values: [
78
+ parts.map((part) => resourceIdentity(context, part)),
79
+ parts.length === 1 ? "copy-single-file" : concatArgs("$parts", "$output"),
80
+ ],
81
+ }, parts.map((part) => part.key));
82
+ recipes.push(audio);
83
+ ordered.push({ value: audio, transcript: entry.text ?? entry.recipe.fingerprint });
84
+ }
85
+ const timeline = ordered.map(({ value, transcript }) => {
86
+ const retained = context.manifest.outputs.find((row) => row.workKey === value.key && row.state === "ready" && selectedReference(row));
87
+ return {
88
+ audio: resourceIdentity(context, value),
89
+ transcript,
90
+ durationMs: retained?.output.durationMs ?? null,
91
+ };
92
+ });
93
+ return {
94
+ recipes,
95
+ mediaFingerprint: fingerprint([
96
+ "audio-media-v1",
97
+ ordered.map(({ value }) => resourceIdentity(context, value)),
98
+ config.silenceGapSeconds,
99
+ ]),
100
+ timeline,
101
+ keys: ordered.map(({ value }) => value.key),
102
+ };
103
+ }
104
+ function narrationParts(context, logicalKey, originalText, segment, dependsOn) {
105
+ const override = context.content.narrationOverrides[logicalKey];
106
+ if (override?.kind === "asset")
107
+ return [
108
+ recipe(context, `${logicalKey}:1`, "audio", {
109
+ kind: "provided",
110
+ version: 1,
111
+ assetId: override.assetId,
112
+ semantic: [normalizeNarrationText(originalText), voiceValues(context)],
113
+ }, dependsOn, { tokenKey: logicalKey }),
114
+ ];
115
+ const logicalText = normalizeNarrationText(override?.kind === "text" ? override.text : originalText);
116
+ const wholeRequest = segment !== "body" || (context.config.chunking?.mode ?? "whole") === "whole";
117
+ const choice = context.config.audio;
118
+ const model = context.catalogue?.tts.find((row) => row.provider === choice?.provider &&
119
+ row.id === choice.model &&
120
+ row.enabled &&
121
+ !row.deprecated);
122
+ const requests = planNarration({
123
+ groups: [
124
+ {
125
+ key: logicalKey,
126
+ text: logicalText,
127
+ segment,
128
+ wholeRequest,
129
+ regenerationToken: narrationRegenerationToken(context.content.regenerationTokens, logicalKey, segment),
130
+ },
131
+ ],
132
+ provider: choice?.provider ?? "",
133
+ model: choice?.model ?? "",
134
+ voice: choice?.voice ?? "",
135
+ maxCharacters: context.catalogue === undefined
136
+ ? Math.max(2, logicalText.length)
137
+ : (model?.tts.maxCharacters ?? Math.max(2, logicalText.length)),
138
+ retained: [],
139
+ });
140
+ return requests.map(({ text, key }) => recipe(context, key, "audio", {
141
+ kind: "tts",
142
+ version: 1,
143
+ provider: choice?.provider ?? "",
144
+ model: choice?.model ?? "",
145
+ voice: choice?.voice ?? "",
146
+ text,
147
+ logicalKey,
148
+ logicalText,
149
+ segment,
150
+ pronunciation: null,
151
+ wholeRequest,
152
+ }, dependsOn, { tokenKey: logicalKey, unresolved: logicalText.length === 0 }));
153
+ }
154
+ function voiceValues(context) {
155
+ return [
156
+ context.config.audio?.provider ?? null,
157
+ context.config.audio?.model ?? null,
158
+ context.config.audio?.voice ?? null,
159
+ null,
160
+ ];
161
+ }
162
+ function chunkingValues(context) {
163
+ const chunking = context.config.chunking ?? defaultChunking;
164
+ return [
165
+ chunking.mode,
166
+ chunking.mode === "words"
167
+ ? (chunking.words ?? 500)
168
+ : chunking.mode === "characters"
169
+ ? (chunking.characters ?? 3000)
170
+ : null,
171
+ ];
172
+ }
@@ -0,0 +1,17 @@
1
+ import { audioRecipes } from "./recipe-audio.js";
2
+ import { exportRecipes } from "./recipe-exports.js";
3
+ import { textRecipes } from "./recipe-text.js";
4
+ import { thumbnailRecipes, visualAssets, visualRecipes } from "./recipe-visual.js";
5
+ export function buildRecipes(context) {
6
+ const text = textRecipes(context);
7
+ const audio = audioRecipes(context, text);
8
+ const exports = exportRecipes(context, audio);
9
+ const captions = exports.find((value) => value.key === "subtitles:files");
10
+ return [
11
+ ...text.recipes,
12
+ ...audio.recipes,
13
+ ...exports,
14
+ ...thumbnailRecipes(context, text.recipes),
15
+ ...visualAssets(context, visualRecipes(context.config, context.content, audio.mediaFingerprint, captions?.fingerprint ?? null)),
16
+ ];
17
+ }
@@ -0,0 +1,75 @@
1
+ import { fingerprint } from "../../kernel/runner/work.js";
2
+ import { checkpointClosure, checkpointFingerprint } from "../checkpoints/fingerprint.js";
3
+ import { buildRecipes } from "./recipe-build.js";
4
+ export function reviewCheckpointSet(runs, catalogue, attachments, fontHash) {
5
+ return runs.flatMap((run, runIndex) => {
6
+ if (!run.draft.checkpoints?.length)
7
+ return [];
8
+ const config = { ...run.draft, rendered: run.rendered };
9
+ const images = config.sources.images === "provide"
10
+ ? (config.provided.images ?? []).map((id) => [
11
+ id,
12
+ { source: "provide", assetId: id, prompt: null, templateKey: null },
13
+ ])
14
+ : config.imagePrompts.flatMap((prompt, index) => Array.from({ length: prompt.number }, (_, number) => [
15
+ `${index}:${number}`,
16
+ {
17
+ source: "generate",
18
+ assetId: null,
19
+ prompt: run.rendered[`imagePrompts.${index}`] ?? null,
20
+ templateKey: `imagePrompts.${index}`,
21
+ },
22
+ ]));
23
+ const content = {
24
+ articleMarkdown: config.sources.article === "provide" ? config.provided.article : undefined,
25
+ provided: { audio: config.provided.audio, thumbnail: config.provided.thumbnail },
26
+ imageOrder: images.map(([key]) => key),
27
+ imageDefinitions: Object.fromEntries(images),
28
+ narrationOverrides: {},
29
+ regenerationTokens: {},
30
+ promptTemplates: run.templates,
31
+ };
32
+ const recipes = buildRecipes({
33
+ config,
34
+ content,
35
+ catalogue,
36
+ manifest: { outputs: [], pieces: [] },
37
+ resolved: {
38
+ articleMarkdown: content.articleMarkdown ?? null,
39
+ researchNotes: config.provided.research ?? null,
40
+ },
41
+ });
42
+ // Project and retained asset IDs do not exist until Start; these are setup identities.
43
+ const revision = {
44
+ id: "review",
45
+ projectId: "review",
46
+ parentId: null,
47
+ restoredFromId: null,
48
+ config,
49
+ content,
50
+ fingerprints: Object.fromEntries(recipes.map((recipe) => [recipe.key, recipe.fingerprint])),
51
+ createdAt: "review",
52
+ };
53
+ return (config.checkpoints?.map((stage) => {
54
+ const closure = checkpointClosure(stage, recipes);
55
+ const video = closure.some((recipe) => recipe.stage === "video");
56
+ const audio = video || closure.some((recipe) => recipe.stage === "audio");
57
+ const inputs = new Set([
58
+ ...(audio && config.provided.audio ? [config.provided.audio] : []),
59
+ ...(video ? (config.provided.images ?? []) : []),
60
+ ]);
61
+ return {
62
+ runIndex,
63
+ checkpointId: stage,
64
+ stage,
65
+ fingerprint: fingerprint({
66
+ recipes: checkpointFingerprint(revision, closure),
67
+ attachments: attachments.filter((file) => inputs.has(file.stagedFileId)),
68
+ font: video ? fontHash : null,
69
+ }),
70
+ workKeys: closure.map((recipe) => recipe.key),
71
+ dependents: [...new Set(closure.map((recipe) => recipe.stage))].filter((kind) => kind !== stage),
72
+ };
73
+ }) ?? []);
74
+ });
75
+ }
@@ -0,0 +1,65 @@
1
+ import { audioExportArgs } from "../video/audio-export-args.js";
2
+ import { recipe, resourceIdentity, } from "./recipe-model.js";
3
+ export function exportRecipes(context, audio) {
4
+ const { config, content } = context;
5
+ if (audio.mediaFingerprint === null)
6
+ return [];
7
+ const recipes = [];
8
+ if (config.sources.video === "off")
9
+ recipes.push(recipe(context, "export:wav", "video", {
10
+ kind: "local",
11
+ version: 1,
12
+ operation: "export-wav",
13
+ values: [
14
+ audio.mediaFingerprint,
15
+ audioExportArgs([{ kind: "body", path: "$body", seconds: 0 }], "$output"),
16
+ ],
17
+ }, audio.keys));
18
+ if (config.subtitles === undefined || config.subtitles.mode === "off")
19
+ return recipes;
20
+ const timing = recipe(context, "subtitles:timing", "video", {
21
+ kind: "local",
22
+ version: 1,
23
+ operation: "wav2vec2-en-a19f851-v2-omissions",
24
+ values: [audio.timeline, config.silenceGapSeconds, config.subtitles.language],
25
+ }, audio.keys);
26
+ recipes.push(timing);
27
+ const cues = content.subtitleCues;
28
+ const cueRecipe = cues === undefined
29
+ ? recipe(context, "subtitles:cues", "video", {
30
+ kind: "local",
31
+ version: 1,
32
+ operation: "automatic-cues-v1",
33
+ values: resourceIdentity(context, timing),
34
+ }, [timing.key])
35
+ : recipe(context, "subtitles:cues", "video", {
36
+ kind: "local",
37
+ version: 1,
38
+ operation: "manual-cues-v1",
39
+ values: [cues.audioFingerprint, cues.cues.map((cue) => ({ ...cue }))],
40
+ }, audio.keys);
41
+ recipes.push(cueRecipe);
42
+ recipes.push(recipe(context, "subtitles:files", "video", {
43
+ kind: "local",
44
+ version: 1,
45
+ operation: "subtitle-files-v1",
46
+ values: [
47
+ resourceIdentity(context, cueRecipe),
48
+ config.format,
49
+ config.subtitles.fontId,
50
+ config.subtitles.fontSize,
51
+ config.subtitles.position,
52
+ ],
53
+ }, [cueRecipe.key]));
54
+ return recipes;
55
+ }
56
+ export function manualCuesNeedReview(context, recipes) {
57
+ const cues = context.content.subtitleCues;
58
+ if (cues === undefined)
59
+ return false;
60
+ const timing = recipes.find((value) => value.key === "subtitles:timing");
61
+ return (timing !== undefined &&
62
+ cues.audioFingerprint !== timing.logicalFingerprint &&
63
+ cues.audioFingerprint !==
64
+ context.manifest.outputs.find((value) => value.workKey === timing.key && value.state === "ready")?.fingerprint);
65
+ }
@@ -0,0 +1,150 @@
1
+ import { planRevision, planRevisionWork } from "./recipes.js";
2
+ export const config = {
3
+ title: "Saved",
4
+ format: "16:9",
5
+ sources: {
6
+ research: "off",
7
+ article: "provide",
8
+ audio: "off",
9
+ images: "generate",
10
+ thumbnail: "off",
11
+ video: "generate",
12
+ },
13
+ llm: { provider: "text", model: "text-model" },
14
+ audio: { provider: "voice", model: "tts", voice: "v1" },
15
+ images: { provider: "fal", model: "image-model" },
16
+ imagePrompts: [],
17
+ values: {},
18
+ provided: { article: "First paragraph.\n\nSecond paragraph." },
19
+ silenceGapSeconds: 0,
20
+ rendered: { article: "Write an article." },
21
+ };
22
+ export const content = {
23
+ articleMarkdown: config.provided.article,
24
+ articleEdited: false,
25
+ provided: {},
26
+ imageOrder: ["harbor", "hill"],
27
+ imageDefinitions: {
28
+ harbor: { source: "generate", assetId: null, prompt: "Harbor" },
29
+ hill: { source: "generate", assetId: null, prompt: "Hill" },
30
+ },
31
+ narrationOverrides: {},
32
+ regenerationTokens: {},
33
+ promptTemplates: {},
34
+ };
35
+ export const catalogue = {
36
+ schemaVersion: 1,
37
+ updatedAt: "2026-09-10",
38
+ providers: {},
39
+ llm: [
40
+ {
41
+ provider: "text",
42
+ id: "text-model",
43
+ name: "Text",
44
+ enabled: true,
45
+ deprecated: false,
46
+ source: "https://example.test",
47
+ keywords: [],
48
+ pricing: {},
49
+ llm: { webSearch: true, thinking: { high: { budget: 1024 } } },
50
+ },
51
+ ],
52
+ tts: [
53
+ {
54
+ provider: "voice",
55
+ id: "tts",
56
+ name: "Voice",
57
+ enabled: true,
58
+ deprecated: false,
59
+ source: "https://example.test",
60
+ keywords: [],
61
+ pricing: { perMillionCharacters: 25 },
62
+ tts: { maxCharacters: 1000, streaming: true },
63
+ },
64
+ ],
65
+ image: [],
66
+ };
67
+ export function emptyView(c = config, value = content) {
68
+ return {
69
+ revision: {
70
+ id: "r1",
71
+ projectId: "p1",
72
+ parentId: null,
73
+ restoredFromId: null,
74
+ config: c,
75
+ content: value,
76
+ fingerprints: {},
77
+ createdAt: "2026-09-10T00:00:00Z",
78
+ },
79
+ outputs: [],
80
+ pieces: [],
81
+ current: true,
82
+ articleMarkdown: value.articleMarkdown ?? c.provided.article ?? null,
83
+ };
84
+ }
85
+ export function readyView(c = config, value = content) {
86
+ const base = emptyView(c, value);
87
+ const initial = planRevision(base, { config: c, content: value });
88
+ if (!initial.ok)
89
+ throw new Error(JSON.stringify(initial.fields));
90
+ return {
91
+ ...base,
92
+ revision: {
93
+ ...base.revision,
94
+ config: initial.config,
95
+ content: initial.content,
96
+ fingerprints: initial.fingerprints,
97
+ },
98
+ outputs: Object.entries(initial.fingerprints).map(([key, fingerprint]) => ({
99
+ recordId: `record-${key}`,
100
+ publicationId: null,
101
+ selected: true,
102
+ available: true,
103
+ slot: key,
104
+ workKey: key,
105
+ assetId: `asset-${key}`,
106
+ fingerprint,
107
+ state: "ready",
108
+ output: {
109
+ id: `output-${key}`,
110
+ projectId: "p1",
111
+ stageKind: key.startsWith("image:")
112
+ ? "images"
113
+ : key.startsWith("article:")
114
+ ? "article"
115
+ : "video",
116
+ role: key.startsWith("image:")
117
+ ? "image"
118
+ : key.startsWith("article:")
119
+ ? "article_md"
120
+ : "video",
121
+ path: `assets/${key}.bin`,
122
+ originalFilename: null,
123
+ bytes: 10,
124
+ durationMs: 1000,
125
+ meta: {},
126
+ createdAt: "2026-09-10T00:00:00Z",
127
+ },
128
+ })),
129
+ };
130
+ }
131
+ export function workFor(base, c = base.revision.config, value = base.revision.content) {
132
+ const next = planRevision(base, { config: c, content: value });
133
+ if (!next.ok)
134
+ throw new Error(JSON.stringify(next.fields));
135
+ const revision = {
136
+ ...base.revision,
137
+ config: next.config,
138
+ content: next.content,
139
+ fingerprints: next.fingerprints,
140
+ };
141
+ return planRevisionWork(revision, next.manifest, catalogue, new Set([
142
+ ...base.outputs.map((row) => row.assetId),
143
+ ...Object.values(value.provided).filter((id) => id !== undefined),
144
+ ]), {
145
+ articleMarkdown: next.content.articleEdited || c.sources.article === "provide"
146
+ ? (next.content.articleMarkdown ?? c.provided.article ?? null)
147
+ : base.articleMarkdown,
148
+ researchNotes: null,
149
+ });
150
+ }
@@ -0,0 +1,82 @@
1
+ import { z } from "zod";
2
+ import { messageRoles, thinkingModes } from "../../kernel/ports/llm.js";
3
+ import { deferredOperations, localOperations } from "./recipe-model.js";
4
+ const value = z.lazy(() => z.union([
5
+ z.null(),
6
+ z.boolean(),
7
+ z.number().finite(),
8
+ z.string(),
9
+ z.array(value),
10
+ z.record(z.string(), value),
11
+ ]));
12
+ const version = z.literal(1);
13
+ export const recipeInputSchema = z.discriminatedUnion("kind", [
14
+ z
15
+ .object({
16
+ kind: z.literal("llm"),
17
+ version,
18
+ provider: z.string(),
19
+ model: z.string(),
20
+ thinking: z.enum(thinkingModes).nullable(),
21
+ thinkingConfig: z
22
+ .object({
23
+ budget: z.number().finite().optional(),
24
+ level: z.enum(["minimal", "low", "medium", "high"]).optional(),
25
+ effort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(),
26
+ })
27
+ .strict()
28
+ .nullable(),
29
+ messages: z.array(z.object({ role: z.enum(messageRoles), content: z.string() }).strict()),
30
+ webSearch: z.boolean(),
31
+ })
32
+ .strict(),
33
+ z
34
+ .object({
35
+ kind: z.literal("tts"),
36
+ version,
37
+ provider: z.string(),
38
+ model: z.string(),
39
+ voice: z.string(),
40
+ text: z.string(),
41
+ logicalKey: z.string(),
42
+ logicalText: z.string(),
43
+ segment: z.enum(["body", "intro", "outro"]),
44
+ pronunciation: z.null(),
45
+ wholeRequest: z.boolean().optional(),
46
+ })
47
+ .strict(),
48
+ z
49
+ .object({
50
+ kind: z.literal("image"),
51
+ version,
52
+ provider: z.string(),
53
+ model: z.string(),
54
+ prompt: z.string(),
55
+ aspect: z.enum(["16:9", "9:16"]),
56
+ })
57
+ .strict(),
58
+ z
59
+ .object({
60
+ kind: z.literal("provided"),
61
+ version,
62
+ assetId: z.string().nullable(),
63
+ semantic: value,
64
+ })
65
+ .strict(),
66
+ z
67
+ .object({
68
+ kind: z.literal("local"),
69
+ version,
70
+ operation: z.enum(localOperations),
71
+ values: value,
72
+ })
73
+ .strict(),
74
+ z
75
+ .object({
76
+ kind: z.literal("deferred"),
77
+ version,
78
+ operation: z.enum(deferredOperations),
79
+ template: value,
80
+ })
81
+ .strict(),
82
+ ]);