Build VidNotes

KINDAreplaces $9.99/mosaves $119.88/yrback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort weekendthe files for this build are in the project pack

RuntimeOne Python process serving localhost with a background worker, SQLite with full-text searchAudioffmpeg to mono at speech sample rate in a compressed codec, split at silenceTranscriptionA hosted model with published limits, chunked under them, key in the environmentJobsA durable queue with per-chunk checkpoints and a recorded cost per callNotesGenerated per section, each claim verified against the transcript before it is shownStorageMedia in a per-project directory beside the database, with a disk ceiling and a real delete

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
APP_PORTrequired8787Bound to localhost only. This is a personal tool.
DATABASE_PATHrequired./data/vidnotes.dbProjects, segments, notes and the job state.
MEDIA_DIRrequired./data/projectsOne directory per project. Deleted in full when a project is deleted.
MEDIA_MAX_BYTESrequired53687091200The disk ceiling. Imports past it are refused with usage shown.
TRANSCRIBE_API_KEYsecretrequiredsk-...redactedEnvironment only. Never in the database, never in a log line.
CHUNK_MAX_BYTESrequired24000000Set below the provider's published limit, which you check rather than assume.
MONTHLY_SPEND_CAP_CENTSrequired2000New transcription is refused past this rather than billed through it.
ENABLE_URL_IMPORTrequiredfalseOff by default. Turning it on is a statement about the source's terms.

The build, in order

  1. Audio the API will actually accept

    Any recording becomes chunks under the published size limit with offsets that still line up.

    1. Stereo at music sample rates doubles the bytes and adds nothing a transcriber uses.

      Files src/audio.py

      terminal
      ffmpeg -i input.mp4 -ac 1 -ar 16000 -c:a libopus prepared.opus
    2. Detect quiet spans near each boundary and cut there.

      Files src/chunking.pytests/test_offsets.py

    3. Every returned timestamp is relative to its chunk until you fix it.

      Files src/chunking.py

    done when · tick each as it passes
    watch 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.
  2. A long job that survives being interrupted

    A failure at chunk forty resumes at chunk forty, and you can see what it cost.

    1. Write the state before the call and the result after it.

      Files src/worker.pytests/test_resume.py

    2. A three-hour lecture is an unpleasant surprise unless the number is on screen.

      Files src/worker.pysrc/templates/project.html

    3. A timeout after the audio was accepted may already have been charged.

      Files src/transcribe.py

    done when · tick each as it passes
    watch 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.
  3. Search that lands on the moment

    A query returns spans with times, and clicking one moves the player there.

    1. The unit of search should be the unit you can seek to.

      Files src/search.pytests/test_search.py

    2. Display grouping must never be allowed to lose it.

      Files src/templates/transcript.html

    3. They are what people actually search a lecture for.

      Files tests/test_search.py

    done when · tick each as it passes
    watch 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.
  4. Notes that can be checked against the recording

    Every generated claim points at a span that really says it, or it is not shown.

    1. A claim with nowhere to point is a claim you cannot check.

      Files src/notes.py

    2. Normalise whitespace and case, nothing more forgiving than that.

      Files src/verify.pytests/test_verify.py

    3. An honest blank is more useful than a plausible sentence.

      Files src/templates/notes.html

    done when · tick each as it passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more audio & video.