@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.
Files changed (207) hide show
  1. package/README.md +28 -6
  2. package/SUBTITLES.md +2 -0
  3. package/dist/adapters/alignment/window.js +46 -5
  4. package/dist/adapters/alignment/worker.js +2 -2
  5. package/dist/adapters/llm/claude-code.js +6 -3
  6. package/dist/adapters/llm/codex.js +102 -12
  7. package/dist/adapters/llm/gemini.js +11 -8
  8. package/dist/adapters/llm/run-cli.js +61 -6
  9. package/dist/adapters/tts/inworld-async.js +4 -4
  10. package/dist/adapters/tts/inworld.js +73 -68
  11. package/dist/catalog/registry.js +13 -73
  12. package/dist/edge/cli.js +5 -2
  13. package/dist/edge/events/hub.js +7 -2
  14. package/dist/edge/events/preview-cache.js +7 -4
  15. package/dist/edge/events/visibility.js +33 -0
  16. package/dist/edge/http/actions.js +74 -79
  17. package/dist/edge/http/app.js +39 -6
  18. package/dist/edge/http/audio-preview.js +9 -1
  19. package/dist/edge/http/checkpoints.js +95 -0
  20. package/dist/edge/http/diagnostics.js +31 -0
  21. package/dist/edge/http/draft-files.js +69 -0
  22. package/dist/edge/http/draft-problem.js +15 -0
  23. package/dist/edge/http/drafts.js +95 -0
  24. package/dist/edge/http/multipart.js +86 -0
  25. package/dist/edge/http/mutations.js +49 -0
  26. package/dist/edge/http/planning.js +12 -1
  27. package/dist/edge/http/project-templates.js +74 -0
  28. package/dist/edge/http/projects.js +19 -2
  29. package/dist/edge/http/revision-files.js +84 -0
  30. package/dist/edge/http/revisions.js +103 -0
  31. package/dist/edge/http/schedules.js +100 -0
  32. package/dist/edge/http/staging.js +11 -52
  33. package/dist/edge/http/storage.js +110 -0
  34. package/dist/edge/http/subtitles.js +9 -79
  35. package/dist/edge/http/tutorial.js +26 -0
  36. package/dist/edge/signal-shutdown.js +23 -0
  37. package/dist/kernel/audio-preview.js +11 -5
  38. package/dist/kernel/db/migrations/0004-project-revisions.sql +91 -0
  39. package/dist/kernel/db/migrations/0005-revision-work.sql +103 -0
  40. package/dist/kernel/db/migrations/0006-play-drafts.sql +38 -0
  41. package/dist/kernel/db/migrations/0007-review-checkpoints.sql +28 -0
  42. package/dist/kernel/db/migrations/0008-project-templates.sql +21 -0
  43. package/dist/kernel/db/migrations/0009-scheduled-jobs.sql +37 -0
  44. package/dist/kernel/db/migrations/0010-retain-schedule-history.sql +62 -0
  45. package/dist/kernel/ports/model.js +5 -0
  46. package/dist/kernel/runner/attempt-repo.js +15 -0
  47. package/dist/kernel/runner/attempt.js +14 -2
  48. package/dist/kernel/runner/checkpoint-authority.js +23 -0
  49. package/dist/kernel/runner/index.js +35 -11
  50. package/dist/kernel/runner/providers.js +18 -8
  51. package/dist/kernel/runner/work-attempt.js +47 -0
  52. package/dist/kernel/runner/work-authority.js +37 -0
  53. package/dist/kernel/runner/work.js +23 -0
  54. package/dist/main.js +227 -69
  55. package/dist/slices/admission/repo.js +5 -56
  56. package/dist/slices/admission/schema.js +64 -0
  57. package/dist/slices/admission/start.js +13 -4
  58. package/dist/slices/article/continuation.js +9 -3
  59. package/dist/slices/article/provided-entries.js +7 -3
  60. package/dist/slices/article/run.js +20 -7
  61. package/dist/slices/article/segments.js +13 -4
  62. package/dist/slices/batch/index.js +8 -11
  63. package/dist/slices/cancel/index.js +78 -34
  64. package/dist/slices/checkpoints/change.js +250 -0
  65. package/dist/slices/checkpoints/fingerprint.js +33 -0
  66. package/dist/slices/checkpoints/index.js +2 -0
  67. package/dist/slices/checkpoints/model.js +1 -0
  68. package/dist/slices/checkpoints/recovery.js +116 -0
  69. package/dist/slices/checkpoints/repo.js +132 -0
  70. package/dist/slices/checkpoints/rules.js +30 -0
  71. package/dist/slices/checkpoints/schema.js +59 -0
  72. package/dist/slices/control/index.js +88 -22
  73. package/dist/slices/control/providers.js +3 -2
  74. package/dist/slices/control/revision-control-schema.js +7 -0
  75. package/dist/slices/control/revision-control.js +59 -0
  76. package/dist/slices/estimate/index.js +77 -59
  77. package/dist/slices/estimate/requests.js +102 -0
  78. package/dist/slices/images/run.js +14 -3
  79. package/dist/slices/library/slots.js +11 -11
  80. package/dist/slices/library/snapshot.js +33 -0
  81. package/dist/slices/narration/legacy-plan.js +58 -0
  82. package/dist/slices/narration/live.js +2 -2
  83. package/dist/slices/narration/plan.js +48 -54
  84. package/dist/slices/narration/run.js +21 -8
  85. package/dist/slices/play-drafts/convert.js +120 -0
  86. package/dist/slices/play-drafts/model.js +1 -0
  87. package/dist/slices/play-drafts/readiness.js +80 -0
  88. package/dist/slices/play-drafts/repo.js +76 -0
  89. package/dist/slices/play-drafts/review-inputs.js +150 -0
  90. package/dist/slices/play-drafts/review.js +109 -0
  91. package/dist/slices/play-drafts/schema.js +218 -0
  92. package/dist/slices/play-drafts/service.js +242 -0
  93. package/dist/slices/play-drafts/start-repo.js +113 -0
  94. package/dist/slices/play-drafts/start.js +148 -0
  95. package/dist/slices/play-drafts/uploads.js +67 -0
  96. package/dist/slices/project-templates/from-project.js +159 -0
  97. package/dist/slices/project-templates/model.js +1 -0
  98. package/dist/slices/project-templates/repo.js +41 -0
  99. package/dist/slices/project-templates/schema.js +37 -0
  100. package/dist/slices/project-templates/service.js +136 -0
  101. package/dist/slices/project-templates/setup.js +55 -0
  102. package/dist/slices/rebuild/admission-repo.js +120 -0
  103. package/dist/slices/rebuild/dependencies.js +33 -0
  104. package/dist/slices/rebuild/model.js +94 -0
  105. package/dist/slices/rebuild/narration-history.js +43 -0
  106. package/dist/slices/rebuild/narration-reuse.js +20 -0
  107. package/dist/slices/rebuild/preview-details.js +144 -0
  108. package/dist/slices/rebuild/preview-plan.js +223 -0
  109. package/dist/slices/rebuild/preview-retained.js +128 -0
  110. package/dist/slices/rebuild/provided-review.js +32 -0
  111. package/dist/slices/rebuild/recipe-audio.js +172 -0
  112. package/dist/slices/rebuild/recipe-build.js +17 -0
  113. package/dist/slices/rebuild/recipe-checkpoints.js +75 -0
  114. package/dist/slices/rebuild/recipe-exports.js +65 -0
  115. package/dist/slices/rebuild/recipe-fixture.js +150 -0
  116. package/dist/slices/rebuild/recipe-input-schema.js +82 -0
  117. package/dist/slices/rebuild/recipe-legacy.js +135 -0
  118. package/dist/slices/rebuild/recipe-model.js +77 -0
  119. package/dist/slices/rebuild/recipe-provider-choice.js +14 -0
  120. package/dist/slices/rebuild/recipe-save.js +221 -0
  121. package/dist/slices/rebuild/recipe-text.js +170 -0
  122. package/dist/slices/rebuild/recipe-validation.js +86 -0
  123. package/dist/slices/rebuild/recipe-visual.js +111 -0
  124. package/dist/slices/rebuild/recipe-work.js +212 -0
  125. package/dist/slices/rebuild/recipes.js +4 -0
  126. package/dist/slices/rebuild/repo.js +64 -0
  127. package/dist/slices/rebuild/runtime-actions.js +76 -0
  128. package/dist/slices/rebuild/runtime-admission.js +71 -0
  129. package/dist/slices/rebuild/runtime-article.js +117 -0
  130. package/dist/slices/rebuild/runtime-checkpoints.js +45 -0
  131. package/dist/slices/rebuild/runtime-export-inputs.js +109 -0
  132. package/dist/slices/rebuild/runtime-export.js +139 -0
  133. package/dist/slices/rebuild/runtime-instructions.js +51 -0
  134. package/dist/slices/rebuild/runtime-local.js +137 -0
  135. package/dist/slices/rebuild/runtime-materialize.js +105 -0
  136. package/dist/slices/rebuild/runtime-narration-reuse.js +134 -0
  137. package/dist/slices/rebuild/runtime-plan.js +103 -0
  138. package/dist/slices/rebuild/runtime-provider.js +184 -0
  139. package/dist/slices/rebuild/runtime-publication.js +172 -0
  140. package/dist/slices/rebuild/runtime-run.js +35 -0
  141. package/dist/slices/rebuild/runtime-store.js +166 -0
  142. package/dist/slices/rebuild/runtime-subtitles.js +202 -0
  143. package/dist/slices/rebuild/service-readiness.js +110 -0
  144. package/dist/slices/rebuild/service.js +149 -0
  145. package/dist/slices/rebuild/transition-repo.js +108 -0
  146. package/dist/slices/rebuild/work-records.js +44 -0
  147. package/dist/slices/reruns/index.js +5 -0
  148. package/dist/slices/research/run.js +33 -12
  149. package/dist/slices/revisions/adopt-assets.js +31 -0
  150. package/dist/slices/revisions/adopt-content.js +112 -0
  151. package/dist/slices/revisions/adopt.js +100 -0
  152. package/dist/slices/revisions/downloads.js +80 -0
  153. package/dist/slices/revisions/index.js +5 -0
  154. package/dist/slices/revisions/manifest-repo.js +125 -0
  155. package/dist/slices/revisions/model.js +1 -0
  156. package/dist/slices/revisions/mutation-assets.js +143 -0
  157. package/dist/slices/revisions/mutation-cues.js +95 -0
  158. package/dist/slices/revisions/mutation-prepare.js +182 -0
  159. package/dist/slices/revisions/mutation-request.js +58 -0
  160. package/dist/slices/revisions/mutation-work.js +47 -0
  161. package/dist/slices/revisions/mutations.js +194 -0
  162. package/dist/slices/revisions/projection.js +82 -0
  163. package/dist/slices/revisions/publication-model.js +1 -0
  164. package/dist/slices/revisions/publication-rules.js +166 -0
  165. package/dist/slices/revisions/publish.js +115 -0
  166. package/dist/slices/revisions/repo.js +83 -0
  167. package/dist/slices/revisions/restore.js +60 -0
  168. package/dist/slices/revisions/rules.js +99 -0
  169. package/dist/slices/revisions/schema.js +160 -0
  170. package/dist/slices/revisions/view.js +44 -0
  171. package/dist/slices/schedules/calendar.js +47 -0
  172. package/dist/slices/schedules/model.js +1 -0
  173. package/dist/slices/schedules/repo.js +281 -0
  174. package/dist/slices/schedules/scheduler.js +144 -0
  175. package/dist/slices/schedules/schema.js +80 -0
  176. package/dist/slices/schedules/service.js +178 -0
  177. package/dist/slices/settings/cli-paths.js +1 -1
  178. package/dist/slices/settings/cli-status.js +45 -5
  179. package/dist/slices/settings/tutorial-schema.js +39 -0
  180. package/dist/slices/settings/tutorial.js +66 -0
  181. package/dist/slices/storage/assets.js +108 -0
  182. package/dist/slices/storage/delete-project.js +2 -1
  183. package/dist/slices/storage/downloads.js +4 -1
  184. package/dist/slices/storage/portable.js +871 -0
  185. package/dist/slices/storage/prepare.js +67 -0
  186. package/dist/slices/storage/reconcile.js +25 -5
  187. package/dist/slices/storage/repo.js +2 -15
  188. package/dist/slices/storage/schema.js +35 -0
  189. package/dist/slices/storage/staging-refs.js +26 -0
  190. package/dist/slices/storage/staging.js +16 -11
  191. package/dist/slices/thumbnail/run.js +26 -9
  192. package/dist/slices/video/audio-export-args.js +33 -0
  193. package/dist/slices/video/audio-export.js +13 -36
  194. package/dist/slices/video/audio-inputs.js +4 -3
  195. package/dist/slices/video/ffmpeg.js +6 -0
  196. package/dist/slices/video/reuse-audio.js +11 -4
  197. package/dist/updater/install-flow.js +27 -2
  198. package/dist/updater/plan.js +28 -2
  199. package/dist/updater/service.js +27 -2
  200. package/dist/updater/worker.js +2 -1
  201. package/dist/web/assets/index-CO7DnVX9.js +137 -0
  202. package/dist/web/assets/index-DX1_chKs.css +1 -0
  203. package/dist/web/index.html +2 -2
  204. package/package.json +2 -2
  205. package/dist/adapters/tts/inworld-text.js +0 -32
  206. package/dist/web/assets/index-Ce2HApus.css +0 -1
  207. package/dist/web/assets/index-xbD0MZOY.js +0 -130
@@ -0,0 +1,223 @@
1
+ import { z } from "zod";
2
+ import { catalogueSchema } from "../../catalog/schema.js";
3
+ import { stageKinds } from "../../kernel/pipeline.js";
4
+ import { estimateRequests } from "../estimate/index.js";
5
+ import { requestHash } from "../revisions/mutation-request.js";
6
+ import { previewDetails } from "./preview-details.js";
7
+ import { hasSubmittedRequest, requiresNewSubmission, retainedPreviewPlan, } from "./preview-retained.js";
8
+ import { providedDependencyFingerprint } from "./provided-review.js";
9
+ import { recipeInputSchema } from "./recipe-input-schema.js";
10
+ import { recipeProviderChoice } from "./recipe-provider-choice.js";
11
+ import { priceRecipe } from "./recipe-work.js";
12
+ import { narrationOrdinal } from "./runtime-narration-reuse.js";
13
+ import { executionCatalogue } from "./runtime-plan.js";
14
+ export const executionSnapshotSchema = z.object({
15
+ version: z.literal(1),
16
+ submissions: z
17
+ .array(z.object({ key: z.string(), submit: z.boolean(), uncertain: z.boolean() }))
18
+ .default([]),
19
+ catalogue: catalogueSchema,
20
+ recipes: z.array(z.object({
21
+ key: z.string(),
22
+ stage: z.enum(stageKinds),
23
+ kind: z.enum(["provider", "local", "provided"]),
24
+ requestFingerprint: z.string(),
25
+ fingerprint: z.string(),
26
+ logicalFingerprint: z.string(),
27
+ dependsOn: z.array(z.string()),
28
+ unresolved: z.boolean(),
29
+ deferred: z.boolean(),
30
+ input: recipeInputSchema,
31
+ })),
32
+ dispositions: z.array(z.object({ key: z.string(), disposition: z.string() })),
33
+ assets: z.array(z.object({ id: z.string(), path: z.string(), available: z.boolean() })),
34
+ reviews: z.array(z.object({ key: z.string(), fingerprint: z.string() })),
35
+ anchors: z.record(z.string(), z.string()),
36
+ ordinals: z.record(z.string(), z.number().int()),
37
+ });
38
+ export function planPreview(deps, view, catalogue, selection, id) {
39
+ const plan = retainedPreviewPlan(deps, view, catalogue);
40
+ const requested = selection.kind === "allAffected"
41
+ ? plan.work.filter((row) => row.disposition !== "reuse").map((row) => row.key)
42
+ : selection.workKeys;
43
+ if (new Set(requested).size !== requested.length)
44
+ return invalid("Choose each work item only once.");
45
+ const selected = new Set();
46
+ const visiting = new Set();
47
+ const visit = (key) => {
48
+ if (selected.has(key))
49
+ return true;
50
+ if (visiting.has(key))
51
+ return false;
52
+ const work = plan.work.find((row) => row.key === key);
53
+ const recipe = plan.recipes.find((row) => row.key === key);
54
+ if (work === undefined || recipe === undefined)
55
+ return false;
56
+ if (recipe.input.kind === "provided" && recipe.unresolved)
57
+ return false;
58
+ if (recipe.unresolved && recipe.dependsOn.length === 0 && !recipe.deferred)
59
+ return false;
60
+ visiting.add(key);
61
+ if (work.disposition !== "reuse")
62
+ for (const dependency of recipe.dependsOn)
63
+ if (!visit(dependency))
64
+ return false;
65
+ visiting.delete(key);
66
+ selected.add(key);
67
+ return true;
68
+ };
69
+ for (const key of requested)
70
+ if (!visit(key))
71
+ return invalid(`Work ${key} is disabled, unknown, or has an unavailable prerequisite.`);
72
+ const recipes = plan.recipes.filter((row) => selected.has(row.key));
73
+ const reservations = deps.db
74
+ .prepare(`SELECT r.work_key,r.fingerprint,w.state,w.recipe_context,r.piece_id FROM revision_work_reservations r JOIN revision_work w ON w.id=r.work_id WHERE r.revision_id=?`)
75
+ .all(view.revision.id);
76
+ const work = plan.work
77
+ .filter((row) => selected.has(row.key))
78
+ .map((row) => {
79
+ const old = reservations.find((one) => one.work_key === row.key &&
80
+ one.fingerprint === row.fingerprint &&
81
+ one.state !== "done" &&
82
+ typeof one.recipe_context === "string");
83
+ return old === undefined
84
+ ? row
85
+ : {
86
+ ...row,
87
+ inflight: old.state === "running",
88
+ pieceIds: typeof old.piece_id === "string" ? [old.piece_id] : [],
89
+ };
90
+ });
91
+ const relevantAssets = new Map();
92
+ for (const row of view.outputs)
93
+ if (row.selected && selected.has(row.workKey))
94
+ relevantAssets.set(row.assetId, {
95
+ id: row.assetId,
96
+ path: row.output.path,
97
+ available: row.available,
98
+ });
99
+ for (const row of view.pieces)
100
+ if (row.selected && selected.has(row.key) && row.assetId !== null) {
101
+ const asset = deps.db
102
+ .prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
103
+ .get(row.assetId, view.revision.projectId);
104
+ relevantAssets.set(row.assetId, {
105
+ id: row.assetId,
106
+ path: String(asset?.path ?? ""),
107
+ available: row.available,
108
+ });
109
+ }
110
+ for (const recipe of recipes)
111
+ if (recipe.input.kind === "provided" && recipe.input.assetId !== null) {
112
+ const asset = deps.db
113
+ .prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
114
+ .get(recipe.input.assetId, view.revision.projectId);
115
+ relevantAssets.set(recipe.input.assetId, {
116
+ id: recipe.input.assetId,
117
+ path: String(asset?.path ?? ""),
118
+ available: !recipe.unresolved,
119
+ });
120
+ }
121
+ const snapshot = selectedCatalogue(executionCatalogue(catalogue, view.revision.config), recipes, view);
122
+ const submissions = work
123
+ .filter((row) => row.kind === "provider")
124
+ .map((row) => {
125
+ const submit = row.disposition !== "reuse" &&
126
+ !row.inflight &&
127
+ requiresNewSubmission(deps, view.revision.id, row.key, row.fingerprint);
128
+ return {
129
+ key: row.key,
130
+ submit,
131
+ uncertain: submit && hasSubmittedRequest(deps, view.revision.id, row.key, row.fingerprint),
132
+ };
133
+ });
134
+ const execution = {
135
+ version: 1,
136
+ submissions,
137
+ catalogue: snapshot,
138
+ recipes: recipes.map((row) => ({ ...row, dependsOn: [...row.dependsOn] })),
139
+ dispositions: work.map((row) => ({ key: row.key, disposition: row.disposition })),
140
+ assets: [...relevantAssets.values()].sort((a, b) => a.id.localeCompare(b.id)),
141
+ reviews: plan.providedReuseRequired
142
+ .filter((key) => selected.has(key))
143
+ .map((key) => ({ key, fingerprint: providedDependencyFingerprint(plan, key) })),
144
+ ordinals: Object.fromEntries(recipes.map((row) => [row.key, narrationOrdinal(plan.recipes, row.key)])),
145
+ anchors: Object.fromEntries(recipes.map((recipe) => {
146
+ const key = recipe.input.kind === "tts" ? `${recipe.input.logicalKey}:1` : recipe.key;
147
+ return [key, view.revision.fingerprints[key] ?? recipe.logicalFingerprint];
148
+ })),
149
+ };
150
+ const preview = {
151
+ id,
152
+ projectId: view.revision.projectId,
153
+ baseRevisionId: view.revision.id,
154
+ selection,
155
+ planFingerprint: executionFingerprint(view.revision.id, execution),
156
+ work,
157
+ review: previewDetails(deps, view, plan.recipes, selected),
158
+ changedInputs: plan.changedInputs.filter((row) => selected.has(row.path)),
159
+ retained: view.outputs
160
+ .filter((row) => row.selected)
161
+ .map((row) => ({
162
+ slot: row.slot,
163
+ outputId: row.output.id,
164
+ assetId: row.assetId,
165
+ state: row.state,
166
+ })),
167
+ providedReuseRequired: execution.reviews.map((row) => row.key),
168
+ costs: estimateRequests(work.map((row) => priceRecipe(row.inflight || !requiresNewSubmission(deps, view.revision.id, row.key, row.fingerprint)
169
+ ? { ...row, disposition: "reuse" }
170
+ : row, recipes.find((recipe) => recipe.key === row.key))), snapshot),
171
+ wholeRequestNotice: plan.wholeRequestNotice,
172
+ warnings: submissions.some((row) => row.uncertain)
173
+ ? [
174
+ "A previous request was submitted without a saved result or resumable job. Retrying may charge you again.",
175
+ ]
176
+ : [],
177
+ };
178
+ return { ok: true, value: { preview, execution } };
179
+ }
180
+ function invalid(message) {
181
+ return {
182
+ ok: false,
183
+ reason: "invalid-selection",
184
+ fields: [{ field: "request.workKeys", message }],
185
+ };
186
+ }
187
+ function selectedCatalogue(catalogue, recipes, view) {
188
+ const uses = new Set(recipes.flatMap((row) => {
189
+ const choice = recipeProviderChoice(row, view.revision.config);
190
+ return choice === undefined ? [] : [`${choice.family}:${choice.provider}:${choice.model}`];
191
+ }));
192
+ const selected = {
193
+ llm: catalogue.llm.filter((row) => uses.has(`llm:${row.provider}:${row.id}`)),
194
+ tts: catalogue.tts.filter((row) => uses.has(`tts:${row.provider}:${row.id}`)),
195
+ image: catalogue.image.filter((row) => uses.has(`image:${row.provider}:${row.id}`)),
196
+ };
197
+ const providers = new Set([...selected.llm, ...selected.tts, ...selected.image].map((row) => row.provider));
198
+ return {
199
+ ...catalogue,
200
+ ...selected,
201
+ providers: Object.fromEntries(Object.entries(catalogue.providers).filter(([id]) => providers.has(id))),
202
+ };
203
+ }
204
+ function executionFingerprint(revisionId, execution) {
205
+ return requestHash("rebuild-plan-v1", revisionId, {
206
+ ...execution,
207
+ catalogue: { ...execution.catalogue, updatedAt: "1970-01-01" },
208
+ });
209
+ }
210
+ export function reviewStillCovers(reviewed, snapshot, fresh) {
211
+ if (reviewed.planFingerprint === fresh.preview.planFingerprint)
212
+ return true;
213
+ const escalated = fresh.execution.submissions.some((current) => {
214
+ const old = snapshot.submissions.find((row) => row.key === current.key);
215
+ return current.submit && (old?.submit !== true || (current.uncertain && !old.uncertain));
216
+ });
217
+ // A paid request may become a free join while readiness loads, never the reverse.
218
+ return (!escalated &&
219
+ executionFingerprint(reviewed.baseRevisionId, {
220
+ ...fresh.execution,
221
+ submissions: snapshot.submissions,
222
+ }) === reviewed.planFingerprint);
223
+ }
@@ -0,0 +1,128 @@
1
+ import { continuationLimit } from "../article/continuation.js";
2
+ import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
3
+ import { workPieces } from "./work-records.js";
4
+ export function retainedPreviewPlan(deps, view, catalogue) {
5
+ const current = executionPlan(deps, view, catalogue);
6
+ const replacements = new Map();
7
+ const groups = new Set();
8
+ const reservations = deps.db
9
+ .prepare(`SELECT r.*,w.revision_id AS origin_revision,w.recipe_context,w.state FROM revision_work_reservations r JOIN revision_work w ON w.id=r.work_id WHERE r.revision_id=? AND w.recipe_context IS NOT NULL AND w.state!='done'`)
10
+ .all(view.revision.id);
11
+ const origins = new Map();
12
+ for (const row of reservations) {
13
+ const anchor = String(row.logical_key ?? row.work_key);
14
+ if (view.revision.fingerprints[anchor] !== row.desired_fingerprint)
15
+ continue;
16
+ const piece = workPieces(deps.db, String(row.work_id)).find((one) => one.id === row.piece_id);
17
+ if (piece === undefined)
18
+ continue;
19
+ const originKey = `${row.origin_revision}:${row.recipe_context}`;
20
+ let original = origins.get(originKey);
21
+ if (original === undefined) {
22
+ const origin = executionView(deps, view.revision.projectId, String(row.origin_revision));
23
+ if (origin === undefined)
24
+ continue;
25
+ original = executionPlan(deps, origin, savedCatalogue(row.recipe_context));
26
+ origins.set(originKey, original);
27
+ }
28
+ const recipe = original.recipes.find((one) => one.key === piece.key);
29
+ if (recipe === undefined)
30
+ continue;
31
+ replacements.set(piece.key, {
32
+ ...recipe,
33
+ input: piece.input,
34
+ fingerprint: piece.fingerprint,
35
+ requestFingerprint: piece.requestFingerprint,
36
+ logicalFingerprint: piece.logicalFingerprint ?? recipe.logicalFingerprint,
37
+ });
38
+ if (piece.input.kind === "tts") {
39
+ const logicalKey = piece.input.logicalKey;
40
+ groups.add(logicalKey);
41
+ for (const sibling of original.recipes)
42
+ if (sibling.input.kind === "tts" &&
43
+ sibling.input.logicalKey === logicalKey &&
44
+ !replacements.has(sibling.key))
45
+ replacements.set(sibling.key, sibling);
46
+ }
47
+ }
48
+ const recipes = current.recipes.flatMap((row) => {
49
+ const replacement = replacements.get(row.key);
50
+ if (replacement !== undefined)
51
+ return [replacement];
52
+ if (row.input.kind === "tts" && groups.has(row.input.logicalKey))
53
+ return [];
54
+ return [row];
55
+ });
56
+ for (const row of replacements.values())
57
+ if (!recipes.some((one) => one.key === row.key))
58
+ recipes.push(row);
59
+ const work = recipes.flatMap((recipe) => {
60
+ const row = current.work.find((one) => one.key === recipe.key);
61
+ if (row !== undefined)
62
+ return [
63
+ {
64
+ ...row,
65
+ requestFingerprint: recipe.requestFingerprint,
66
+ fingerprint: recipe.fingerprint,
67
+ dependsOn: recipe.dependsOn,
68
+ kind: recipe.kind,
69
+ stage: recipe.stage,
70
+ },
71
+ ];
72
+ if (!replacements.has(recipe.key))
73
+ return [];
74
+ return [
75
+ {
76
+ key: recipe.key,
77
+ stage: recipe.stage,
78
+ kind: recipe.kind,
79
+ requestFingerprint: recipe.requestFingerprint,
80
+ fingerprint: recipe.fingerprint,
81
+ dependsOn: recipe.dependsOn,
82
+ disposition: recipe.kind === "local" ? "local" : "generate",
83
+ reason: "Continue the exact admitted request.",
84
+ inflight: false,
85
+ pieceIds: [],
86
+ },
87
+ ];
88
+ });
89
+ return { ...current, recipes, work };
90
+ }
91
+ export function requiresNewSubmission(deps, revisionId, key, fingerprint) {
92
+ const row = deps.db
93
+ .prepare(`SELECT w.id,w.state,w.dispatch_state,p.continuation FROM revision_work_reservations r JOIN revision_work w ON w.id=r.work_id JOIN revision_work_pieces p ON p.id=r.piece_id WHERE r.revision_id=? AND r.work_key=? AND r.fingerprint=? AND w.recipe_context IS NOT NULL`)
94
+ .get(revisionId, key, fingerprint);
95
+ return (row === undefined ||
96
+ (row.state !== "running" &&
97
+ !(row.state === "pending" && row.dispatch_state === "allowed") &&
98
+ row.continuation === null &&
99
+ !(key === "article:body" && cachedArticleComplete(deps, revisionId, String(row.id)))));
100
+ }
101
+ function cachedArticleComplete(deps, revisionId, workId) {
102
+ const answers = deps.db
103
+ .prepare(`SELECT p.work_key,json_extract(p.result_json,'$.finishReason') AS finish_reason
104
+ FROM revision_work_pieces p
105
+ JOIN revision_work_reservations r ON r.piece_id=p.id AND r.work_id=p.work_id
106
+ AND r.work_key=p.work_key AND r.fingerprint=p.fingerprint
107
+ JOIN project_revisions v ON v.id=r.revision_id
108
+ JOIN json_each(v.fingerprints) f ON f.key=COALESCE(r.logical_key,r.work_key)
109
+ AND f.value=COALESCE(r.desired_fingerprint,r.fingerprint)
110
+ WHERE r.revision_id=? AND p.work_id=? AND p.result_json IS NOT NULL
111
+ AND json_extract(p.input_json,'$.kind')='llm'
112
+ AND COALESCE(r.logical_key,r.work_key)='article:body'`)
113
+ .all(revisionId, workId);
114
+ for (let part = 0; part <= continuationLimit; part += 1) {
115
+ const key = part === 0 ? "article:body" : `article:continuation:${part}`;
116
+ const answer = answers.find((row) => row.work_key === key);
117
+ if (answer === undefined)
118
+ return false;
119
+ if (answer.finish_reason !== "length")
120
+ return true;
121
+ }
122
+ return false;
123
+ }
124
+ export function hasSubmittedRequest(deps, revisionId, key, fingerprint) {
125
+ return (deps.db
126
+ .prepare(`SELECT 1 FROM revision_work_reservations r JOIN revision_work_pieces p ON p.id=r.piece_id WHERE r.revision_id=? AND r.work_key=? AND r.fingerprint=? AND p.submitted_at IS NOT NULL AND p.continuation IS NULL AND p.state!='done'`)
127
+ .get(revisionId, key, fingerprint) !== undefined);
128
+ }
@@ -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
+ }