@gentbajko/slopify 3.0.0 → 3.0.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/README.md +6 -4
- package/dist/adapter-registry.js +28 -0
- package/dist/adapters/alignment/text.js +3 -1
- package/dist/adapters/key-probes.js +57 -8
- package/dist/adapters/tts/gemini.js +206 -0
- package/dist/adapters/tts/pcm-mp3.js +62 -0
- package/dist/adapters/tts/system.js +147 -0
- package/dist/assets/models.yaml +38 -0
- package/dist/assets/style-preview/narration.mp3 +0 -0
- package/dist/assets/style-preview/tall-1.jpg +0 -0
- package/dist/assets/style-preview/tall-2.jpg +0 -0
- package/dist/assets/style-preview/tall-3.jpg +0 -0
- package/dist/assets/style-preview/wide-1.jpg +0 -0
- package/dist/assets/style-preview/wide-2.jpg +0 -0
- package/dist/assets/style-preview/wide-3.jpg +0 -0
- package/dist/catalog/registry.js +4 -1
- package/dist/catalog/validate.js +2 -2
- package/dist/edge/autostart/docker-record.js +2 -2
- package/dist/edge/autostart/docker.js +68 -0
- package/dist/edge/autostart/prompt.js +7 -1
- package/dist/edge/autostart/service.js +18 -9
- package/dist/edge/cli-args.js +71 -0
- package/dist/edge/cli.js +11 -16
- package/dist/edge/docker-install/run.js +7 -1
- package/dist/edge/events/hub.js +3 -1
- package/dist/edge/http/actions.js +6 -1
- package/dist/edge/http/app.js +2 -0
- package/dist/edge/http/byte-range.js +42 -0
- package/dist/edge/http/diagnostics.js +1 -0
- package/dist/edge/http/draft-files.js +2 -4
- package/dist/edge/http/files.js +10 -6
- package/dist/edge/http/onboarding.js +64 -4
- package/dist/edge/http/project-templates.js +19 -2
- package/dist/edge/http/projects.js +6 -1
- package/dist/edge/http/providers.js +39 -9
- package/dist/edge/http/reviews.js +18 -8
- package/dist/edge/http/revision-files.js +2 -4
- package/dist/edge/http/schedules.js +9 -5
- package/dist/edge/http/storage.js +16 -2
- package/dist/edge/http/studio.js +111 -31
- package/dist/edge/http/style-preview.js +3 -42
- package/dist/edge/http/tutorials.js +59 -0
- package/dist/edge/http/youtube-edits.js +11 -2
- package/dist/extension/slopify-studio-chrome.zip +0 -0
- package/dist/extension/slopify-studio-firefox.zip +0 -0
- package/dist/host-cli/gemini-login.js +83 -0
- package/dist/host-cli/status.js +4 -2
- package/dist/kernel/db/migrations/0041-cast-hosts.sql +3 -0
- package/dist/kernel/ports/model.js +5 -3
- package/dist/kernel/ports/system-speech.js +203 -0
- package/dist/kernel/runner/graph.js +17 -1
- package/dist/kernel/runner/index.js +4 -4
- package/dist/kernel/runner/providers.js +1 -1
- package/dist/main.js +5 -0
- package/dist/model-catalog.js +2 -0
- package/dist/patch-notes/3.0.1.md +105 -0
- package/dist/patch-notes/3.0.2.md +20 -0
- package/dist/patch-notes/index.json +12 -0
- package/dist/slices/admission/rules.js +3 -3
- package/dist/slices/admission/start.js +1 -1
- package/dist/slices/channels/cast-match.js +1 -1
- package/dist/slices/channels/repo.js +3 -0
- package/dist/slices/channels/schema.js +23 -0
- package/dist/slices/channels/service.js +6 -6
- package/dist/slices/control/index.js +3 -1
- package/dist/slices/control/providers.js +4 -1
- package/dist/slices/episodes/related.js +2 -2
- package/dist/slices/estimate/requests.js +9 -1
- package/dist/slices/eta/history.js +56 -0
- package/dist/slices/eta/model.js +63 -0
- package/dist/slices/eta/view.js +25 -0
- package/dist/slices/fixes/rules.js +1 -1
- package/dist/slices/fonts/coverage.js +1 -1
- package/dist/slices/loudness/model.js +1 -1
- package/dist/slices/model-upkeep/usage.js +4 -2
- package/dist/slices/narration/pauses-model.js +1 -1
- package/dist/slices/narration/pronunciation.js +2 -2
- package/dist/slices/notifications/notifier.js +28 -2
- package/dist/slices/notifications/rules.js +26 -2
- package/dist/slices/onboarding/first-run.js +40 -14
- package/dist/slices/onboarding/full-video.js +81 -0
- package/dist/slices/onboarding/model.js +21 -0
- package/dist/slices/onboarding/quick-short.js +25 -10
- package/dist/slices/play-drafts/convert.js +3 -3
- package/dist/slices/play-drafts/readiness.js +1 -1
- package/dist/slices/project-templates/from-project.js +10 -3
- package/dist/slices/project-templates/next-chapter.js +94 -0
- package/dist/slices/project-templates/one-off.js +1 -1
- package/dist/slices/project-templates/schema.js +7 -1
- package/dist/slices/rebuild/recipe-exports.js +5 -0
- package/dist/slices/rebuild/recipe-save.js +1 -1
- package/dist/slices/rebuild/recipe-voices.js +1 -1
- package/dist/slices/rebuild/review-redo.js +15 -0
- package/dist/slices/rebuild/runtime-export-bed.js +1 -1
- package/dist/slices/rebuild/runtime-review.js +35 -5
- package/dist/slices/rebuild/runtime-store.js +23 -1
- package/dist/slices/rebuild/runtime-voices.js +2 -2
- package/dist/slices/rebuild/service-readiness.js +3 -3
- package/dist/slices/reviews/repo.js +16 -4
- package/dist/slices/run-cost/panel.js +30 -8
- package/dist/slices/run-cost/pricing.js +4 -1
- package/dist/slices/schedules/agenda.js +66 -2
- package/dist/slices/schedules/repo.js +1 -1
- package/dist/slices/schedules/scheduler.js +1 -1
- package/dist/slices/schedules/schema.js +43 -2
- package/dist/slices/schedules/similar.js +41 -3
- package/dist/slices/schedules/topic-list.js +2 -2
- package/dist/slices/schedules/topics.js +118 -17
- package/dist/slices/settings/first-run.js +5 -1
- package/dist/slices/settings/health.js +64 -7
- package/dist/slices/settings/key-guides.js +17 -7
- package/dist/slices/settings/key-test.js +12 -4
- package/dist/slices/settings/keys.js +10 -5
- package/dist/slices/settings/model-reach.js +52 -0
- package/dist/slices/settings/model.js +49 -0
- package/dist/slices/settings/models.js +5 -1
- package/dist/slices/settings/readiness.js +14 -2
- package/dist/slices/settings/tutorial-schema.js +5 -0
- package/dist/slices/shorts/render.js +18 -17
- package/dist/slices/storage/documents.js +11 -3
- package/dist/slices/storage/downloads.js +33 -1
- package/dist/slices/storage/portable.js +4 -2
- package/dist/slices/studio/model.js +2 -0
- package/dist/slices/studio/pack.js +6 -4
- package/dist/slices/studio/queue.js +69 -0
- package/dist/slices/studio/settings.js +42 -5
- package/dist/slices/style-preview/narration.js +33 -0
- package/dist/slices/style-preview/render.js +43 -56
- package/dist/slices/style-preview/schema.js +17 -3
- package/dist/slices/style-preview/service.js +3 -4
- package/dist/slices/style-preview/settings.js +75 -25
- package/dist/slices/tutorials/anchors.js +57 -0
- package/dist/slices/tutorials/library.js +185 -0
- package/dist/slices/voices/audio-files.js +7 -2
- package/dist/slices/voices/cast.js +26 -8
- package/dist/slices/voices/grouping.js +10 -0
- package/dist/slices/voices/model.js +49 -10
- package/dist/slices/voices/script.js +1 -1
- package/dist/slices/youtube/edits-repo.js +19 -4
- package/dist/slices/youtube/placeholders.js +2 -2
- package/dist/tutorials/AI-CLIs.md +146 -0
- package/dist/tutorials/Backups.md +121 -0
- package/dist/tutorials/Calendar.md +103 -0
- package/dist/tutorials/Cast-Library.md +112 -0
- package/dist/tutorials/Channels.md +148 -0
- package/dist/tutorials/Costs-and-Run-Cost.md +136 -0
- package/dist/tutorials/Docker.md +171 -0
- package/dist/tutorials/Document-Themes.md +207 -0
- package/dist/tutorials/Editing-a-Project.md +204 -0
- package/dist/tutorials/FAQ.md +134 -0
- package/dist/tutorials/First-Launch-and-Welcome.md +153 -0
- package/dist/tutorials/Home-and-Projects.md +174 -0
- package/dist/tutorials/Home.md +119 -0
- package/dist/tutorials/Install.md +154 -0
- package/dist/tutorials/Intros-and-Outros.md +86 -0
- package/dist/tutorials/Keyboard-Shortcuts-and-Command-Palette.md +215 -0
- package/dist/tutorials/Library-Overview.md +75 -0
- package/dist/tutorials/Models.md +133 -0
- package/dist/tutorials/Multiple-Voices.md +161 -0
- package/dist/tutorials/Narration-Aliases-and-Glossary.md +129 -0
- package/dist/tutorials/Notifications.md +86 -0
- package/dist/tutorials/Other-Languages.md +110 -0
- package/dist/tutorials/PDF-Documents.md +100 -0
- package/dist/tutorials/Play-Images.md +135 -0
- package/dist/tutorials/Play-Narration.md +167 -0
- package/dist/tutorials/Play-Outputs.md +109 -0
- package/dist/tutorials/Play-Overview.md +216 -0
- package/dist/tutorials/Play-Title-and-Article.md +135 -0
- package/dist/tutorials/Play-Video-and-Style.md +143 -0
- package/dist/tutorials/Project-Page.md +228 -0
- package/dist/tutorials/Prompts.md +136 -0
- package/dist/tutorials/Providers-and-Keys.md +290 -0
- package/dist/tutorials/Publishing-to-YouTube.md +135 -0
- package/dist/tutorials/Recovery-and-Retries.md +141 -0
- package/dist/tutorials/Reviews-and-Checkpoints.md +143 -0
- package/dist/tutorials/Schedules.md +154 -0
- package/dist/tutorials/Settings-Reference.md +258 -0
- package/dist/tutorials/Shorts.md +116 -0
- package/dist/tutorials/Start-at-Login.md +126 -0
- package/dist/tutorials/Studio-Extension.md +146 -0
- package/dist/tutorials/Templates.md +146 -0
- package/dist/tutorials/Trash.md +87 -0
- package/dist/tutorials/Troubleshooting.md +149 -0
- package/dist/tutorials/Uninstalling-and-Moving.md +113 -0
- package/dist/tutorials/Updating-and-Patch-Notes.md +120 -0
- package/dist/tutorials/Video-Editing.md +135 -0
- package/dist/tutorials/Where-Your-Files-Live.md +149 -0
- package/dist/tutorials/YouTube-Description.md +132 -0
- package/dist/tutorials/Your-First-Short.md +125 -0
- package/dist/tutorials/_Footer.md +1 -0
- package/dist/tutorials/_Sidebar.md +69 -0
- package/dist/web/assets/index-C4McGNke.js +307 -0
- package/dist/web/assets/index-DBLSfqA-.css +1 -0
- package/dist/web/assets/{pdf-BG8n_uc5.js → pdf-COowcLcz.js} +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +2 -2
- package/dist/web/assets/index-CUA2sUxU.css +0 -1
- package/dist/web/assets/index-CX590kE-.js +0 -303
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Slopify 3.0.1
|
|
2
|
+
|
|
3
|
+
Released 27 September 2026.
|
|
4
|
+
|
|
5
|
+
3.0.1 finishes what 3.0 started. Your first short needs no keys at all, the help is inside the app, videos and audio play in Slopify's own player, and fewer clicks stand between you and the common tasks. The Studio extension now ships with Slopify, and a running project says how long each step has left.
|
|
6
|
+
|
|
7
|
+
What 3.0 brought is in Slopify 3.0.0 and Slopify 2.0 to 3.0, further down in Settings → Patch notes.
|
|
8
|
+
|
|
9
|
+
## Highlights
|
|
10
|
+
|
|
11
|
+
- **A first short with no keys:** the computer's own voice narrates it, and the first run is three steps that end with the short being made.
|
|
12
|
+
- **Help → Tutorials** inside the app, offline, and every info button ends in Learn more.
|
|
13
|
+
- **Slopify's own player** for videos and audio, with chapters, speed, captions and YouTube's keys.
|
|
14
|
+
- **Time left** on every running step, and a Needs you list on the calendar.
|
|
15
|
+
- **Fewer clicks:** edit a schedule's topics in place, rename in the row, Ctrl+K for everything, and keyboard shortcuts.
|
|
16
|
+
- **Google Gemini voices,** channel hosts, and audiobooks as chapters of a book.
|
|
17
|
+
- **The Studio extension ships with Slopify** and fills one upload after another.
|
|
18
|
+
|
|
19
|
+
## What's new
|
|
20
|
+
|
|
21
|
+
### Your first video
|
|
22
|
+
- A new **System voice** narrates with the computer's own speech: say on macOS, the built-in voices on Windows, and Piper or eSpeak on Linux. It is free and needs no key. Settings → Providers shows what was found, and Settings → Voices lists its voices.
|
|
23
|
+
- **Make a 60-second short** uses the system voice when no voice key is saved.
|
|
24
|
+
- The first-run screen is three steps: what you have, pick a style, make your first short. It ends with the short being made and a link to watch it.
|
|
25
|
+
- A finished short offers **Make the full video on this topic**, which opens Play with the same topic and starter pack.
|
|
26
|
+
|
|
27
|
+
### Voices
|
|
28
|
+
- Google Gemini is a voice provider, with 30 voices, on the key you saved for Google images. Two Gemini speakers talking in turn go in one request.
|
|
29
|
+
- A cast member can be one of the channel's hosts (Channels → Cast → edit → One of the channel's hosts). A new podcast or interview on Play starts with the hosts who have a voice.
|
|
30
|
+
- An audiobook can be a chapter of a book (Speakers → A chapter of a book). Its MP3 and M4B name the book and chapter, and Projects shows "Book · Chapter 2". **Make the next chapter** on a finished audiobook opens Play with the same speakers and settings.
|
|
31
|
+
|
|
32
|
+
### Help
|
|
33
|
+
- **Help → Tutorials** shows every tutorial page inside the app, with contents, search across all pages and Copy section. It works offline and always matches the version you run. Open it from the book button at the foot of the sidebar, or from Ctrl+K.
|
|
34
|
+
- Every info button's help ends in **Learn more**, which opens the tutorial section that explains it.
|
|
35
|
+
- Messages and help name controls where they are now, such as Level the volume under Play's Video and style.
|
|
36
|
+
|
|
37
|
+
### Players and pictures
|
|
38
|
+
- Videos play in Slopify's own player: a poster and a big play key, then a bar with chapters on the track, volume, speed from 0.75× to 2×, captions, picture in picture and full screen. YouTube's keys work: Space or K, J and L, the arrows, M, F, C and 0–9.
|
|
39
|
+
- Narration and every other audio play in the same kind of player. Both can jump to any time.
|
|
40
|
+
- The finished video shows its first thumbnail until it plays, and each short shows its first still.
|
|
41
|
+
- Images, thumbnails, shorts and cast pictures use one gallery grid, and every picture opens full size with Regenerate and Download.
|
|
42
|
+
|
|
43
|
+
### Fewer clicks
|
|
44
|
+
- Calendar → Schedules: add, rename, move and remove a schedule's queued topics in place. Each change saves at once, with Undo.
|
|
45
|
+
- Library → Prompts, Intros & Outros and Templates: the pencil beside a name renames it in the row.
|
|
46
|
+
- Library → Templates rows have Edit, Duplicate, Use in Play, History and Delete, like the other Library tabs.
|
|
47
|
+
- A press anywhere on a row or tile opens it: Library lists, channels, schedules, projects, Home, the calendar, samples and media.
|
|
48
|
+
- Ctrl+K finds commands by several words in any order. "cleopatra regenerate image 3" opens that project on Images and asks first.
|
|
49
|
+
- Keyboard shortcuts: C for New video, G then H, P, C, S, L, K or comma to go to a screen, / to search a list, Ctrl+S to save in the Library editors. Press ? for the full list.
|
|
50
|
+
- A finished project with a YouTube description opens on the YouTube section, so Copy description is one press.
|
|
51
|
+
- Each stage has one **Download** and one **Open folder**.
|
|
52
|
+
|
|
53
|
+
### While a run goes
|
|
54
|
+
- Every running step on the project page and on Home says its time left, from its own speed or from past runs of the same step, or says that time left is unknown.
|
|
55
|
+
- Waiting for plan limits shows everywhere a project appears: "Waiting for Codex limits (resets at 14:00)".
|
|
56
|
+
- The calendar has a **Needs you** list: failed, paused, review checkpoints, flagged reviews and videos ready to upload, each with the button that deals with it.
|
|
57
|
+
- Schedules are a tab of the calendar: **Calendar → Schedules** holds the list, each schedule's topics and detail, New schedule, and Pause and Resume. Old `/schedules` links and G then S open it.
|
|
58
|
+
- A signed-out CLI gets **Copy sign-in command** and **Check again**, which carries on the step once you are signed in.
|
|
59
|
+
- A notification says "Review needs a decision" when an automatic review keeps an item flagged.
|
|
60
|
+
- Home names the queued runs under Running now, and Projects has a Queued filter.
|
|
61
|
+
- A finished project offers to free its working files and keep the outputs, and says how much space that frees.
|
|
62
|
+
- A finished project shows its run cost at the top, with See cost by stage.
|
|
63
|
+
|
|
64
|
+
### Play, channels and schedules
|
|
65
|
+
- The style preview plays six seconds of the sample's narration and images with captions timed to it. Under More shorts options, a 9:16 preview shows how the shorts will look.
|
|
66
|
+
- Play says when the channel already made or uploaded a video on the topic you typed.
|
|
67
|
+
- Each channel keeps its own links under Channel links on its Brand tab. `{{Patreon}}` fills from the project's channel.
|
|
68
|
+
- Topic suggestions are compared with the schedule's own channel only, and a one- or two-word topic must match a title nearly exactly.
|
|
69
|
+
- Edit on a held topic sets its other keywords too.
|
|
70
|
+
- Overrule on a review whose redo hasn't started yet calls the redo off.
|
|
71
|
+
|
|
72
|
+
### Settings and providers
|
|
73
|
+
- **Test** in Settings → Providers checks a pasted key before you save it, and never stores it.
|
|
74
|
+
- The health check asks each keyed provider whether your chosen models are reachable, and reads Gemini CLI's sign-in. Each provider has **Check again**.
|
|
75
|
+
- A template or schedule that uses a retired model is flagged on its row, with the same Switch button as Settings → Models.
|
|
76
|
+
|
|
77
|
+
### YouTube Studio
|
|
78
|
+
- The Slopify Studio extension comes with Slopify: Settings → YouTube Studio → Install the Studio extension, with Download for Chrome or Firefox and three steps.
|
|
79
|
+
- **Fill in YouTube Studio** adds to a waiting list, and each new upload dialog gets the next item. Prepare upload lists what waits, with Remove. The list survives a restart.
|
|
80
|
+
- Set a playlist per channel in Settings → YouTube Studio.
|
|
81
|
+
- If Studio's dialog is missing a field, the extension fills nothing, copies the whole pack and names what is missing.
|
|
82
|
+
|
|
83
|
+
### The sidebar
|
|
84
|
+
- On a desktop the sidebar holds everything the top bar had: updates, the tutorial, Help → Tutorials, GitHub, the donation links and what is running. The wordmark shows the running version.
|
|
85
|
+
|
|
86
|
+
## Upgrading from 3.0.0
|
|
87
|
+
|
|
88
|
+
- **Nothing becomes outdated.** Projects keep every file and fingerprint.
|
|
89
|
+
- **Channel links move to each channel.** Links saved in Settings → Channel links keep filling the default channel's projects until you save its Brand tab.
|
|
90
|
+
- **Style previews are drawn again** the first time you open them, with the sample's narration.
|
|
91
|
+
- **Docker installs:** the image now includes eSpeak NG for the system voice. With Docker Desktop, Settings and the first-run screen name Docker Desktop's own "Start Docker Desktop when you sign in" setting. If you updated, Slopify asks once whether to start at login.
|
|
92
|
+
|
|
93
|
+
## Fixes
|
|
94
|
+
|
|
95
|
+
- A failed thumbnail no longer holds the PDF back. The PDF is made without a cover.
|
|
96
|
+
- The narration review compares the narration with the text that was actually spoken.
|
|
97
|
+
- The bundled samples no longer appear under Ready to upload, and paused runs are listed under Needs you.
|
|
98
|
+
- The connection error says how to fix it for your kind of install, native or Docker.
|
|
99
|
+
- Without a terminal, start at login says where its switch is instead of failing.
|
|
100
|
+
- On Windows, a Documents folder with non-ASCII letters in its path works.
|
|
101
|
+
- `slopify --help` and `--version` answer instead of crashing, and an unknown option is named plainly.
|
|
102
|
+
- A style pack used by the first short still offers Add pack for its Play template.
|
|
103
|
+
- Two cast members whose ids start alike no longer share one speaker.
|
|
104
|
+
- An editor's Save bar no longer hides a heading, and on phones it sits above the bottom bar.
|
|
105
|
+
- On Linux, a `piper` that is not the Piper voice engine (such as the mouse settings app of the same name) is no longer taken for it. Settings → Providers leaves Piper out, and the system voice uses Pico or eSpeak when either is installed.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Slopify 3.0.2
|
|
2
|
+
|
|
3
|
+
Released 28 September 2026.
|
|
4
|
+
|
|
5
|
+
3.0.2 fixes a video that stopped at subtitle timing when the text had a lone "&", and makes regenerating images plain: one button remakes them all, and a marked image says so.
|
|
6
|
+
|
|
7
|
+
## Fixes
|
|
8
|
+
|
|
9
|
+
- A standalone "&" said as "and" through a pronunciation alias no longer stops the video. Subtitle timing failed on it with "Local subtitles currently require an English transcript with spoken words." A project that stopped there carries on with **Try the video again**.
|
|
10
|
+
- Deleting every image and saving says what to do: press Discard changes to get them back, then Regenerate all, or set Images and Video to Off on Inputs. The list above the settings no longer starts each message with an internal field name.
|
|
11
|
+
|
|
12
|
+
## Regenerating images
|
|
13
|
+
|
|
14
|
+
- The Images stage has **Regenerate all**, beside Download all. It opens the settings with every slideshow image marked to be made again, ready to save.
|
|
15
|
+
- In the settings, **Regenerate image N** now shows that it worked: the image says it is made again after you press Save changes, and the button becomes **Keep image N** to take the mark off.
|
|
16
|
+
- Marked and outdated images are made again when you remake them: press the Remake button on the project page once the run has nothing else waiting, or Ctrl+K → Remake everything outdated.
|
|
17
|
+
|
|
18
|
+
## Upgrading from 3.0.1
|
|
19
|
+
|
|
20
|
+
- **Nothing becomes outdated.** Projects keep every file and fingerprint.
|
|
@@ -1,4 +1,16 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"id": "3.0.2",
|
|
4
|
+
"title": "Slopify 3.0.2",
|
|
5
|
+
"version": "3.0.2",
|
|
6
|
+
"date": "2026-09-28"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"id": "3.0.1",
|
|
10
|
+
"title": "Slopify 3.0.1",
|
|
11
|
+
"version": "3.0.1",
|
|
12
|
+
"date": "2026-09-27"
|
|
13
|
+
},
|
|
2
14
|
{
|
|
3
15
|
"id": "3.0.0",
|
|
4
16
|
"title": "Slopify 3.0.0",
|
|
@@ -271,7 +271,7 @@ function checkShortsMusic(draft, staged, fields) {
|
|
|
271
271
|
const id = draft.provided.shortsMusic;
|
|
272
272
|
if (draft.shorts?.enabled !== true || id === undefined)
|
|
273
273
|
return;
|
|
274
|
-
const where = "under Outputs →
|
|
274
|
+
const where = "under Outputs → More shorts options → Background music";
|
|
275
275
|
const file = staged.find((candidate) => candidate.id === id);
|
|
276
276
|
if (file === undefined || file.stageKind !== "audio")
|
|
277
277
|
fields.push({
|
|
@@ -348,7 +348,7 @@ export function voicesFields(draft) {
|
|
|
348
348
|
!draft.voices.speakers.some((speaker) => speaker.voice.provider === "inworld" && speaker.voice.model === "inworld-tts-2"))
|
|
349
349
|
fields.push({
|
|
350
350
|
field: "narrationPrompt",
|
|
351
|
-
message: "Narration preparation adds delivery cues only to speakers on Inworld's TTS-2 model, and no speaker uses it. Choose that model for a speaker under Speakers, or clear Narration Preparation
|
|
351
|
+
message: "Narration preparation adds delivery cues only to speakers on Inworld's TTS-2 model, and no speaker uses it. Choose that model for a speaker under Speakers, or clear Narration Preparation under Narration → Audio Advanced.",
|
|
352
352
|
});
|
|
353
353
|
return fields;
|
|
354
354
|
}
|
|
@@ -432,7 +432,7 @@ export function ambientBedFields(draft, staged) {
|
|
|
432
432
|
const bed = draft.ambientBed;
|
|
433
433
|
if (bed === undefined || !usesAmbientBed(draft))
|
|
434
434
|
return [];
|
|
435
|
-
const where = "under
|
|
435
|
+
const where = "under Video and style → Ambient sound";
|
|
436
436
|
const fields = ambientBedProblems(bed).map((problem) => ({
|
|
437
437
|
field: `ambientBed.${problem.field}`,
|
|
438
438
|
message: `${problem.message} Change it ${where}.`,
|
|
@@ -38,7 +38,7 @@ export function startRun(deps, draft, rendered, retainStaged = false, templates)
|
|
|
38
38
|
const aliases = draft.sources.audio === "generate" && draft.audio?.useNarrationAliases === true
|
|
39
39
|
? listNarrationAliases(deps.db)
|
|
40
40
|
: [];
|
|
41
|
-
// The title may name keywords too ("
|
|
41
|
+
// The title may name keywords too ("History: {{Topic}}"), filled like a prompt's, so a
|
|
42
42
|
// template or Play keeps the pattern and each project gets its own title.
|
|
43
43
|
const title = render(draft.title, draft.values).trim() || draft.title;
|
|
44
44
|
// The channel's related earlier episodes as they are now (`slices/episodes`): later edits
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Which cast members a text mentions: a member's name or one of its aliases as a whole word,
|
|
2
2
|
// ignoring case. A word boundary is anything that is not a letter or digit in any script, so
|
|
3
|
-
// "
|
|
3
|
+
// "Cleopatra's palace" and "(Cleopatra)" mention Cleopatra and "Cleopatraic" or "Cleopatras" do not; a plural
|
|
4
4
|
// is an alias to add. Spaces inside a name match any run of whitespace. The members come back
|
|
5
5
|
// in the order the text first mentions them, then in cast order.
|
|
6
6
|
export function castMentions(text, cast) {
|
|
@@ -15,6 +15,7 @@ const brandSchema = z
|
|
|
15
15
|
documentTheme: z.string().optional(),
|
|
16
16
|
ambientBed: channelAmbientBedSchema.optional(),
|
|
17
17
|
language: z.string().optional(),
|
|
18
|
+
links: z.array(z.object({ name: z.string(), url: z.string() })).optional(),
|
|
18
19
|
})
|
|
19
20
|
.readonly();
|
|
20
21
|
const channelRow = z.object({
|
|
@@ -36,6 +37,7 @@ const memberRow = z.object({
|
|
|
36
37
|
aliases_json: z.string(),
|
|
37
38
|
description: z.string(),
|
|
38
39
|
voice_json: z.string().nullable().optional(),
|
|
40
|
+
host: z.number().optional(),
|
|
39
41
|
version: z.number(),
|
|
40
42
|
created_at: z.string(),
|
|
41
43
|
updated_at: z.string(),
|
|
@@ -145,6 +147,7 @@ function memberOf(row, images) {
|
|
|
145
147
|
...(row.voice_json === null || row.voice_json === undefined
|
|
146
148
|
? {}
|
|
147
149
|
: { voice: voiceJson.parse(JSON.parse(row.voice_json)) }),
|
|
150
|
+
...(row.host === 1 ? { host: true } : {}),
|
|
148
151
|
version: row.version,
|
|
149
152
|
images,
|
|
150
153
|
createdAt: row.created_at,
|
|
@@ -4,6 +4,7 @@ import { aiDisclosureSettings } from "../studio/disclosure.js";
|
|
|
4
4
|
import { ambientBedProblems } from "../video/ambient-bed.js";
|
|
5
5
|
import { channelAmbientBedSchema } from "../video/ambient-bed-schema.js";
|
|
6
6
|
import { paceSteps } from "../voices/model.js";
|
|
7
|
+
import { channelLinkNameMax, channelLinksMax, channelLinksProblem, channelLinkUrlMax, } from "../youtube/placeholders.js";
|
|
7
8
|
import { castKinds } from "./model.js";
|
|
8
9
|
const id = z.uuid();
|
|
9
10
|
const colour = z
|
|
@@ -40,6 +41,26 @@ export const brandKitSchema = z
|
|
|
40
41
|
})
|
|
41
42
|
.optional(),
|
|
42
43
|
language: languageSchema.optional(),
|
|
44
|
+
// Kept even when empty: an empty list on the default channel says "no links", where an
|
|
45
|
+
// absent one still reads the older Settings list.
|
|
46
|
+
links: z
|
|
47
|
+
.array(z
|
|
48
|
+
.object({
|
|
49
|
+
name: z.string().max(channelLinkNameMax * 4),
|
|
50
|
+
url: z.string().max(channelLinkUrlMax * 2),
|
|
51
|
+
})
|
|
52
|
+
.strict())
|
|
53
|
+
.max(channelLinksMax * 2)
|
|
54
|
+
.transform((links) => links.map((link) => ({ name: link.name.trim(), url: link.url.trim() })))
|
|
55
|
+
.superRefine((links, context) => {
|
|
56
|
+
const refused = channelLinksProblem(links);
|
|
57
|
+
if (refused !== undefined)
|
|
58
|
+
context.addIssue({
|
|
59
|
+
code: "custom",
|
|
60
|
+
message: `The channel's links weren't saved: ${refused} Then press Save channel on the channel's Brand tab.`,
|
|
61
|
+
});
|
|
62
|
+
})
|
|
63
|
+
.optional(),
|
|
43
64
|
})
|
|
44
65
|
.strict()
|
|
45
66
|
.transform((kit) => Object.fromEntries(Object.entries(kit).filter(([, value]) => value !== undefined && value !== "")));
|
|
@@ -94,6 +115,8 @@ export const castMemberInputSchema = z
|
|
|
94
115
|
.default(""),
|
|
95
116
|
// Absent keeps the saved voice; null removes it.
|
|
96
117
|
voice: castVoiceSchema.nullable().optional(),
|
|
118
|
+
// One of the channel's hosts. Absent keeps what is saved.
|
|
119
|
+
host: z.boolean().optional(),
|
|
97
120
|
})
|
|
98
121
|
.strict();
|
|
99
122
|
export const castMemberCreateSchema = castMemberInputSchema.extend({ id }).strict();
|
|
@@ -116,7 +116,7 @@ export function createCastMember(deps, channelId, input) {
|
|
|
116
116
|
const parsed = castMemberCreateSchema.safeParse(input);
|
|
117
117
|
if (!parsed.success)
|
|
118
118
|
return { ok: false, reason: "invalid-input", message: parsed.error.issues[0]?.message };
|
|
119
|
-
const { id, kind, name, aliases, description, voice } = parsed.data;
|
|
119
|
+
const { id, kind, name, aliases, description, voice, host } = parsed.data;
|
|
120
120
|
return transact(deps.db, () => {
|
|
121
121
|
if (channelById(deps.db, channelId) === undefined)
|
|
122
122
|
return { ok: false, reason: "not-found" };
|
|
@@ -127,8 +127,8 @@ export function createCastMember(deps, channelId, input) {
|
|
|
127
127
|
: { ok: false, reason: "conflict" };
|
|
128
128
|
const at = deps.clock.now().toISOString();
|
|
129
129
|
deps.db
|
|
130
|
-
.prepare("INSERT INTO cast_members(id,channel_id,kind,name,aliases_json,description,voice_json,created_at,updated_at) VALUES (
|
|
131
|
-
.run(id, channelId, kind, name, JSON.stringify(unique(aliases, name)), description, voice === undefined || voice === null ? null : JSON.stringify(voice), at, at);
|
|
130
|
+
.prepare("INSERT INTO cast_members(id,channel_id,kind,name,aliases_json,description,voice_json,host,created_at,updated_at) VALUES (?,?,?,?,?,?,?,?,?,?)")
|
|
131
|
+
.run(id, channelId, kind, name, JSON.stringify(unique(aliases, name)), description, voice === undefined || voice === null ? null : JSON.stringify(voice), host === true ? 1 : 0, at, at);
|
|
132
132
|
const created = castMemberById(deps.db, id);
|
|
133
133
|
if (created === undefined)
|
|
134
134
|
throw new Error("The new cast member could not be read back");
|
|
@@ -139,7 +139,7 @@ export function updateCastMember(deps, id, input) {
|
|
|
139
139
|
const parsed = castMemberUpdateSchema.safeParse(input);
|
|
140
140
|
if (!parsed.success)
|
|
141
141
|
return { ok: false, reason: "invalid-input", message: parsed.error.issues[0]?.message };
|
|
142
|
-
const { kind, name, aliases, description, voice, baseVersion } = parsed.data;
|
|
142
|
+
const { kind, name, aliases, description, voice, host, baseVersion } = parsed.data;
|
|
143
143
|
return transact(deps.db, () => {
|
|
144
144
|
const previous = castMemberById(deps.db, id);
|
|
145
145
|
if (previous === undefined)
|
|
@@ -147,14 +147,14 @@ export function updateCastMember(deps, id, input) {
|
|
|
147
147
|
if (previous.version !== baseVersion)
|
|
148
148
|
return { ok: false, reason: "conflict" };
|
|
149
149
|
deps.db
|
|
150
|
-
.prepare("UPDATE cast_members SET kind=?,name=?,aliases_json=?,description=?,voice_json=?,version=version+1,updated_at=? WHERE id=? AND version=?")
|
|
150
|
+
.prepare("UPDATE cast_members SET kind=?,name=?,aliases_json=?,description=?,voice_json=?,host=?,version=version+1,updated_at=? WHERE id=? AND version=?")
|
|
151
151
|
.run(kind, name, JSON.stringify(unique(aliases, name)), description, voice === undefined
|
|
152
152
|
? previous.voice === undefined
|
|
153
153
|
? null
|
|
154
154
|
: JSON.stringify(previous.voice)
|
|
155
155
|
: voice === null
|
|
156
156
|
? null
|
|
157
|
-
: JSON.stringify(voice), deps.clock.now().toISOString(), id, baseVersion);
|
|
157
|
+
: JSON.stringify(voice), (host ?? previous.host === true) ? 1 : 0, deps.clock.now().toISOString(), id, baseVersion);
|
|
158
158
|
const saved = castMemberById(deps.db, id);
|
|
159
159
|
if (saved === undefined)
|
|
160
160
|
throw new Error("The saved cast member could not be read back");
|
|
@@ -167,7 +167,9 @@ export function changeProviders(deps, id, changes) {
|
|
|
167
167
|
...provider,
|
|
168
168
|
readiness: provider.auth === "cli"
|
|
169
169
|
? { kind: "cli", installed: true }
|
|
170
|
-
:
|
|
170
|
+
: provider.auth === "local"
|
|
171
|
+
? { kind: "local", available: true }
|
|
172
|
+
: { kind: "keyed", hasKey: hasKey(deps.db, provider.id) },
|
|
171
173
|
})),
|
|
172
174
|
voices: listVoices(deps.db),
|
|
173
175
|
});
|
|
@@ -48,7 +48,10 @@ export function validateLocalProviderChanges(changes, current) {
|
|
|
48
48
|
message: provider.readiness.kind === "cli"
|
|
49
49
|
? (provider.readiness.issue ??
|
|
50
50
|
"Install this command-line tool on your computer and sign in to it, then try again.")
|
|
51
|
-
:
|
|
51
|
+
: provider.readiness.kind === "local"
|
|
52
|
+
? (provider.readiness.issue ??
|
|
53
|
+
"No speech program was found on this computer. Install espeak-ng, or choose a voice provider with a key in Settings → Providers.")
|
|
54
|
+
: "Add this provider's API key in Settings → Providers first.",
|
|
52
55
|
});
|
|
53
56
|
}
|
|
54
57
|
}
|
|
@@ -14,7 +14,7 @@ const castWeight = 100;
|
|
|
14
14
|
export function relatedEpisodes(memories, cast, subject, title) {
|
|
15
15
|
const mentioned = new Set(castMentions(subject, cast).map((member) => member.name.toLowerCase()));
|
|
16
16
|
const titles = memories.map((memory) => titleWords(memory.title));
|
|
17
|
-
// A word most of the channel's titles share is the template's ("
|
|
17
|
+
// A word most of the channel's titles share is the template's ("History: …"), not the
|
|
18
18
|
// topic's, and relates nothing.
|
|
19
19
|
const seen = new Map();
|
|
20
20
|
for (const set of titles)
|
|
@@ -42,7 +42,7 @@ export function relatedEpisodes(memories, cast, subject, title) {
|
|
|
42
42
|
.slice(0, earlierEpisodesMax)
|
|
43
43
|
.map((row) => row.memory);
|
|
44
44
|
}
|
|
45
|
-
// A title's words as the topic checks compare them, less the one- and two-letter ones ("
|
|
45
|
+
// A title's words as the topic checks compare them, less the one- and two-letter ones ("R&D"
|
|
46
46
|
// is "d and d"), which relate nothing.
|
|
47
47
|
function titleWords(text) {
|
|
48
48
|
return new Set([...topicWords(text)].filter((word) => word.length > 2 || /\d/.test(word)));
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { apiEquivalentOf, tokenCost } from "../run-cost/pricing.js";
|
|
3
|
-
import { isLocalCliProvider } from "../settings/model.js";
|
|
3
|
+
import { isLocalCliProvider, systemVoiceProvider } from "../settings/model.js";
|
|
4
4
|
const quantity = z.number().finite().nonnegative();
|
|
5
5
|
const common = z.object({
|
|
6
6
|
stage: z.string(),
|
|
@@ -123,6 +123,14 @@ function price(request, catalogue) {
|
|
|
123
123
|
};
|
|
124
124
|
if ((request.kind === "llm" || request.kind === "image") && isLocalCliProvider(request.provider))
|
|
125
125
|
return onPlan(request, catalogue);
|
|
126
|
+
if ((request.kind === "tts" || request.kind === "tts-estimate") &&
|
|
127
|
+
request.provider === systemVoiceProvider)
|
|
128
|
+
return {
|
|
129
|
+
stage: request.stage,
|
|
130
|
+
low: 0,
|
|
131
|
+
high: 0,
|
|
132
|
+
detail: "Spoken by your computer's built-in voice, so it costs nothing.",
|
|
133
|
+
};
|
|
126
134
|
const family = request.kind === "tts-estimate" ? "tts" : request.kind;
|
|
127
135
|
const model = catalogue[family].find((entry) => entry.provider === request.provider &&
|
|
128
136
|
entry.id === request.model &&
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { modelKeyOf } from "./model.js";
|
|
2
|
+
// How long steps took in finished runs, read from the stage rows' own start and finish times.
|
|
3
|
+
// ceiling: the most recent finished steps are what the machine and the providers are like now;
|
|
4
|
+
// a few hundred rows is a fraction of a millisecond to read and plenty for a median.
|
|
5
|
+
const sampleLimit = 400;
|
|
6
|
+
// A model's own history is used once it has this many finished steps; below that, every
|
|
7
|
+
// step of the same kind is a fairer guess than one lucky or unlucky run.
|
|
8
|
+
const ownModelMinimum = 2;
|
|
9
|
+
export function readStageHistory(db) {
|
|
10
|
+
const samples = db
|
|
11
|
+
.prepare(`SELECT s.kind, s.started_at, s.finished_at, s.progress_total, p.config FROM stages s
|
|
12
|
+
JOIN projects p ON p.id=s.project_id
|
|
13
|
+
WHERE s.state='done' AND s.started_at IS NOT NULL AND s.finished_at IS NOT NULL
|
|
14
|
+
ORDER BY s.finished_at DESC LIMIT ?`)
|
|
15
|
+
.all(sampleLimit)
|
|
16
|
+
.flatMap((row) => {
|
|
17
|
+
const seconds = (Date.parse(String(row.finished_at)) - Date.parse(String(row.started_at))) / 1000;
|
|
18
|
+
if (!Number.isFinite(seconds) || seconds <= 0)
|
|
19
|
+
return [];
|
|
20
|
+
const kind = String(row.kind);
|
|
21
|
+
const units = typeof row.progress_total === "number" && row.progress_total > 0
|
|
22
|
+
? row.progress_total
|
|
23
|
+
: undefined;
|
|
24
|
+
return [{ kind, model: modelKeyOf(kind, configOf(row.config)), seconds, units }];
|
|
25
|
+
});
|
|
26
|
+
return {
|
|
27
|
+
typicalSeconds: (kind, config, units) => {
|
|
28
|
+
const model = modelKeyOf(kind, config);
|
|
29
|
+
const own = samples.filter((one) => one.kind === kind && one.model === model);
|
|
30
|
+
const pool = own.length >= ownModelMinimum ? own : samples.filter((one) => one.kind === kind);
|
|
31
|
+
if (pool.length === 0)
|
|
32
|
+
return undefined;
|
|
33
|
+
// A step with a count scales with it: eight images take longer than two.
|
|
34
|
+
const counted = pool.filter((one) => one.units !== undefined);
|
|
35
|
+
if (units !== null && units > 0 && counted.length > 0)
|
|
36
|
+
return median(counted.map((one) => one.seconds / (one.units ?? 1))) * units;
|
|
37
|
+
return median(pool.map((one) => one.seconds));
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function configOf(value) {
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(String(value));
|
|
44
|
+
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return {};
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export function median(values) {
|
|
51
|
+
const sorted = values.toSorted((left, right) => left - right);
|
|
52
|
+
const middle = Math.floor(sorted.length / 2);
|
|
53
|
+
return sorted.length % 2 === 1
|
|
54
|
+
? (sorted[middle] ?? 0)
|
|
55
|
+
: ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2;
|
|
56
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Time left on a running step, said honestly. Two bases, in this order:
|
|
2
|
+
// - progress: the step has counted some of its chapters, chunks or images, so the rest is
|
|
3
|
+
// estimated from the rate so far;
|
|
4
|
+
// - history: nothing counted yet (a CLI image job drawing its first picture for minutes), so the
|
|
5
|
+
// estimate is how long the same kind of step, on the same provider and model when known,
|
|
6
|
+
// took in finished runs (`history.ts`), minus the time spent already.
|
|
7
|
+
// Neither: "unknown", never a made-up number. A step past its usual time is "overdue".
|
|
8
|
+
// Browser-safe: the project page and Home recompute it every second from the same inputs.
|
|
9
|
+
export const etaBases = ["progress", "history", "overdue", "unknown"];
|
|
10
|
+
// Undefined for a step that isn't running: a finished one has no time left, and a waiting one
|
|
11
|
+
// hasn't started the clock.
|
|
12
|
+
export function stageEta(stage, now) {
|
|
13
|
+
if (stage.state !== "running")
|
|
14
|
+
return undefined;
|
|
15
|
+
const started = stage.startedAt === null ? Number.NaN : Date.parse(stage.startedAt);
|
|
16
|
+
const spent = Number.isFinite(started) ? Math.max(0, (now - started) / 1000) : undefined;
|
|
17
|
+
const current = stage.progressCurrent ?? 0;
|
|
18
|
+
const total = stage.progressTotal ?? 0;
|
|
19
|
+
if (spent !== undefined && total > 0 && current > 0) {
|
|
20
|
+
if (current >= total)
|
|
21
|
+
return { basis: "progress", seconds: 0 };
|
|
22
|
+
return { basis: "progress", seconds: Math.round((spent / current) * (total - current)) };
|
|
23
|
+
}
|
|
24
|
+
const typical = stage.typicalSeconds;
|
|
25
|
+
if (typical === undefined || !Number.isFinite(typical) || typical <= 0)
|
|
26
|
+
return { basis: "unknown" };
|
|
27
|
+
const left = typical - (spent ?? 0);
|
|
28
|
+
return left > 0 ? { basis: "history", seconds: Math.round(left) } : { basis: "overdue" };
|
|
29
|
+
}
|
|
30
|
+
// "about 4 min left", "under a minute left", "about 1 h 10 min left", "finishing up",
|
|
31
|
+
// "taking longer than usual", "time left unknown".
|
|
32
|
+
export function etaLabel(eta) {
|
|
33
|
+
if (eta.basis === "unknown")
|
|
34
|
+
return "time left unknown";
|
|
35
|
+
if (eta.basis === "overdue")
|
|
36
|
+
return "taking longer than usual";
|
|
37
|
+
const seconds = eta.seconds ?? 0;
|
|
38
|
+
if (seconds <= 0)
|
|
39
|
+
return "finishing up";
|
|
40
|
+
if (seconds < 60)
|
|
41
|
+
return "under a minute left";
|
|
42
|
+
const minutes = Math.round(seconds / 60);
|
|
43
|
+
if (minutes < 60)
|
|
44
|
+
return `about ${String(minutes)} min left`;
|
|
45
|
+
const hours = Math.floor(minutes / 60);
|
|
46
|
+
const rest = minutes % 60;
|
|
47
|
+
return rest === 0
|
|
48
|
+
? `about ${String(hours)} h left`
|
|
49
|
+
: `about ${String(hours)} h ${String(rest)} min left`;
|
|
50
|
+
}
|
|
51
|
+
export function modelKeyOf(kind, config) {
|
|
52
|
+
const choice = kind === "research" || kind === "article"
|
|
53
|
+
? config.llm
|
|
54
|
+
: kind === "audio"
|
|
55
|
+
? config.audio
|
|
56
|
+
: kind === "images" || kind === "thumbnail"
|
|
57
|
+
? config.images
|
|
58
|
+
: undefined;
|
|
59
|
+
// A hand-edited config may hold anything here.
|
|
60
|
+
if (typeof choice?.provider !== "string" || typeof choice.model !== "string")
|
|
61
|
+
return undefined;
|
|
62
|
+
return `${choice.provider}/${choice.model}`;
|
|
63
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { readStageHistory } from "./history.js";
|
|
2
|
+
import { stageEta } from "./model.js";
|
|
3
|
+
// The project's stage rows with each running step's time left. The history is read only when
|
|
4
|
+
// something is running, so a finished project's page costs no extra query.
|
|
5
|
+
export function stagesWithEta(db, stages, config, now) {
|
|
6
|
+
if (!stages.some((stage) => stage.state === "running"))
|
|
7
|
+
return [...stages];
|
|
8
|
+
const history = readStageHistory(db);
|
|
9
|
+
return stages.map((stage) => {
|
|
10
|
+
if (stage.state !== "running")
|
|
11
|
+
return stage;
|
|
12
|
+
const typical = history.typicalSeconds(stage.kind, config, stage.progressTotal);
|
|
13
|
+
const eta = stageEta({ ...stage, ...(typical === undefined ? {} : { typicalSeconds: typical }) }, now.valueOf());
|
|
14
|
+
return {
|
|
15
|
+
...stage,
|
|
16
|
+
...(typical === undefined ? {} : { typicalSeconds: Math.round(typical) }),
|
|
17
|
+
...(eta === undefined
|
|
18
|
+
? {}
|
|
19
|
+
: {
|
|
20
|
+
etaBasis: eta.basis,
|
|
21
|
+
...(eta.seconds === undefined ? {} : { etaSeconds: eta.seconds }),
|
|
22
|
+
}),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -26,7 +26,7 @@ export function fixFor(step) {
|
|
|
26
26
|
if (step.kind === "refusal" || refusedWords.test(reason))
|
|
27
27
|
return drawn.includes(step.stage)
|
|
28
28
|
? { kind: "refused", label: "Soften and retry", soften: true }
|
|
29
|
-
: { kind: "refused", label: "Edit prompt", soften: false };
|
|
29
|
+
: { kind: "refused", label: "Edit the prompt", soften: false };
|
|
30
30
|
if (retiredModel.test(reason))
|
|
31
31
|
return { kind: "switch-model", label: "Switch model" };
|
|
32
32
|
if (step.kind === "auth" || step.kind === "missing_key" || keyTrouble.test(reason)) {
|
|
@@ -45,7 +45,7 @@ export async function captionFont(deps, fontId, language) {
|
|
|
45
45
|
for (const font of await deps.system())
|
|
46
46
|
if (await covers(font, letters))
|
|
47
47
|
return font;
|
|
48
|
-
throw new Error(`The caption font ${chosen.name} has no ${info.name} letters, and Slopify doesn't ship a font for ${info.name} (they are 10-20 MB). Upload a font that has them
|
|
48
|
+
throw new Error(`The caption font ${chosen.name} has no ${info.name} letters, and Slopify doesn't ship a font for ${info.name} (they are 10-20 MB). In Edit project → Subtitles, use Upload font in the caption font picker to add a font that has them (for example Noto Sans ${info.code === "ja" ? "JP" : info.code === "ko" ? "KR" : "SC"} from fonts.google.com), choose it there, then Try again.`);
|
|
49
49
|
}
|
|
50
50
|
// The same for text drawn bold (the shorts' captions): the chosen font's bold face when it has
|
|
51
51
|
// the letters, else the fallback, which the renderer emboldens.
|
|
@@ -87,7 +87,7 @@ export function loudnessProblem(lufs, target) {
|
|
|
87
87
|
db >= loudnessDbMin - 1e-9 &&
|
|
88
88
|
db <= loudnessDbMax + 1e-9
|
|
89
89
|
? undefined
|
|
90
|
-
: `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (Play →
|
|
90
|
+
: `The volume must be between ${String(loudnessDbMin)} dB and +${String(loudnessDbMax)} dB of the recommended level, in steps of ${String(loudnessDbStep)} dB. Change it under Level the volume (Play → Video and style, Edit project → Pauses and volume, or Settings → General).`;
|
|
91
91
|
}
|
|
92
92
|
export function loudnessFields(settings) {
|
|
93
93
|
if (settings === undefined)
|
|
@@ -5,7 +5,7 @@ import { draftRows } from "../play-drafts/repo.js";
|
|
|
5
5
|
import { documentOf } from "../play-drafts/service.js";
|
|
6
6
|
import { templateById } from "../project-templates/repo.js";
|
|
7
7
|
import { currentRevisionId, revisionById } from "../revisions/repo.js";
|
|
8
|
-
import {
|
|
8
|
+
import { isUncataloguedProvider } from "../settings/model.js";
|
|
9
9
|
import { usageSlots } from "./model.js";
|
|
10
10
|
export { slotLabels, usageKinds, usageSlots } from "./model.js";
|
|
11
11
|
function familyOf(slot) {
|
|
@@ -38,7 +38,9 @@ export function configChoices(config) {
|
|
|
38
38
|
// Whether a choice names a catalogued model that is no longer offered. Command-line tools
|
|
39
39
|
// list their own models and are checked by the health check instead.
|
|
40
40
|
export function retiredStatus(catalogue, slot, choice) {
|
|
41
|
-
if (choice.provider === "" ||
|
|
41
|
+
if (choice.provider === "" ||
|
|
42
|
+
choice.model.trim() === "" ||
|
|
43
|
+
isUncataloguedProvider(choice.provider))
|
|
42
44
|
return null;
|
|
43
45
|
const row = catalogue[familyOf(slot)].find((model) => model.provider === choice.provider &&
|
|
44
46
|
model.id === choice.model &&
|
|
@@ -33,7 +33,7 @@ export function pauseProblem(value, name) {
|
|
|
33
33
|
value <= pauseSecondsMax &&
|
|
34
34
|
Math.abs(steps - Math.round(steps)) < 1e-6
|
|
35
35
|
? undefined
|
|
36
|
-
: `The pause between ${name} must be between 0 and ${String(pauseSecondsMax)} seconds, in steps of ${String(pauseSecondsStep)}. Change it under Play →
|
|
36
|
+
: `The pause between ${name} must be between 0 and ${String(pauseSecondsMax)} seconds, in steps of ${String(pauseSecondsStep)}. Change it under Play → Video and style, or Edit project → Pauses and volume.`;
|
|
37
37
|
}
|
|
38
38
|
export function pauseFields(config) {
|
|
39
39
|
const fields = [];
|
|
@@ -25,7 +25,7 @@ function rowsOf(node) {
|
|
|
25
25
|
if (cells.length < 2)
|
|
26
26
|
return textOf(row);
|
|
27
27
|
const [term, ipa] = cells.slice(0, 2).map((cell) => textOf(cell).trim());
|
|
28
|
-
// A table's IPA column is often written without the slashes ("|
|
|
28
|
+
// A table's IPA column is often written without the slashes ("| Cleopatra | kliːəˈpætrə |");
|
|
29
29
|
// the column already says what the cell is, so a bare cell reads as one /…/ pronunciation.
|
|
30
30
|
return `${term}: ${ipa !== undefined && ipa !== "" && !ipa.includes("/") ? `/${ipa}/` : ipa}`;
|
|
31
31
|
});
|
|
@@ -125,7 +125,7 @@ export function skippedSpeakerPronunciationsNotice(rows) {
|
|
|
125
125
|
return null;
|
|
126
126
|
const parts = rows.map((row) => `${row.speaker}: ${row.skipped.map((one) => `entry ${one.row}: ${one.reason}`).join("; ")}`);
|
|
127
127
|
return (`Speaker pronunciations: some entries are skipped and read as ordinary text (${parts.join(". ")}). ` +
|
|
128
|
-
"The rest are used. To use them, fix those entries under Speakers → Pronunciations for that speaker (Play →
|
|
128
|
+
"The rest are used. To use them, fix those entries under Speakers → Pronunciations for that speaker (Play → Narration, or Edit project → Providers).");
|
|
129
129
|
}
|
|
130
130
|
export function pronunciationMatches(source, entries) {
|
|
131
131
|
if (entries.length === 0)
|