Build VidNotes
KINDAreplaces $9.99/mosaves $119.88/yrback to the verdict
A local workspace turning long recordings into timestamped transcripts and study material, whose hard problems are cutting audio into pieces a transcription API will accept without losing words at the seams, surviving a two-hour job that fails at minute ninety, and never showing a generated claim that cannot be found in the recording.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the server and the worker.
Get it python.org or your package manager. open ↗
Verify
python3 --version prints 3.12 or higher - installfree
Why Extracts and compresses the audio, and a build without the encoder you chose fails only at the first real import.
Get it Your package manager, or a static build from ffmpeg.org. open ↗
Verify
ffmpeg -encoders lists the encoder you intend to use - API keyper minute of audio, a few pence per hour of recording
Why Transcription is the one part not worth writing, and the key is also the thing that can quietly cost money.
Get it Create a key with its own spending limit rather than reusing an unlimited one.
Verify
A one-minute sample returns timestamped text and the call appears on the usage page - have readyfree
Why Chunk offsets and seek accuracy cannot be tested against a recording nobody has listened to.
Get it Record two minutes saying a distinct word at known times, or annotate a lecture you own.
Verify
The known words appear in the transcript at the times you spoke them - decidefree
Why Each is enforced by code and asserted by a test, and deciding them later means accepting whatever the first month produced.
Get it Write down the media directory limit, the monthly spend cap, and whether the downloader is enabled and on what basis.
Verify
All three appear in the configuration and in the README
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- projects(id, title, source_kind, source_ref, duration_ms, created_at, deleted_at) · source_kind is file or url and source_ref is kept so you know where it came from - media(project_id, path, sha256, bytes, codec, sample_rate, channels, prepared_path) - chunks(id, project_id, ordinal, start_ms, end_ms, path, bytes, state, attempts, cost_cents, transcribed_at) · offsets are absolute in the original recording - segments(id, project_id, chunk_id, start_ms, end_ms, text) · one row per timed span, and the only thing search ever reads - notes(id, project_id, kind, section_start_ms, section_end_ms, content, quote, quote_segment_id, generated_at) · kind is summary, key_point, flashcard or quiz - jobs(id, project_id, stage, state, attempts, last_error, updated_at) · stage is extracting, chunking, transcribing or generating - A note without a resolvable quote_segment_id is never displayed, which makes the join the honesty check rather than a nicety.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
APP_PORT | required | 8787 | Bound to localhost only. This is a personal tool. |
DATABASE_PATH | required | ./data/vidnotes.db | Projects, segments, notes and the job state. |
MEDIA_DIR | required | ./data/projects | One directory per project. Deleted in full when a project is deleted. |
MEDIA_MAX_BYTES | required | 53687091200 | The disk ceiling. Imports past it are refused with usage shown. |
TRANSCRIBE_API_KEYsecret | required | sk-...redacted | Environment only. Never in the database, never in a log line. |
CHUNK_MAX_BYTES | required | 24000000 | Set below the provider's published limit, which you check rather than assume. |
MONTHLY_SPEND_CAP_CENTS | required | 2000 | New transcription is refused past this rather than billed through it. |
ENABLE_URL_IMPORT | required | false | Off by default. Turning it on is a statement about the source's terms. |
The build, in order
Audio the API will actually accept
Any recording becomes chunks under the published size limit with offsets that still line up.
Stereo at music sample rates doubles the bytes and adds nothing a transcriber uses.
Files
src/audio.pyterminalffmpeg -i input.mp4 -ac 1 -ar 16000 -c:a libopus prepared.opus
Detect quiet spans near each boundary and cut there.
Files
src/chunking.pytests/test_offsets.pyEvery returned timestamp is relative to its chunk until you fix it.
Files
src/chunking.py
done when · tick each as it passeswatch out- Cutting the file at a fixed byte offset drops a word at every seam and, worse, each chunk's returned timestamps start at zero, so unless you add the chunk's own start offset back every timestamp after the first chunk is wrong by a growing amount and every link in the app lands in the wrong place. Cut at silence, store absolute offsets, and assert against a recording whose true times you know.
A long job that survives being interrupted
A failure at chunk forty resumes at chunk forty, and you can see what it cost.
Write the state before the call and the result after it.
Files
src/worker.pytests/test_resume.pyA three-hour lecture is an unpleasant surprise unless the number is on screen.
Files
src/worker.pysrc/templates/project.htmlA timeout after the audio was accepted may already have been charged.
Files
src/transcribe.py
done when · tick each as it passeswatch out- Treating transcription as one long call means a network blip, a rate limit or a laptop lid closing at minute ninety throws away every chunk already paid for, and the user retries the whole two hours. Checkpoint per chunk, persist before and after each call, and make resume the default path rather than an option.
Search that lands on the moment
A query returns spans with times, and clicking one moves the player there.
The unit of search should be the unit you can seek to.
Files
src/search.pytests/test_search.pyDisplay grouping must never be allowed to lose it.
Files
src/templates/transcript.htmlThey are what people actually search a lecture for.
Files
tests/test_search.py
done when · tick each as it passeswatch out- Storing the transcript as one text blob and searching it gives you a hit that says the phrase is somewhere in a ninety-minute recording, which is the same as no answer; and rebuilding paragraphs for display without carrying the segment times means the clickable timestamp drifts from the words shown. Index per segment, and keep the time on every displayed line.
Notes that can be checked against the recording
Every generated claim points at a span that really says it, or it is not shown.
A claim with nowhere to point is a claim you cannot check.
Files
src/notes.pyNormalise whitespace and case, nothing more forgiving than that.
Files
src/verify.pytests/test_verify.pyAn honest blank is more useful than a plausible sentence.
Files
src/templates/notes.html
done when · tick each as it passeswatch out- Passing a ninety-minute transcript in one request and asking for ten flashcards produces confident cards about things nobody said, because the model is compressing far too much and has no obligation to point at anything. Generate per section, demand a quote, verify the quote exists, and show a section as unsummarised rather than inventing a summary of it.
Getting it out, and keeping the disk and the bill in handproduct builder
Everything exports, deletion really deletes, and neither disk nor spend can run away unwatched.
Subtitles and JSON are what other tools can read.
Files
src/export.pytests/test_export.pyThen search for the unique phrase and expect nothing.
Files
src/delete.pytests/test_delete.pyA refusal you can see beats a bill you cannot.
Files
src/limits.py
done when · tick each as it passeswatch out- Deleting a project by marking a row deleted leaves gigabytes of video and every transcript line in the search index, so the disk fills and a search still surfaces text from a recording the person believes they removed. Delete the files, the rows and the index entries in one operation, then assert the search returns nothing for a phrase that was unique to that project.
That is the whole plan for VidNotes. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- Native mobile apps and a browser extension.
- Extraction from sites whose terms forbid it.
- Accounts and cross-device sync.
- A public API, CLI or agent server.
- native iOS and Android apps, a Chrome extension, and cross-device Premium state
- managed extraction for TikTok, Instagram, Vimeo, and changing source sites
- background processing, queue reliability, and long-video handling
- 30+ language polish and prebuilt flashcards, quizzes, and chat
- hosted web, API, CLI, and MCP access for agents
- A local transcription model as a second backend, chosen per project, once the chunking is proven
- Speaker labelling, which changes the segment table and should not be retrofitted casually
- A revision mode that schedules flashcards over time, which needs nothing new from the pipeline
Need the files? The project pack on the verdict page hands your agent the whole brief · more audio & video.