@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.
- package/dist/edge/http/studio.js +45 -25
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/patch-notes/3.11.5.md +9 -0
- package/dist/patch-notes/3.11.6.md +5 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/loudness/level-pieces.js +15 -7
- package/dist/slices/loudness/phrase-level.js +3 -0
- package/dist/slices/studio/videos.js +4 -0
- package/package.json +1 -1
package/dist/edge/http/studio.js
CHANGED
|
@@ -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({
|
|
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,
|
|
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:
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
-
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
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")
|