@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.
- package/dist/adapters/tts/elevenlabs.js +2 -0
- package/dist/assets/document/fonts/Literata-Italic.ttf +0 -0
- package/dist/assets/document/fonts/Literata-OFL.txt +93 -0
- package/dist/assets/document/fonts/Literata-Regular.ttf +0 -0
- package/dist/assets/document/fonts/Literata-SemiBold.ttf +0 -0
- package/dist/assets/document/fonts/Literata-SemiBoldItalic.ttf +0 -0
- package/dist/assets/document/fonts/SOURCE.txt +13 -1
- package/dist/assets/models.yaml +26 -0
- package/dist/edge/autostart/native.js +12 -3
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/kernel/db/migrations/0042-narration-retries.sql +13 -0
- package/dist/main.js +9 -1
- package/dist/patch-notes/3.0.7.md +33 -0
- package/dist/patch-notes/3.0.8.md +23 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/document/fonts.js +29 -11
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/document/theme.js +3 -3
- package/dist/slices/rebuild/narration-retry.js +128 -0
- package/dist/slices/rebuild/recovery-selection.js +7 -0
- package/dist/slices/rebuild/runtime-subtitles.js +53 -11
- package/dist/slices/schedules/service.js +2 -1
- package/dist/slices/storage/backup-format.js +1 -0
- package/dist/slices/youtube/answer.js +1 -1
- package/dist/slices/youtube/model.js +3 -2
- package/dist/tutorials/Document-Themes.md +1 -1
- package/dist/tutorials/Home-and-Projects.md +4 -4
- package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +3 -3
- package/dist/tutorials/Play-Overview.md +1 -1
- package/dist/tutorials/Schedules.md +2 -2
- package/dist/tutorials/YouTube-Description.md +1 -1
- package/dist/web/assets/index-Cdb0Jh3P.js +310 -0
- package/dist/web/assets/{pdf-C5xctS6p.js → pdf-BhFuoBjr.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
21
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
##
|
|
137
|
+
## When you edit the template
|
|
138
138
|
|
|
139
|
-
A schedule
|
|
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
|
|
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`.
|