@gentbajko/slopify 3.0.5 → 3.0.7
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/adapters/tts/elevenlabs.js +2 -0
- package/dist/assets/document/fonts/Literata-Italic.ttf +0 -0
- package/dist/assets/document/fonts/Literata-OFL.txt +93 -0
- package/dist/assets/document/fonts/Literata-Regular.ttf +0 -0
- package/dist/assets/document/fonts/Literata-SemiBold.ttf +0 -0
- package/dist/assets/document/fonts/Literata-SemiBoldItalic.ttf +0 -0
- package/dist/assets/document/fonts/SOURCE.txt +13 -1
- package/dist/assets/models.yaml +26 -0
- package/dist/edge/http/studio.js +32 -11
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/main.js +1 -1
- package/dist/patch-notes/3.0.6.md +34 -0
- package/dist/patch-notes/3.0.7.md +33 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/document/fonts.js +29 -11
- package/dist/slices/document/render.js +1 -1
- package/dist/slices/document/theme.js +3 -3
- package/dist/slices/rebuild/recipe-legacy.js +2 -0
- package/dist/slices/rebuild/runtime-youtube.js +15 -4
- package/dist/slices/schedules/agenda.js +1 -1
- package/dist/slices/schedules/scheduler.js +3 -2
- package/dist/slices/schedules/service.js +11 -8
- package/dist/slices/schedules/topics.js +3 -3
- package/dist/slices/storage/downloads.js +14 -3
- package/dist/slices/storage/layout.js +4 -0
- package/dist/slices/storage/model.js +5 -0
- package/dist/slices/storage/portable.js +4 -4
- package/dist/slices/storage/trim.js +2 -0
- package/dist/slices/studio/pack.js +16 -5
- package/dist/slices/studio/settings.js +89 -24
- package/dist/slices/youtube/answer.js +54 -5
- package/dist/slices/youtube/edits-repo.js +8 -2
- package/dist/slices/youtube/edits.js +18 -4
- package/dist/slices/youtube/model.js +9 -1
- package/dist/tutorials/Document-Themes.md +1 -1
- package/dist/tutorials/Home-and-Projects.md +4 -4
- package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +3 -3
- package/dist/tutorials/Play-Overview.md +1 -1
- package/dist/tutorials/Publishing-to-YouTube.md +4 -4
- package/dist/tutorials/Schedules.md +2 -2
- package/dist/tutorials/Settings-Reference.md +3 -3
- package/dist/tutorials/Studio-Extension.md +2 -2
- package/dist/tutorials/YouTube-Description.md +13 -5
- package/dist/web/assets/{index-KWtMGqKB.css → index-C-Ib18Ec.css} +1 -1
- package/dist/web/assets/index-Cdb0Jh3P.js +310 -0
- package/dist/web/assets/{pdf-CkHBkfTB.js → pdf-BhFuoBjr.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-QDLAjCSW.js +0 -307
|
@@ -13,6 +13,8 @@ export const elevenLabsBase = "https://api.elevenlabs.io/v1";
|
|
|
13
13
|
// keep arriving.
|
|
14
14
|
export const elevenLabsModel = "eleven_multilingual_v2";
|
|
15
15
|
export const elevenLabsModels = [
|
|
16
|
+
{ id: "eleven_v4", name: "Eleven v4" },
|
|
17
|
+
{ id: "eleven_v4_turbo", name: "Eleven v4 Turbo" },
|
|
16
18
|
{ id: "eleven_v3", name: "Eleven v3" },
|
|
17
19
|
{ id: "eleven_multilingual_v2", name: "Eleven Multilingual v2" },
|
|
18
20
|
{ id: "eleven_flash_v2_5", name: "Eleven Flash v2.5" },
|
|
Binary file
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2017 The Literata Project Authors (https://github.com/googlefonts/literata)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -3,9 +3,21 @@ Cinzel 2.000 by Natanael Gama, from the embedded font data of the lore2script2 P
|
|
|
3
3
|
(same owner). Copyright and licence are read from each file's own name table.
|
|
4
4
|
Upstream: https://github.com/NDISCOVER/Cinzel
|
|
5
5
|
|
|
6
|
+
Literata-Regular.ttf, Literata-SemiBold.ttf, Literata-Italic.ttf, Literata-SemiBoldItalic.ttf
|
|
7
|
+
Literata 3 by TypeTogether, instanced from Google Fonts' variable Literata[opsz,wght].ttf and
|
|
8
|
+
Literata-Italic[opsz,wght].ttf (https://github.com/google/fonts/tree/main/ofl/literata) at
|
|
9
|
+
opsz 12 and wght 400 / 600, subset to Latin, Greek, Cyrillic, punctuation and symbols
|
|
10
|
+
(fontTools instancer and subsetter).
|
|
11
|
+
|
|
6
12
|
OFL.txt
|
|
7
|
-
SIL Open Font License 1.1, with the copyright line from the fonts' name table.
|
|
13
|
+
SIL Open Font License 1.1, with the copyright line from the fonts' name table (Cinzel).
|
|
14
|
+
Literata-OFL.txt
|
|
15
|
+
SIL Open Font License 1.1 for Literata, as published with it.
|
|
8
16
|
7445469e9f302618200b15b1645d6aa3565b93a6634a0f9e5cd069c7fc2343fe Cinzel-Black.ttf
|
|
9
17
|
cbe8f6a665cc58d4540b6ce2a29e6f1b7e096fa6835aea1d2ffb40c088c5cdf6 Cinzel-Bold.ttf
|
|
10
18
|
3d50001a451ba6cfd3ef07a2dfb0c564980a5229f70decdfa23f08f4a30e1bed Cinzel-Medium.ttf
|
|
11
19
|
540b628614b728fc87e50b883189a524dac4d8163ca4a0f8e49b499910762e86 Cinzel-Regular.ttf
|
|
20
|
+
184bbf724352ffbf7313dc9463f4d242246bcc587aaf10a78f0f4fe826a547bb Literata-Italic.ttf
|
|
21
|
+
efcb6ed9f994f21e3a1759d193d655708d4b836568616967cef80c6a015481cd Literata-Regular.ttf
|
|
22
|
+
a0eeda32c8098fee2cabf5951f6f65aa9396121053e511a11406076f2cd41569 Literata-SemiBoldItalic.ttf
|
|
23
|
+
68415d934f51efd396dfdfc5628ba4bc35042b7ac753e3a4b75b677b970c89a1 Literata-SemiBold.ttf
|
package/dist/assets/models.yaml
CHANGED
|
@@ -328,6 +328,32 @@ image:
|
|
|
328
328
|
- 16:9
|
|
329
329
|
- 9:16
|
|
330
330
|
tts:
|
|
331
|
+
- provider: elevenlabs
|
|
332
|
+
id: eleven_v4
|
|
333
|
+
name: Eleven v4
|
|
334
|
+
source: https://elevenlabs.io/docs/overview/models
|
|
335
|
+
keywords:
|
|
336
|
+
- tts
|
|
337
|
+
pricing:
|
|
338
|
+
perMillionCharacters: 22
|
|
339
|
+
note: Launch API rate, verified 2026-09-28 and offered for two weeks; the most expressive ElevenLabs model, 90+ languages. Your plan,
|
|
340
|
+
voice and account credits affect actual cost.
|
|
341
|
+
tts:
|
|
342
|
+
maxCharacters: 10000
|
|
343
|
+
streaming: true
|
|
344
|
+
- provider: elevenlabs
|
|
345
|
+
id: eleven_v4_turbo
|
|
346
|
+
name: Eleven v4 Turbo
|
|
347
|
+
source: https://elevenlabs.io/docs/overview/models
|
|
348
|
+
keywords:
|
|
349
|
+
- tts
|
|
350
|
+
pricing:
|
|
351
|
+
perMillionCharacters: 11
|
|
352
|
+
note: Launch API rate, verified 2026-09-28 and offered for two weeks; the fast v4, for low latency. Your plan,
|
|
353
|
+
voice and account credits affect actual cost.
|
|
354
|
+
tts:
|
|
355
|
+
maxCharacters: 10000
|
|
356
|
+
streaming: true
|
|
331
357
|
- provider: elevenlabs
|
|
332
358
|
id: eleven_v3
|
|
333
359
|
name: Eleven v3
|
package/dist/edge/http/studio.js
CHANGED
|
@@ -10,7 +10,7 @@ import { findDownload } from "../../slices/storage/downloads.js";
|
|
|
10
10
|
import { studioPlaylistMax } from "../../slices/studio/model.js";
|
|
11
11
|
import { packItem, uploadPack } from "../../slices/studio/pack.js";
|
|
12
12
|
import { enqueueFill, readFillQueue, removeFill, } from "../../slices/studio/queue.js";
|
|
13
|
-
import { bearerToken, isExtensionOrigin, pairStudioExtension, readChannelPlaylists,
|
|
13
|
+
import { bearerToken, isExtensionOrigin, pairStudioExtension, readChannelPlaylists, readStudioPlaylists, resetStudioPairing, saveProjectPlaylists, saveRealFootage, saveStudioPlaylists, studioPairing, studioPlaylistsMax, studioPlaylistsProblem, studioRequestAllowed, } from "../../slices/studio/settings.js";
|
|
14
14
|
import { onInvalid, problem, titleOf } from "./problem.js";
|
|
15
15
|
const id = z
|
|
16
16
|
.string()
|
|
@@ -30,11 +30,20 @@ const queueItemBody = z.object({
|
|
|
30
30
|
projectId: id,
|
|
31
31
|
short: z.number().int().min(1).max(99).nullable().optional(),
|
|
32
32
|
});
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
const playlistsBody = z.object({
|
|
34
|
+
playlists: z
|
|
35
|
+
.array(z.object({
|
|
36
|
+
name: z.string().max(studioPlaylistMax * 2),
|
|
37
|
+
byDefault: z.boolean(),
|
|
38
|
+
}))
|
|
39
|
+
.max(studioPlaylistsMax),
|
|
40
|
+
// A channel's own list; absent is the default every other channel uses.
|
|
36
41
|
channelId: id.optional(),
|
|
37
42
|
});
|
|
43
|
+
// The names this project's uploads go into; null goes back to the channel's defaults.
|
|
44
|
+
const projectPlaylistsBody = z.object({
|
|
45
|
+
playlists: z.array(z.string().max(studioPlaylistMax)).max(studioPlaylistsMax).nullable(),
|
|
46
|
+
});
|
|
38
47
|
const realFootageBody = z.object({ realFootage: z.boolean() });
|
|
39
48
|
// The extension builds the app ships (`scripts/copy-extension.mjs`), by browser.
|
|
40
49
|
const extensionFiles = {
|
|
@@ -89,32 +98,32 @@ export function studioRoutes(deps) {
|
|
|
89
98
|
if (denied !== undefined)
|
|
90
99
|
return denied;
|
|
91
100
|
return c.json({
|
|
92
|
-
|
|
101
|
+
playlists: readStudioPlaylists(deps.db),
|
|
93
102
|
channelPlaylists: readChannelPlaylists(deps.db),
|
|
94
103
|
pairing: studioPairing(deps.db),
|
|
95
104
|
});
|
|
96
105
|
})
|
|
97
|
-
.put("/settings/
|
|
106
|
+
.put("/settings/playlists", zValidator("json", playlistsBody, onInvalid), (c) => {
|
|
98
107
|
const denied = samePage(c);
|
|
99
108
|
if (denied !== undefined)
|
|
100
109
|
return denied;
|
|
101
|
-
const {
|
|
110
|
+
const { playlists: raw, channelId } = c.req.valid("json");
|
|
102
111
|
if (channelId !== undefined && channelById(deps.db, channelId) === undefined)
|
|
103
112
|
return problem(c, {
|
|
104
113
|
status: 404,
|
|
105
114
|
title: titleOf(404),
|
|
106
|
-
detail: "The
|
|
115
|
+
detail: "The playlists weren't saved: that channel no longer exists. Reload Settings → YouTube Studio and pick the channel again.",
|
|
107
116
|
});
|
|
108
|
-
const invalid =
|
|
117
|
+
const invalid = studioPlaylistsProblem(raw);
|
|
109
118
|
if (invalid !== undefined)
|
|
110
119
|
return problem(c, {
|
|
111
120
|
status: 400,
|
|
112
121
|
title: titleOf(400),
|
|
113
122
|
detail: invalid,
|
|
114
|
-
extensions: { fields: [{ field: "
|
|
123
|
+
extensions: { fields: [{ field: "playlists", message: invalid }] },
|
|
115
124
|
});
|
|
116
125
|
return c.json({
|
|
117
|
-
|
|
126
|
+
playlists: saveStudioPlaylists(deps.db, raw, channelId),
|
|
118
127
|
channelId: channelId ?? null,
|
|
119
128
|
});
|
|
120
129
|
})
|
|
@@ -141,6 +150,18 @@ export function studioRoutes(deps) {
|
|
|
141
150
|
saveRealFootage(deps.db, projectId, c.req.valid("json").realFootage);
|
|
142
151
|
const result = uploadPack(deps, projectId);
|
|
143
152
|
return result.ok ? c.json(result.pack) : unknownProject(c);
|
|
153
|
+
})
|
|
154
|
+
// Prepare upload's playlist ticks: which of the channel's playlists this project goes into.
|
|
155
|
+
.put("/packs/:projectId/playlists", zValidator("param", projectParam, onInvalid), zValidator("json", projectPlaylistsBody, onInvalid), (c) => {
|
|
156
|
+
const denied = samePage(c);
|
|
157
|
+
if (denied !== undefined)
|
|
158
|
+
return denied;
|
|
159
|
+
const { projectId } = c.req.valid("param");
|
|
160
|
+
if (!uploadPack(deps, projectId).ok)
|
|
161
|
+
return unknownProject(c);
|
|
162
|
+
saveProjectPlaylists(deps.db, projectId, c.req.valid("json").playlists);
|
|
163
|
+
const result = uploadPack(deps, projectId);
|
|
164
|
+
return result.ok ? c.json(result.pack) : unknownProject(c);
|
|
144
165
|
})
|
|
145
166
|
.post("/packs/:projectId/choose", zValidator("param", projectParam, onInvalid), zValidator("json", chooseBody, onInvalid), (c) => {
|
|
146
167
|
const denied = samePage(c);
|
|
Binary file
|
|
Binary file
|
package/dist/main.js
CHANGED
|
@@ -419,7 +419,7 @@ export async function boot(config, options = {}) {
|
|
|
419
419
|
};
|
|
420
420
|
const scheduleRunnerDeps = {
|
|
421
421
|
...draftDeps,
|
|
422
|
-
template: (id
|
|
422
|
+
template: (id) => templateById(runtimeDb, id),
|
|
423
423
|
topicLlm: (call) => standaloneLlm(standalone, call),
|
|
424
424
|
topicsWaiting: (event) => {
|
|
425
425
|
hub.emitGlobal(event);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Slopify 3.0.6
|
|
2
|
+
|
|
3
|
+
Released 28 September 2026.
|
|
4
|
+
|
|
5
|
+
3.0.6 writes more of what YouTube asks for: a comment to pin and two more titles to test, alongside the description and tags. A channel can send its videos to several playlists, and schedules follow the latest version of their template.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- **A pinned comment** written with every description.
|
|
10
|
+
- **Two more titles** for YouTube's title A/B test.
|
|
11
|
+
- **Several playlists** per channel, ticked per project.
|
|
12
|
+
- **Schedules use the template as it is now.**
|
|
13
|
+
|
|
14
|
+
## YouTube description
|
|
15
|
+
|
|
16
|
+
- The description step also writes a comment to pin under the video: a thank-you, a question viewers can answer from their own experience, a pointer to the chapters and an ask for the next topic. It shows under Tags in the project's YouTube section with **Copy**, and **Edit** keeps your changes through a rewrite. Your Description prompt can say how you want it.
|
|
17
|
+
- The same step writes two **Other titles** for YouTube's A/B Testing, next to the title in Studio. Each takes a different angle on the same video, within YouTube's 100 characters. They show in the YouTube section and in Prepare upload's Title step, with **Copy others**.
|
|
18
|
+
- **Write again**, next to Description, writes the description, tags, pinned comment and titles anew after asking. It no longer takes a trip through Edit settings and a remake.
|
|
19
|
+
|
|
20
|
+
## Publishing
|
|
21
|
+
|
|
22
|
+
- Settings → YouTube Studio → **Playlists** is a list for each channel (or the default list for all channels). Each playlist is either **On by default** or not.
|
|
23
|
+
- Prepare upload's Playlist step has a tick for each playlist, saved for that project, and the Studio extension ticks every chosen playlist in Studio.
|
|
24
|
+
- **Select all** above Prepare upload's steps ticks them all, or clears them.
|
|
25
|
+
|
|
26
|
+
## Schedules
|
|
27
|
+
|
|
28
|
+
- A schedule runs, previews and checks its topics against its template as it is now. It used to keep the template version it was made with, so edits to the template never reached its runs, and the Calendar showed the template's old name.
|
|
29
|
+
|
|
30
|
+
## Upgrading from 3.0.5
|
|
31
|
+
|
|
32
|
+
- **Nothing becomes outdated.** A description written before shows no pinned comment or other titles until you press **Write again**.
|
|
33
|
+
- A playlist you set before becomes the first entry of the channel's list, on by default.
|
|
34
|
+
- Update the Studio extension to have it tick more than one playlist. An older extension still ticks the first.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Slopify 3.0.7
|
|
2
|
+
|
|
3
|
+
Released 28 September 2026.
|
|
4
|
+
|
|
5
|
+
3.0.7 makes schedules show the template version they really use, adds Literata to PDF themes, and keeps pinned comments short.
|
|
6
|
+
|
|
7
|
+
## Highlights
|
|
8
|
+
|
|
9
|
+
- **Schedules show the template they run.** Editing a schedule shows each template at its newest version.
|
|
10
|
+
- **Literata for PDFs**, a text face made for long reading.
|
|
11
|
+
- **Short pinned comments:** a line or two, not paragraphs.
|
|
12
|
+
- **New project** in the sidebar.
|
|
13
|
+
|
|
14
|
+
## Schedules
|
|
15
|
+
|
|
16
|
+
- Edit schedule lists every template at its newest version, the one every run uses, and saving records it. It used to show the version the schedule was first saved with ("· v3") and offer no other, even though runs already used the newest.
|
|
17
|
+
|
|
18
|
+
## PDF themes
|
|
19
|
+
|
|
20
|
+
- PDF themes can use **Literata** in regular, bold, italic and bold italic: a text face made for long reading on screens.
|
|
21
|
+
|
|
22
|
+
## YouTube description
|
|
23
|
+
|
|
24
|
+
- The pinned comment is one or two short sentences (about 200 characters): a question viewers can answer and an ask for the next topic. It was two to four paragraphs.
|
|
25
|
+
|
|
26
|
+
## Everywhere
|
|
27
|
+
|
|
28
|
+
- **New video** is now **New project**, in the sidebar, on Home and Projects, and as the `C` command: a project can be an audiobook, a podcast or a PDF as well as a video.
|
|
29
|
+
- ElevenLabs lists **Eleven v4** and **Eleven v4 Turbo** even before the model list is checked.
|
|
30
|
+
|
|
31
|
+
## Upgrading from 3.0.6
|
|
32
|
+
|
|
33
|
+
- **Nothing becomes outdated.** To see a schedule's newest template version, open it and press **Save changes** once.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.0.7",
|
|
4
|
+
"title": "Slopify 3.0.7",
|
|
5
|
+
"version": "3.0.7",
|
|
6
|
+
"date": "2026-09-28"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.0.6",
|
|
10
|
+
"title": "Slopify 3.0.6",
|
|
11
|
+
"version": "3.0.6",
|
|
12
|
+
"date": "2026-09-28"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"id": "3.0.5",
|
|
4
16
|
"title": "Slopify 3.0.5",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
// The bundled Cinzel weights (SIL OFL 1.1, assets/document/fonts
|
|
3
|
-
// texture. Read on every render rather than kept in memory:
|
|
4
|
-
//
|
|
2
|
+
// The bundled Cinzel weights and Literata styles (SIL OFL 1.1, assets/document/fonts) and the
|
|
3
|
+
// page texture. Read on every render rather than kept in memory: eight fonts and one image
|
|
4
|
+
// are about a megabyte and a half, and a document is made once per article.
|
|
5
5
|
const assets = new URL("../../assets/document/", import.meta.url);
|
|
6
6
|
const cinzelFiles = {
|
|
7
7
|
normal: "Cinzel-Regular.ttf",
|
|
@@ -9,13 +9,27 @@ const cinzelFiles = {
|
|
|
9
9
|
bold: "Cinzel-Bold.ttf",
|
|
10
10
|
black: "Cinzel-Black.ttf",
|
|
11
11
|
};
|
|
12
|
+
// Literata, a text face made for long reading on screens: its text optical size, at 400 and
|
|
13
|
+
// 600 for bold (a heavier bold crowds a page of body text).
|
|
14
|
+
const literataFiles = {
|
|
15
|
+
normal: "Literata-Regular.ttf",
|
|
16
|
+
bold: "Literata-SemiBold.ttf",
|
|
17
|
+
italic: "Literata-Italic.ttf",
|
|
18
|
+
bolditalic: "Literata-SemiBoldItalic.ttf",
|
|
19
|
+
};
|
|
20
|
+
function readFonts(files) {
|
|
21
|
+
return Object.fromEntries(Object.entries(files).map(([style, file]) => [
|
|
22
|
+
style,
|
|
23
|
+
readFileSync(new URL(`fonts/${file}`, assets)).toString("base64"),
|
|
24
|
+
]));
|
|
25
|
+
}
|
|
12
26
|
export function readDocumentAssets() {
|
|
13
27
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
28
|
+
return {
|
|
29
|
+
cinzel: readFonts(cinzelFiles),
|
|
30
|
+
literata: readFonts(literataFiles),
|
|
31
|
+
parchment: new Uint8Array(readFileSync(new URL("background.jpg", assets))),
|
|
32
|
+
};
|
|
19
33
|
}
|
|
20
34
|
catch (error) {
|
|
21
35
|
throw new Error(`Slopify couldn't read the fonts and page texture it makes documents with (${error instanceof Error ? error.message : String(error)}). Its installation is incomplete: reinstall or update Slopify, then use Try again on Document.`, { cause: error });
|
|
@@ -23,12 +37,16 @@ export function readDocumentAssets() {
|
|
|
23
37
|
}
|
|
24
38
|
export function registerFonts(doc, fonts) {
|
|
25
39
|
for (const [style, file] of Object.entries(cinzelFiles)) {
|
|
26
|
-
doc.addFileToVFS(file, fonts[style]);
|
|
40
|
+
doc.addFileToVFS(file, fonts.cinzel[style]);
|
|
27
41
|
doc.addFont(file, "Cinzel", style);
|
|
28
42
|
}
|
|
43
|
+
for (const [style, file] of Object.entries(literataFiles)) {
|
|
44
|
+
doc.addFileToVFS(file, fonts.literata[style]);
|
|
45
|
+
doc.addFont(file, "Literata", style);
|
|
46
|
+
}
|
|
29
47
|
}
|
|
30
|
-
// Cinzel exists only in the four bundled weights; the standard fonts only in
|
|
31
|
-
// italic and bold italic. A face asking for anything else gets the nearest one.
|
|
48
|
+
// Cinzel exists only in the four bundled weights; Literata and the standard fonts only in
|
|
49
|
+
// normal, bold, italic and bold italic. A face asking for anything else gets the nearest one.
|
|
32
50
|
export function useFace(doc, face, size) {
|
|
33
51
|
doc.setFont(face.family, styleFor(face));
|
|
34
52
|
doc.setFontSize(size);
|
|
@@ -22,7 +22,7 @@ export function renderDocument(input) {
|
|
|
22
22
|
format: theme.page.format,
|
|
23
23
|
compress: true,
|
|
24
24
|
});
|
|
25
|
-
registerFonts(doc, input.assets
|
|
25
|
+
registerFonts(doc, input.assets);
|
|
26
26
|
const fill = (template) => template.replaceAll("{title}", input.title).trim();
|
|
27
27
|
doc.setProperties({
|
|
28
28
|
title: input.title,
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { legacyDiceMasterTheme } from "./legacy-dicemaster.js";
|
|
2
2
|
import { defaultDocumentTheme, documentThemeOf, legacyDocumentTheme, } from "./model.js";
|
|
3
|
-
// "Cinzel"
|
|
4
|
-
//
|
|
5
|
-
export const fontFamilies = ["Cinzel", "times", "helvetica", "courier"];
|
|
3
|
+
// "Cinzel" and "Literata" are bundled (assets/document/fonts); the other three are the PDF
|
|
4
|
+
// standard fonts every reader has.
|
|
5
|
+
export const fontFamilies = ["Cinzel", "Literata", "times", "helvetica", "courier"];
|
|
6
6
|
export const fontStyles = ["normal", "medium", "bold", "black", "italic", "bolditalic"];
|
|
7
7
|
const cinzel = (style, letterSpacing) => ({
|
|
8
8
|
family: "Cinzel",
|
|
@@ -68,6 +68,8 @@ export function legacyOutputWorkKey(output, config) {
|
|
|
68
68
|
return "document:pdf";
|
|
69
69
|
case "youtube_description":
|
|
70
70
|
case "youtube_tags":
|
|
71
|
+
case "youtube_pinned_comment":
|
|
72
|
+
case "youtube_titles":
|
|
71
73
|
return "youtube:description";
|
|
72
74
|
case "shorts":
|
|
73
75
|
return "shorts:pick";
|
|
@@ -8,7 +8,7 @@ import { exportSnapshot, revisionAudio } from "./runtime-export-inputs.js";
|
|
|
8
8
|
import { savedCatalogue } from "./runtime-plan.js";
|
|
9
9
|
import { preparedTexts, publishResult } from "./runtime-publication.js";
|
|
10
10
|
import { wordsSchema } from "./runtime-subtitles.js";
|
|
11
|
-
// Writes the YouTube description and
|
|
11
|
+
// Writes the YouTube description, tags, pinned comment and other titles from the saved word timing: the transcript's
|
|
12
12
|
// passages carry their start in the final video, so the chapters the model picks are real
|
|
13
13
|
// times. An answer that breaks YouTube's chapter or tag rules is a failed attempt, which the
|
|
14
14
|
// provider wrapper asks again while attempts remain.
|
|
@@ -66,21 +66,32 @@ export async function executeYoutubeRecipe(deps, context, providers, piece) {
|
|
|
66
66
|
webSearch: false,
|
|
67
67
|
previewLabel: piece.key,
|
|
68
68
|
check: (value) => {
|
|
69
|
-
const checked = checkDescriptionAnswer(value.text, durationSeconds);
|
|
69
|
+
const checked = checkDescriptionAnswer(value.text, durationSeconds, config.title);
|
|
70
70
|
return checked.ok ? undefined : checked.reason;
|
|
71
71
|
},
|
|
72
72
|
});
|
|
73
73
|
if (!answer.ok)
|
|
74
74
|
return "held";
|
|
75
|
-
const checked = checkDescriptionAnswer(answer.value.text, durationSeconds);
|
|
75
|
+
const checked = checkDescriptionAnswer(answer.value.text, durationSeconds, config.title);
|
|
76
76
|
if (!checked.ok)
|
|
77
77
|
throw new Error(checked.reason);
|
|
78
78
|
const description = assembleDescription(checked.value);
|
|
79
79
|
const tags = tagsText(checked.value.tags);
|
|
80
|
+
const pinnedComment = checked.value.pinnedComment;
|
|
81
|
+
const titles = checked.value.titles.join("\n");
|
|
80
82
|
await publishResult(deps, context, piece, preparedTexts(deps, context, piece, [
|
|
81
83
|
["youtube_description", "description.txt", description],
|
|
82
84
|
["youtube_tags", "tags.txt", tags],
|
|
83
|
-
|
|
85
|
+
["youtube_pinned_comment", "pinned-comment.txt", pinnedComment],
|
|
86
|
+
["youtube_titles", "titles.txt", titles],
|
|
87
|
+
]), {
|
|
88
|
+
description,
|
|
89
|
+
tags: checked.value.tags,
|
|
90
|
+
chapters: checked.value.chapters,
|
|
91
|
+
pinnedComment,
|
|
92
|
+
titles: checked.value.titles,
|
|
93
|
+
durationSeconds,
|
|
94
|
+
});
|
|
84
95
|
deps.count?.("stage.completed", {
|
|
85
96
|
provider: llm.provider,
|
|
86
97
|
model: llm.model,
|
|
@@ -52,7 +52,7 @@ function upcomingRuns(deps, schedule, now, from, to) {
|
|
|
52
52
|
: paused
|
|
53
53
|
? nextOccurrence(schedule.cadence, schedule.timezone, now)
|
|
54
54
|
: new Date(schedule.nextRunAt);
|
|
55
|
-
const template = deps.template(schedule.templateId
|
|
55
|
+
const template = deps.template(schedule.templateId);
|
|
56
56
|
const templateName = template?.name ?? null;
|
|
57
57
|
const form = template?.document.form;
|
|
58
58
|
const { items } = schedule;
|
|
@@ -133,12 +133,13 @@ function noTopicReason(schedule) {
|
|
|
133
133
|
async function execute(deps, claimed) {
|
|
134
134
|
let run = claimed.run;
|
|
135
135
|
try {
|
|
136
|
-
|
|
136
|
+
// The template as it is now, so an edit to it reaches the next run.
|
|
137
|
+
const template = deps.template(claimed.schedule.templateId);
|
|
137
138
|
if (template === undefined)
|
|
138
139
|
throw new ScheduleDispatchError("missing-template");
|
|
139
140
|
const document = freshTemplateDraft(deps, template.document, {
|
|
140
141
|
id: claimed.schedule.templateId,
|
|
141
|
-
version:
|
|
142
|
+
version: template.version,
|
|
142
143
|
});
|
|
143
144
|
const topic = claimed.schedule.items[0];
|
|
144
145
|
const fresh = {
|
|
@@ -35,7 +35,7 @@ export function createSchedule(deps, input) {
|
|
|
35
35
|
? { ok: true, value: scheduleById(deps.db, parsed.data.id) }
|
|
36
36
|
: { ok: false, reason: "conflict" };
|
|
37
37
|
}
|
|
38
|
-
const checked = checkTemplate(deps, parsed.data.templateId
|
|
38
|
+
const checked = checkTemplate(deps, parsed.data.templateId);
|
|
39
39
|
if (!checked.ok)
|
|
40
40
|
return checked;
|
|
41
41
|
const topics = checkTopics(deps, parsed.data, []);
|
|
@@ -45,7 +45,8 @@ export function createSchedule(deps, input) {
|
|
|
45
45
|
const next = nextOccurrence(parsed.data.cadence, parsed.data.timezone, now);
|
|
46
46
|
if (next === null)
|
|
47
47
|
return { ok: false, reason: "not-due" };
|
|
48
|
-
|
|
48
|
+
// The version it was saved against, for the record; runs use the newest.
|
|
49
|
+
const value = summary({ ...parsed.data, templateVersion: checked.value }, next.toISOString(), now.toISOString());
|
|
49
50
|
insertSchedule(deps.db, value, hash);
|
|
50
51
|
return { ok: true, value };
|
|
51
52
|
});
|
|
@@ -77,7 +78,7 @@ export function updateSchedule(deps, input) {
|
|
|
77
78
|
return { ok: false, reason: "conflict" };
|
|
78
79
|
if (previous.status === "canceled" || previous.status === "completed")
|
|
79
80
|
return { ok: false, reason: "conflict" };
|
|
80
|
-
const checked = checkTemplate(deps, parsed.data.templateId
|
|
81
|
+
const checked = checkTemplate(deps, parsed.data.templateId);
|
|
81
82
|
if (!checked.ok)
|
|
82
83
|
return checked;
|
|
83
84
|
const topics = checkTopics(deps, parsed.data, previous.items);
|
|
@@ -89,7 +90,8 @@ export function updateSchedule(deps, input) {
|
|
|
89
90
|
: previous.nextRunAt === null
|
|
90
91
|
? null
|
|
91
92
|
: nextOccurrence(parsed.data.cadence, parsed.data.timezone, now);
|
|
92
|
-
|
|
93
|
+
// The version it was saved against, for the record; runs use the newest.
|
|
94
|
+
const value = summary({ ...parsed.data, templateVersion: checked.value }, next?.toISOString() ?? null, now.toISOString(), previous.status, previous.version + 1,
|
|
93
95
|
// Saving clears a recorded generation failure, so the next tick tries again.
|
|
94
96
|
{ ...previous.topics, failedAt: null, error: null });
|
|
95
97
|
if (!updateScheduleRow(deps.db, value, parsed.data.mutationId, hash, parsed.data.baseVersion))
|
|
@@ -155,8 +157,9 @@ function control(deps, input, status) {
|
|
|
155
157
|
return { ok: false, reason: "conflict" };
|
|
156
158
|
return { ok: true, value: scheduleById(deps.db, previous.id) };
|
|
157
159
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
+
// Answers the template's newest version when a schedule may use it.
|
|
161
|
+
function checkTemplate(deps, id) {
|
|
162
|
+
const template = deps.template(id);
|
|
160
163
|
if (!template)
|
|
161
164
|
return { ok: false, reason: "missing-template" };
|
|
162
165
|
const sources = template.document.form.sources;
|
|
@@ -169,13 +172,13 @@ function checkTemplate(deps, id, version) {
|
|
|
169
172
|
form.ambientBed?.source === "upload" ||
|
|
170
173
|
(sources.images === "generate" && form.reference?.source === "provide"))
|
|
171
174
|
return { ok: false, reason: "unsupported-media" };
|
|
172
|
-
return { ok: true, value:
|
|
175
|
+
return { ok: true, value: template.version };
|
|
173
176
|
}
|
|
174
177
|
// The queue's keywords must be the template's: a topic (or an every-run value) naming a keyword
|
|
175
178
|
// the template doesn't use would silently do nothing. Topics saved before, unchanged, are left
|
|
176
179
|
// alone, so a queue from before this check still saves.
|
|
177
180
|
function checkTopics(deps, input, kept) {
|
|
178
|
-
const template = deps.template(input.templateId
|
|
181
|
+
const template = deps.template(input.templateId);
|
|
179
182
|
if (!template)
|
|
180
183
|
return { ok: false, reason: "missing-template" };
|
|
181
184
|
const keywords = templateKeywords(template.document.form);
|
|
@@ -101,7 +101,7 @@ async function ask(deps, schedule, options) {
|
|
|
101
101
|
.run(failedAt.toISOString(), text, schedule.id);
|
|
102
102
|
return { ok: false, reason: "failed", message: text };
|
|
103
103
|
};
|
|
104
|
-
const template = deps.template(schedule.templateId
|
|
104
|
+
const template = deps.template(schedule.templateId);
|
|
105
105
|
const llm = schedule.topicGeneration.llm ?? template?.document.form.llm;
|
|
106
106
|
if (llm === undefined)
|
|
107
107
|
return fail("the template this schedule uses was deleted, so there is no LLM to ask. Edit the schedule and choose a template, or pick an LLM under Topic generation.");
|
|
@@ -394,7 +394,7 @@ export function editHeldTopic(deps, scheduleId, topicId, input) {
|
|
|
394
394
|
const schedule = scheduleById(deps.db, scheduleId);
|
|
395
395
|
if (schedule === undefined)
|
|
396
396
|
return { ok: false, reason: "not-found" };
|
|
397
|
-
const template = deps.template(schedule.templateId
|
|
397
|
+
const template = deps.template(schedule.templateId);
|
|
398
398
|
if (template === undefined)
|
|
399
399
|
return { ok: false, reason: "missing-template" };
|
|
400
400
|
const problems = topicRowProblems([{ title: parsed.data, values }], {
|
|
@@ -463,7 +463,7 @@ export function replaceTopics(deps, scheduleId, input) {
|
|
|
463
463
|
const schedule = open.value;
|
|
464
464
|
if (schedule.version !== parsed.data.baseVersion)
|
|
465
465
|
return { ok: false, reason: "conflict" };
|
|
466
|
-
const template = deps.template(schedule.templateId
|
|
466
|
+
const template = deps.template(schedule.templateId);
|
|
467
467
|
if (!template)
|
|
468
468
|
return { ok: false, reason: "missing-template" };
|
|
469
469
|
const problems = topicRowProblems(parsed.data.items, {
|