@gentbajko/slopify 3.25.1 → 3.25.2
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.25.2.md +5 -0
- package/dist/patch-notes/index.json +6 -0
- package/dist/slices/loudness/line-level.js +5 -6
- package/dist/slices/loudness/phrase-level.js +5 -6
- package/dist/slices/loudness/sentence-level.js +6 -6
- package/dist/slices/loudness/volume-commands.js +17 -0
- package/package.json +1 -1
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Slopify 3.25.2
|
|
2
|
+
|
|
3
|
+
Released 16 October 2026.
|
|
4
|
+
|
|
5
|
+
- **Level the volume works for projects with an apostrophe in their title.** A project like "Drizzt Do'Urden" or "Baldur's Gate" failed at the narration step with "Error applying option 'precision' to filter 'asendcmd'" whenever its loud phrases, its speakers' lines or its sentences were levelled. Try again on the failed step after updating.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { rmSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
2
|
+
import { volumeCommands } from "./volume-commands.js";
|
|
3
3
|
// Level the volume, line by line, for a narration with several speakers. The pieces are levelled
|
|
4
4
|
// before they are joined (`level-pieces.ts`), but a piece is one request to the voice provider,
|
|
5
5
|
// and with native dialogue one request speaks several people's lines. Its average hides them: a
|
|
@@ -102,10 +102,9 @@ export async function levelLines(run, input, output, words, offset) {
|
|
|
102
102
|
if (gains.every((one) => one.gainDb === 0))
|
|
103
103
|
return false;
|
|
104
104
|
// The gain for each line, set at its start by a timed command to the volume filter.
|
|
105
|
-
const commands =
|
|
106
|
-
writeFileSync(commands, gains
|
|
105
|
+
const commands = volumeCommands(gains
|
|
107
106
|
.map((one) => `${Math.max(0, one.at).toFixed(3)} volume@lines volume ${(10 ** (one.gainDb / 20)).toFixed(5)};`)
|
|
108
|
-
.join("\n")
|
|
107
|
+
.join("\n"));
|
|
109
108
|
try {
|
|
110
109
|
await runFfmpeg({
|
|
111
110
|
bin: run.bin,
|
|
@@ -119,7 +118,7 @@ export async function levelLines(run, input, output, words, offset) {
|
|
|
119
118
|
"-i",
|
|
120
119
|
input,
|
|
121
120
|
"-af",
|
|
122
|
-
`asendcmd=f=${commands.
|
|
121
|
+
`asendcmd=f=${commands.filterPath},volume@lines=volume=1:precision=float:eval=frame`,
|
|
123
122
|
"-c:a",
|
|
124
123
|
"pcm_s16le",
|
|
125
124
|
"-f",
|
|
@@ -132,7 +131,7 @@ export async function levelLines(run, input, output, words, offset) {
|
|
|
132
131
|
});
|
|
133
132
|
}
|
|
134
133
|
finally {
|
|
135
|
-
|
|
134
|
+
commands.dispose();
|
|
136
135
|
}
|
|
137
136
|
return true;
|
|
138
137
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { rmSync, writeFileSync } from "node:fs";
|
|
2
1
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
3
2
|
import { parseFrames } from "./line-level.js";
|
|
3
|
+
import { volumeCommands } from "./volume-commands.js";
|
|
4
4
|
// The voice provider speaks a narration piece in one request, and now and then a phrase bursts
|
|
5
5
|
// out above the narration around it - above all the first words of a request, which start with
|
|
6
6
|
// more energy than the voice keeps. Levelling the piece by its average can't see a few loud
|
|
@@ -129,10 +129,9 @@ export async function lowerLoudPhrases(run, input, output) {
|
|
|
129
129
|
if (gains === undefined)
|
|
130
130
|
return false;
|
|
131
131
|
// Each change, at its time, as a command to the volume filter.
|
|
132
|
-
const commands =
|
|
133
|
-
writeFileSync(commands, gains
|
|
132
|
+
const commands = volumeCommands(gains
|
|
134
133
|
.map((one) => `${one.at.toFixed(3)} volume@phrases volume ${(10 ** (one.gainDb / 20)).toFixed(5)};`)
|
|
135
|
-
.join("\n")
|
|
134
|
+
.join("\n"));
|
|
136
135
|
try {
|
|
137
136
|
await runFfmpeg({
|
|
138
137
|
bin: run.bin,
|
|
@@ -146,7 +145,7 @@ export async function lowerLoudPhrases(run, input, output) {
|
|
|
146
145
|
"-i",
|
|
147
146
|
input,
|
|
148
147
|
"-af",
|
|
149
|
-
`asendcmd=f=${commands.
|
|
148
|
+
`asendcmd=f=${commands.filterPath},volume@phrases=volume=1:precision=float:eval=frame`,
|
|
150
149
|
"-c:a",
|
|
151
150
|
"pcm_s16le",
|
|
152
151
|
"-f",
|
|
@@ -159,7 +158,7 @@ export async function lowerLoudPhrases(run, input, output) {
|
|
|
159
158
|
});
|
|
160
159
|
}
|
|
161
160
|
finally {
|
|
162
|
-
|
|
161
|
+
commands.dispose();
|
|
163
162
|
}
|
|
164
163
|
return true;
|
|
165
164
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { mkdirSync
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
3
|
import { isAbsolute, join, resolve as resolvePath } from "node:path";
|
|
4
4
|
import { measurePiece } from "../narration/pauses.js";
|
|
5
5
|
import { runFfmpeg } from "../video/ffmpeg.js";
|
|
6
6
|
import { sentenceLevel } from "./sentence-level-model.js";
|
|
7
|
+
import { volumeCommands } from "./volume-commands.js";
|
|
7
8
|
export { sentenceLevel } from "./sentence-level-model.js";
|
|
8
9
|
// Level each sentence (`levelSentences` on a run's config). The voice provider reads a narration
|
|
9
10
|
// a request at a time, and inside a request the voice drifts: the first sentences come out
|
|
@@ -288,10 +289,9 @@ export async function levelSentences(run, files, directory) {
|
|
|
288
289
|
continue;
|
|
289
290
|
}
|
|
290
291
|
const output = join(directory, `sentences-${String(index + 1).padStart(4, "0")}.wav`);
|
|
291
|
-
const commands =
|
|
292
|
-
writeFileSync(commands, changes
|
|
292
|
+
const commands = volumeCommands(changes
|
|
293
293
|
.map((one) => `${one.at.toFixed(3)} volume@sentences volume ${(10 ** (one.gainDb / 20)).toFixed(5)};`)
|
|
294
|
-
.join("\n")
|
|
294
|
+
.join("\n"));
|
|
295
295
|
try {
|
|
296
296
|
await runFfmpeg({
|
|
297
297
|
bin: run.bin,
|
|
@@ -305,7 +305,7 @@ export async function levelSentences(run, files, directory) {
|
|
|
305
305
|
"-i",
|
|
306
306
|
file,
|
|
307
307
|
"-af",
|
|
308
|
-
`asendcmd=f=${commands.
|
|
308
|
+
`asendcmd=f=${commands.filterPath},volume@sentences=volume=${(10 ** ((changes[0]?.at === 0 ? changes[0].gainDb : 0) / 20)).toFixed(5)}:precision=float:eval=frame`,
|
|
309
309
|
"-c:a",
|
|
310
310
|
"pcm_f32le",
|
|
311
311
|
"-f",
|
|
@@ -318,7 +318,7 @@ export async function levelSentences(run, files, directory) {
|
|
|
318
318
|
});
|
|
319
319
|
}
|
|
320
320
|
finally {
|
|
321
|
-
|
|
321
|
+
commands.dispose();
|
|
322
322
|
}
|
|
323
323
|
written.push(output);
|
|
324
324
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// The timed volume changes a levelling step sends ffmpeg's volume filter, as the commands file
|
|
5
|
+
// `asendcmd` reads. The filter graph names the file, and the graph's syntax reads some
|
|
6
|
+
// characters a project folder's name may hold (the apostrophe in "Drizzt Do'Urden" opens a
|
|
7
|
+
// quote), so the file is written to a folder of its own under the system's temporary folder,
|
|
8
|
+
// whose name has none of them. `dispose` removes it.
|
|
9
|
+
export function volumeCommands(text) {
|
|
10
|
+
const directory = mkdtempSync(join(tmpdir(), "slopify-volume-"));
|
|
11
|
+
const path = join(directory, "commands.txt");
|
|
12
|
+
writeFileSync(path, text, { mode: 0o600 });
|
|
13
|
+
return {
|
|
14
|
+
filterPath: path.replaceAll("\\", "/").replaceAll(":", "\\\\:"),
|
|
15
|
+
dispose: () => rmSync(directory, { recursive: true, force: true }),
|
|
16
|
+
};
|
|
17
|
+
}
|