@gentbajko/slopify 3.11.4 → 3.11.6

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.
@@ -26,7 +26,7 @@ import { parseStudioExport, readReport, saveReport } from "../../slices/studio/r
26
26
  import { statsFromReport } from "../../slices/studio/report-stats.js";
27
27
  import { autoCommentKey, bearerToken, exportViewProblem, exportViews, isExtensionOrigin, pairStudioExtension, readChannelPlaylists, readExportView, readStudioPlaylists, resetStudioPairing, saveExportView, saveProjectPlaylists, saveRealFootage, saveStudioPlaylists, studioPairing, studioPlaylistsMax, studioPlaylistsProblem, studioRequestAllowed, } from "../../slices/studio/settings.js";
28
28
  import { abResults, abVariantSchema, projectStats, saveAbResult, saveStats, } from "../../slices/studio/stats.js";
29
- import { confirmUpload, doneVideos, forgetVideo, projectVideos, recordVideo, setChecks, setTaskState, videoIdOf, videoIdPattern, videoOf, waitingTasks, } from "../../slices/studio/videos.js";
29
+ import { confirmUpload, doneVideos, forgetVideo, projectVideos, recordedVideos, recordVideo, setChecks, setTaskState, videoIdOf, videoIdPattern, videoOf, waitingTasks, } from "../../slices/studio/videos.js";
30
30
  import { setAsideProjects, uploadedProjects } from "../../slices/uploads/repo.js";
31
31
  import { onInvalid, problem, titleOf } from "./problem.js";
32
32
  const id = z
@@ -74,7 +74,10 @@ const extVideoBody = z.object({
74
74
  short: shortField,
75
75
  videoId: z.string().regex(videoIdPattern),
76
76
  });
77
- const goneBody = z.object({ videos: z.array(extVideoBody).max(1000) });
77
+ const goneBody = z.object({
78
+ videos: z.array(extVideoBody).max(1000),
79
+ confirmed: z.array(extVideoBody).max(1000).optional(),
80
+ });
78
81
  const videoLinkBody = z.object({ short: shortField, link: z.string().max(500) });
79
82
  const slotBody = z.object({
80
83
  slot: z.object({ row: z.string().min(1).max(20), longAt: z.string().max(40) }).nullable(),
@@ -809,37 +812,50 @@ export function studioRoutes(deps) {
809
812
  })),
810
813
  });
811
814
  })
812
- // Every video Slopify takes to be on YouTube, for the extension to ask Studio whether
813
- // each still exists.
815
+ // Every video Slopify takes to be on YouTube, and every upload it saw start, for the
816
+ // extension to ask Studio whether each still exists and which Studio has scheduled.
814
817
  .get("/ext/recorded-videos", (c) => {
815
818
  allowOrigin(c, false);
816
819
  if (!extAllowed(c))
817
820
  return refused(c);
818
821
  const channels = projectChannels(deps.db);
819
822
  return c.json({
820
- videos: doneVideos(deps.db).map((video) => ({
823
+ videos: recordedVideos(deps.db).map((video) => ({
821
824
  projectId: video.projectId,
822
825
  short: video.short,
823
826
  videoId: video.videoId,
827
+ uploadState: video.uploadState,
824
828
  channelId: channels.get(video.projectId) ?? null,
825
829
  })),
826
830
  });
827
831
  })
828
832
  // Videos Studio says were deleted: each is forgotten, as Deleted on YouTube does, so it
829
- // can be uploaded again. A slot that has since become another video keeps it.
833
+ // can be uploaded again. Uploads Studio has scheduled or published whose confirmation
834
+ // the upload dialog missed are confirmed. A slot that has since become another video
835
+ // keeps it.
830
836
  .post("/ext/gone", zValidator("json", goneBody, onInvalid), (c) => {
831
837
  allowOrigin(c, false);
832
838
  if (!extAllowed(c))
833
839
  return refused(c);
840
+ const body = c.req.valid("json");
834
841
  let forgotten = 0;
835
- for (const { projectId, short, videoId } of c.req.valid("json").videos) {
842
+ for (const { projectId, short, videoId } of body.videos) {
836
843
  if (videoOf(deps.db, projectId, short ?? null)?.videoId !== videoId)
837
844
  continue;
838
845
  forgetVideo(deps.db, projectId, short ?? null);
839
846
  deps.hub.emit(projectId, { type: "project.updated", projectId });
840
847
  forgotten++;
841
848
  }
842
- return c.json({ forgotten });
849
+ let confirmed = 0;
850
+ for (const { projectId, short, videoId } of body.confirmed ?? []) {
851
+ const video = videoOf(deps.db, projectId, short ?? null);
852
+ if (video?.videoId !== videoId || video.uploadState !== "filled")
853
+ continue;
854
+ confirmVideoUpload(deps, projectId, short ?? null, videoId);
855
+ deps.hub.emit(projectId, { type: "project.updated", projectId });
856
+ confirmed++;
857
+ }
858
+ return c.json({ forgotten, confirmed });
843
859
  })
844
860
  .post("/ext/stats", zValidator("json", statsBody, onInvalid), (c) => {
845
861
  allowOrigin(c, false);
@@ -896,23 +912,7 @@ export function studioRoutes(deps) {
896
912
  if (!extAllowed(c))
897
913
  return refused(c);
898
914
  const { projectId, short, videoId } = c.req.valid("json");
899
- if (!confirmUpload(deps.db, projectId, short ?? null, videoId))
900
- recordVideo(deps.db, projectId, short ?? null, videoId, deps.clock.now().toISOString());
901
- // The Details touches (a short's related video, the long video's end screen and
902
- // captions) wait for the extension now, and the comment for the video to be public.
903
- const confirmedPack = uploadPack(deps, projectId);
904
- const confirmedItem = confirmedPack.ok
905
- ? packItem(confirmedPack.pack, short ?? undefined)
906
- : undefined;
907
- if (confirmedItem !== undefined &&
908
- (confirmedItem.relatedVideoId !== undefined ||
909
- confirmedItem.endScreenVideoId !== undefined ||
910
- confirmedItem.captions !== undefined))
911
- setTaskState(deps.db, "finish", projectId, short ?? null, "waiting", null);
912
- if (confirmedItem?.kind === "video" &&
913
- confirmedItem.pinnedComment !== undefined &&
914
- readSetting(deps.db, autoCommentKey) === "on")
915
- setTaskState(deps.db, "comment", projectId, short ?? null, "waiting", null);
915
+ confirmVideoUpload(deps, projectId, short ?? null, videoId);
916
916
  return c.json({ confirmed: true });
917
917
  })
918
918
  .get("/ext/files/:projectId/:asset", zValidator("param", fileParam, onInvalid), (c) => {
@@ -942,6 +942,26 @@ export function studioRoutes(deps) {
942
942
  });
943
943
  }));
944
944
  }
945
+ // Studio said the upload was scheduled or published: it is on YouTube, and the Details touches
946
+ // (a short's related video, the long video's end screen and captions) wait for the extension,
947
+ // the pinned comment for the video to be public.
948
+ function confirmVideoUpload(deps, projectId, short, videoId) {
949
+ if (!confirmUpload(deps.db, projectId, short, videoId))
950
+ recordVideo(deps.db, projectId, short, videoId, deps.clock.now().toISOString());
951
+ const confirmedPack = uploadPack(deps, projectId);
952
+ const confirmedItem = confirmedPack.ok
953
+ ? packItem(confirmedPack.pack, short ?? undefined)
954
+ : undefined;
955
+ if (confirmedItem !== undefined &&
956
+ (confirmedItem.relatedVideoId !== undefined ||
957
+ confirmedItem.endScreenVideoId !== undefined ||
958
+ confirmedItem.captions !== undefined))
959
+ setTaskState(deps.db, "finish", projectId, short, "waiting", null);
960
+ if (confirmedItem?.kind === "video" &&
961
+ confirmedItem.pinnedComment !== undefined &&
962
+ readSetting(deps.db, autoCommentKey) === "on")
963
+ setTaskState(deps.db, "comment", projectId, short, "waiting", null);
964
+ }
945
965
  function unknownProject(c) {
946
966
  return problem(c, {
947
967
  status: 404,
@@ -0,0 +1,9 @@
1
+ # Slopify 3.11.5
2
+
3
+ Released 6 October 2026.
4
+
5
+ ## YouTube Studio extension (1.3.2): uploads Studio scheduled count as uploaded
6
+
7
+ - **An upload whose confirmation was missed is caught up.** The extension marks an upload as on YouTube when Studio's "Video scheduled" or "Video published" message shows. When that was missed (the dialog closed early, or the next short opened), the upload stayed at "Upload again", and its captions, end screen and pinned comment never followed. The Studio check (Check for deleted videos, Read Studio now, or every two hours) now also reads these uploads: one Studio has scheduled or published counts as uploaded and its Details touches follow; a draft stays as it is.
8
+
9
+ Download the extension again from Settings → YouTube Studio and reload it.
@@ -0,0 +1,5 @@
1
+ # Slopify 3.11.6
2
+
3
+ Released 6 October 2026.
4
+
5
+ - **Loud bursts come down further, quiet passages stay quiet.** Level the volume takes the narration phrase by phrase twice now. After the first pass lowers the phrases that burst out, the narration's loud words are quieter too, so a chunk's opening still stood above them; the second pass holds it to them. Whispers and low passages are never raised, and the export is still mastered to your target, so normal speech plays as loud as before. Videos rendered from now on get it.
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "id": "3.11.6",
4
+ "title": "Slopify 3.11.6",
5
+ "version": "3.11.6",
6
+ "date": "2026-10-06"
7
+ },
8
+ {
9
+ "id": "3.11.5",
10
+ "title": "Slopify 3.11.5",
11
+ "version": "3.11.5",
12
+ "date": "2026-10-06"
13
+ },
2
14
  {
3
15
  "id": "3.11.4",
4
16
  "title": "Slopify 3.11.4",
@@ -2,7 +2,7 @@ import { mkdirSync, rmSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { levelFile, measurable } from "./loudnorm.js";
4
4
  import { pieceLufs, pieceTruePeak, spreadOf } from "./model.js";
5
- import { lowerLoudPhrases } from "./phrase-level.js";
5
+ import { lowerLoudPhrases, phrasePasses } from "./phrase-level.js";
6
6
  // Every narration piece brought to the one common loudness before the join (`levelFile`), its
7
7
  // loud phrases lowered first (`phrase-level.ts`), each
8
8
  // written again as a WAV in `directory` for the join to read in their place. They come out in one format (44.1 kHz mono, which is what the
@@ -21,12 +21,20 @@ export async function levelPieces(run, files, directory) {
21
21
  run.signal.throwIfAborted();
22
22
  const output = join(directory, `piece-${String(index + 1).padStart(4, "0")}.wav`);
23
23
  // A phrase that bursts out above the narration around it (most of all a request's first
24
- // words) comes down first.
25
- const phrased = `${output}.phrases.wav`;
26
- const lowered = await lowerLoudPhrases(run, file, phrased);
27
- const measured = await levelFile(run, lowered ? phrased : file, output, goal, format);
28
- if (lowered)
29
- rmSync(phrased, { force: true });
24
+ // words) comes down first, twice: once loud phrases are lowered, the narration's loud words
25
+ // are quieter too, and an opening still stands above them. Quiet passages are never raised.
26
+ let source = file;
27
+ const made = [];
28
+ for (let pass = 1; pass <= phrasePasses; pass++) {
29
+ const phrased = `${output}.phrases-${String(pass)}.wav`;
30
+ if (!(await lowerLoudPhrases(run, source, phrased)))
31
+ break;
32
+ made.push(phrased);
33
+ source = phrased;
34
+ }
35
+ const measured = await levelFile(run, source, output, goal, format);
36
+ for (const one of made)
37
+ rmSync(one, { force: true });
30
38
  levelled.push(output);
31
39
  if (!measurable(measured.before)) {
32
40
  skipped += 1;
@@ -11,6 +11,9 @@ import { parseFrames } from "./line-level.js";
11
11
  // like a compressor letting go: snapping back after a lowered phrase made the next one jump
12
12
  // out. Phrases at the usual level are left as they are, nothing moves in time, and the export's
13
13
  // own levelling then brings the narration to its target, so it is not made quieter.
14
+ // How often the narration is taken phrase by phrase (`level-pieces.ts`): a second pass holds
15
+ // what is still loud against the narration the first left.
16
+ export const phrasePasses = 2;
14
17
  // ceiling: the most a phrase is lowered.
15
18
  const capDb = 8;
16
19
  // A momentary reading under this is a pause, not speech.
@@ -75,6 +75,10 @@ export function waitingTasks(db, task) {
75
75
  .map(rowOf);
76
76
  }
77
77
  // Every video on YouTube, for the numbers the extension reads from Studio.
78
+ // Every video recorded, on YouTube or only started (`filled`), newest first.
79
+ export function recordedVideos(db) {
80
+ return db.prepare("SELECT * FROM youtube_videos ORDER BY recorded_at DESC").all().map(rowOf);
81
+ }
78
82
  export function doneVideos(db) {
79
83
  return db
80
84
  .prepare("SELECT * FROM youtube_videos WHERE upload_state='done' ORDER BY recorded_at DESC")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gentbajko/slopify",
3
- "version": "3.11.4",
3
+ "version": "3.11.6",
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",