@gentbajko/slopify 3.0.6 → 3.0.8

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 (37) hide show
  1. package/dist/adapters/tts/elevenlabs.js +2 -0
  2. package/dist/assets/document/fonts/Literata-Italic.ttf +0 -0
  3. package/dist/assets/document/fonts/Literata-OFL.txt +93 -0
  4. package/dist/assets/document/fonts/Literata-Regular.ttf +0 -0
  5. package/dist/assets/document/fonts/Literata-SemiBold.ttf +0 -0
  6. package/dist/assets/document/fonts/Literata-SemiBoldItalic.ttf +0 -0
  7. package/dist/assets/document/fonts/SOURCE.txt +13 -1
  8. package/dist/assets/models.yaml +26 -0
  9. package/dist/edge/autostart/native.js +12 -3
  10. package/dist/extension/slopify-studio-chrome.zip +0 -0
  11. package/dist/extension/slopify-studio-firefox.zip +0 -0
  12. package/dist/kernel/db/migrations/0042-narration-retries.sql +13 -0
  13. package/dist/main.js +9 -1
  14. package/dist/patch-notes/3.0.7.md +33 -0
  15. package/dist/patch-notes/3.0.8.md +23 -0
  16. package/dist/patch-notes/index.json +12 -0
  17. package/dist/slices/document/fonts.js +29 -11
  18. package/dist/slices/document/render.js +1 -1
  19. package/dist/slices/document/theme.js +3 -3
  20. package/dist/slices/rebuild/narration-retry.js +128 -0
  21. package/dist/slices/rebuild/recovery-selection.js +7 -0
  22. package/dist/slices/rebuild/runtime-subtitles.js +53 -11
  23. package/dist/slices/schedules/service.js +2 -1
  24. package/dist/slices/storage/backup-format.js +1 -0
  25. package/dist/slices/youtube/answer.js +1 -1
  26. package/dist/slices/youtube/model.js +3 -2
  27. package/dist/tutorials/Document-Themes.md +1 -1
  28. package/dist/tutorials/Home-and-Projects.md +4 -4
  29. package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +3 -3
  30. package/dist/tutorials/Play-Overview.md +1 -1
  31. package/dist/tutorials/Schedules.md +2 -2
  32. package/dist/tutorials/YouTube-Description.md +1 -1
  33. package/dist/web/assets/index-Cdb0Jh3P.js +310 -0
  34. package/dist/web/assets/{pdf-C5xctS6p.js → pdf-BhFuoBjr.js} +1 -1
  35. package/dist/web/index.html +1 -1
  36. package/package.json +1 -1
  37. package/dist/web/assets/index-CskH1GRY.js +0 -310
@@ -16,6 +16,7 @@ import { speakerColour } from "../voices/palette.js";
16
16
  import { speakerPanelEvents, usesSpeakerPanel } from "../voices/panel.js";
17
17
  import { panelPortraits } from "../voices/portraits.js";
18
18
  import { attributeWords } from "../voices/timing.js";
19
+ import { narrationRetryLimit, requestNarrationRetry } from "./narration-retry.js";
19
20
  import { exportSnapshot, retainedOutput, revisionAudio, revisionTranscript, revisionTurns, } from "./runtime-export-inputs.js";
20
21
  import { narrationChunks, narrationTextParts, } from "./runtime-narration-text.js";
21
22
  import { preparedResult, preparedText, publishResult } from "./runtime-publication.js";
@@ -77,7 +78,7 @@ async function timing(deps, context, piece, snapshot) {
77
78
  context.signal.throwIfAborted();
78
79
  const { path, kind } = segment;
79
80
  if (path !== null && spoken(kind)) {
80
- const aligned = await located(snapshot, kind, () => alignSubtitles({
81
+ const aligned = await located(deps, context, snapshot, kind, () => alignSubtitles({
81
82
  audioPath: path,
82
83
  text: revisionTranscript(deps, snapshot, kind),
83
84
  aliases: narrationAliasesOf(snapshot.view.revision.config),
@@ -250,45 +251,86 @@ async function files(deps, context, piece, snapshot) {
250
251
  }
251
252
  // A mismatch names the place to fix: the time in the narration, the chunk that holds it (as
252
253
  // the Narration editor numbers them) and what was expected against what was heard. The
253
- // usual cause is a TTS request that skipped or reworded a sentence.
254
- async function located(snapshot, segment, align) {
254
+ // usual cause is a TTS request that skipped or reworded a sentence, which a second request
255
+ // almost always reads right, so a chunk made by a voice is first recorded again by itself
256
+ // (`narration-retry.ts`); the message says so, and says when its tries are used up.
257
+ async function located(deps, context, snapshot, segment, align) {
255
258
  try {
256
259
  return await align();
257
260
  }
258
261
  catch (error) {
259
262
  if (!(error instanceof SubtitleMismatch))
260
263
  throw error;
261
- throw new Error(mismatchMessage(snapshot, segment, error), { cause: error });
264
+ const chunks = mismatchChunks(snapshot, segment);
265
+ const at = mismatchChunk(chunks, error);
266
+ const chunk = at === -1 ? undefined : chunks[at];
267
+ const retry = chunk !== undefined && retryable(snapshot, chunk.key)
268
+ ? {
269
+ chunk: at + 1,
270
+ try: requestNarrationRetry(deps.db, {
271
+ projectId: context.work.projectId,
272
+ chunkKey: chunk.key,
273
+ now: deps.clock.now().toISOString(),
274
+ }),
275
+ }
276
+ : undefined;
277
+ throw new Error(describeMismatch(chunks, segment, error, retry), { cause: error });
262
278
  }
263
279
  }
264
- export function mismatchMessage(snapshot, segment, mismatch) {
265
- let chunks = [];
280
+ // A chunk a voice made from the text: not uploaded audio, not captions edited by hand (their
281
+ // remake waits for the person) and not a multi-voice turn, whose key the regeneration list
282
+ // can't hold yet.
283
+ function retryable(snapshot, key) {
284
+ if (snapshot.view.revision.content.subtitleCues !== undefined)
285
+ return false;
286
+ if (/:turn:\d+$/.test(key))
287
+ return false;
288
+ return snapshot.plan.recipes.some((row) => row.input.kind === "tts" && row.input.logicalKey === key);
289
+ }
290
+ function mismatchChunks(snapshot, segment) {
266
291
  try {
267
- chunks = narrationChunks(narrationTextParts(snapshot.view, snapshot.plan, segment));
292
+ return narrationChunks(narrationTextParts(snapshot.view, snapshot.plan, segment));
268
293
  }
269
294
  catch {
270
295
  // Without a readable plan the message still gives the time and the words.
296
+ return [];
271
297
  }
272
- return describeMismatch(chunks, segment, mismatch);
273
298
  }
274
- export function describeMismatch(chunks, segment, mismatch) {
299
+ export function mismatchMessage(snapshot, segment, mismatch) {
300
+ return describeMismatch(mismatchChunks(snapshot, segment), segment, mismatch);
301
+ }
302
+ // The chunk that holds the words the audio stopped matching, by index; -1 when none does.
303
+ export function mismatchChunk(chunks, mismatch) {
275
304
  const probe = comparable(mismatch.expected).split(" ").slice(0, 6).join(" ");
276
- const at = probe === ""
305
+ return probe === ""
277
306
  ? chunks.length - 1
278
307
  : chunks.findIndex((chunk) => comparable(chunk.spokenText).includes(probe));
308
+ }
309
+ export function describeMismatch(chunks, segment, mismatch,
310
+ // Set when the chunk is one a voice made: `try` is the automatic try just asked for, or
311
+ // undefined once its tries are used up.
312
+ retry) {
313
+ const at = mismatchChunk(chunks, mismatch);
279
314
  const chunk = at === -1 ? undefined : chunks[at];
280
315
  const where = chunk === undefined
281
316
  ? `${clock(mismatch.at)} into the ${segment} narration`
282
317
  : `${clock(mismatch.at)} into the ${segment} narration, in narration chunk ${String(at + 1)} of ${String(chunks.length)} (it starts "${opening(chunk.spokenText)}")`;
283
318
  const expected = mismatch.expected === "" ? "the end of the text" : `"${mismatch.expected}…"`;
284
319
  const heard = mismatch.heard === "" ? "no more speech" : `"${mismatch.heard.toLowerCase()}…"`;
320
+ const stopped = `Subtitles stopped matching the audio at ${where}. The text expected ${expected} but the audio has ${heard}`;
321
+ if (retry !== undefined) {
322
+ const n = String(retry.chunk);
323
+ return retry.try === undefined
324
+ ? `${stopped} Slopify already recorded narration chunk ${n} again ${String(narrationRetryLimit)} times and the audio still doesn't match there, so the voice probably reads something its own way (a year, an abbreviation or a name). Listen at ${clock(mismatch.at)}: if words are missing or wrong, reword that sentence in Edit project → Narration → narration chunk ${n}, save, then Continue the run. If it says them right, use Download diagnostics in Settings and report it.`
325
+ : `${stopped} The voice probably skipped or garbled words there, so Slopify is recording narration chunk ${n} again by itself (try ${String(retry.try)} of ${String(narrationRetryLimit)}) and carries on with the video when it's done. If this message is still here in a few minutes, regenerate narration chunk ${n} in Edit project → Narration, then Continue the run.`;
326
+ }
285
327
  const fix = chunk === undefined
286
328
  ? `Listen at ${clock(mismatch.at)}: if words are missing or wrong, regenerate that part in Edit project → Narration, then Continue the run.`
287
329
  : `Listen at ${clock(mismatch.at)}: if words are missing or wrong, regenerate narration chunk ${String(at + 1)} in Edit project → Narration, then Continue the run.`;
288
330
  // A voice that reads a year, an abbreviation or a name its own way reads it the same way
289
331
  // again, so a remake would cost a chunk and fail here once more.
290
332
  const same = "If it says them right, only another way (a year, an abbreviation or a name read its own way), regenerating gives the same reading: use Download diagnostics in Settings and report it.";
291
- return `Subtitles stopped matching the audio at ${where}. The text expected ${expected} but the audio has ${heard} The recording there probably skips or changes words. ${fix} ${same}`;
333
+ return `${stopped} The recording there probably skips or changes words. ${fix} ${same}`;
292
334
  }
293
335
  function comparable(text) {
294
336
  return text
@@ -90,7 +90,8 @@ export function updateSchedule(deps, input) {
90
90
  : previous.nextRunAt === null
91
91
  ? null
92
92
  : nextOccurrence(parsed.data.cadence, parsed.data.timezone, now);
93
- const value = summary(parsed.data, next?.toISOString() ?? null, now.toISOString(), previous.status, previous.version + 1,
93
+ // The version it was saved against, for the record; runs use the newest.
94
+ const value = summary({ ...parsed.data, templateVersion: checked.value }, next?.toISOString() ?? null, now.toISOString(), previous.status, previous.version + 1,
94
95
  // Saving clears a recorded generation failure, so the next tick tries again.
95
96
  { ...previous.topics, failedAt: null, error: null });
96
97
  if (!updateScheduleRow(deps.db, value, parsed.data.mutationId, hash, parsed.data.baseVersion))
@@ -57,6 +57,7 @@ export const projectTables = [
57
57
  "project_uploads",
58
58
  // Since 2.5.0. Left out on purpose: plan_limit_waits and plan_limit_waiters (an account's
59
59
  // current wait, a lease of this install) and prompt_softening (a pending one-off request).
60
+ // Since 3.0.8, narration_retries is left out too: the tries of a run, not of the project.
60
61
  "review_verdicts",
61
62
  "provider_usage",
62
63
  "plan_limit_readings",
@@ -23,7 +23,7 @@ export function descriptionMessages(brief) {
23
23
  `- The pinned comment is plain text without links, at most ${String(pinnedCommentMaxCharacters)} characters.`,
24
24
  `- Titles are exactly ${String(alternativeTitles)} other titles for this video, for YouTube's title A/B test beside the video's own title: each at most ${String(titleMaxCharacters)} characters, one line, different from the video title and from each other.`,
25
25
  "",
26
- "The pinned comment is what the channel pins under the video, unless the instructions below say otherwise: thank the viewer in one line, ask one question a viewer can answer from their own experience of the subject, point to the chapters in the description, and end by inviting a suggestion for the next video. Two to four short paragraphs, in the same voice as the summary.",
26
+ "The pinned comment is what the channel pins under the video, unless the instructions below say otherwise: one or two short sentences, at most 200 characters, in the same voice as the summary. Ask one question a viewer can answer from their own experience of the subject, and invite a suggestion for the next video.",
27
27
  "The other titles try different angles on the same video (a question, a stake, a surprising fact) while staying true to it and in the video title's language and tone.",
28
28
  ].join("\n"),
29
29
  },
@@ -17,8 +17,9 @@ export const descriptionMaxCharacters = 5000;
17
17
  // these alternatives. YouTube's limit on a title.
18
18
  export const alternativeTitles = 2;
19
19
  export const titleMaxCharacters = 100;
20
- // YouTube's limit on a comment is 10,000; a pinned comment people read in full is far shorter.
21
- export const pinnedCommentMaxCharacters = 1500;
20
+ // YouTube's limit on a comment is 10,000; a pinned comment people read in full is a line or
21
+ // two. The model is asked for 200; this is the most a hand-picked prompt may make it.
22
+ export const pinnedCommentMaxCharacters = 500;
22
23
  // What a project uses when no Description prompt from the library is picked. No keywords,
23
24
  // so it never asks Play for a field.
24
25
  export const defaultDescriptionPrompt = [
@@ -57,7 +57,7 @@ Sizes are in millimetres (mm) unless marked pt (points; 1 pt is about 0.35 mm).
57
57
 
58
58
  ### Fonts
59
59
 
60
- Each font has three controls: the family, its weight or italic, and **Letter spacing (mm)** from −0.5 to 2. The families are **Cinzel** (bundled; four weights, no italics) and **Times**, **Helvetica** and **Courier** (built into every PDF reader).
60
+ Each font has three controls: the family, its weight or italic, and **Letter spacing (mm)** from −0.5 to 2. The families are **Cinzel** (bundled; four weights, no italics), **Literata** (bundled; a text face made for long reading, in regular, bold, italic and bold italic) and **Times**, **Helvetica** and **Courier** (built into every PDF reader).
61
61
 
62
62
  | Setting | Where it is used | Default |
63
63
  |---|---|---|
@@ -19,7 +19,7 @@ From top to bottom:
19
19
  | Destinations | **Home**, **Projects**, **Calendar** (also lit on Schedules), **Channels**, **Library** (Prompts, Intros & Outros, Templates, Document themes, Narration aliases), **Settings** (also lit on Usage). |
20
20
  | **N running** | Shown while runs are going. It links to Running now on Home. |
21
21
  | **Channel** | The channel picker (see below). |
22
- | **New video** | Opens Play (`C` from anywhere). |
22
+ | **New project** | Opens Play (`C` from anywhere). |
23
23
  | Links | **GitHub**, **Patreon** and **Buy Me a Coffee**, each opening in a new tab. |
24
24
  | Foot | "Free. Your keys, your machine.", the updates button (circular arrows, see [Updating and Patch Notes](Updating-and-Patch-Notes)) and the tutorial button (question mark, **Start interactive tutorial**). |
25
25
 
@@ -33,7 +33,7 @@ In every list, a press anywhere on a row or tile opens or picks it, not only on
33
33
 
34
34
  ### The command palette
35
35
 
36
- Press `Ctrl+K`, or click **Search or run a command**, to jump anywhere or run an action by typing. The **Go to** group has **Open home**, **Open projects**, **Open calendar**, **Open schedules**, **Open channels**, **Open library**, **Open settings**, **Open usage and costs** and **Show patch notes**. **New video** opens Play. The **Channel** group has **Show all channels** and **Switch to <channel>**. See [Keyboard Shortcuts and Command Palette](Keyboard-Shortcuts-and-Command-Palette).
36
+ Press `Ctrl+K`, or click **Search or run a command**, to jump anywhere or run an action by typing. The **Go to** group has **Open home**, **Open projects**, **Open calendar**, **Open schedules**, **Open channels**, **Open library**, **Open settings**, **Open usage and costs** and **Show patch notes**. **New project** opens Play. The **Channel** group has **Show all channels** and **Switch to <channel>**. See [Keyboard Shortcuts and Command Palette](Keyboard-Shortcuts-and-Command-Palette).
37
37
 
38
38
  ### The Channel picker
39
39
 
@@ -43,7 +43,7 @@ The **Channel** picker near the foot of the sidebar (at the top of Home on a pho
43
43
 
44
44
  **Where to find it:** the sidebar → **Home**.
45
45
 
46
- The page title is the channel you picked (or **Home**), under today's date. At the top right are **Open calendar** and **New video**.
46
+ The page title is the channel you picked (or **Home**), under today's date. At the top right are **Open calendar** and **New project**.
47
47
 
48
48
  On a fresh install, Home sends you to the first-run screen once. See [First Launch and Welcome](First-Launch-and-Welcome).
49
49
 
@@ -98,7 +98,7 @@ See [Costs and Run Cost](Costs-and-Run-Cost).
98
98
 
99
99
  **Where to find it:** the sidebar → **Projects**.
100
100
 
101
- Every run ever started, newest first, for the channel picked in the sidebar. The heading says how many projects there are and for which channel. **New video** opens Play.
101
+ Every run ever started, newest first, for the channel picked in the sidebar. The heading says how many projects there are and for which channel. **New project** opens Play.
102
102
 
103
103
  ### Search and filter
104
104
 
@@ -7,7 +7,7 @@ The command palette lets you reach every screen, every project and the frequent
7
7
  ## Use the command palette
8
8
 
9
9
  1. Press `Ctrl+K`.
10
- 2. Type a few letters of what you want, such as `set` for **Open settings** or `back` for **Back up now**. Letters only need to appear in order, so `nwv` finds **New video**. Commands also match other words, for example `keys` finds **Open settings**. Several words match in any order across the command's name, the project it acts on and its other words: `cleopatra regenerate image 3` finds **Regenerate image 3 in Cleopatra**.
10
+ 2. Type a few letters of what you want, such as `set` for **Open settings** or `back` for **Back up now**. Letters only need to appear in order, so `nwv` finds **New project**. Commands also match other words, for example `keys` finds **Open settings**. Several words match in any order across the command's name, the project it acts on and its other words: `cleopatra regenerate image 3` finds **Regenerate image 3 in Cleopatra**.
11
11
  3. Use `Up` and `Down` to move through the list, `Home` and `End` to jump to the first or last, and `Enter` to run the highlighted command. You can also click a command.
12
12
  4. Press `Esc` (or `Ctrl+K` again) to close the palette without running anything.
13
13
 
@@ -23,7 +23,7 @@ Press `?` anywhere (outside a text field) to see **Keyboard shortcuts**: every k
23
23
  |---|---|---|
24
24
  | `Ctrl+K` / `Cmd+K` | Anywhere | Opens or closes the command palette. |
25
25
  | `?` | Anywhere | Shows the keyboard shortcuts. |
26
- | `C` | Anywhere | **New video** (opens Play). |
26
+ | `C` | Anywhere | **New project** (opens Play). |
27
27
  | `G` then `H` | Anywhere | Go to Home. |
28
28
  | `G` then `P` | Anywhere | Go to Projects. |
29
29
  | `G` then `C` | Anywhere | Go to the Calendar. |
@@ -70,7 +70,7 @@ Commands are grouped under headings in the palette. Group names below are the he
70
70
 
71
71
  | Command | What it does |
72
72
  |---|---|
73
- | **New video** (`C`) | Opens Play. |
73
+ | **New project** (`C`) | Opens Play. |
74
74
  | **Show all channels** | Shows every channel's projects, schedules and costs. |
75
75
  | **Switch to <channel>** | Shows one channel only. There is one command per channel. |
76
76
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  Play is the screen where you set up a new video and start it. You pick a template (or none), type what the video is about, check the setup rows, and press the Play key. You get a project that writes the article, records the narration, draws the images and renders the video, plus any extra outputs you switched on.
4
4
 
5
- **Where to find it:** **New video** in the sidebar, **New video** on Home or Projects, `Ctrl+K` → **New video**, or press `C` anywhere. The page header reads **What's the video about?**
5
+ **Where to find it:** **New project** in the sidebar, **New project** on Home or Projects, `Ctrl+K` → **New project**, or press `C` anywhere. The page header reads **What's the video about?**
6
6
 
7
7
  ## How Play is laid out
8
8
 
@@ -134,9 +134,9 @@ Pick a schedule in the list to see its detail: **When**, **Timezone**, **Next ru
134
134
 
135
135
  **Run history** lists each run with its status (running, done, failed, skipped and so on), the time it was due, and either the project it made or the reason it didn't run. Deleted schedules are listed under **Deleted schedules** at the bottom; pick one to see its history.
136
136
 
137
- ## Use a newer template version
137
+ ## When you edit the template
138
138
 
139
- A schedule keeps the template version it was set up with, so saving a newer template doesn't change what the schedule makes. To switch, edit the schedule, pick the template again in **Template**, and save.
139
+ A schedule always uses its template as it is now: save a change to the template and the next run, the Calendar and topic generation use it. If the change adds a keyword the schedule has no value for, fill it in under the schedule's topics or every-run values.
140
140
 
141
141
  ## Tips
142
142
 
@@ -12,7 +12,7 @@ Slopify can write your video's YouTube description for you: a short summary, cha
12
12
  | **Chapters** | Timestamped lines such as `0:00 Introduction`, at the narration's real times. YouTube turns them into chapters. |
13
13
  | **Hashtags** | The hashtags at the end of the description. YouTube shows the first three above the title. |
14
14
  | **Tags** | Search tags for YouTube Studio's **Tags** field, kept separate from the description. Viewers don't see them. |
15
- | **Pinned comment** | A comment to post under the video and pin: a thank-you, a question viewers can answer from their own experience, a pointer to the chapters and an ask for the next topic. Your Description prompt can say how you want it. |
15
+ | **Pinned comment** | A comment to post under the video and pin: a line or two asking viewers a question they can answer from their own experience, and what to cover next. Your Description prompt can say how you want it. |
16
16
  | **Other titles** | Two more titles for YouTube's **A/B Testing** (Test & compare), beside the project's own title: each a different angle on the same video, one per line. |
17
17
 
18
18
  Slopify lays out the description itself (summary, blank line, one chapter per line, blank line, hashtags), so the layout YouTube needs never depends on the model's spacing. The files are saved in the project as `description.txt`, `tags.txt`, `pinned-comment.txt` and `titles.txt`.