Build Bluedot
KINDAback to the verdict
A private meeting recorder whose hard problems are capturing audio that survives a crash, transcribing without letting the model invent sentences during silence, and writing notes that can be checked against the recording rather than trusted.
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 recorder, the transcription 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 audio conversion and the chunk stitching go through it, including the recovery path.
Get it Your package manager, or ffmpeg.org. open ↗
Verify
ffmpeg -version prints a version and the stitcher runs on a fixture - have readyfree, plus disk for the weights
Why Transcription happens on your machine because the recording contains other people's voices, and the model size decides both accuracy and how long a meeting takes to process.
Get it Download weights for a maintained local implementation and pick a size your machine can run in reasonable time.
Verify
A five minute fixture transcribes and the elapsed time is recorded - installfree
Why It is the component that stops silence reaching the model, which is where the fabricated sentences come from.
Get it Any maintained VAD for your runtime, configured with the aggressiveness you tested.
Verify
A silent fixture yields no speech regions - decidefree
Why The technical part is one field; the part that matters is what you actually say at the start of a call, and deciding it now stops it being skipped later.
Get it Write the sentence you will say and the sentence stored in the consent note, and put both in the README.
Verify
Both appear in the README and the recorder refuses to start without the note
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- recordings(id, started_at, ended_at, path, sample_rate, duration_s, consent_note, source_device, state) - chunks(recording_id, ordinal, path, start_s, duration_s, sealed) · written during capture, stitched on stop - segments(id, recording_id, start_s, end_s, text, avg_logprob, no_speech_prob, compression_ratio, model_id, flag_reason) - speakers(id, recording_id, cluster_label, display_name) · the cluster is what the algorithm found, the name is what a human typed - segment_speaker(segment_id, speaker_id, confidence) · attribution is stored with its confidence or not stored - notes(id, recording_id, kind, text, created_at, model_id, prompt_version) · kind is summary, decision or action - note_sources(note_id, segment_id) · a note with no rows here cannot be saved, which is the whole design - There is no column holding a note without a segment behind it, and no column holding a speaker name as a fact.
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, speakers and notes. Survives the audio, so back it up. |
AUDIO_DIR | required | ./data/audio | Recordings and chunk directories. Deleted on expiry, rows retained. |
WHISPER_MODEL | required | a local model identifier | Stored with every segment, because a different model gives different text. |
AUDIO_RETENTION_DAYS | required | 30 | After this, audio is deleted and the transcript stays. Shorter is a reasonable default. |
FLAG_NO_SPEECH_PROB | required | 0.6 | Above this, a segment renders marked and is excluded from note generation. |
FLAG_COMPRESSION_RATIO | required | 2.4 | Above this, the segment is treated as runaway repetition rather than speech. |
CHUNK_SECONDS | required | 30 | How much audio a crash can cost. Lower means more files and less loss. |
The build, in order
Capture that survives the crash
Recording writes to disk continuously, shows its state, and cannot start without a consent note.
The only defence against losing an hour is not holding an hour.
Files
src/capture/recorder.pytests/test_crash_recovery.pyFriction here is the feature, not a bug to be smoothed off.
Files
src/capture/consent.pyRecovery has to work with no process alive to help it.
Files
src/capture/stitch.py
done when · tick each as it passeswatch out- Buffering the whole meeting in memory and writing one file at the end means a crash, a full disk or a closed laptop at minute 58 loses the hour, and this is the failure people discover exactly once. Write sealed chunks as you go, keep a manifest beside them, and make the stitcher able to run against a directory with no process attached to it.
Transcription that admits what it is unsure about
Speech is detected before it is transcribed, and every segment keeps the scores that say how confident it was.
The model should never be asked what silence said.
Files
src/transcribe/vad.pytests/test_silence.pyYou cannot mark a doubtful segment you did not measure.
Files
src/transcribe/segments.pyA loop is the model's tell and it reads as normal text.
Files
src/transcribe/flags.py
done when · tick each as it passeswatch out- Feeding a whole meeting file straight into the model, silences included, is where the fabricated sentences come from: the published audit found roughly one percent of transcriptions containing phrases present nowhere in the audio, concentrated in recordings with long non-vocal stretches, which describes every meeting recording ever made. Detect speech first, never send a region that is all silence, and keep the scores so a doubtful segment can be shown as doubtful.
- A repeated phrase looping for thirty seconds is the classic signature of a model stuck on low-information audio, and it reads as a real transcript at a glance. Check the compression ratio of each segment's text against a threshold and flag runaway repetition instead of storing it as speech.
Speakers are a guess with a name attached
Speaker attribution is presented as a cluster somebody labelled, never as a fact the system knows.
The algorithm found a cluster; a person supplied a name.
Files
src/speakers/cluster.pyA wrong attribution in notes is worse than no attribution.
Files
src/speakers/attribute.pytests/test_attribution.pyYou will re-run diarisation and you will want the names back.
Files
src/speakers/rename.py
done when · tick each as it passeswatch out- Rendering a diarised label as a plain name means a decision gets attributed to the wrong person in the notes, and nobody catches it because a transcript with names in it looks authoritative. Keep the cluster id separate from the display name, show the attribution confidence, and make a segment with low confidence obvious in both the transcript and anything generated from it.
Notes that point back at the recording
Every summary line, decision and action item carries the segment ids it came from, or it is not saved.
A citation you did not ask for will not arrive.
Files
src/notes/generate.pysrc/notes/prompt.pyAn invented id is the model telling you it guessed.
Files
src/notes/verify.pytests/test_note_sources.pyCode paths get added later; constraints do not.
Files
src/db/schema.sql
done when · tick each as it passeswatch out- Asking for a summary of the whole transcript and printing what comes back gives you action items that sound exactly right and belong to nobody, and the person who missed the meeting will act on one. Number the segments in the prompt, require ids per note, verify each id exists, and treat an unresolvable id as a dropped note rather than a note with a missing link.
Retention, export and actually deletingproduct builder
Audio expires on a schedule, everything exports, and deleting a meeting removes every trace of it.
The recording stops being useful long before the notes do.
Files
src/retention/expire.pytests/test_retention.pyRows, files, chunks and index, or it is not deletion.
Files
src/recordings/delete.pytests/test_delete.pyAn export nobody has reimported is a hope.
Files
src/export/bundle.py
done when · tick each as it passeswatch out- Deleting the recording row while the audio file, the chunk directory and the search index keep their copies means somebody who asked you to delete a recording is still findable by searching for a phrase they said. Make deletion a single transaction plus a filesystem sweep, and test it by searching for a phrase from a deleted meeting and getting nothing.
- Retention that runs only when the application is open never runs on the machine that recorded the sensitive meeting and then sat closed for a month. Run expiry at startup as well as on a schedule, and log what it removed.
That is the whole plan for Bluedot. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A meeting bot that joins calls.
- Live transcription during the call.
- Cross-meeting analytics about colleagues.
- Calendar and customer system integrations.
- cross-call team analytics
- meeting-bot auto-join
- live multi-speaker accuracy
- calendar and CRM integrations
- Speaker enrolment from a short labelled sample, which makes attribution better without making it a fact
- A vocabulary list for names and product terms the model gets wrong every single time
- A weekly digest of decisions across recordings, built from the same note source links so it stays checkable
Need the files? The project pack on the verdict page hands your agent the whole brief · more meeting notes.