@gentbajko/slopify 2.3.1 → 2.5.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 (146) hide show
  1. package/dist/adapter-registry.js +9 -2
  2. package/dist/adapters/host-cli/index.js +11 -0
  3. package/dist/adapters/host-cli/transport.js +6 -1
  4. package/dist/adapters/image/codex-output.js +59 -17
  5. package/dist/adapters/image/codex.js +92 -16
  6. package/dist/adapters/image/fal.js +99 -8
  7. package/dist/adapters/image/google.js +13 -1
  8. package/dist/adapters/image/openai.js +37 -15
  9. package/dist/adapters/image/reference.js +7 -0
  10. package/dist/adapters/image/replicate.js +48 -2
  11. package/dist/adapters/image/video.js +28 -0
  12. package/dist/adapters/llm/codex-models.js +2 -1
  13. package/dist/adapters/llm/codex-server-models.js +2 -1
  14. package/dist/adapters/llm/run-cli.js +14 -2
  15. package/dist/assets/fonts/Barlow-Bold.ttf +0 -0
  16. package/dist/assets/fonts/SOURCE.txt +6 -0
  17. package/dist/assets/models.yaml +89 -1
  18. package/dist/catalog/registry.js +24 -3
  19. package/dist/catalog/runtime-models.js +1 -1
  20. package/dist/catalog/schema.js +20 -1
  21. package/dist/catalog/store.js +2 -2
  22. package/dist/catalog/validate.js +47 -4
  23. package/dist/edge/docker-launch.js +4 -0
  24. package/dist/edge/docker-projects/engine.js +57 -1
  25. package/dist/edge/docker-projects/install.js +10 -2
  26. package/dist/edge/docker-projects/prune.js +66 -0
  27. package/dist/edge/events/hub.js +11 -0
  28. package/dist/edge/http/app.js +2 -0
  29. package/dist/edge/http/backups.js +40 -0
  30. package/dist/edge/http/host-cli.js +17 -2
  31. package/dist/edge/http/problem.js +1 -0
  32. package/dist/edge/http/providers.js +8 -0
  33. package/dist/edge/http/schedules.js +1 -1
  34. package/dist/edge/http/settings.js +44 -0
  35. package/dist/edge/http/timeouts.js +38 -0
  36. package/dist/host-cli/runtime.js +3 -2
  37. package/dist/kernel/db/migrations/0019-shorts-prompts.sql +16 -0
  38. package/dist/kernel/db/migrations/0022-drop-duplicate-held-work.sql +56 -0
  39. package/dist/kernel/db/migrations/0024-reference-attachments.sql +18 -0
  40. package/dist/kernel/ports/host-cli.js +17 -2
  41. package/dist/kernel/ports/image.js +3 -1
  42. package/dist/kernel/ports/llm.js +3 -1
  43. package/dist/kernel/runner/attempt.js +5 -2
  44. package/dist/kernel/runner/providers.js +42 -2
  45. package/dist/main.js +55 -7
  46. package/dist/slices/admission/model.js +8 -0
  47. package/dist/slices/admission/rules.js +91 -2
  48. package/dist/slices/admission/schema.js +42 -1
  49. package/dist/slices/admission/start.js +53 -15
  50. package/dist/slices/backups/files.js +135 -0
  51. package/dist/slices/backups/folder.js +35 -0
  52. package/dist/slices/backups/model.js +65 -0
  53. package/dist/slices/backups/repo.js +28 -0
  54. package/dist/slices/backups/schedule.js +60 -0
  55. package/dist/slices/backups/service.js +311 -0
  56. package/dist/slices/batch/index.js +2 -0
  57. package/dist/slices/estimate/index.js +65 -2
  58. package/dist/slices/fonts/catalog.js +23 -0
  59. package/dist/slices/fonts/index.js +1 -1
  60. package/dist/slices/library/model.js +8 -1
  61. package/dist/slices/library/slots.js +13 -2
  62. package/dist/slices/library/snapshot.js +8 -2
  63. package/dist/slices/notifications/notifier.js +76 -0
  64. package/dist/slices/notifications/rules.js +81 -0
  65. package/dist/slices/notifications/send.js +40 -0
  66. package/dist/slices/notifications/settings.js +34 -0
  67. package/dist/slices/play-drafts/convert.js +72 -4
  68. package/dist/slices/play-drafts/readiness.js +7 -3
  69. package/dist/slices/play-drafts/repo.js +7 -1
  70. package/dist/slices/play-drafts/review-inputs.js +2 -0
  71. package/dist/slices/play-drafts/schema.js +44 -3
  72. package/dist/slices/play-drafts/service.js +6 -0
  73. package/dist/slices/play-drafts/uploads.js +2 -1
  74. package/dist/slices/project-templates/from-project.js +42 -0
  75. package/dist/slices/project-templates/setup.js +13 -0
  76. package/dist/slices/rebuild/preview-details.js +53 -3
  77. package/dist/slices/rebuild/preview-plan.js +2 -2
  78. package/dist/slices/rebuild/recipe-build.js +13 -3
  79. package/dist/slices/rebuild/recipe-edit.js +178 -0
  80. package/dist/slices/rebuild/recipe-exports.js +9 -4
  81. package/dist/slices/rebuild/recipe-input-schema.js +9 -1
  82. package/dist/slices/rebuild/recipe-legacy.js +12 -0
  83. package/dist/slices/rebuild/recipe-model.js +7 -1
  84. package/dist/slices/rebuild/recipe-provider-choice.js +15 -0
  85. package/dist/slices/rebuild/recipe-reference.js +44 -0
  86. package/dist/slices/rebuild/recipe-save.js +25 -1
  87. package/dist/slices/rebuild/recipe-shorts.js +186 -0
  88. package/dist/slices/rebuild/recipe-validation.js +33 -2
  89. package/dist/slices/rebuild/recipe-visual.js +29 -8
  90. package/dist/slices/rebuild/recipe-work.js +83 -1
  91. package/dist/slices/rebuild/runtime-actions.js +9 -2
  92. package/dist/slices/rebuild/runtime-animate.js +110 -0
  93. package/dist/slices/rebuild/runtime-export-edit.js +119 -0
  94. package/dist/slices/rebuild/runtime-export.js +13 -1
  95. package/dist/slices/rebuild/runtime-image.js +50 -0
  96. package/dist/slices/rebuild/runtime-local.js +6 -3
  97. package/dist/slices/rebuild/runtime-materialize.js +48 -0
  98. package/dist/slices/rebuild/runtime-piece-label.js +17 -0
  99. package/dist/slices/rebuild/runtime-provider.js +17 -10
  100. package/dist/slices/rebuild/runtime-publication.js +25 -4
  101. package/dist/slices/rebuild/runtime-run.js +11 -5
  102. package/dist/slices/rebuild/runtime-shorts.js +328 -0
  103. package/dist/slices/rebuild/runtime-store.js +7 -3
  104. package/dist/slices/rebuild/service-readiness.js +15 -1
  105. package/dist/slices/rebuild/transition-repo.js +40 -1
  106. package/dist/slices/revisions/adopt-content.js +7 -0
  107. package/dist/slices/revisions/adopt.js +7 -2
  108. package/dist/slices/revisions/mutation-assets.js +51 -9
  109. package/dist/slices/revisions/mutation-prepare.js +14 -14
  110. package/dist/slices/revisions/mutations.js +2 -1
  111. package/dist/slices/revisions/publish.js +20 -2
  112. package/dist/slices/revisions/schema.js +16 -1
  113. package/dist/slices/schedules/repo.js +1 -1
  114. package/dist/slices/schedules/service.js +6 -1
  115. package/dist/slices/settings/model.js +5 -0
  116. package/dist/slices/shorts/captions.js +272 -0
  117. package/dist/slices/shorts/clips.js +89 -0
  118. package/dist/slices/shorts/model.js +186 -0
  119. package/dist/slices/shorts/pick.js +232 -0
  120. package/dist/slices/shorts/prompts.js +68 -0
  121. package/dist/slices/shorts/render.js +163 -0
  122. package/dist/slices/storage/asset-name.js +9 -2
  123. package/dist/slices/storage/layout.js +18 -0
  124. package/dist/slices/storage/model.js +9 -0
  125. package/dist/slices/storage/portable.js +6 -3
  126. package/dist/slices/storage/reconcile.js +5 -1
  127. package/dist/slices/storage/schema.js +3 -0
  128. package/dist/slices/telemetry/model.js +3 -0
  129. package/dist/slices/video/cards.js +72 -0
  130. package/dist/slices/video/chapters.js +89 -0
  131. package/dist/slices/video/cuts.js +141 -0
  132. package/dist/slices/video/edit-list.js +36 -6
  133. package/dist/slices/video/edit-settings.js +191 -0
  134. package/dist/slices/video/ffmpeg.js +155 -29
  135. package/dist/slices/video/look.js +164 -0
  136. package/dist/slices/video/plan.js +68 -11
  137. package/dist/slices/video/slideshow.js +22 -6
  138. package/dist/slices/video/transitions.js +51 -0
  139. package/dist/slices/youtube/transcript.js +54 -0
  140. package/dist/web/assets/index-CktpxlJd.js +171 -0
  141. package/dist/web/assets/index-DB-vOnGU.css +1 -0
  142. package/dist/web/assets/{pdf-DR97Byps.js → pdf-BLF2i8c2.js} +1 -1
  143. package/dist/web/index.html +2 -2
  144. package/package.json +1 -1
  145. package/dist/web/assets/index-DfXmBzCk.js +0 -168
  146. package/dist/web/assets/index-Y3RhfTEh.css +0 -1
@@ -7,6 +7,7 @@ import { listCheckpoints } from "../checkpoints/repo.js";
7
7
  import { documentThemeOf } from "../document/model.js";
8
8
  import { requestHash } from "../play-drafts/repo.js";
9
9
  import { currentRevisionId, revisionById } from "../revisions/repo.js";
10
+ import { shortsExtrasForm } from "../shorts/model.js";
10
11
  import { createTemplate, readTemplate } from "./service.js";
11
12
  export const projectTemplateCreateSchema = z
12
13
  .object({
@@ -97,6 +98,15 @@ function documentFromProject(deps, revision) {
97
98
  return { name: prompt.name, number: String(prompt.number) };
98
99
  });
99
100
  addPrompt("thumbnail", config.thumbnailPrompt, "thumbnailPrompt");
101
+ addPrompt("shorts", config.shorts?.prompt, "shorts");
102
+ // The shorts' image style may be the same Library prompt one of the slideshow images uses,
103
+ // and a snapshot holds one prompt per kind and name.
104
+ if (!prompts.some((prompt) => prompt.kind === "image" && prompt.name === config.shorts?.imagePrompt))
105
+ addPrompt("image", config.shorts?.imagePrompt, "shortsImage");
106
+ // The establishing image's prompt, the same way.
107
+ if (config.reference?.source === "prompt" &&
108
+ !prompts.some((prompt) => prompt.kind === "image" && prompt.name === config.reference?.prompt))
109
+ addPrompt("image", config.reference.prompt, "referencePrompt");
100
110
  for (const category of ["intro", "outro"]) {
101
111
  const choice = config[category];
102
112
  const body = revision.content.promptTemplates[category] ?? config.rendered[category];
@@ -119,9 +129,16 @@ function documentFromProject(deps, revision) {
119
129
  : "",
120
130
  audio: config.sources.audio === "provide" ? attachment("Audio from project") : null,
121
131
  thumbnail: config.sources.thumbnail === "provide" ? attachment("Thumbnail from project") : null,
132
+ ...(config.reference?.source === "provide"
133
+ ? { reference: attachment("Establishing image from project") }
134
+ : {}),
122
135
  images: config.sources.images === "provide"
123
136
  ? (definitions.length ? definitions : (config.provided.images ?? [])).map((_, index) => attachment(`Image ${index + 1}`))
124
137
  : [],
138
+ // Named like the project's other files, to be attached again on Play.
139
+ ...(config.shorts?.enabled === true && revision.content.shortsMusic !== undefined
140
+ ? { shortsMusic: attachment("Music from project") }
141
+ : {}),
125
142
  };
126
143
  return {
127
144
  schemaVersion: 1,
@@ -141,12 +158,37 @@ function documentFromProject(deps, revision) {
141
158
  llm: config.llm ?? { provider: "", model: "" },
142
159
  audio: config.audio ?? { provider: "", model: "", voice: "" },
143
160
  images: config.images ?? { provider: "", model: "" },
161
+ ...(config.reference === undefined
162
+ ? {}
163
+ : {
164
+ reference: {
165
+ source: config.reference.source,
166
+ prompt: config.reference.prompt ?? "",
167
+ thumbnail: config.reference.thumbnail !== false,
168
+ },
169
+ }),
144
170
  articlePrompt: config.articlePrompt ?? "",
145
171
  ...(config.narrationPrompt === undefined ? {} : { narrationPrompt: config.narrationPrompt }),
146
172
  ...(config.youtubeDescription === true ? { youtubeDescription: true } : {}),
147
173
  ...(config.descriptionPrompt === undefined
148
174
  ? {}
149
175
  : { descriptionPrompt: config.descriptionPrompt }),
176
+ ...(config.shorts === undefined
177
+ ? {}
178
+ : {
179
+ shorts: {
180
+ enabled: config.shorts.enabled,
181
+ count: String(config.shorts.count),
182
+ minSeconds: String(config.shorts.minSeconds),
183
+ maxSeconds: String(config.shorts.maxSeconds),
184
+ prompt: config.shorts.prompt ?? "",
185
+ imagePrompt: config.shorts.imagePrompt ?? "",
186
+ ...shortsExtrasForm(config.shorts),
187
+ },
188
+ }),
189
+ // A project without edit settings makes a template without them, so a video made from
190
+ // it cuts every N seconds like the project did.
191
+ ...(config.videoEdit === undefined ? {} : { videoEdit: config.videoEdit }),
150
192
  imagePrompts,
151
193
  thumbnailPrompt: config.thumbnailPrompt ?? "",
152
194
  intro: config.intro?.name ?? "",
@@ -8,6 +8,12 @@ export function templateSetup(deps, input) {
8
8
  { kind: "description", name: input.form.descriptionPrompt ?? "" },
9
9
  ...input.form.imagePrompts.map((row) => ({ kind: "image", name: row.name })),
10
10
  { kind: "thumbnail", name: input.form.thumbnailPrompt },
11
+ { kind: "shorts", name: input.form.shorts?.prompt ?? "" },
12
+ { kind: "image", name: input.form.shorts?.imagePrompt ?? "" },
13
+ {
14
+ kind: "image",
15
+ name: input.form.reference?.source === "prompt" ? input.form.reference.prompt : "",
16
+ },
11
17
  ];
12
18
  for (const choice of choices) {
13
19
  if (choice.name === "")
@@ -51,6 +57,13 @@ export function freshTemplateDraft(deps, document, source) {
51
57
  audio: provided.audio === null ? null : fresh(provided.audio),
52
58
  thumbnail: provided.thumbnail === null ? null : fresh(provided.thumbnail),
53
59
  images: provided.images.map(fresh),
60
+ ...(provided.reference === undefined
61
+ ? {}
62
+ : { reference: provided.reference === null ? null : fresh(provided.reference) }),
63
+ // Like the images: the name is kept and the file is attached again on Play.
64
+ ...(provided.shortsMusic === undefined
65
+ ? {}
66
+ : { shortsMusic: provided.shortsMusic === null ? null : fresh(provided.shortsMusic) }),
54
67
  },
55
68
  },
56
69
  };
@@ -3,6 +3,8 @@ import { sourceOf } from "../admission/model.js";
3
3
  import { motionStyleLabels } from "../admission/rules.js";
4
4
  import { documentThemeLabel } from "../document/model.js";
5
5
  import { getRevisionView } from "../revisions/view.js";
6
+ import { musicVolumeOf, shortsSpeedOf } from "../shorts/model.js";
7
+ import { videoEditOf, videoEditRows } from "../video/edit-settings.js";
6
8
  export function previewDetails(deps, view, allRecipes, selected) {
7
9
  const parent = view.revision.parentId === null
8
10
  ? undefined
@@ -44,9 +46,19 @@ export function previewDetails(deps, view, allRecipes, selected) {
44
46
  return [
45
47
  {
46
48
  key: row.key,
47
- label: index < 0 ? "Thumbnail image" : `Image ${index + 1}`,
49
+ label: row.key === "reference:image"
50
+ ? "Establishing image"
51
+ : index < 0
52
+ ? "Thumbnail image"
53
+ : `Image ${index + 1}`,
48
54
  text: input.prompt,
49
- settings: [input.provider, input.model, input.aspect].join(" · "),
55
+ settings: [
56
+ input.provider,
57
+ input.model,
58
+ ...(input.thinking === undefined ? [] : [`effort ${input.thinking}`]),
59
+ input.aspect,
60
+ ...(input.reference === undefined ? [] : ["drawn from the establishing image"]),
61
+ ].join(" · "),
50
62
  },
51
63
  ];
52
64
  }
@@ -62,7 +74,9 @@ export function previewDetails(deps, view, allRecipes, selected) {
62
74
  ? "Article"
63
75
  : row.stage === "research"
64
76
  ? "Research"
65
- : "Thumbnail prompt";
77
+ : row.key.startsWith("shorts:")
78
+ ? "Short image prompts"
79
+ : "Thumbnail prompt";
66
80
  return [
67
81
  {
68
82
  key: row.key,
@@ -129,11 +143,41 @@ function inputChanges(parent, view) {
129
143
  add("Narration chunking", before.chunking, after.chunking);
130
144
  add("YouTube description", before.youtubeDescription === true ? "On" : "Off", after.youtubeDescription === true ? "On" : "Off");
131
145
  add("Description prompt", before.descriptionPrompt, after.descriptionPrompt);
146
+ add("Shorts", before.shorts?.enabled === true ? "On" : "Off", after.shorts?.enabled === true ? "On" : "Off");
147
+ add("Number of shorts", before.shorts?.count, after.shorts?.count);
148
+ add("Short length (seconds)", before.shorts === undefined
149
+ ? undefined
150
+ : `${String(before.shorts.minSeconds)}-${String(before.shorts.maxSeconds)}`, after.shorts === undefined
151
+ ? undefined
152
+ : `${String(after.shorts.minSeconds)}-${String(after.shorts.maxSeconds)}`);
153
+ add("Shorts prompt", before.shorts?.prompt, after.shorts?.prompt);
154
+ add("Shorts image prompt", before.shorts?.imagePrompt, after.shorts?.imagePrompt);
155
+ add("Establishing image", referenceLabel(before), referenceLabel(after));
156
+ add("Thumbnail drawn from the establishing image", before.reference === undefined
157
+ ? undefined
158
+ : before.reference.thumbnail === false
159
+ ? "Off"
160
+ : "On", after.reference === undefined ? undefined : after.reference.thumbnail === false ? "Off" : "On");
161
+ add("Title on the shorts", before.shorts === undefined ? undefined : before.shorts.titleOnScreen === true ? "On" : "Off", after.shorts === undefined ? undefined : after.shorts.titleOnScreen === true ? "On" : "Off");
162
+ add("Shorts speed", before.shorts === undefined ? undefined : `${shortsSpeedOf(before.shorts).toFixed(2)}×`, after.shorts === undefined ? undefined : `${shortsSpeedOf(after.shorts).toFixed(2)}×`);
163
+ add("Shorts music", old.shortsMusic === undefined ? "None" : "Uploaded file", next.shortsMusic === undefined
164
+ ? "None"
165
+ : next.shortsMusic === old.shortsMusic
166
+ ? "Uploaded file"
167
+ : "New uploaded file");
168
+ add("Shorts music volume (%)", before.shorts === undefined ? undefined : musicVolumeOf(before.shorts), after.shorts === undefined ? undefined : musicVolumeOf(after.shorts));
169
+ add("Full video link", before.shorts?.fullVideoLink, after.shorts?.fullVideoLink);
170
+ add("Short ranges set by hand", old.shortsRanges, next.shortsRanges);
132
171
  add("Silence gap (seconds)", before.silenceGapSeconds, after.silenceGapSeconds);
133
172
  add("Silence at start and end (seconds)", before.edgeSilenceSeconds, after.edgeSilenceSeconds);
134
173
  add("Seconds per image", before.imageSeconds, after.imageSeconds);
135
174
  add("Zoom (%)", before.zoomPercent, after.zoomPercent);
136
175
  add("Motion", motionStyleLabels[before.motionStyle], motionStyleLabels[after.motionStyle]);
176
+ // Row by row, so turning on grain says "Film grain: Off → Subtle" and nothing else.
177
+ const editBefore = videoEditRows(videoEditOf(before));
178
+ const editAfter = new Map(videoEditRows(videoEditOf(after)));
179
+ for (const [label, value] of editBefore)
180
+ add(label, value, editAfter.get(label));
137
181
  add("Intro", before.intro, after.intro);
138
182
  add("Outro", before.outro, after.outro);
139
183
  add("Subtitle settings", before.subtitles, after.subtitles);
@@ -163,3 +207,9 @@ function inputChanges(parent, view) {
163
207
  add("Manual captions", old.subtitleCues?.cues, next.subtitleCues?.cues);
164
208
  return changes;
165
209
  }
210
+ function referenceLabel(config) {
211
+ const reference = config.reference;
212
+ if (reference === undefined)
213
+ return "Off";
214
+ return reference.source === "provide" ? "Uploaded image" : `Prompt: ${reference.prompt ?? ""}`;
215
+ }
@@ -8,7 +8,7 @@ import { hasSubmittedRequest, requiresNewSubmission, retainedPreviewPlan, } from
8
8
  import { providedDependencyFingerprint } from "./provided-review.js";
9
9
  import { recipeInputSchema } from "./recipe-input-schema.js";
10
10
  import { recipeProviderChoice } from "./recipe-provider-choice.js";
11
- import { priceRecipe } from "./recipe-work.js";
11
+ import { priceRecipes } from "./recipe-work.js";
12
12
  import { reservationKey } from "./runtime-admission.js";
13
13
  import { narrationOrdinal } from "./runtime-narration-reuse.js";
14
14
  import { executionCatalogue } from "./runtime-plan.js";
@@ -166,7 +166,7 @@ export function planPreview(deps, view, catalogue, selection, id) {
166
166
  state: row.state,
167
167
  })),
168
168
  providedReuseRequired: execution.reviews.map((row) => row.key),
169
- costs: estimateRequests(work.map((row) => priceRecipe(row.inflight || !requiresNewSubmission(deps, view.revision.id, row.key, row.fingerprint)
169
+ costs: estimateRequests(work.flatMap((row) => priceRecipes(row.inflight || !requiresNewSubmission(deps, view.revision.id, row.key, row.fingerprint)
170
170
  ? { ...row, disposition: "reuse" }
171
171
  : row, recipes.find((recipe) => recipe.key === row.key))), snapshot),
172
172
  wholeRequestNotice: plan.wholeRequestNotice,
@@ -1,6 +1,10 @@
1
+ import { referenceForThumbnail } from "../admission/rules.js";
1
2
  import { audioRecipes } from "./recipe-audio.js";
2
3
  import { documentRecipes } from "./recipe-document.js";
4
+ import { editPlan } from "./recipe-edit.js";
3
5
  import { exportRecipes } from "./recipe-exports.js";
6
+ import { imageReference, referenceRecipe } from "./recipe-reference.js";
7
+ import { shortsRecipes } from "./recipe-shorts.js";
4
8
  import { textRecipes } from "./recipe-text.js";
5
9
  import { thumbnailRecipes, visualAssets, visualRecipes } from "./recipe-visual.js";
6
10
  import { youtubeRecipes } from "./recipe-youtube.js";
@@ -9,14 +13,20 @@ export function buildRecipes(context) {
9
13
  const audio = audioRecipes(context, text);
10
14
  const exports = exportRecipes(context, audio);
11
15
  const captions = exports.find((value) => value.key === "subtitles:files");
12
- const thumbnail = thumbnailRecipes(context, text.recipes);
16
+ // The establishing image, when it is on: every other image is drawn from it.
17
+ const reference = referenceRecipe(context);
18
+ const drawnFrom = imageReference(context, reference);
19
+ const thumbnail = thumbnailRecipes(context, text.recipes, referenceForThumbnail(context.config) ? drawnFrom : undefined);
20
+ const youtube = youtubeRecipes(context, exports);
13
21
  return [
14
22
  ...text.recipes,
15
23
  ...audio.recipes,
16
24
  ...exports,
17
- ...youtubeRecipes(context, exports),
25
+ ...youtube,
26
+ ...(reference === undefined ? [] : [reference]),
27
+ ...shortsRecipes(context, exports, drawnFrom),
18
28
  ...thumbnail,
19
29
  ...documentRecipes(context, text, thumbnail),
20
- ...visualAssets(context, visualRecipes(context.config, context.content, audio.mediaFingerprint, captions?.fingerprint ?? null)),
30
+ ...visualAssets(context, visualRecipes(context.config, context.content, audio.mediaFingerprint, captions?.fingerprint ?? null, (images) => editPlan(context, exports, youtube, images), drawnFrom)),
21
31
  ];
22
32
  }
@@ -0,0 +1,178 @@
1
+ import { z } from "zod";
2
+ import { fingerprint } from "../../kernel/runner/work.js";
3
+ import { usesYoutubeDescription } from "../admission/rules.js";
4
+ import { headingChapters, topHeadings } from "../video/chapters.js";
5
+ import { chapterOpeningShots, sentenceCutPoints } from "../video/cuts.js";
6
+ import { animatedClipSeconds, animatedImageIndexes, animatePrompt, editNeedsTiming, usesAnimation, usesChapterCards, usesNarrationCuts, videoEditOf, } from "../video/edit-settings.js";
7
+ import { fps } from "../video/plan.js";
8
+ import { recipe, resourceIdentity, selectedReference, } from "./recipe-model.js";
9
+ // What the Video stage's edit settings add to the plan: the word timing and chapters the
10
+ // render reads, and a paid `animate:<image>` request for each image brought to life. Every
11
+ // setting left at today's behaviour adds nothing, so a project without them, or with them all
12
+ // off, plans the same export fingerprint it always did and no finished video turns outdated.
13
+ //
14
+ // "Chapter openers" can only be told once the word timing (and the YouTube chapters, when
15
+ // that step runs) has landed, so until then one deferred `animate:future` stands in for the
16
+ // requests and carries their cost, the way `shorts:future` does.
17
+ export const animateFutureKey = "animate:future";
18
+ export function editPlan(context, exports, youtube, images) {
19
+ const { config } = context;
20
+ const edit = videoEditOf(config);
21
+ if (config.sources.video !== "generate")
22
+ return { recipes: [], values: [], dependsOn: [] };
23
+ const timing = editNeedsTiming(config)
24
+ ? exports.find((value) => value.key === "subtitles:timing")
25
+ : undefined;
26
+ const description = youtube.find((value) => value.key === "youtube:description");
27
+ const fromDescription = usesYoutubeDescription(config) && description !== undefined;
28
+ const values = [];
29
+ const dependsOn = [];
30
+ if (usesNarrationCuts(config))
31
+ values.push(["cuts", "narration-v1"]);
32
+ if (edit.transition !== "cut")
33
+ values.push(["transition", edit.transition, edit.transitionSeconds]);
34
+ if (edit.vignette !== "off" ||
35
+ edit.grain !== "off" ||
36
+ edit.grade !== "none" ||
37
+ edit.atmosphere !== "none")
38
+ values.push(["look-v1", edit.vignette, edit.grain, edit.grade, edit.atmosphere]);
39
+ if (usesChapterCards(config))
40
+ values.push(["cards-v1", config.subtitles?.fontId ?? "default"]);
41
+ if (timing !== undefined) {
42
+ values.push(["timing", resourceIdentity(context, timing)]);
43
+ dependsOn.push(timing.key);
44
+ if (fromDescription && description !== undefined) {
45
+ values.push(["chapters", resourceIdentity(context, description)]);
46
+ dependsOn.push(description.key);
47
+ }
48
+ else
49
+ values.push(["chapters", "headings", headingsIdentity(context)]);
50
+ }
51
+ const recipes = usesAnimation(config)
52
+ ? animateRecipes(context, images, timing, fromDescription ? description : undefined)
53
+ : [];
54
+ if (recipes.length > 0) {
55
+ values.push(["animate-v1", recipes.map((value) => value.fingerprint)]);
56
+ dependsOn.push(...recipes.map((value) => value.key));
57
+ }
58
+ return { recipes, values, dependsOn };
59
+ }
60
+ function animateRecipes(context, images, timing, description) {
61
+ const { config, content } = context;
62
+ const edit = videoEditOf(config);
63
+ const provider = config.images?.provider ?? "";
64
+ // Only images the project draws are animated; an uploaded picture or clip is shown as it
65
+ // was given.
66
+ const drawn = (index) => {
67
+ const key = content.imageOrder[index];
68
+ return key !== undefined && content.imageDefinitions[key]?.source === "generate";
69
+ };
70
+ let indexes;
71
+ if (edit.animate === "every")
72
+ indexes = animatedImageIndexes(edit, content.imageOrder.length);
73
+ else {
74
+ const opening = openingImages(context, timing, description);
75
+ if (opening === undefined) {
76
+ const upper = Math.min(content.imageOrder.filter((_key, index) => drawn(index)).length, chapterEstimate(context));
77
+ return [
78
+ recipe(context, animateFutureKey, "video", {
79
+ kind: "deferred",
80
+ version: 1,
81
+ operation: "animate",
82
+ template: [
83
+ timing?.fingerprint ?? null,
84
+ description?.fingerprint ?? null,
85
+ provider,
86
+ edit.animateModel,
87
+ upper,
88
+ ],
89
+ }, [
90
+ ...(timing === undefined ? [] : [timing.key]),
91
+ ...(description ? [description.key] : []),
92
+ ]),
93
+ ];
94
+ }
95
+ indexes = opening;
96
+ }
97
+ return indexes.filter(drawn).flatMap((index) => {
98
+ const key = content.imageOrder[index];
99
+ const still = images.find((value) => value.key === `image:${key ?? ""}`);
100
+ if (key === undefined || still === undefined)
101
+ return [];
102
+ return [
103
+ recipe(context, `animate:${key}`, "video", {
104
+ kind: "image",
105
+ version: 1,
106
+ provider,
107
+ model: edit.animateModel,
108
+ prompt: animatePrompt,
109
+ aspect: config.format,
110
+ animate: { image: still.fingerprint, seconds: animatedClipSeconds },
111
+ }, [still.key], { unresolved: still.unresolved }),
112
+ ];
113
+ });
114
+ }
115
+ // The images (0-based, in slideshow order) the video and its chapters open on, once the word
116
+ // timing and chapters are saved; undefined until then.
117
+ function openingImages(context, timing, description) {
118
+ const count = context.content.imageOrder.length;
119
+ if (count === 0)
120
+ return [];
121
+ const words = timing === undefined ? undefined : savedWords(context, timing);
122
+ if (words === undefined)
123
+ return undefined;
124
+ const chapters = description === undefined
125
+ ? headingChapters(context.resolved.articleMarkdown ?? "", words)
126
+ : savedChapters(context, description);
127
+ if (chapters === undefined)
128
+ return undefined;
129
+ const shots = chapterOpeningShots({
130
+ fps,
131
+ imageSeconds: context.config.imageSeconds,
132
+ narration: usesNarrationCuts(context.config),
133
+ cutPoints: sentenceCutPoints(words),
134
+ chapterStarts: chapters.map((chapter) => chapter.start),
135
+ });
136
+ return [...new Set(shots.map((shot) => shot % count))];
137
+ }
138
+ const wordsPayload = z.object({
139
+ words: z.array(z.object({ text: z.string(), start: z.number(), end: z.number() })),
140
+ });
141
+ const chaptersPayload = z.object({
142
+ chapters: z.array(z.object({ start: z.number(), title: z.string() })),
143
+ });
144
+ function savedPayload(context, value) {
145
+ const piece = context.manifest.pieces.find((row) => row.key === value.key &&
146
+ selectedReference(row) &&
147
+ row.piece.state === "done" &&
148
+ row.fingerprint === value.fingerprint);
149
+ if (piece?.piece.payload === undefined || piece.piece.payload === null)
150
+ return undefined;
151
+ try {
152
+ return JSON.parse(piece.piece.payload);
153
+ }
154
+ catch {
155
+ return undefined;
156
+ }
157
+ }
158
+ function savedWords(context, timing) {
159
+ const parsed = wordsPayload.safeParse(savedPayload(context, timing));
160
+ return parsed.success ? parsed.data.words : undefined;
161
+ }
162
+ function savedChapters(context, description) {
163
+ const parsed = chaptersPayload.safeParse(savedPayload(context, description));
164
+ return parsed.success ? parsed.data.chapters : undefined;
165
+ }
166
+ // The article's chapter headings, which decide where heading chapters fall; null while the
167
+ // article is not written yet.
168
+ function headingsIdentity(context) {
169
+ const markdown = context.resolved.articleMarkdown;
170
+ return markdown === null ? null : fingerprint(topHeadings(markdown).map((one) => one.title));
171
+ }
172
+ // ceiling: how many chapters "Chapter openers" is charged for before they are known: the
173
+ // article's headings and the opening, or twelve (the most a YouTube description is asked for)
174
+ // while the article is not written yet.
175
+ function chapterEstimate(context) {
176
+ const markdown = context.resolved.articleMarkdown;
177
+ return markdown === null ? 12 : topHeadings(markdown).length + 1;
178
+ }
@@ -1,5 +1,6 @@
1
- import { usesYoutubeDescription } from "../admission/rules.js";
1
+ import { usesShorts, usesYoutubeDescription } from "../admission/rules.js";
2
2
  import { audioExportArgs } from "../video/audio-export-args.js";
3
+ import { editNeedsTiming } from "../video/edit-settings.js";
3
4
  import { recipe, resourceIdentity, } from "./recipe-model.js";
4
5
  export function exportRecipes(context, audio) {
5
6
  const { config, content } = context;
@@ -17,9 +18,13 @@ export function exportRecipes(context, audio) {
17
18
  ],
18
19
  }, audio.keys));
19
20
  const captions = config.subtitles !== undefined && config.subtitles.mode !== "off";
20
- // The YouTube description's chapters use the same word timing, so it runs for them even
21
- // with captions off; only the caption files below wait for captions.
22
- if (!captions && !usesYoutubeDescription(config))
21
+ // The YouTube description's chapters, the shorts' clips and captions, and the video's cuts,
22
+ // chapter cards and chapter openers use the same word timing, so it runs for them even with
23
+ // captions off; only the caption files below wait for captions.
24
+ if (!captions &&
25
+ !usesYoutubeDescription(config) &&
26
+ !usesShorts(config) &&
27
+ !editNeedsTiming(config))
23
28
  return recipes;
24
29
  const timing = recipe(context, "subtitles:timing", "video", {
25
30
  kind: "local",
@@ -23,7 +23,9 @@ export const recipeInputSchema = z.discriminatedUnion("kind", [
23
23
  .object({
24
24
  budget: z.number().finite().optional(),
25
25
  level: z.enum(["minimal", "low", "medium", "high"]).optional(),
26
- effort: z.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).optional(),
26
+ effort: z
27
+ .enum(["none", "minimal", "low", "medium", "high", "xhigh", "max", "ultra"])
28
+ .optional(),
27
29
  })
28
30
  .strict()
29
31
  .nullable(),
@@ -66,6 +68,12 @@ export const recipeInputSchema = z.discriminatedUnion("kind", [
66
68
  model: z.string(),
67
69
  prompt: z.string(),
68
70
  aspect: z.enum(["16:9", "9:16"]),
71
+ animate: z.object({ image: z.string(), seconds: z.number() }).strict().optional(),
72
+ thinking: z.enum(thinkingModes).optional(),
73
+ reference: z
74
+ .object({ fingerprint: z.string(), assetId: z.string().nullable() })
75
+ .strict()
76
+ .optional(),
69
77
  })
70
78
  .strict(),
71
79
  z
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { referenceKey } from "../admission/model.js";
2
3
  import { buildRecipes } from "./recipe-build.js";
3
4
  export function baselineFingerprints(project, outputs, pieces, content, stages = []) {
4
5
  const recipes = buildRecipes({
@@ -34,6 +35,8 @@ export function legacyOutputWorkKey(output, config) {
34
35
  return `image:${output.id}`;
35
36
  case "thumbnail":
36
37
  return "thumbnail:image";
38
+ case "reference":
39
+ return referenceKey;
37
40
  case "notes":
38
41
  return "research:notes";
39
42
  case "article_md":
@@ -59,6 +62,15 @@ export function legacyOutputWorkKey(output, config) {
59
62
  case "youtube_description":
60
63
  case "youtube_tags":
61
64
  return "youtube:description";
65
+ case "shorts":
66
+ return "shorts:pick";
67
+ case "short_image":
68
+ return `shorts:${String(output.meta.short ?? 1)}:image:${String(output.meta.index ?? 1)}`;
69
+ case "short_video":
70
+ return `shorts:${String(output.meta.short ?? 1)}:render`;
71
+ // Only ever made by a revision, never by the legacy stages.
72
+ case "animated_image":
73
+ return `animate:${String(output.meta.index ?? 1)}`;
62
74
  case "subtitles_srt":
63
75
  case "subtitles_vtt":
64
76
  case "subtitle_ass":
@@ -17,6 +17,8 @@ export const localOperations = [
17
17
  "narration-files-v1",
18
18
  "render-document",
19
19
  "youtube-description-v1",
20
+ "shorts-pick-v1",
21
+ "short-render-v1",
20
22
  ];
21
23
  export const deferredOperations = [
22
24
  "narration-preparation",
@@ -30,6 +32,8 @@ export const deferredOperations = [
30
32
  "intro-narration",
31
33
  "outro-narration",
32
34
  "resolve-revision-recipe",
35
+ "shorts",
36
+ "animate",
33
37
  ];
34
38
  export function recipe(context, key, stage, input, dependsOn = [], options = {}) {
35
39
  const requestFingerprint = input.kind === "tts"
@@ -40,7 +44,9 @@ export function recipe(context, key, stage, input, dependsOn = [], options = {})
40
44
  : fingerprint(JSON.parse(JSON.stringify(input, (_key, value) => typeof value === "number" ? z.number().finite().parse(value) : value)));
41
45
  const token = input.kind === "tts"
42
46
  ? narrationRegenerationToken(context.content.regenerationTokens, input.logicalKey, input.segment)
43
- : (context.content.regenerationTokens[options.tokenKey ?? key] ?? null);
47
+ : options.token !== undefined
48
+ ? options.token
49
+ : (context.content.regenerationTokens[options.tokenKey ?? key] ?? null);
44
50
  const researchToken = stage === "research" ? context.content.regenerationTokens["research:all"] : undefined;
45
51
  const workFingerprint = fingerprint([
46
52
  requestFingerprint,
@@ -5,6 +5,21 @@ export function recipeProviderChoice(recipe, config) {
5
5
  // The YouTube description's request is built when it runs, from the project's LLM row.
6
6
  if (input.kind === "local" && input.operation === "youtube-description-v1")
7
7
  return config.llm === undefined ? undefined : { ...config.llm, family: "llm" };
8
+ // So is the shorts' pick; what stands in for the rest of the step until it lands is
9
+ // mostly images, which are its cost.
10
+ if (input.kind === "local" && input.operation === "shorts-pick-v1")
11
+ return config.llm === undefined ? undefined : { ...config.llm, family: "llm" };
12
+ if (input.kind === "deferred" && input.operation === "shorts")
13
+ return config.images === undefined ? undefined : { ...config.images, family: "image" };
14
+ // The chapter openers' clips are asked of the image provider's image-to-video model.
15
+ if (input.kind === "deferred" && input.operation === "animate")
16
+ return config.images === undefined
17
+ ? undefined
18
+ : {
19
+ provider: config.images.provider,
20
+ model: config.videoEdit?.animateModel ?? "",
21
+ family: "image",
22
+ };
8
23
  if (input.kind !== "deferred")
9
24
  return undefined;
10
25
  const family = input.operation === "narration-preparation"
@@ -0,0 +1,44 @@
1
+ import { referenceKey } from "../admission/model.js";
2
+ import { usesReference } from "../admission/rules.js";
3
+ import { recipe, selectedAsset, } from "./recipe-model.js";
4
+ import { renderedPrompt } from "./recipe-text.js";
5
+ // The establishing image (`RunDraft.reference`): one step of the Images stage, made before
6
+ // every other image of the run and never part of the video. Made from its library prompt, whose
7
+ // keywords are filled like every other image prompt's, or the file the user uploaded.
8
+ export const referencePromptKey = "referencePrompt";
9
+ // The image provider's choice as an image request carries it: the effort only when one is set,
10
+ // so a request made without one keeps the fingerprint it always had.
11
+ export function imageChoice(config) {
12
+ return {
13
+ provider: config.images?.provider ?? "",
14
+ model: config.images?.model ?? "",
15
+ ...(config.images?.thinking === undefined ? {} : { thinking: config.images.thinking }),
16
+ };
17
+ }
18
+ export function referenceRecipe(context) {
19
+ const { config, content } = context;
20
+ if (!usesReference(config) || config.reference === undefined)
21
+ return undefined;
22
+ if (config.reference.source === "provide")
23
+ return recipe(context, referenceKey, "images", {
24
+ kind: "provided",
25
+ version: 1,
26
+ assetId: content.provided.reference ?? null,
27
+ semantic: "reference",
28
+ }, [], { unresolved: content.provided.reference === undefined });
29
+ const prompt = renderedPrompt(context, referencePromptKey);
30
+ return recipe(context, referenceKey, "images", { kind: "image", version: 1, ...imageChoice(config), aspect: config.format, prompt }, [], { unresolved: prompt.trim() === "" });
31
+ }
32
+ // Which image the others are drawn from: the step's fingerprint, so making it again or
33
+ // changing it marks every image drawn from it outdated, and the asset it made.
34
+ export function imageReference(context, reference) {
35
+ return reference === undefined
36
+ ? undefined
37
+ : {
38
+ key: reference.key,
39
+ input: {
40
+ fingerprint: reference.fingerprint,
41
+ assetId: selectedAsset(context, reference.key, reference.fingerprint),
42
+ },
43
+ };
44
+ }