Videos
Short videos about MinusPod: a tour, the pipeline, and one per feature that rewards a closer look. Each comes in a portrait cut for TikTok and other 9:16 feeds and a landscape cut for YouTube, a README or a talk. Portrait is 1080x1920 and landscape is 1920x1080, both 30 fps H.264 with no audio track, so a caption or music can be added in the posting app. The landscape cuts also play inline in the docs that cover each feature; see Embedding in the docs.
| Video | Portrait | Landscape | Length | What it shows |
|---|---|---|---|---|
| UI tour | minuspod-demo.mp4 | minuspod-demo-landscape.mp4 | 108 s | Dashboard, the What’s new dialog after an upgrade, processing queue, detected ads, the waveform editor, review, patterns, search, stats, settings and the 32 themes, the Add feed processing mode select, then YouTube feeds: adding a show by name, SponsorBlock marks, the episode rating and the YouTube settings. Recorded from a running instance. |
| How it works | minuspod-how-it-works.mp4 | minuspod-how-it-works-landscape.mp4 | 107 s | One episode across all eleven pipeline stages: download, transcribe, second download and audio analysis, detect (free matches, then the model), validate, boundary review, cut, verify pass, transcript and chapters, publish. Then the learning loop and what happens when something fails. Follows How It Works and Episode Processing Workflows. |
| Review queue | minuspod-review-queue.mp4 | minuspod-review-queue-landscape.mp4 | 40 s | A held detection from the queue to the waveform editor: play the span, confirm it, watch the recut, see it on the record. Recorded from a running instance. |
| Audio cues | minuspod-audio-cues.mp4 | minuspod-audio-cues-landscape.mp4 | 44 s | Find a show’s ad-break chime on one episode, templates and their health on the feed, the cross-episode scan, cuts that snapped to a cue, and the settings behind it. Recorded. |
| Processing modes | minuspod-processing-modes.mp4 | minuspod-processing-modes-landscape.mp4 | 73 s | One episode under each of the five modes, then segment categories and the remove, beep, keep and mark actions. Animated, source in processing-modes/. |
| Local feeds | minuspod-local-feeds.mp4 | minuspod-local-feeds-landscape.mp4 | 63 s | Create a feed from your own files with cover art, upload an episode, run it through the pipeline, and find it next to your subscriptions. Recorded. |
| Providers and cost | minuspod-providers.mp4 | minuspod-providers-landscape.mp4 | 48 s | Two provider slots, a model per stage, failover, and the spend ledger from provider down to a single episode. Recorded. |
| Podcasting 2.0 | minuspod-podcasting-2.mp4 | minuspod-podcasting-2-landscape.mp4 | 37 s | Chapters and transcript for the cut audio, the served feed’s namespace tags, and the chapter settings. Recorded, plus one rendered feed excerpt. |
| Patterns | minuspod-patterns.mp4 | minuspod-patterns-landscape.mp4 | 70 s | Learned patterns: the list, one created from a confirmed detection, its detail and edit forms, merge suggestions, then Pattern Cleanup from Run now to approve and undo. Recorded. |
| YouTube feeds | minuspod-youtube.mp4 | minuspod-youtube-landscape.mp4 | 71 s | Search by show name and preview, the feed page and its YouTube panel, SponsorBlock marks on an episode, rating, submitting reviewed segments, the status bar during a SponsorBlock outage, and the settings with a generated user ID. Rating, submit and generate are answered by in-page mocks. |
Rebuilding the animated videos
Section titled “Rebuilding the animated videos”Each animation is a single HTML canvas page, how-it-works/index.html
or processing-modes/index.html, holding its
constants and scenes. The drawing helpers, palette, caption layout and the
frame driver they share live in lib/canvas.js. Every frame is a
pure function of time, so render.mjs steps window.render(t) at
30 fps, screenshots each frame with Playwright, and ffmpeg encodes the sequence.
The logo comes from frontend/public/logo-dark.svg and the colors mirror the
app’s dark theme tokens, so the video tracks the app’s own look.
cd docs/videos/how-it-works # or docs/videos/processing-modesnpm install # playwright, oncenpx playwright install chromium # once, if Chromium is not installed yetnpm run frames && npm run encode # portraitnpm run frames:landscape && npm run encode:landscape # landscaperm -rf framesThe scenes are drawn once, in portrait coordinates. The landscape layout keeps
the caption and the footer rail in a left column and scales the scene content
into the right side of the frame. npm run preview -- --landscape previews
that layout.
npm run preview -- 2,25.5,49 writes single frames at those times to
preview/ for checking a scene without a full render; with no times it
writes the midpoint of every scene. Rendering needs network
access for the Roboto web font. frames/ and preview/ are gitignored.
Scenes, captions and timings are the add(...) calls at the bottom of each
index.html: a duration, a headline, a subline, a draw function of local
time, and what to light up in the footer rail (pipeline stages in one video,
the processing mode in the other). The episode runtime and ad positions are
constants at the top, and every duration label is derived from them. A new
animation is a folder with an index.html that loads ../lib/canvas.js,
defines its scenes and ends with finish(...), plus a copy of
processing-modes/package.json with its output names.
Recording the screen-capture videos
Section titled “Recording the screen-capture videos”The UI tour and the feature videos are screen recordings of a running instance, driven by Playwright so every take is the same. The tooling lives in recordings/:
| File | Role |
|---|---|
shoot.mjs |
Opens the instance in Chromium on a virtual X display and records one clip per beat with ffmpeg. |
helpers.mjs |
Scrolling, typing, section expanders, a wait for the waveform to draw, and the safeguards below. |
devices.json |
Pixel size, scale factor and default X display of the phone, laptop and desktop shapes. |
screenshot.mjs |
Headless screenshot of a page after some actions, for planning a beat before recording it. |
beats/<video>.mjs |
The beats of one video: a device, a URL, off-camera setup and the on-camera actions. |
beats/<video>.json |
The cut: which clip, from when, for how long, with what caption, framed how. |
compose.py |
Frames each clip in a phone or laptop shape, adds the caption, joins the beats with crossfades. |
targets.example.json |
The instance URL, the feeds and episodes each video uses, and the strings that must never appear. |
feedcard.mjs |
Renders a served RSS feed as a code card clip, for the Podcasting 2.0 video. |
ocrcheck.py |
OCR over a finished video for the same strings, the check the DOM cannot do. |
Prerequisites: Node with Playwright and Chromium (npm install then
npx playwright install chromium in recordings/), Xvfb, ffmpeg, and
Python with Pillow, plus rapidocr_onnxruntime for the OCR check. Fonts come
from Liberation Sans.
-
Copy
targets.example.jsontotargets.jsonand fill it in. It is gitignored.leakPatternsare regular expressions for your hostname, your feed host and anything else that identifies the instance;excludeShowsnames podcasts that must stay out of frame. -
Start one virtual display per device shape. The sizes are the pixel sizes the recorder expects:
Terminal window Xvfb :97 -screen 0 1170x2532x24 -nolisten tcp & # phoneXvfb :96 -screen 0 2000x1800x24 -nolisten tcp & # laptopXvfb :98 -screen 0 2880x1800x24 -nolisten tcp & # desktop -
Log in to the web UI and pass the session cookie, either as
MINUSPOD_SESSION=<value>or asMINUSPOD_COOKIES=/path/to/cookies.txt. -
Record the beats of a video, phone beats and laptop beats in separate runs:
Terminal window cd docs/videos/recordingsnode shoot.mjs beats/review.mjs phone queuenode shoot.mjs beats/review.mjs laptop held modal confirm afterClips land in
clips/<video>/<beat>.mp4. Pull a frame withffmpeg -ss 3 -i clips/review/held.mp4 -frames:v 1 check.pngand look at it before moving on. A clip is cheap to redo. -
Compose both cuts:
Terminal window python3 compose.py beats/review.json portraitpython3 compose.py beats/review.json landscapeThe result is
out/<output>.mp4andout/<output>-landscape.mp4, named by theoutputfield of the beats file. Pass beat indexes after the mode to rebuild only those segments while tuning a crop or a caption.
A beat may export a mock(page, T) hook that registers page.route handlers before navigation. The YouTube beats use it to answer rating, submit and generate in the page, so the instance is untouched.
The UI tour beats live in beats/demo.mjs and are read-only: they navigate, scroll and open dialogs, and never save, confirm, rate, submit or generate anything.
Some beats change the instance: the review and patterns videos each confirm
a held marker and recut its episode, the patterns video sets one pattern’s
category and starts a Pattern Cleanup run, the local feed video creates a
feed and uploads an episode, and processing an uploaded episode spends one
detection call. Take a
configuration export and a database backup first, and point targets.json at
an episode you are happy to see change.
Three safeguards keep instance details out of the video. Before a take, and
every 400 ms while it runs, hideSensitive walks the rendered text, image alt
text and input values, a dropdown’s selected value included. It blanks the row or field
around anything that matches a pattern from targets.json, and around any URL
in an input. leakCheck runs the same walk and refuses the take if a match is
still visible, so a dialog or toast opened mid-beat is covered too. After
composing, ocrcheck.py out/*.mp4 reads every second of the finished video,
because the frontend can draw text the DOM check does not see, such as chart
labels on a canvas.
To add a video, write beats/<name>.mjs with a BEATS object (dev, url
or a function of the targets, optional prep, run), a matching
beats/<name>.json with the output name and the beat list, and record. A
laptop crop is given in source pixels ([x, y, w, h] on the 2000x1800
capture); the landscape cut reshapes it around the same center.
The feed excerpt in the Podcasting 2.0 video is not a recording. Fetch the
served RSS of the demo feed and run node feedcard.mjs feed.xml: every URL
loses its query string, every host except the namespace hosts becomes
example.com, and the result is an 8 s laptop-sized clip at
clips/p20/feedxml.mp4 that the composer frames like any other.
Embedding in the docs
Section titled “Embedding in the docs”GitHub shows a video player only for files uploaded through its own editor, as
github.com/user-attachments/assets/... URLs. A video committed in the repo,
linked or wrapped in a <video> tag, renders as a plain link. So each
landscape cut was dropped into a comment on the pull request that added it,
and the doc that covers the feature carries that URL on a line of its own:
| Video | Embedded in |
|---|---|
| UI tour | Project README, Web Interface |
| How it works | How It Works |
| Review queue | Web Interface, Held for Review |
| Audio cues | Audio Cue Detection |
| Processing modes | Episode Processing Workflows, Processing modes |
| Local feeds | Local Feeds |
| Providers and cost | LLM Providers |
| Podcasting 2.0 | Podcasting 2.0 |
| Patterns | Pattern Cleanup |
| YouTube feeds | YouTube Feeds |
To replace a video, re-record or re-render it and commit the new files here. Then drag the new landscape file into a comment on a pull request or issue in this repository, copy the asset URL GitHub puts in the comment, and swap it in on the doc above. The upload cap is 10 MB per file on a free plan; every landscape cut here is under it. Keep the comment, so there is a record of where the asset came from.