Build Fellow
KINDAback to the verdict
A local meeting notes pipeline whose hard problems are chunking long audio without losing words at the seams, extracting decisions and actions that are anchored to what was actually said, and being honest about speaker labels it cannot reliably produce.
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 and the local interface.
Get it python.org, or your package manager. open ↗
Verify
python3 --version prints 3.12 or higher - installfree
Why Every container and codec a colleague sends you arrives differently, and the conversion to the model's documented input format is the step that decides whether the transcript is right.
Get it Your package manager, or ffmpeg.org. open ↗
Verify
ffmpeg -version prints a version and converts a sample to the target format - have readyfree, a download of a few hundred megabytes upward
Why The model file is the transcription quality, and its documented sample rate and channel count are what the conversion step has to hit exactly.
Get it Download a model for your chosen local runtime and read its current input requirements.
Verify
A one minute sample transcribes and the documented format is written into your configuration - have readyfree
Why Chunking and seam handling only fail on long files, and a three minute sample will pass every test while a ninety minute one finds the bug.
Get it A recording of your own, or a public domain long-form recording.
Verify
It is over an hour and contains speech across every chunk boundary - decidefree
Why Both are decisions about other people's voices, and deciding them after the first awkward request means deciding them badly.
Get it Write down what you confirm at import, how long audio is kept, and what deletion removes.
Verify
All three appear in the configuration and the README
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- recordings(id, filename, sha256, duration_s, imported_at, consent_confirmed_at, consent_note, delete_after) - chunks(recording_id, ordinal, start_ms, end_ms, overlap_ms, transcribed_at, model_name) · the seams are recorded because that is where words go missing - segments(id, recording_id, start_ms, end_ms, text, confidence, speaker_label) · immutable, and speaker_label is usually null and honest about it - meetings(id, recording_id, title, held_at, attendees_json, agenda_md) · what you knew before the recording existed - items(id, meeting_id, kind, text, segment_id, start_ms, quote, owner, due_on, status) · kind is decision, action or question, and segment_id is not optional - searches · a full-text index over segments, rebuilt from segments and therefore disposable - Every item in the notes points at a segment. An extraction that cannot name the moment it came from does not become a row.
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 | Recordings, segments, items. The transcript is the evidence. |
AUDIO_DIR | required | ./data/audio | Converted audio. Subject to the retention period below. |
MODEL_PATH | required | ./models/ggml-medium.bin | The local speech model. Its documented input format drives the conversion. |
TARGET_SAMPLE_RATE | required | 16000 | From the model's documentation, not from memory. Asserted in a test. |
CHUNK_SECONDS | required | 600 | Chunk length before overlap. Long enough to keep context, short enough to fail cheaply. |
CHUNK_OVERLAP_SECONDS | required | 10 | The seam insurance. Deduplicated by text match, not by clock. |
AUDIO_RETENTION_DAYS | required | 60 | Audio expires; transcripts and items do not. Stated in the README. |
EXTRACTION_API_KEYsecret | optional | sk-...redacted | Optional. Without it a local model does the extraction and the tool still works. |
The build, in order
Audio in, normalised, with consent recorded
A file is accepted, converted correctly, and stored with a record of why you were allowed to have it.
The model's documented input format is a requirement, not a suggestion.
Files
src/audio.pytests/test_convert.pyA confirmation stored with the recording, and a README that does not pretend to be a lawyer.
Files
src/ingest.pysrc/templates/import.htmlRe-importing the same meeting twice is the most common user error in this category.
Files
src/ingest.py
done when · tick each as it passeswatch out- Feeding a forty four kilohertz stereo file straight to a model that documents sixteen kilohertz mono produces a transcript that looks plausible and is quietly wrong, which is the worst failure mode available here because nobody notices. Convert explicitly with FFmpeg, assert the output format in a test, and re-read the model's current requirement rather than trusting this sentence.
Long audio without losing the seams
A ninety minute recording transcribes in chunks that overlap and rejoin without dropping words.
The seam is where the words go, so put the seam somewhere quiet.
Files
src/chunk.pytests/test_seams.pyClocks drift across chunk boundaries and text does not.
Files
src/merge.pyA gap you can see beats a sentence that was never there.
Files
src/transcribe.py
done when · tick each as it passeswatch out- Cutting a long recording at exact intervals slices a word in half at every boundary, and the two halves come back as two wrong words that read perfectly well, so the error is invisible in the output and shows up months later in a quoted decision. Overlap the chunks, split at silence where you can find one, and reconcile the overlap by matching text rather than by trusting the clock.
Decisions and actions that point at the tape
Every extracted item carries a timestamp and the verbatim words it came from.
The check is trivial and it is the difference between notes and fiction.
Files
src/extract.pytests/test_anchoring.pyA visible discard count tells you when the extraction is going wrong.
Files
src/extract.pyAn item you can hear is an item somebody can disagree with.
Files
src/templates/meeting.html
done when · tick each as it passeswatch out- Asking a model for action items and storing whatever comes back produces a tidy list containing one item nobody said, assigned to somebody who was not there, and it looks exactly like the four correct ones beside it. Require a verbatim quote and a segment identifier for every item, check the quote actually occurs in the segment, and drop the ones that do not rather than showing them with a warning.
Finding the meeting where somebody said it
Search spans every meeting and lands on the moment, not the file.
The unsummarised half is the half you will search for.
Files
src/search.pyThen it is disposable, and a schema change stops being frightening.
Files
src/search.pytests/test_reindex.pyReading the words is half of it; hearing the tone is the other half.
Files
src/templates/search.html
done when · tick each as it passeswatch out- Indexing the generated notes instead of the transcript makes the half of every meeting that nobody summarised unsearchable, which is precisely the half you go looking for six months later. Index the segments, treat the notes as a view, and make the index rebuildable so a schema change is a reindex rather than a migration.
Notes that leave, audio that expiresproduct builder
Meetings export as Markdown with their anchors, and recordings do not accumulate forever.
A note without its timestamp is an assertion.
Files
src/export.pyThe voices are the sensitive part; the words are the useful part.
Files
src/retention.pytests/test_retention.pyDerived files are how a deletion becomes a half-deletion.
Files
src/delete.py
done when · tick each as it passeswatch out- Keeping every recording forever because storage is cheap turns a notes tool into an archive of colleagues' voices that nobody agreed to, and the first time somebody asks you to delete a meeting you discover the audio is also in three exports and a cache. Set a default retention for audio from the first release, keep transcripts longer if you like, and make deletion remove every copy including derived files.
That is the whole plan for Fellow. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A meeting bot that joins calls.
- Speaker attribution presented as fact.
- Calendar and CRM integrations.
- Cross-team analytics over other people's meetings.
- live multi-speaker accuracy
- calendar and CRM integrations
- cross-call team analytics
- meeting-bot auto-join
- A template per meeting type, which shapes the extraction without loosening the anchoring rule
- Action items pushed to your own task tool, once the anchoring has been trusted for a month
- A weekly digest of open actions across meetings, built entirely from anchored items
Need the files? The project pack on the verdict page hands your agent the whole brief · more meeting notes.