@gentbajko/slopify 3.18.0 → 3.19.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 (30) hide show
  1. package/dist/extension/slopify-studio-chrome.zip +0 -0
  2. package/dist/extension/slopify-studio-firefox.zip +0 -0
  3. package/dist/patch-notes/3.18.1.md +11 -0
  4. package/dist/patch-notes/3.18.2.md +10 -0
  5. package/dist/patch-notes/3.19.0.md +9 -0
  6. package/dist/patch-notes/index.json +18 -0
  7. package/dist/slices/admission/schema.js +2 -0
  8. package/dist/slices/narration/breaths-detect.js +261 -0
  9. package/dist/slices/narration/breaths-fixture.js +52 -0
  10. package/dist/slices/narration/breaths-model.js +17 -0
  11. package/dist/slices/narration/breaths.js +132 -0
  12. package/dist/slices/play-drafts/convert.js +3 -0
  13. package/dist/slices/play-drafts/schema.js +3 -0
  14. package/dist/slices/project-templates/from-project.js +1 -0
  15. package/dist/slices/rebuild/preview-details.js +1 -0
  16. package/dist/slices/rebuild/recipe-audio.js +3 -0
  17. package/dist/slices/rebuild/recipe-breaths.js +16 -0
  18. package/dist/slices/rebuild/recipe-voices.js +2 -0
  19. package/dist/slices/rebuild/runtime-local.js +16 -3
  20. package/dist/slices/storage/arrange.js +21 -8
  21. package/dist/slices/storage/trim.js +6 -2
  22. package/dist/tutorials/Editing-a-Project.md +3 -2
  23. package/dist/tutorials/Project-Page.md +1 -1
  24. package/dist/tutorials/Settings-Reference.md +1 -1
  25. package/dist/tutorials/Where-Your-Files-Live.md +3 -3
  26. package/dist/web/assets/index-OxnO0Bfh.js +326 -0
  27. package/dist/web/assets/{pdf-CQFUyijy.js → pdf-Dxrlh3NL.js} +1 -1
  28. package/dist/web/index.html +1 -1
  29. package/package.json +1 -1
  30. package/dist/web/assets/index-Drth_MfQ.js +0 -325
@@ -5,12 +5,14 @@ import { referenceKey, thumbnailVariant } from "../admission/model.js";
5
5
  import { plainText } from "../article/plain.js";
6
6
  import { splitEndMatter } from "../article/split.js";
7
7
  import { levelPieces } from "../loudness/level-pieces.js";
8
+ import { removeBreaths } from "../narration/breaths.js";
8
9
  import { joinNarration } from "../narration/concat.js";
9
10
  import { applyPauses, planPieces } from "../narration/pauses.js";
10
11
  import { allocateAsset, discardPreparedAssets, sealAsset } from "../storage/assets.js";
11
12
  import { outputPath } from "../storage/layout.js";
12
13
  import { probeDurationMs, runFfmpeg } from "../video/ffmpeg.js";
13
14
  import { turnJoinArgs } from "../voices/join.js";
15
+ import { breathsAsked } from "./recipe-breaths.js";
14
16
  import { levelOperation } from "./recipe-loudness.js";
15
17
  import { executeFigureCard } from "./runtime-figure-card.js";
16
18
  import { publishNarrationText } from "./runtime-narration-text.js";
@@ -134,16 +136,27 @@ async function concatenate(deps, context, piece) {
134
136
  try {
135
137
  const run = { bin: deps.ffmpeg, log: deps.log, signal: context.signal };
136
138
  const levelled = level ? await levelPieces(run, files, piecesDirectory) : undefined;
139
+ // Remove breaths: muted in what is joined, before the pauses; nothing moves, so the pauses
140
+ // are still planned from the pieces as spoken.
141
+ const breathless = breathsAsked(values)
142
+ ? await removeBreaths(run, levelled?.files ?? files, join(piecesDirectory, "breaths"))
143
+ : undefined;
144
+ if (breathless !== undefined)
145
+ deps.log.write("info", "narration.breaths", {
146
+ projectId: context.work.projectId,
147
+ stage: "audio",
148
+ detail: `${String(breathless.removed)} breaths muted in ${String(files.length)} pieces`,
149
+ });
137
150
  // Pauses between sentences: planned from the pieces as spoken, made in what is joined.
138
151
  const pauses = pausesIn(values);
139
152
  const paced = pauses === undefined
140
153
  ? undefined
141
- : await applyPauses(run, levelled?.files ?? files, paceScaled(await planPieces(run, files.map((file, at) => ({
154
+ : await applyPauses(run, breathless?.files ?? levelled?.files ?? files, paceScaled(await planPieces(run, files.map((file, at) => ({
142
155
  file,
143
156
  text: pieceText(plan.recipes.find((one) => one.key === recipe.dependsOn[at])),
144
157
  next: pauses.next[at] ?? "end",
145
158
  })), pauses.settings), operation === "concat-turns-v1" ? values : undefined), join(piecesDirectory, "paced"));
146
- const joined = paced?.files ?? levelled?.files ?? files;
159
+ const joined = paced?.files ?? breathless?.files ?? levelled?.files ?? files;
147
160
  const durationMs = operation === "concat-turns-v1"
148
161
  ? await joinTurns(run, joined, values, pending.absolutePath)
149
162
  : await joinNarration({ bin: deps.ffmpeg, log: deps.log }, {
@@ -151,7 +164,7 @@ async function concatenate(deps, context, piece) {
151
164
  output: pending.absolutePath,
152
165
  listPath: join(dirname(pending.absolutePath), "parts.txt"),
153
166
  signal: context.signal,
154
- reencode: level || paced !== undefined,
167
+ reencode: level || paced !== undefined || breathless !== undefined,
155
168
  });
156
169
  rmSync(piecesDirectory, { recursive: true, force: true });
157
170
  context.signal.throwIfAborted();
@@ -5,7 +5,13 @@ import { backupsFolderName, projectDir, renderCacheFolder } from "./layout.js";
5
5
  // The file in each project folder that says which project it is, for when the database and
6
6
  // the folders disagree (a restored database, a folder renamed by hand). Reconcile keeps it.
7
7
  export const projectMarker = ".slopify-project";
8
- // What makes a project's arrangement current: its title, head version and files. A project
8
+ // The pictures a video or short shows. Every version's stay in Upload, an older version's under
9
+ // Images/Earlier/<date>/, never in History: making them again never gives the same ones.
10
+ const imageRoles = new Set(["image", "animated_image", "short_image"]);
11
+ // Raised when a role's place changes, so every project arranged by the older places is moved
12
+ // once: 2 put every version's images the video and shorts show into Upload (3.18.1).
13
+ const layoutVersion = 2;
14
+ // What makes a project's arrangement current: the layout, its title, head version and files. A project
9
15
  // whose key is unchanged since it was last arranged is left alone.
10
16
  export function arrangeKeys(db) {
11
17
  const keys = new Map();
@@ -16,7 +22,10 @@ export function arrangeKeys(db) {
16
22
  (SELECT count(*)||':'||COALESCE(max(r.id),'') FROM revision_outputs r WHERE r.project_id=p.id AND r.selected=1 AND r.revision_id=h.revision_id) AS current
17
23
  FROM projects p LEFT JOIN project_heads h ON h.project_id=p.id`)
18
24
  .all())
19
- keys.set(String(row.id), [row.title, row.revision_id, row.assets, row.legacy, row.current].map(String).join("|"));
25
+ keys.set(String(row.id), [
26
+ `layout ${layoutVersion}`,
27
+ ...[row.title, row.revision_id, row.assets, row.legacy, row.current].map(String),
28
+ ].join("|"));
20
29
  return keys;
21
30
  }
22
31
  // Whether anything of the project runs or may start: its files stay where they are until then.
@@ -241,7 +250,9 @@ export function desiredPlaces(db, projectId) {
241
250
  const place = placeFor(reference, order, titles);
242
251
  out.set(reference.path, reference.current
243
252
  ? place
244
- : `History/${day(reference.createdAt)}/${place.replace(/^Upload\//u, "")}`);
253
+ : imageRoles.has(reference.role)
254
+ ? `Upload/Images/Earlier/${day(reference.createdAt)}/${place.replace(/^Upload\/(?:Images\/)?/u, "")}`
255
+ : `History/${day(reference.createdAt)}/${place.replace(/^Upload\//u, "")}`);
245
256
  }
246
257
  return out;
247
258
  }
@@ -286,14 +297,16 @@ function placeFor(reference, order, titles) {
286
297
  const key = /^image:(.+)$/u.exec(slot)?.[1];
287
298
  const at = key === undefined ? undefined : order.get(key);
288
299
  return at === undefined
289
- ? `Working/Images/${file}`
290
- : `Working/Images/${pad(at + 1)}${extension}`;
300
+ ? `Upload/Images/${file}`
301
+ : `Upload/Images/${pad(at + 1)}${extension}`;
291
302
  }
292
303
  case "short_image": {
293
304
  const match = /^shorts:([0-9]+):image:([0-9]+)$/u.exec(slot);
294
- return match === null
295
- ? `Working/Shorts/${file}`
296
- : `Working/Shorts/Short ${match[1]}/image ${match[2]}${extension}`;
305
+ if (match === null)
306
+ return `Upload/Shorts/Images/${file}`;
307
+ // Beside the short's video, in a folder named like it: Shorts/3 - Title/image 1.png.
308
+ const title = titles.get(Number(match[1]));
309
+ return `Upload/Shorts/${match[1]}${title === undefined || title === "" ? "" : ` - ${clean(title)}`}/image ${match[2]}${extension}`;
297
310
  }
298
311
  case "figure_card":
299
312
  return `Working/Cards/${file}`;
@@ -5,8 +5,9 @@ import { liveProject, projectExists, projectPaused, stagesOf } from "../admissio
5
5
  import { projectSettled } from "./arrange.js";
6
6
  import { backupsFolderName, outputPath, projectDir } from "./layout.js";
7
7
  import { pieceFile } from "./reconcile.js";
8
- // Keep outputs only: a finished project drops the working files it was made from (images,
9
- // narration parts, subtitle timing, render settings) and keeps what gets published. Two
8
+ // Keep outputs only: a finished project drops the working files it was made from (narration
9
+ // parts, subtitle timing, render settings) and keeps what gets published, the images the
10
+ // video and shorts show included: making them again costs money and never gives the same ones. Two
10
11
  // rules keep it safe. A file the user supplied is never removed, because nothing could make
11
12
  // it again. And only files a project record names are removed: anything else in the folder
12
13
  // is reconcile's business (`reconcile.ts`), which leaves the Backups folder alone.
@@ -28,6 +29,9 @@ export const outputRoles = [
28
29
  "audio_export",
29
30
  "subtitles_srt",
30
31
  "subtitles_vtt",
32
+ "image",
33
+ "animated_image",
34
+ "short_image",
31
35
  ];
32
36
  export function projectStorage(deps, projectId) {
33
37
  const files = filesOf(deps.paths, projectId);
@@ -128,12 +128,13 @@ See [Narration Aliases and Glossary](Narration-Aliases-and-Glossary).
128
128
 
129
129
  ## Pauses and volume
130
130
 
131
- **Edit project → Inputs → Pauses and volume** holds four settings. All run on your computer with no API calls.
131
+ **Edit project → Inputs → Pauses and volume** holds these settings. All run on your computer with no API calls.
132
132
 
133
133
  | Option | What it does | Default / range |
134
134
  | --- | --- | --- |
135
135
  | **Pause between sentences (seconds)** | The least quiet after each sentence. Adds silence where the pause is shorter; never shortens one or cuts a word. About 0.25 s is brisk, 0.35 to 0.45 natural, 0.5 to 0.8 an audiobook's pace, 0.8 and up sleep content. 0 keeps the voice's own. | 0.4; 0 to 2 in steps of 0.05 |
136
136
  | **Pause between paragraphs (seconds)** | The least quiet at the end of a paragraph, when you want more than between sentences. At 0 paragraphs get the sentence pause. Try 0.8 to 1.2 for a slower read. A speaker's turn gap is set under Speakers instead. | 0; 0 to 2 |
137
+ | **Remove breaths** | Mutes the audible in-breaths a voice takes between sentences and after commas (Inworld's TTS-2 takes a loud one almost every time). Only the breath goes quiet: words, pauses and captions stay where they were. | Off |
137
138
  | **Level the volume** | Brings every narration piece to one loudness before they are joined, then sets the video, shorts and audio files to a finished loudness. | On for new runs (Settings → General → **Level the volume for new runs**) |
138
139
  | **Video volume** | How loud the video and shorts are, in dB from the recommended level (0 dB is -14 LUFS, what YouTube and Spotify play at). -6 dB is about 50%, +4 dB about 158%. Peaks stay under -1.5 dBTP. | 0 dB; -10 to +4 dB in 0.5 dB steps |
139
140
  | **Audio files volume** | How loud the WAV, MP3 and M4B are, in dB from -18 LUFS (what audiobook shops such as ACX ask for). Peaks stay under -3 dBTP. | 0 dB; -10 to +4 dB |
@@ -142,7 +143,7 @@ You can type a volume in dB or as a percentage of the recommended level.
142
143
 
143
144
  What changing them remakes:
144
145
 
145
- - **Pauses**: the narration is joined again from the pieces it already has and timed again, which remakes what is timed from it: captions, the video and, when on, the YouTube description and the shorts' pick (those two are text calls). No speech is made again.
146
+ - **Pauses or Remove breaths**: the narration is joined again from the pieces it already has and timed again, which remakes what is timed from it: captions, the video and, when on, the YouTube description and the shorts' pick (those two are text calls). No speech is made again.
146
147
  - **Level the volume on or off**: the levelled narration is joined again and the exports rendered again. No speech is made again, and word timing, description, shorts' pick and reviews are not redone.
147
148
  - **A volume only**: the exports are mastered again, nothing else.
148
149
 
@@ -197,7 +197,7 @@ Only generated steps can have a checkpoint. Pausing the project still applies on
197
197
 
198
198
  ## Free space
199
199
 
200
- A finished project can drop the working files it was made from (images, narration parts, subtitle timing, render settings) and keep what gets published: the video, shorts, thumbnail, article, description, document and anything you uploaded. In the right rail, **Free space** shows the size of both, for example "Outputs 1.4 GB · working files 1.2 GB", with the button **Free 1.2 GB: keep the outputs, drop the working files**.
200
+ A finished project can drop the working files it was made from (narration parts, subtitle timing, render settings) and keep what gets published: the video, shorts, the images they show, thumbnail, article, description, document and anything you uploaded. In the right rail, **Free space** shows the size of both, for example "Outputs 1.4 GB · working files 1.2 GB", with the button **Free 1.2 GB: keep the outputs, drop the working files**.
201
201
 
202
202
  1. Press the button.
203
203
  2. Read what goes, then confirm with **Free 1.2 GB**, or press **Keep the working files**.
@@ -198,7 +198,7 @@ After importing, enter your keys again in Settings → Providers. You can also r
198
198
  |---|---|
199
199
  | **Disk space** | What Slopify stores: the total, project files, staged uploads, and deleted projects still in the trash (freed when the trash removes them after 30 days, or with **Delete now**). |
200
200
  | Storage by project | Each project's size, split into outputs and working files, largest first. A finished project's page offers the same on its own (**Free space**, see [Project Page](Project-Page#free-space)). |
201
- | **Keep outputs only** | On a finished project, deletes the working files (images, narration parts, subtitle timing, render settings) and keeps the video, shorts, thumbnail, article, description, document and your uploads. You confirm first. Changing the project later makes those files again, which takes time and provider credits. |
201
+ | **Keep outputs only** | On a finished project, deletes the working files (narration parts, subtitle timing, render settings) and keeps the video, shorts, the images they show, thumbnail, article, description, document and your uploads. You confirm first. Changing the project later makes those files again, which takes time and provider credits. |
202
202
 
203
203
  ### Sample projects
204
204
 
@@ -31,8 +31,8 @@ Each project has one folder in `Projects/`, named after its title (`Lolth | D&D
31
31
 
32
32
  | Folder | What is in it | Safe to delete? |
33
33
  | --- | --- | --- |
34
- | `Upload/` | The current version, ready to publish: `video.mp4`, `thumbnail.png` (and `thumbnail 2.png`…), `subtitles.srt` and `.vtt`, `description.txt`, `tags.txt`, `titles.txt`, `pinned comment.txt`, `document.pdf`, `article.md`, and `Shorts/1 - <title>.mp4`… | No: it is what you upload. |
35
- | `Working/` | What the current version was made from: `Images/`, `Narration/` (each chunk, the joined and levelled narration), `Research/`, `Article/`, `Subtitles/`, `Render/`, `Shorts/` (each short's images). | Only to free space on a finished video: changing that project later makes these again, which takes time and provider credits. **Keep outputs only** does it for you. |
34
+ | `Upload/` | The current version, ready to publish: `video.mp4`, `thumbnail.png` (and `thumbnail 2.png`…), `subtitles.srt` and `.vtt`, `description.txt`, `tags.txt`, `titles.txt`, `pinned comment.txt`, `document.pdf`, `article.md`, `Images/` (every image the video shows, in order; an older version's images in `Images/Earlier/<date>/`), and `Shorts/1 - <title>.mp4`… with each short's images beside it in `Shorts/1 - <title>/`. No cleanup ever deletes these images. | No: it is what you upload. |
35
+ | `Working/` | What the current version was made from: `Narration/` (each chunk, the joined and levelled narration), `Research/`, `Article/`, `Subtitles/`, `Render/`. | Only to free space on a finished video: changing that project later makes these again, which takes time and provider credits. **Keep outputs only** does it for you. |
36
36
  | `History/<date>/` | Files of older versions, by the day they were made (an earlier render, the narration before a voice change). | Yes. Delete a day's folder, or all of `History/`, to free its space. Going back to that version in History makes its files again. |
37
37
 
38
38
  A hidden `.slopify-project` file tells Slopify which project the folder holds and where its files went. Leave it there.
@@ -132,7 +132,7 @@ Deleted projects free their space when the trash removes them after 30 days, or
132
132
 
133
133
  ### Keep outputs only
134
134
 
135
- On a finished project, **Keep outputs only** deletes the working files it was made from (images, narration parts, subtitle timing, render settings) and keeps the video, shorts, thumbnail, article, description, document and your uploads.
135
+ On a finished project, **Keep outputs only** deletes the working files it was made from (narration parts, subtitle timing, render settings) and keeps the video, shorts, the images they show, thumbnail, article, description, document and your uploads.
136
136
 
137
137
  1. Find the project in the **Disk space** list. The button says how much it frees.
138
138
  2. Press **Keep outputs only**.