Written 2026-07-12, end of the session that scoped this project, for a new chat thread to pick up cleanly. Nothing has been built yet; this document is the full requirements/architecture record from the design conversation. No code exists in this repo yet.
Revised 2026-07-12 (spec review pass): storage model changed from record-direct-to-SMB to local working storage with NAS archive; output/recording format decisions added (resolution, audio codec path, target browser); PPTX speaker-notes import added; take management and caption chunking rules added; Mac prerequisites listed.
A self-hosted tool for recording narrated lesson videos, similar in spirit to Descript's core recording workflow but self-hosted and purpose-built for Dave's use: feed it slides (a PowerPoint deck or image files) and a script (what to read per slide), and it gives a teleprompter to read from while recording voice-only narration, then produces a finished MP4 by pairing each slide with its narration.
- Create a project: one per video being produced.
- Import slides: upload a
.pptxdeck (auto-split into one image per slide) or upload image files directly. - Import the script: paste or upload text, mapped one block per slide/scene. When a
.pptxis uploaded, offer to import the script from the deck's speaker notes (one note per slide maps naturally to one script block per scene); Dave can then edit from there instead of pasting from scratch. - Edit before recording: reorder slides, replace a slide's image, delete a slide, add a new slide, and edit/add/delete/reorder script blocks to match, all before any recording happens. This is a full pre-production editing pass, not just an import step.
- Record scenes: teleprompter shows the current slide full-screen with the script auto-scrolling at a lesson-appropriate pace (default ~145 words/minute, adjustable). Record scenes back-to-back in one sitting, or jump to any single scene independently.
- Produce the video: slides plus narration combined into a single MP4.
- Replace a scene: re-record just one scene's audio (or swap its slide) and re-produce the video without touching anything else, after recording has already happened.
- Project management: each project keeps its slides, script, per-scene audio, and produced video together, browsable/re-openable later.
- Slide ingestion:
.pptxauto-converted to one image per slide; also accepts image files directly. - Pre-production editing: reorder, replace, delete and add slides; same for script blocks; keep slides and script blocks in sync as the deck changes.
- Script handling: one script block per scene, editable after import.
- Teleprompter: adjustable scroll speed with a lesson-pace default (~145 wpm), pause/resume, manual speed nudge during recording.
- Recording: microphone-only, no webcam. This pairs a static slide with voice, not a talking-head overlay. Record in sequence or one scene at a time.
- Take management: re-recording a scene must never destroy the previous take before the new one is safely written. Keep prior takes per scene (with a simple "active take" selector); storage is cheap, re-recording a good take by accident is expensive.
- Production: mux each slide+audio pair into a segment, concatenate into the final video. Output MP4 (H.264/AAC), universally compatible with Moodle and other LMS platforms.
- Output format: 1920x1080 (16:9) default. All slide images are normalised to the output resolution at production time (scale-to-fit with letterbox/pillarbox padding for odd aspect ratios), so directly-uploaded images of any size are safe and the concat demuxer always sees uniform streams.
- Re-production: replacing one scene's audio/slide and re-running production must be fast and must not require re-recording anything untouched.
- Captions: generate an SRT file per produced video, script-timed. Captions are built from the known script text, evenly spread across each scene's actual recorded length (decided over speech-accurate transcription, for v1 simplicity; can upgrade later if needed). Chunk the script into caption cues at sentence/clause boundaries, max ~2 lines of ~42 characters each (standard subtitle convention), rather than one giant cue per scene.
- UI: best-in-class, intuitive. This is an explicit design bar, not just a functional checkbox; budget real design effort, including the branding below.
- Branding: needs a cool, modern logo/visual identity. Not yet designed; do this as part of the design phase, ideally previewed with the visualize tool before committing engineering time to it.
- Runs in a browser, driven from Dave's MacBook Pro. Target browser: Chrome (or another Chromium browser). This pins MediaRecorder behaviour to one well-understood implementation (WebM/Opus output) instead of chasing Safari's quirks; the app should detect and warn on unsupported browsers.
- Storage: source files and finished videos persist per-project, held centrally so they survive a Mac restart. See the revised storage model in the architecture section: local working directory, NAS as archive target, not record-direct-to-SMB.
Local-first on the Mac, not a NAS-hosted service. Dave flagged that his MacBook Pro likely outguns the NAS. Two concrete technical reasons this makes local-first clearly the right call, not just a preference:
- The browser's microphone API (
MediaRecorder/getUserMedia) only works on secure origins: HTTPS orlocalhost. Running the app locally gets a secure origin for free, with zero certificate setup. Hosting it on the NAS over the LAN would have forced a TLS cert (self-signed or Let's Encrypt) onto every recording device. - Apple Silicon has a hardware H.264 encoder ffmpeg can use directly
(
-c:v h264_videotoolbox), fast and efficient, versus software-encoding on a NAS that's already running Gitea, Keycloak, Moodle and a media stack (see[[project-strayanas]]/[[project-compass]]memory for what else lives on that box).
Resulting shape:
- Local server + browser UI, launched from the Mac with one command, opened at
http://localhost:PORT. - Tech stack (Dave delegated this choice): Node.js backend + React frontend, matching
patterns already proven in the Compass codebase (
~/Documents/Claude/Projects/compass), so conventions and any reusable pieces (e.g. file upload handling, project-based data model) can be borrowed rather than reinvented. - PPTX to slide images: LibreOffice headless (
soffice --headless --convert-to pdfthenpdftoppm), the same pipeline already used elsewhere in these projects (see thedocx/pptxClaude skills, and the DHE Moodle build pipeline's slide generation). - Video production: ffmpeg. Per scene: loop the static slide image for the duration of its
audio track, encode with
h264_videotoolbox, AAC audio. Concatenate all scene MP4s into the final video via ffmpeg's concat demuxer. Note two implementation details decided in review: MediaRecorder in Chrome records WebM/Opus, so each scene's audio is transcoded to AAC during the per-scene mux (no extra pipeline step needed); andh264_videotoolboxignores CRF-style quality flags, so use an explicit bitrate (e.g.-b:v 4000kis ample for static slides). - Storage (revised in spec review): the app works against a local working directory
(e.g.
~/LessonStudio/projects/), so recording, editing and production all hit local disk. Recording live audio and running ffmpeg directly against an SMB mount was rejected: network latency during MediaRecorder flushes, SMB locking oddities, and a hard dependency on the share being mounted make it fragile exactly when it hurts most (mid-recording). The NAS remains the system of record via an explicit archive/sync step: a per-project "Archive to NAS" action (and an "everything not yet archived" indicator) copies the project to the NAS share. Restoring a project from the archive is the inverse. Exact NAS share/path still to be decided (open item below). - Code repository: this repo (
lesson-studio), intended to live on the NAS Gitea instance (http://192.168.50.200:3000), matching how Compass, dev-health-astro and Dave's Website are set up. Not yet pushed to Gitea as of this handover, see Open items below.
Dave asked that gstack (Garry Tan's open-source Claude Code skill suite, github.com/garrytan/gstack) be used to drive this project's design/build/QA/review cycle. Status:
- Installed to
~/.claude/skills/gstack/viagit cloneplus./setup --host claude --no-team --no-plan-tune-hooks(standard mode, no team mode, no global Claude Code hooks added to~/.claude/settings.json). bunwas installed via Homebrew as a prerequisite (brew install oven-sh/bun/bun, v1.3.14).- The gstack skill router is registered and its 23 skills' files are present
(
~/.claude/skills/gstack/); core planning/review/text-based skills should be usable. - The Playwright Chromium download (needed for gstack's browser-driven QA skill only) hung twice, both times showing the download reach 100% and then stalling indefinitely: elapsed wall-clock time far exceeding CPU time consumed (e.g. 44 minutes elapsed, 1.37 seconds CPU time), no active network connection, and an incomplete extraction (39 files / 428K instead of the ~2,000 files / ~350-400MB a real Chromium install produces). Killed both times; the second attempt was supervised with a 5-minute background timeout and still hung the same way. This looks like a genuine, reproducible hang in Playwright's post-download extraction/verification step on this machine, not a slow network.
- Per Dave's instruction, proceeding without Chromium for now. Only gstack's browser-QA
skill needs it; the design/planning/code-review/release skills do not. Chromium can be
retried later (maybe at a different time, or investigate whether a specific Playwright/Chrome
version pin, a manual
curldownload into the expected cache path, orPLAYWRIGHT_DOWNLOAD_HOSTpointed at a mirror resolves the hang) whenever the browser-QA skill is actually needed. - Not yet done: actually invoking gstack (its router skill, or whichever of its 23 skills fits a "scope a new project" step) to run this requirements doc through its own design process. Dave asked to pause before that happened, so it's the first real task for the next session.
- Project name: Lesson Studio.
- Recording is audio-only (no webcam/PIP).
- Tech stack: Node.js + React (Dave delegated this choice).
- Captions: script-timed SRT, not speech-accurate transcription, for v1.
- Architecture: local-first on the Mac; NAS is storage, not compute.
- gstack: use it for the design/build/QA/review cycle; proceed without its browser-QA component for now.
- Storage model (spec review): local working directory for all recording/editing/production; NAS is an explicit archive target, not the live working store.
- Output: 1920x1080 MP4 (H.264/AAC), slide images normalised with letterbox padding.
- Target browser: Chrome/Chromium; audio recorded as WebM/Opus and transcoded to AAC at mux.
- Take management: prior takes are kept per scene with an active-take selector.
ffmpeg(with videotoolbox support, standard in the Homebrew build)libreoffice(headless PPTX conversion)poppler(pdftoppmfor PDF to slide images)- Node.js LTS
- The scaffold should include a startup dependency check that reports anything missing rather than failing obscurely mid-pipeline.
Push this repo to NAS Gitea.Done: repo lives athttp://192.168.50.200:3000/dkempson/lesson-studio.git.- Invoke gstack properly: read its
ARCHITECTURE.md/ETHOS.md/SKILL.md(in~/.claude/skills/gstack/) and use its planning skill to turn this handover into a formal build plan, per Dave's explicit request to use gstack for this project. - Decide the NAS archive share/path for the project archive step (working storage is now local, per the revised storage model); needs an SMB share confirmed on the NAS and mounted on the Mac.
- Design the logo/brand identity: Dave asked for something "cool" and "modern"; worth
previewing concepts with the
visualizetool before committing to one. - Scaffold the Node.js + React app and get a minimal end-to-end path working: import one slide plus one script line, record it, produce a one-scene MP4. Prove the full pipeline before building out the editing/reordering UI on top of it.
- Then iterate: design, build, validate, review, using gstack's cycle, until the tool is finished, tested and high quality (Dave's stated bar, verbatim).