@gentbajko/slopify 3.25.0 → 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.
@@ -0,0 +1,5 @@
1
+ # Slopify 3.25.1
2
+
3
+ Released 16 October 2026.
4
+
5
+ - **Turning on Soften breaths or Soften S sounds really keeps the description and the shorts now.** In 3.25.0 a finished project still asked for its description and shorts' pick again once the softened version was saved. Now only the narration is joined again and the video and shorts are rendered again.
@@ -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,4 +1,16 @@
1
1
  [
2
+ {
3
+ "id": "3.25.2",
4
+ "title": "Slopify 3.25.2",
5
+ "version": "3.25.2",
6
+ "date": "2026-10-16"
7
+ },
8
+ {
9
+ "id": "3.25.1",
10
+ "title": "Slopify 3.25.1",
11
+ "version": "3.25.1",
12
+ "date": "2026-10-16"
13
+ },
2
14
  {
3
15
  "id": "3.25.0",
4
16
  "title": "Slopify 3.25.0",
@@ -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 = `${output}.lines.txt`;
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"), { mode: 0o600 });
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.replaceAll("\\", "/").replaceAll(":", "\\\\:")},volume@lines=volume=1:precision=float:eval=frame`,
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
- rmSync(commands, { force: true });
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 = `${output}.phrases.txt`;
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"), { mode: 0o600 });
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.replaceAll("\\", "/").replaceAll(":", "\\\\:")},volume@phrases=volume=1:precision=float:eval=frame`,
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
- rmSync(commands, { force: true });
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, rmSync, writeFileSync } from "node:fs";
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 = `${output}.gains.txt`;
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"), { mode: 0o600 });
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.replaceAll("\\", "/").replaceAll(":", "\\\\:")},volume@sentences=volume=${(10 ** ((changes[0]?.at === 0 ? changes[0].gainDb : 0) / 20)).toFixed(5)}:precision=float:eval=frame`,
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
- rmSync(commands, { force: true });
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
+ }
@@ -23,7 +23,12 @@ export function unsoftenedTimelines(context, text) {
23
23
  const entries = plain.timeline;
24
24
  const timeline = entries.map((entry, at) => {
25
25
  const key = plain.keys[at];
26
- const made = past.outputs.find((row) => row.workKey === key && row.fingerprint === entry.audio[0] && row.state === "ready");
26
+ const made = past.outputs.find(
27
+ // Found by its fingerprint: once the softened version is saved, the join it will make
28
+ // again is marked outdated.
29
+ (row) => row.workKey === key &&
30
+ row.fingerprint === entry.audio[0] &&
31
+ (row.state === "ready" || row.state === "outdated"));
27
32
  return made === undefined
28
33
  ? undefined
29
34
  : {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gentbajko/slopify",
3
- "version": "3.25.0",
3
+ "version": "3.25.2",
4
4
  "description": "Turn a prompt into a finished video, locally, with your own provider keys",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Gent Bajko",