@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,134 @@
1
+ import { z } from "zod";
2
+ import { insertManifestOutput, insertManifestPiece, selectOutputRecord, selectPieceRecord, } from "../revisions/manifest-repo.js";
3
+ import { retainedNarrationPieces } from "./narration-history.js";
4
+ import { matchingNarrationPiece } from "./narration-reuse.js";
5
+ export function bindNarrationReuse(deps, view, recipe, ordinal) {
6
+ if (recipe.input.kind === "local" && recipe.input.operation === "concat-narration") {
7
+ const completed = view.pieces.find((one) => one.available &&
8
+ one.piece.state === "done" &&
9
+ one.key === recipe.key &&
10
+ one.fingerprint === recipe.fingerprint);
11
+ const output = completed === undefined
12
+ ? undefined
13
+ : view.outputs.find((one) => one.available && one.assetId === completed.assetId && one.workKey === recipe.key);
14
+ if (output !== undefined &&
15
+ !(output.selected &&
16
+ output.state === "ready" &&
17
+ output.fingerprint === recipe.logicalFingerprint)) {
18
+ const id = deps.ids.next();
19
+ insertManifestOutput(deps.db, view.revision, {
20
+ slot: output.slot,
21
+ workKey: output.workKey,
22
+ assetId: output.assetId,
23
+ output: output.output,
24
+ fingerprint: recipe.logicalFingerprint,
25
+ state: "ready",
26
+ }, id);
27
+ selectOutputRecord(deps.db, view.revision.id, output.slot, id);
28
+ }
29
+ return;
30
+ }
31
+ if (recipe.input.kind === "provided" && /^audio:(body:.+|intro|outro):[0-9]+$/.test(recipe.key)) {
32
+ if (view.pieces.some((one) => one.selected &&
33
+ one.available &&
34
+ one.key === recipe.key &&
35
+ one.fingerprint === recipe.fingerprint &&
36
+ one.piece.state === "done" &&
37
+ one.piece.idx === ordinal))
38
+ return;
39
+ const asset = deps.db
40
+ .prepare("SELECT path FROM project_assets WHERE id=? AND project_id=?")
41
+ .get(recipe.input.assetId, view.revision.projectId);
42
+ if (asset === undefined)
43
+ return;
44
+ const semantic = z
45
+ .tuple([z.string(), z.array(z.string().nullable())])
46
+ .parse(recipe.input.semantic);
47
+ const segment = recipe.key.startsWith("audio:body:")
48
+ ? "body"
49
+ : recipe.key.startsWith("audio:intro:")
50
+ ? "intro"
51
+ : "outro";
52
+ const stage = deps.db
53
+ .prepare("SELECT id FROM stages WHERE project_id=? AND kind='audio'")
54
+ .get(view.revision.projectId);
55
+ const id = deps.ids.next();
56
+ insertManifestPiece(deps.db, view.revision, {
57
+ key: recipe.key,
58
+ stageKind: "audio",
59
+ assetId: recipe.input.assetId,
60
+ fingerprint: recipe.fingerprint,
61
+ piece: {
62
+ id: deps.ids.next(),
63
+ stageId: z.string().parse(stage?.id),
64
+ kind: segment === "body" ? "chunk" : "segment",
65
+ idx: ordinal,
66
+ state: "done",
67
+ payload: JSON.stringify({
68
+ provided: true,
69
+ file: asset.path,
70
+ logicalKey: recipe.key.slice(0, recipe.key.lastIndexOf(":")),
71
+ logicalText: semantic[0],
72
+ text: semantic[0],
73
+ segment,
74
+ provider: semantic[1][0],
75
+ model: semantic[1][1],
76
+ voice: semantic[1][2],
77
+ requestFingerprint: recipe.requestFingerprint,
78
+ }),
79
+ },
80
+ }, id);
81
+ selectPieceRecord(deps.db, view.revision.id, recipe.key, id);
82
+ return;
83
+ }
84
+ if (recipe.input.kind !== "tts")
85
+ return;
86
+ if (view.pieces.some((one) => one.selected &&
87
+ one.available &&
88
+ one.key === recipe.key &&
89
+ one.fingerprint === recipe.fingerprint &&
90
+ one.piece.state === "done" &&
91
+ one.piece.idx === ordinal))
92
+ return;
93
+ const history = [...view.pieces, ...retainedNarrationPieces(deps, view.revision.projectId)];
94
+ const available = new Set(history
95
+ .filter((one) => one.available)
96
+ .flatMap((one) => (one.assetId === null ? [] : [one.assetId])));
97
+ const old = matchingNarrationPiece(recipe, history, available);
98
+ if (old === undefined)
99
+ return;
100
+ const input = recipe.input;
101
+ const recordId = deps.ids.next();
102
+ const payload = JSON.parse(old.piece.payload ?? "{}");
103
+ insertManifestPiece(deps.db, view.revision, {
104
+ key: recipe.key,
105
+ stageKind: "audio",
106
+ assetId: old.assetId,
107
+ fingerprint: recipe.fingerprint,
108
+ piece: {
109
+ ...old.piece,
110
+ id: deps.ids.next(),
111
+ idx: ordinal,
112
+ payload: JSON.stringify({
113
+ ...(typeof payload === "object" && payload !== null ? payload : {}),
114
+ text: input.text,
115
+ logicalKey: input.logicalKey,
116
+ logicalText: input.logicalText,
117
+ segment: input.segment,
118
+ requestFingerprint: recipe.requestFingerprint,
119
+ provider: input.provider,
120
+ model: input.model,
121
+ voice: input.voice,
122
+ }),
123
+ },
124
+ }, recordId);
125
+ selectPieceRecord(deps.db, view.revision.id, recipe.key, recordId);
126
+ }
127
+ export function narrationOrdinal(recipes, key) {
128
+ const entry = /^audio:(intro|outro):([0-9]+)$/.exec(key);
129
+ if (entry !== null)
130
+ return Number(entry[2]) * 2 - (entry[1] === "intro" ? 1 : 0);
131
+ return (recipes
132
+ .filter((row) => /^audio:body:.+:[0-9]+$/.test(row.key))
133
+ .findIndex((row) => row.key === key) + 1);
134
+ }
@@ -0,0 +1,103 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { z } from "zod";
3
+ import { catalogueSchema } from "../../catalog/schema.js";
4
+ import { getRevisionView } from "../revisions/view.js";
5
+ import { outputPath } from "../storage/layout.js";
6
+ import { referencedAssetsAvailable, retainedNarrationPieces } from "./narration-history.js";
7
+ import { applyProvidedReviews } from "./provided-review.js";
8
+ import { textRecipes } from "./recipe-text.js";
9
+ import { planRevisionWork } from "./recipe-work.js";
10
+ export function executionCatalogue(catalogue, config) {
11
+ const choices = [config.llm, config.audio, config.images];
12
+ const uses = (model) => choices.some((choice) => choice?.provider === model.provider && choice.model === model.id);
13
+ const providerIds = new Set(choices.flatMap((choice) => (choice ? [choice.provider] : [])));
14
+ return {
15
+ ...catalogue,
16
+ providers: Object.fromEntries(Object.entries(catalogue.providers).filter(([id]) => providerIds.has(id))),
17
+ llm: catalogue.llm.filter(uses),
18
+ tts: catalogue.tts.filter(uses),
19
+ image: catalogue.image.filter(uses),
20
+ };
21
+ }
22
+ export function savedCatalogue(value) {
23
+ return catalogueSchema.parse(JSON.parse(z.string().parse(value)));
24
+ }
25
+ export function executionPlan(deps, view, catalogue) {
26
+ const textOutput = (role) => {
27
+ const row = view.outputs.find((output) => output.selected && output.available && output.output.role === role);
28
+ return row === undefined
29
+ ? null
30
+ : readFileSync(outputPath(deps.paths, view.revision.projectId, row.output.path), "utf8");
31
+ };
32
+ const payloads = view.pieces.filter((piece) => piece.selected && piece.piece.state === "done" && piece.piece.payload !== null);
33
+ const resolved = {
34
+ articleMarkdown: view.articleMarkdown ?? textOutput("article_md"),
35
+ researchNotes: textOutput("notes"),
36
+ };
37
+ const research = matchingResearch({
38
+ config: view.revision.config,
39
+ content: view.revision.content,
40
+ manifest: view,
41
+ catalogue,
42
+ resolved,
43
+ }, payloads);
44
+ const history = retainedNarrationPieces(deps, view.revision.projectId);
45
+ const available = new Set([
46
+ ...history
47
+ .filter((row) => row.available && row.assetId !== null)
48
+ .flatMap((row) => (row.assetId === null ? [] : [row.assetId])),
49
+ ...referencedAssetsAvailable(deps, view.revision.projectId, Object.values(view.revision.content.narrationOverrides).flatMap((value) => value.kind === "asset" ? [value.assetId] : [])),
50
+ ...view.outputs.filter((row) => row.available).map((row) => row.assetId),
51
+ ...view.pieces
52
+ .filter((row) => row.available && row.assetId !== null)
53
+ .flatMap((row) => (row.assetId === null ? [] : [row.assetId])),
54
+ ]);
55
+ return applyProvidedReviews(deps.db, view.revision.id, planRevisionWork(view.revision, view, catalogue, available, {
56
+ ...resolved,
57
+ ...(research === undefined ? {} : { research }),
58
+ }, history));
59
+ }
60
+ export function executionView(deps, projectId, revisionId) {
61
+ const view = getRevisionView(deps, projectId, revisionId);
62
+ if (view === undefined)
63
+ return undefined;
64
+ const carried = new Set(deps.db
65
+ .prepare("SELECT r.piece_id FROM revision_work_reservations r JOIN project_heads h ON h.revision_id=r.revision_id AND h.project_id=r.project_id JOIN revision_work w ON w.id=r.work_id WHERE r.project_id=? AND w.revision_id=?")
66
+ .all(projectId, revisionId)
67
+ .map((row) => row.piece_id));
68
+ return {
69
+ ...view,
70
+ outputs: view.outputs.map((row) => ({
71
+ ...row,
72
+ selected: row.selected || (row.publicationId !== null && carried.has(row.publicationId)),
73
+ })),
74
+ pieces: view.pieces.map((row) => ({
75
+ ...row,
76
+ selected: row.selected || (row.publicationId !== null && carried.has(row.publicationId)),
77
+ })),
78
+ };
79
+ }
80
+ function matchingResearch(context, payloads) {
81
+ const planner = textRecipes(context).recipes.find((row) => row.key === "research:planner");
82
+ if (planner === undefined)
83
+ return undefined;
84
+ const savedPlanner = payloads.find((row) => row.key === planner.key && row.fingerprint === planner.fingerprint);
85
+ if (savedPlanner?.piece.payload === undefined || savedPlanner.piece.payload === null)
86
+ return undefined;
87
+ const { outline } = z
88
+ .object({ outline: z.array(z.string()) })
89
+ .parse(JSON.parse(savedPlanner.piece.payload));
90
+ const chapters = textRecipes({
91
+ ...context,
92
+ resolved: { ...context.resolved, research: { outline, findings: [] } },
93
+ }).recipes.filter((row) => row.key.startsWith("research:chapter:"));
94
+ const findings = chapters.flatMap((chapter) => {
95
+ const saved = payloads.find((row) => row.key === chapter.key && row.fingerprint === chapter.fingerprint);
96
+ if (saved?.piece.payload === undefined || saved.piece.payload === null)
97
+ return [];
98
+ return [
99
+ z.object({ title: z.string(), notes: z.string() }).parse(JSON.parse(saved.piece.payload)),
100
+ ];
101
+ });
102
+ return { outline, findings };
103
+ }
@@ -0,0 +1,184 @@
1
+ import { plainText } from "../article/plain.js";
2
+ import { splitEndMatter } from "../article/split.js";
3
+ import { observeNarration } from "../narration/live.js";
4
+ import { chaptersFrom } from "../research/planner.js";
5
+ import { sourcedAnswer } from "../research/synthesis.js";
6
+ import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
7
+ import { outputPath } from "../storage/layout.js";
8
+ import { probeDurationMs } from "../video/ffmpeg.js";
9
+ import { executeArticleRequests } from "./runtime-article.js";
10
+ import { frozenInstructions } from "./runtime-instructions.js";
11
+ import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
12
+ import { preparedResult, preparedTexts, publishResult } from "./runtime-publication.js";
13
+ export async function executeProviderRecipe(deps, context, providers, piece) {
14
+ if (deps.db
15
+ .prepare("SELECT state FROM revision_work_pieces WHERE id=? AND work_id=?")
16
+ .get(piece.id, context.work.workId)?.state === "done")
17
+ return "done";
18
+ const input = piece.input;
19
+ const wrapped = providers.forPiece(piece.id);
20
+ if (input.kind === "llm") {
21
+ const answer = piece.key === "article:body"
22
+ ? await executeArticleRequests(deps, context, providers, piece)
23
+ : await wrapped.llm({
24
+ provider: input.provider,
25
+ model: input.model,
26
+ ...(input.thinking === null ? {} : { thinking: input.thinking }),
27
+ thinkingConfig: input.thinkingConfig,
28
+ messages: input.messages,
29
+ webSearch: input.webSearch,
30
+ previewLabel: piece.key,
31
+ check: (value) => checkAnswer(piece, value),
32
+ });
33
+ if (!answer.ok)
34
+ return "held";
35
+ await publishText(deps, context, piece, answer.value);
36
+ const segment = piece.key === "entry:intro:text"
37
+ ? "intro"
38
+ : piece.key === "entry:outro:text"
39
+ ? "outro"
40
+ : undefined;
41
+ deps.count?.("stage.completed", {
42
+ stage: context.work.kind,
43
+ provider: input.provider,
44
+ model: input.model,
45
+ ...(segment === undefined ? {} : { segment }),
46
+ tokensIn: answer.value.usage?.inputTokens ?? 0,
47
+ tokensOut: answer.value.usage?.outputTokens ?? 0,
48
+ });
49
+ return "done";
50
+ }
51
+ if (input.kind === "tts") {
52
+ const spoken = await wrapped.tts({ provider: input.provider, model: input.model, voiceId: input.voice, text: input.text }, observeNarration(deps.audioPreviews, context.work.projectId, piece.id, narrationLabel(deps, context, piece), { revisionId: context.work.revisionId, workId: context.work.workId, workPieceId: piece.id }));
53
+ if (!spoken.ok)
54
+ return "held";
55
+ const asset = writeAsset(deps, context.work.projectId, "narration.mp3", spoken.value.bytes);
56
+ try {
57
+ const durationMs = deps.count === undefined
58
+ ? undefined
59
+ : deps.measureAudio !== undefined
60
+ ? await deps.measureAudio(outputPath(deps.paths, context.work.projectId, asset.path), context.signal)
61
+ : deps.ffmpeg === undefined
62
+ ? undefined
63
+ : await probeDurationMs(deps.ffmpeg, outputPath(deps.paths, context.work.projectId, asset.path), context.signal, deps.log);
64
+ await publishResult(deps, context, piece, [], {
65
+ text: input.text,
66
+ logicalKey: input.logicalKey,
67
+ logicalText: input.logicalText,
68
+ segment: input.segment,
69
+ provider: input.provider,
70
+ model: input.model,
71
+ voice: input.voice,
72
+ ...(durationMs === undefined ? {} : { durationMs }),
73
+ }, asset);
74
+ deps.count?.("stage.completed", {
75
+ stage: "audio",
76
+ segment: input.segment,
77
+ provider: input.provider,
78
+ model: input.model,
79
+ ...(durationMs === undefined ? {} : { audioSeconds: durationMs / 1000 }),
80
+ });
81
+ }
82
+ finally {
83
+ discardPreparedAssets(deps, [asset]);
84
+ }
85
+ return "done";
86
+ }
87
+ if (input.kind === "image") {
88
+ const image = await wrapped.image({
89
+ provider: input.provider,
90
+ model: input.model,
91
+ prompt: input.prompt,
92
+ aspect: input.aspect,
93
+ });
94
+ if (!image.ok)
95
+ return "held";
96
+ const asset = writeAsset(deps, context.work.projectId, image.value.mime === "image/jpeg" ? "image.jpg" : "image.png", image.value.bytes);
97
+ const view = executionView(deps, context.work.projectId, context.work.revisionId);
98
+ const index = piece.key.startsWith("image:")
99
+ ? (view?.revision.content.imageOrder.indexOf(piece.key.slice(6)) ?? 0) + 1
100
+ : undefined;
101
+ const output = preparedResult(deps, context, piece, piece.key === "thumbnail:image" ? "thumbnail" : "image", asset, null, {
102
+ prompt: input.prompt,
103
+ provider: input.provider,
104
+ model: input.model,
105
+ ...(index === undefined ? {} : { index }),
106
+ });
107
+ await publishResult(deps, context, piece, [output], { prompt: input.prompt }, asset);
108
+ deps.count?.("stage.completed", {
109
+ stage: context.work.kind,
110
+ provider: input.provider,
111
+ model: input.model,
112
+ ...(piece.key === "thumbnail:image" ? { thumbnails: 1 } : { images: 1 }),
113
+ });
114
+ return "done";
115
+ }
116
+ throw new Error("Expected an exact provider recipe.");
117
+ }
118
+ function checkAnswer(piece, answer) {
119
+ if (answer.text.trim() === "")
120
+ return "The provider answered with nothing.";
121
+ if (piece.key === "research:planner")
122
+ return chaptersFrom(answer.text).length === 0 ? "The planner named no chapters." : undefined;
123
+ if (piece.key === "research:notes" || piece.key.startsWith("research:chapter:"))
124
+ return sourcedAnswer(piece.key, answer.text);
125
+ return undefined;
126
+ }
127
+ async function publishText(deps, context, piece, answer) {
128
+ if (piece.key === "article:body") {
129
+ const parts = splitEndMatter(answer.text);
130
+ await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
131
+ ["instructions", "instructions.md", frozenInstructions(deps, context, piece)],
132
+ ["article_md", "article.md", answer.text],
133
+ ["article_txt", "article.txt", plainText(parts.body)],
134
+ ...(parts.sources ? [["sources", "sources.md", parts.sources]] : []),
135
+ ...(parts.glossary ? [["glossary", "glossary.md", parts.glossary]] : []),
136
+ ]), { text: answer.text });
137
+ return;
138
+ }
139
+ if (piece.key === "research:notes") {
140
+ await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
141
+ ["notes", "notes.md", answer.text],
142
+ ["instructions", "instructions.md", frozenInstructions(deps, context, piece)],
143
+ ]), { text: answer.text });
144
+ return;
145
+ }
146
+ if (piece.key === "research:planner") {
147
+ await publishResult(deps, context, piece, [], { outline: chaptersFrom(answer.text) });
148
+ return;
149
+ }
150
+ if (piece.key.startsWith("research:chapter:")) {
151
+ const view = executionView(deps, context.work.projectId, context.work.revisionId);
152
+ const planner = view?.pieces.find((row) => row.selected && row.key === "research:planner");
153
+ const outline = JSON.parse(planner?.piece.payload ?? "{}");
154
+ const titles = typeof outline === "object" &&
155
+ outline !== null &&
156
+ "outline" in outline &&
157
+ Array.isArray(outline.outline)
158
+ ? outline.outline
159
+ : [];
160
+ const title = titles[Number(piece.key.split(":").at(-1)) - 1];
161
+ if (typeof title !== "string")
162
+ throw new Error("Research chapter has no pinned title.");
163
+ await publishResult(deps, context, piece, [], { title, notes: answer.text });
164
+ return;
165
+ }
166
+ await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
167
+ ["instructions", "instructions.md", frozenInstructions(deps, context, piece)],
168
+ ]), { text: answer.text, prompt: answer.text });
169
+ }
170
+ function narrationLabel(deps, context, piece) {
171
+ if (piece.input.kind !== "tts")
172
+ return "Narration";
173
+ const name = piece.input.segment === "body" ? "Body" : piece.input.segment === "intro" ? "Intro" : "Outro";
174
+ const view = executionView(deps, context.work.projectId, context.work.revisionId);
175
+ const row = deps.db
176
+ .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
177
+ .get(context.work.workId);
178
+ if (view === undefined || row?.recipe_context === undefined || row.recipe_context === null)
179
+ return name;
180
+ const segment = piece.input.segment;
181
+ const parts = executionPlan(deps, view, savedCatalogue(row.recipe_context)).recipes.filter((recipe) => recipe.input.kind === "tts" && recipe.input.segment === segment);
182
+ const index = parts.findIndex((recipe) => recipe.key === piece.key);
183
+ return parts.length > 1 && index >= 0 ? `${name} part ${index + 1} of ${parts.length}` : name;
184
+ }
@@ -0,0 +1,172 @@
1
+ import { transact } from "../../kernel/db/tx.js";
2
+ import { fingerprint } from "../../kernel/runner/work.js";
3
+ import { publicationAuthority } from "../revisions/publication-rules.js";
4
+ import { commitRevisionOutputs } from "../revisions/publish.js";
5
+ import { insertAsset, insertManifestOutput, revisionById } from "../revisions/repo.js";
6
+ import { discardPreparedAssets, writeAsset } from "../storage/assets.js";
7
+ import { executionPlan, executionView, savedCatalogue } from "./runtime-plan.js";
8
+ export function preparedResult(deps, context, piece, role, asset, durationMs = null, meta = {}) {
9
+ return {
10
+ slot: role === "image" ? piece.key : `${context.work.kind}:${role}`,
11
+ workKey: piece.key,
12
+ fingerprint: piece.logicalFingerprint ?? piece.fingerprint,
13
+ asset,
14
+ output: {
15
+ id: deps.ids.next(),
16
+ projectId: context.work.projectId,
17
+ stageKind: context.work.kind,
18
+ role,
19
+ path: asset.path,
20
+ originalFilename: null,
21
+ bytes: asset.bytes,
22
+ durationMs,
23
+ meta,
24
+ createdAt: asset.createdAt,
25
+ },
26
+ };
27
+ }
28
+ export function preparedText(deps, context, piece, role, filename, text) {
29
+ const output = preparedTexts(deps, context, piece, [[role, filename, text]])[0];
30
+ if (output === undefined)
31
+ throw new Error("The text output was not prepared.");
32
+ return output;
33
+ }
34
+ export function preparedTexts(deps, context, piece, texts) {
35
+ const assets = [];
36
+ try {
37
+ return texts.map(([role, filename, text]) => {
38
+ const asset = writeAsset(deps, context.work.projectId, filename, Buffer.from(text));
39
+ assets.push(asset);
40
+ return preparedResult(deps, context, piece, role, asset);
41
+ });
42
+ }
43
+ catch (error) {
44
+ discardPreparedAssets(deps, assets);
45
+ throw error;
46
+ }
47
+ }
48
+ export async function publishResult(deps, context, piece, outputs, payload, asset = null) {
49
+ try {
50
+ const record = {
51
+ key: piece.key,
52
+ stageKind: context.work.kind,
53
+ asset,
54
+ fingerprint: piece.fingerprint,
55
+ piece: {
56
+ id: piece.id,
57
+ stageId: context.work.stageId,
58
+ kind: pieceKind(piece),
59
+ idx: pieceIndex(deps, context, piece),
60
+ state: "done",
61
+ payload: JSON.stringify({
62
+ ...payload,
63
+ requestFingerprint: piece.requestFingerprint,
64
+ ...(asset === null ? {} : { file: asset.path }),
65
+ }),
66
+ },
67
+ };
68
+ const result = await commitRevisionOutputs(deps, { work: context.work, pieceId: piece.id, publicationId: piece.id }, outputs, [record]);
69
+ deps.db
70
+ .prepare("UPDATE revision_work_pieces SET state='done' WHERE id=? AND work_id=?")
71
+ .run(piece.id, context.work.workId);
72
+ if (result.currentAttached)
73
+ context.emit({ type: "project.updated", projectId: context.work.projectId });
74
+ }
75
+ finally {
76
+ discardPreparedAssets(deps, [
77
+ ...outputs.map((output) => output.asset),
78
+ ...(asset === null ? [] : [asset]),
79
+ ]);
80
+ }
81
+ }
82
+ function pieceKind(piece) {
83
+ if (piece.input.kind === "tts")
84
+ return piece.input.segment === "body" ? "chunk" : "segment";
85
+ if (piece.input.kind === "provided" && /^audio:body:.+:\d+$/.test(piece.key))
86
+ return "chunk";
87
+ if (piece.input.kind === "provided" && /^audio:(intro|outro):\d+$/.test(piece.key))
88
+ return "segment";
89
+ if (piece.key.startsWith("image:"))
90
+ return "image";
91
+ if (piece.key.startsWith("research:chapter:"))
92
+ return "chapter";
93
+ if (piece.key.startsWith("entry:"))
94
+ return "segment";
95
+ if (piece.key === "research:planner" || piece.key === "thumbnail:prompt")
96
+ return "prompt_written";
97
+ return "article_written";
98
+ }
99
+ function pieceIndex(deps, context, piece) {
100
+ // Intro and outro can resolve independently. Reserve alternating ordinals so a
101
+ // late intro part cannot collide with an outro that was already published.
102
+ const segment = /^audio:(intro|outro):(\d+)$/.exec(piece.key);
103
+ if (segment !== null)
104
+ return Number(segment[2]) * 2 - (segment[1] === "intro" ? 1 : 0);
105
+ if (piece.key === "entry:intro:text")
106
+ return 1;
107
+ if (piece.key === "entry:outro:text")
108
+ return 2;
109
+ const localOrder = [
110
+ "audio:body:concat",
111
+ "audio:intro",
112
+ "audio:outro",
113
+ "audio:provided",
114
+ "subtitles:timing",
115
+ "subtitles:cues",
116
+ "subtitles:files",
117
+ "export:wav",
118
+ "export:video",
119
+ ];
120
+ const localIndex = localOrder.indexOf(piece.key);
121
+ if (localIndex >= 0)
122
+ return localIndex + 1;
123
+ const view = executionView(deps, context.work.projectId, context.work.revisionId);
124
+ const row = deps.db
125
+ .prepare("SELECT recipe_context FROM revision_work WHERE id=?")
126
+ .get(context.work.workId);
127
+ if (view !== undefined && row?.recipe_context !== null && row?.recipe_context !== undefined) {
128
+ const keys = executionPlan(deps, view, savedCatalogue(row.recipe_context)).recipes.filter((recipe) => recipe.stage === context.work.kind &&
129
+ pieceKind({ ...piece, key: recipe.key, input: recipe.input }) === pieceKind(piece));
130
+ const index = keys.findIndex((recipe) => recipe.key === piece.key);
131
+ if (index >= 0)
132
+ return index + 1;
133
+ }
134
+ const last = piece.key.split(":").at(-1);
135
+ return last !== undefined && /^\d+$/.test(last) ? Number(last) : 1;
136
+ }
137
+ export function retainPartialArticle(deps, context, piece, text) {
138
+ const publicationId = fingerprint(["partial-article", piece.id]);
139
+ if (deps.db
140
+ .prepare("SELECT 1 FROM revision_outputs WHERE revision_id=? AND publication_id=?")
141
+ .get(context.work.revisionId, publicationId) !== undefined)
142
+ return;
143
+ const result = preparedText(deps, context, piece, "article_md", "partial-article.md", text);
144
+ try {
145
+ transact(deps.db, () => {
146
+ const authority = publicationAuthority(deps.db, {
147
+ work: context.work,
148
+ pieceId: piece.id,
149
+ publicationId: piece.id,
150
+ });
151
+ if (authority === undefined)
152
+ return;
153
+ if (authority.key !== piece.key || authority.fingerprint !== piece.fingerprint)
154
+ throw new Error("Partial article differs from its durable piece authority.");
155
+ const revision = revisionById(deps.db, context.work.projectId, context.work.revisionId);
156
+ if (revision === undefined)
157
+ throw new Error("Partial article origin disappeared.");
158
+ insertAsset(deps.db, result.asset);
159
+ insertManifestOutput(deps.db, revision, {
160
+ slot: `article:partial:${piece.id}`,
161
+ workKey: `article:partial:${piece.id}`,
162
+ assetId: result.asset.id,
163
+ output: { ...result.output, originalFilename: "partial-article.md" },
164
+ fingerprint: piece.fingerprint,
165
+ state: "outdated",
166
+ }, deps.ids.next(), publicationId);
167
+ });
168
+ }
169
+ finally {
170
+ discardPreparedAssets(deps, [result.asset]);
171
+ }
172
+ }
@@ -0,0 +1,35 @@
1
+ import { executeExportRecipe } from "./runtime-export.js";
2
+ import { executeLocalRecipe } from "./runtime-local.js";
3
+ import { executeProviderRecipe } from "./runtime-provider.js";
4
+ import { executeSubtitleRecipe } from "./runtime-subtitles.js";
5
+ import { workPieces } from "./work-records.js";
6
+ export async function runRevisionInvocation(deps, context, providers) {
7
+ const pieces = workPieces(deps.db, context.work.workId).filter((piece) => !piece.key.startsWith("article:continuation:"));
8
+ for (const piece of pieces) {
9
+ if (piece.state === "done")
10
+ continue;
11
+ if (piece.input.kind === "deferred")
12
+ return "held";
13
+ try {
14
+ const outcome = piece.key.startsWith("subtitles:")
15
+ ? await executeSubtitleRecipe(deps, context, piece)
16
+ : piece.key.startsWith("export:")
17
+ ? await executeExportRecipe(deps, context, piece)
18
+ : piece.input.kind === "llm" || piece.input.kind === "tts" || piece.input.kind === "image"
19
+ ? await executeProviderRecipe(deps, context, providers, piece)
20
+ : await executeLocalRecipe(deps, context, piece);
21
+ if (outcome === "held")
22
+ return outcome;
23
+ }
24
+ catch (error) {
25
+ deps.db
26
+ .prepare("UPDATE revision_work_pieces SET state=? WHERE id=? AND work_id=? AND state!='done'")
27
+ .run(context.signal.aborted ? "pending" : "failed", piece.id, context.work.workId);
28
+ throw error;
29
+ }
30
+ }
31
+ deps.db
32
+ .prepare("UPDATE revision_work_pieces SET state='done' WHERE work_id=? AND result_json IS NOT NULL")
33
+ .run(context.work.workId);
34
+ return "done";
35
+ }