@gentbajko/slopify 3.18.2 → 3.20.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.
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/patch-notes/3.19.0.md +9 -0
- package/dist/patch-notes/3.20.0.md +10 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/admission/schema.js +3 -0
- package/dist/slices/narration/breaths-detect.js +291 -0
- package/dist/slices/narration/breaths-fixture.js +52 -0
- package/dist/slices/narration/breaths-model.js +30 -0
- package/dist/slices/narration/breaths.js +134 -0
- package/dist/slices/narration/clean-pieces.js +69 -0
- package/dist/slices/play-drafts/convert.js +4 -0
- package/dist/slices/play-drafts/schema.js +4 -0
- package/dist/slices/project-templates/from-project.js +2 -0
- package/dist/slices/rebuild/preview-details.js +2 -0
- package/dist/slices/rebuild/recipe-audio.js +3 -0
- package/dist/slices/rebuild/recipe-breaths.js +26 -0
- package/dist/slices/rebuild/recipe-voices.js +2 -0
- package/dist/slices/rebuild/runtime-local.js +17 -3
- package/dist/tutorials/Editing-a-Project.md +4 -2
- package/dist/web/assets/{index-DASWFus0.js → index-DNPct8pl.js} +143 -143
- package/dist/web/assets/{pdf-CF97Fw5v.js → pdf-tyJWWiKA.js} +1 -1
- package/dist/web/index.html +1 -1
- package/package.json +1 -1
|
@@ -159,6 +159,8 @@ function inputChanges(parent, view) {
|
|
|
159
159
|
add("Show tables and figures on screen", before.showFigures === true ? "On" : "Off", after.showFigures === true ? "On" : "Off");
|
|
160
160
|
add("Pause between sentences (seconds)", before.sentencePauseSeconds ?? 0, after.sentencePauseSeconds ?? 0);
|
|
161
161
|
add("Pause between paragraphs (seconds)", before.paragraphPauseSeconds ?? 0, after.paragraphPauseSeconds ?? 0);
|
|
162
|
+
add("Soften breaths", before.removeBreaths === true ? "On" : "Off", after.removeBreaths === true ? "On" : "Off");
|
|
163
|
+
add("Soften S sounds", before.deEss === true ? "On" : "Off", after.deEss === true ? "On" : "Off");
|
|
162
164
|
add("Level the volume", before.loudness === undefined ? "Off" : loudnessSummary(before.loudness), after.loudness === undefined ? "Off" : loudnessSummary(after.loudness));
|
|
163
165
|
add("YouTube description", before.youtubeDescription === true ? "On" : "Off", after.youtubeDescription === true ? "On" : "Off");
|
|
164
166
|
add("Description prompt", before.descriptionPrompt, after.descriptionPrompt);
|
|
@@ -5,6 +5,7 @@ import { concatArgs } from "../narration/concat.js";
|
|
|
5
5
|
import { normalizeNarrationText } from "../narration/plan.js";
|
|
6
6
|
import { pronunciationChunks } from "../narration/pronunciation-chunks.js";
|
|
7
7
|
import { aliasFutureValues, narrationParts, pronunciationFutureValues, voiceValues, } from "./recipe-audio-parts.js";
|
|
8
|
+
import { breathValues } from "./recipe-breaths.js";
|
|
8
9
|
import { levelRecipe } from "./recipe-loudness.js";
|
|
9
10
|
import { recipe, resourceIdentity, selectedReference, } from "./recipe-model.js";
|
|
10
11
|
import { narrationFileRecipe } from "./recipe-narration-text.js";
|
|
@@ -142,6 +143,7 @@ export function audioRecipes(context, text) {
|
|
|
142
143
|
: (config.chunking ?? defaultChunking).mode === "paragraph"
|
|
143
144
|
? "paragraph"
|
|
144
145
|
: "sentence")),
|
|
146
|
+
...breathValues(config),
|
|
145
147
|
],
|
|
146
148
|
}, parts.map((part) => part.key), { unresolved: text.narrationText !== null && groups.length === 0 });
|
|
147
149
|
recipes.push(body);
|
|
@@ -199,6 +201,7 @@ export function audioRecipes(context, text) {
|
|
|
199
201
|
parts.map((part) => resourceIdentity(context, part)),
|
|
200
202
|
parts.length === 1 ? "copy-single-file" : concatArgs("$parts", "$output"),
|
|
201
203
|
...pauseValues(config, parts.map((_part, at) => (at === parts.length - 1 ? "end" : "sentence"))),
|
|
204
|
+
...breathValues(config),
|
|
202
205
|
],
|
|
203
206
|
}, parts.map((part) => part.key));
|
|
204
207
|
recipes.push(audio);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { breathsOn, deEsser, deEssOn } from "../narration/breaths-model.js";
|
|
2
|
+
// Soften breaths and Soften S sounds (`narration/breaths-model.ts`) are made in the narration
|
|
3
|
+
// joins, so a join carries them in its values, after the pauses when they are set. Nothing at
|
|
4
|
+
// all while they are off, so every join made before them keeps its fingerprint. The levelled
|
|
5
|
+
// join copies these values, so it softens the same.
|
|
6
|
+
// v2: 3.19.0's v1 muted breaths to silence and could clip word edges; its joins are remade.
|
|
7
|
+
export const breathsValue = "breaths-v2";
|
|
8
|
+
export const deEssValue = "deess-v1";
|
|
9
|
+
export function breathValues(config) {
|
|
10
|
+
return [
|
|
11
|
+
...(breathsOn(config) ? [[breathsValue]] : []),
|
|
12
|
+
...(deEssOn(config) ? [[deEssValue, deEsser.intensity, deEsser.max, deEsser.frequency]] : []),
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
// Whether a join's values ask for it: any value after the join's own two.
|
|
16
|
+
export function breathsAsked(values) {
|
|
17
|
+
return laterValues(values).some((value) => value.length === 1 && value[0] === breathsValue);
|
|
18
|
+
}
|
|
19
|
+
export function deEssAsked(values) {
|
|
20
|
+
return laterValues(values).some((value) => value[0] === deEssValue);
|
|
21
|
+
}
|
|
22
|
+
function laterValues(values) {
|
|
23
|
+
return Array.isArray(values)
|
|
24
|
+
? values.slice(2).filter((value) => Array.isArray(value))
|
|
25
|
+
: [];
|
|
26
|
+
}
|
|
@@ -9,6 +9,7 @@ import { prepareRequests } from "../narration/steering.js";
|
|
|
9
9
|
import { groupTurns } from "../voices/grouping.js";
|
|
10
10
|
import { speakerPace } from "../voices/model.js";
|
|
11
11
|
import { parseScript } from "../voices/script.js";
|
|
12
|
+
import { breathValues } from "./recipe-breaths.js";
|
|
12
13
|
import { describedText, describeFuture, describing } from "./recipe-describe.js";
|
|
13
14
|
import { recipe, resourceIdentity, } from "./recipe-model.js";
|
|
14
15
|
import { pauseValues } from "./recipe-pauses.js";
|
|
@@ -125,6 +126,7 @@ export function voiceBodyRecipes(context, voices, script, glossary) {
|
|
|
125
126
|
layout,
|
|
126
127
|
// Pauses between sentences inside a turn; the turn gap stays the layout's.
|
|
127
128
|
...pauseValues(context.config, layout.map(([, gap], at) => at === layout.length - 1 ? "end" : gap > 0 || turnEnds.has(at) ? "turn" : "sentence")),
|
|
129
|
+
...breathValues(context.config),
|
|
128
130
|
],
|
|
129
131
|
}, parts.map((part) => part.key), { unresolved: parsed?.ok !== true || pending });
|
|
130
132
|
return {
|
|
@@ -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 { cleanPieces } from "../narration/clean-pieces.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, deEssAsked } 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,28 @@ 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
|
+
// Soften breaths, then Soften S sounds: made in what is joined, before the pauses; nothing
|
|
140
|
+
// moves, so the pauses are still planned from the pieces as spoken.
|
|
141
|
+
const cleaned = await cleanPieces(run, levelled?.files ?? files, piecesDirectory, {
|
|
142
|
+
breaths: breathsAsked(values),
|
|
143
|
+
deEss: deEssAsked(values),
|
|
144
|
+
});
|
|
145
|
+
if (cleaned?.breaths !== undefined)
|
|
146
|
+
deps.log.write("info", "narration.breaths", {
|
|
147
|
+
projectId: context.work.projectId,
|
|
148
|
+
stage: "audio",
|
|
149
|
+
detail: `${String(cleaned.breaths.softened)} breaths softened, ${String(cleaned.breaths.skipped)} left alone (a word sound in them) in ${String(files.length)} pieces`,
|
|
150
|
+
});
|
|
137
151
|
// Pauses between sentences: planned from the pieces as spoken, made in what is joined.
|
|
138
152
|
const pauses = pausesIn(values);
|
|
139
153
|
const paced = pauses === undefined
|
|
140
154
|
? undefined
|
|
141
|
-
: await applyPauses(run, levelled?.files ?? files, paceScaled(await planPieces(run, files.map((file, at) => ({
|
|
155
|
+
: await applyPauses(run, cleaned?.files ?? levelled?.files ?? files, paceScaled(await planPieces(run, files.map((file, at) => ({
|
|
142
156
|
file,
|
|
143
157
|
text: pieceText(plan.recipes.find((one) => one.key === recipe.dependsOn[at])),
|
|
144
158
|
next: pauses.next[at] ?? "end",
|
|
145
159
|
})), pauses.settings), operation === "concat-turns-v1" ? values : undefined), join(piecesDirectory, "paced"));
|
|
146
|
-
const joined = paced?.files ?? levelled?.files ?? files;
|
|
160
|
+
const joined = paced?.files ?? cleaned?.files ?? levelled?.files ?? files;
|
|
147
161
|
const durationMs = operation === "concat-turns-v1"
|
|
148
162
|
? await joinTurns(run, joined, values, pending.absolutePath)
|
|
149
163
|
: await joinNarration({ bin: deps.ffmpeg, log: deps.log }, {
|
|
@@ -151,7 +165,7 @@ async function concatenate(deps, context, piece) {
|
|
|
151
165
|
output: pending.absolutePath,
|
|
152
166
|
listPath: join(dirname(pending.absolutePath), "parts.txt"),
|
|
153
167
|
signal: context.signal,
|
|
154
|
-
reencode: level || paced !== undefined,
|
|
168
|
+
reencode: level || paced !== undefined || cleaned !== undefined,
|
|
155
169
|
});
|
|
156
170
|
rmSync(piecesDirectory, { recursive: true, force: true });
|
|
157
171
|
context.signal.throwIfAborted();
|
|
@@ -128,12 +128,14 @@ 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
|
|
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
|
+
| **Soften breaths** | Turns the audible in-breaths a voice takes between sentences and after commas down by 12 dB, with soft fades (Inworld's TTS-2 takes a loud one almost every time). A breath with any word sound in it is left alone; words, pauses and captions stay where they were. | Off |
|
|
138
|
+
| **Soften S sounds** | Takes the sharp edge off s and sh sounds with a very light de-esser; the voice keeps its brightness. Works with or without Soften breaths. | Off |
|
|
137
139
|
| **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
140
|
| **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
141
|
| **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 +144,7 @@ You can type a volume in dB or as a percentage of the recommended level.
|
|
|
142
144
|
|
|
143
145
|
What changing them remakes:
|
|
144
146
|
|
|
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.
|
|
147
|
+
- **Pauses, Soften breaths or Soften S sounds**: 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
148
|
- **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
149
|
- **A volume only**: the exports are mastered again, nothing else.
|
|
148
150
|
|