Build Grain
KINDAback to the verdict
A local meeting archive whose hard problems are refusing to process a recording nobody consented to, transcribing long audio in chunks without losing or duplicating words at the seams, and producing notes where every claim cites a timestamp you can play.
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 pipeline, the job runner and the local interface.
Get it python.org, or your package manager. open ↗
Verify
python3 --version prints 3.12 or higher - installfree
Why Probing, chunking and clipping all go through it, and a build without ffprobe fails at the first import rather than at the first clip.
Get it Your package manager, or a static build from the official downloads page. open ↗
Verify
ffprobe -version and ffmpeg -version both print a version - installfree
Why Transcription runs locally, and the model file is a separate download that is easy to forget until the first job fails.
Get it Build whisper.cpp and download a model file of a size your machine can actually run. open ↗
Verify
The binary transcribes the bundled sample and prints timestamps - have readyfree
Why Chunking, resume and timestamp offsets are all fine on a five minute file and all break on a long one, which is the only length that matters.
Get it A recording you made of a meeting where everyone was told, or a long public talk you are permitted to use.
Verify
It transcribes end to end and three random citations play the right words - decidefree
Why Both are enforced by code and asserted by tests, and writing them after the fact means writing whatever the code already does.
Get it Write down who must be informed, how, and how long recordings and transcripts are kept.
Verify
Both appear in the README and their values are the ones the tests use
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- meetings(id, title, occurred_at, source_path, media_sha256, duration_seconds, imported_at, delete_after) - consent(meeting_id, parties_json, informed_how, informed_at, recorded_by, note) · written before transcription is allowed to start - segments(meeting_id, ordinal, start_ms, end_ms, speaker_label, text, confidence) · timestamps are absolute in the meeting, never per chunk - words(segment_id, start_ms, end_ms, text) · what makes a clip cut land between words instead of through one - notes(id, meeting_id, kind, text, source_segment_id, quote, created_at) · kind is decision, action, question or summary, and every row cites a segment - clips(id, meeting_id, start_ms, end_ms, path, created_at) - jobs(id, meeting_id, kind, state, chunk_index, attempts, last_error) · transcription is resumable, because a two hour file fails at minute ninety
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 |
|---|---|---|---|
DATABASE_PATH | required | ./data/meetings.db | Meetings, consent, transcripts and notes. |
MEDIA_DIR | required | ./data/media | Source recordings, chunks and clips. Deletion must clear all three. |
WHISPER_BINARY | required | /usr/local/bin/whisper-cli | The local transcription binary. |
WHISPER_MODEL_PATH | required | ./models/ggml-base.en.bin | The model file. Bigger is slower and usually better; measure on your own machine. |
CHUNK_SECONDS | required | 600 | Chunk length for long audio. Timestamps are offset by the chunk start. |
CHUNK_OVERLAP_SECONDS | required | 5 | Overlap between chunks, de-duplicated on merge. Zero here means words lost at every seam. |
RETENTION_DAYS | required | 365 | Set at import. The scheduled job reports what it removed. |
NOTES_MODEL | optional | none | Optional. Unset means the extractive fallback, which must stay usable. |
The build, in order
Intake that will not start without consent
A recording cannot be transcribed until who was told is recorded.
Duration, streams and codecs decide whether the rest of the pipeline can work at all.
Files
src/intake.pysrc/media/probe.pyIn the queueing code, so there is one place it can be bypassed and it is visible.
Files
src/jobs.pytests/test_consent_gate.pyDecided once, when nobody is attached to the recording yet.
Files
src/retention.py
done when · tick each as it passeswatch out- Making consent a checkbox that is already ticked turns the whole record into a lie that looks like compliance, and a consent row written after transcription proves nothing at all. Require the parties and the method to be typed, block the transcription job until the row exists, and store the time it was written rather than the time the meeting happened.
Long audio without losing the seams
A two hour recording transcribes in chunks with one continuous clock.
A few seconds of overlap is the cheapest fix for words cut in half.
Files
src/transcribe/chunk.pyThis one line is the difference between citations that work and citations that lie.
Files
src/transcribe/merge.pytests/test_timestamps.pyLong jobs fail, and restarting a ninety minute transcription is how people give up.
Files
src/jobs.py
done when · tick each as it passeswatch out- Cutting a file into fixed ten minute chunks and concatenating the results slices words in half at every boundary and leaves each chunk's timestamps starting again at zero, so the transcript reads fine and every citation in it points at the wrong moment. Overlap the chunks, offset the timestamps by the chunk start, and de-duplicate across the overlap by matching text.
Speakers labelled as honestly as you can label them
Attribution is either evidenced or clearly provisional.
A letter is honest; a guessed name is a claim.
Files
src/speakers.pyOtherwise people rename the first three segments and give up.
Files
src/speakers.pytests/test_rename.pySo the note can show that the attribution is weak.
Files
src/notes/render.py
done when · tick each as it passeswatch out- Guessing that the voice saying 'I will do it' belongs to the person whose name was said earlier produces an action item assigned to the wrong colleague, which is discovered in the following meeting. Keep labels anonymous until a person renames them, and carry the confidence into the notes so an attribution never looks more certain than it is.
Notes that cite the second they came from
Every decision, action and question quotes the transcript and links to a timestamp.
The check is a substring match, and it catches almost everything invented.
Files
src/notes/verify.pytests/test_quote_verification.pyA caveat next to a wrong action item does not stop anyone acting on it.
Files
src/notes/build.pyThen the model is an improvement rather than a dependency.
Files
src/notes/extractive.py
done when · tick each as it passeswatch out- A model asked to summarise a meeting will produce a clean, plausible action item that nobody raised, and it will be the one item somebody acts on. Require a quote and a segment id on every row, check the quote actually appears in that segment before saving, and delete the rows that fail rather than keeping them with a caveat.
Clips, search, and deletion that actually deletesproduct builder
Getting a moment out is easy and getting a meeting gone is complete.
Word timestamps exist precisely so a clip does not start mid syllable.
Files
src/clips.pyMedia, chunks, transcript, notes, clips, in one function with one test.
Files
src/delete.pytests/test_delete_complete.pyA retention policy nobody has seen run is a paragraph, not a policy.
Files
src/retention.py
done when · tick each as it passeswatch out- Deleting the meeting row and leaving the audio, the chunk files and the exported clips on disk means a person who asked for a recording to be removed still has it sitting in three folders, and nobody notices until an audit. Make deletion a single operation over every artefact, and assert in the suite that no file with that meeting's identifier survives it.
That is the whole plan for Grain. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A bot that joins calls.
- Live transcription.
- CRM and calendar integrations.
- Cross meeting coaching scores.
- live multi-speaker accuracy
- calendar and CRM integrations
- cross-call team analytics
- meeting-bot auto-join
- Topic grouping across meetings, once you trust the transcripts enough to aggregate them
- A shareable read only page per meeting, which is a rendering job plus a decision about who sees it
- Translation of the transcript, kept beside the original rather than replacing it
Need the files? The project pack on the verdict page hands your agent the whole brief · more meeting notes.