Vibecode Bluedot
track this build4 phases, 12 steps, beginner friendly0%The visible meeting recorder loop is buildable, but a credible replacement needs more than the first screen. Bluedot earns its keep through capture, integrations, reliability, so expect a weekend or multi-day build and a narrower personal scope.
You are building a lean indie version of Bluedot. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Bluedot · indie build 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. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a local web interface, nothing leaving the machine | a recording of a meeting is other people's words as well as yours | | Audio | ffmpeg for conversion, fixed length chunks written to disk as you record | a crash at minute 58 should cost seconds, not the meeting | | Transcription | A local Whisper-family model, run after voice activity detection | silence is where speech models invent things, so silence never reaches it | | Storage | SQLite for segments and notes, audio as files beside it | the transcript outlives the audio and the two have different retention | | Notes | Generated only with segment ids, and dropped if the ids do not resolve | a note you cannot trace to the recording is a rumour with a timestamp | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the recorder, the transcription pipeline and the local interface. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg** · free - Why: Every audio conversion and the chunk stitching go through it, including the recovery path. - Get it: Your package manager, or ffmpeg.org. - Verify: ffmpeg -version prints a version and the stitcher runs on a fixture - [ ] **A local Whisper-family model and its weights** · free, 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 - [ ] **A voice activity detector** · free - 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 - [ ] **How you tell people they are being recorded** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Bluedot, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bluedot The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local meeting recorder that transcribes on your own machine and links every note back to the audio to replace Bluedot. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things decide this build, and both of them are about honesty rather than features. First, recording other people. This app records only when everyone in the room has been told, and it shows a recording state that is visible without looking for it. No silent capture, no bot that joins a call uninvited, no pulling another participant's audio out of a meeting platform in a way that platform has not published a way to do. The consent note is a field on the recording and the recorder refuses to start without it. If that feels like friction, it is the point. Second, the transcript is not a record of what was said, it is a model's best guess, and sometimes it is a fabrication. The audit published at FAccT 2024 as 'Careless Whisper: Speech-to-Text Hallucination Harms' found roughly one percent of transcriptions containing entire phrases or sentences that did not exist in the audio in any form, with 38 percent of those hallucinations carrying explicit harms such as invented violence or false authority, and they clustered in recordings with long non-vocal stretches, a common symptom of aphasia. A meeting recording is mostly silence and cross-talk, which is exactly that condition. So: cut silence before the model ever sees it, keep the per-segment confidence scores and show the weak ones as weak, and never let a note reach the summary without the segment ids it came from. Anyone reading a decision in these notes must be two clicks from hearing it said. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a local web interface, nothing leaving the machine · a recording of a meeting is other people's words as well as yours - Audio: ffmpeg for conversion, fixed length chunks written to disk as you record · a crash at minute 58 should cost seconds, not the meeting - Transcription: A local Whisper-family model, run after voice activity detection · silence is where speech models invent things, so silence never reaches it - Storage: SQLite for segments and notes, audio as files beside it · the transcript outlives the audio and the two have different retention - Notes: Generated only with segment ids, and dropped if the ids do not resolve · a note you cannot trace to the recording is a rumour with a timestamp ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that survives the crash Build: Record to fixed length chunks written and sealed as you go, stitching them on stop. Show a recording indicator that is visible from across the room. Require the consent note before the first chunk is written. Provide a recovery command that stitches an orphaned chunk directory into a recording after a crash. Trap: 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. Done when: Killing the process mid-recording leaves sealed chunks that the recovery command stitches into a playable file, tested by killing it. The recorder refuses to start without a consent note and the note is stored on the recording, asserted. The recording indicator is visible in the interface and in the system tray or title bar while capture is running. Disk use per hour of audio at the configured format is measured and written in the README rather than estimated. Do not build yet: No transcription, speakers, notes or search yet. ### Phase 2 · Transcription that admits what it is unsure about Build: Run voice activity detection first and transcribe only the speech regions, with a small overlap so a word on a boundary is not cut. Store avg_logprob, no_speech_prob and the compression ratio with every segment, along with the model identifier. Flag any segment past the configured thresholds and render it visibly marked rather than as ordinary text. Trap: 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. Trap: 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. Done when: A minute of room tone produces zero segments, tested with a silent fixture. Every segment row carries its three scores and the model identifier. A segment past the flag thresholds renders visibly marked in the interface and is excluded from note generation unless a human clears it. Clicking a segment plays the audio at its start time within a stated tolerance, checked by hand on a real recording. Do not build yet: No speaker attribution, notes or retention work yet. ### Phase 3 · Speakers are a guess with a name attached Build: Cluster segments into speakers and label them A, B, C until a human renames them. Store the attribution confidence per segment and show low confidence attributions differently. Renaming a cluster renames it everywhere while leaving the cluster identity intact, so a later correction of the clustering does not lose the names. Trap: 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. Done when: A transcript renders cluster labels until a human supplies names, and never invents a name from the audio. Attribution confidence is stored per segment and low confidence attributions are visually distinct, checked by review. Renaming a speaker updates every segment and leaves the cluster id unchanged, tested. Splitting or merging clusters is possible without losing the names already applied. Do not build yet: No notes, search, retention or export yet. ### Phase 4 · Notes that point back at the recording Build: Feed the model numbered segments and require it to return, for each note, the list of segment ids that support it. Resolve every returned id against the database, drop any note whose ids do not all resolve, and record what was dropped. Clicking a note in the interface plays the earliest supporting segment. Trap: 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. Done when: A note whose returned segment ids do not resolve is dropped and counted, tested with a fabricated id. Every saved note has at least one row in note_sources, enforced by a constraint rather than by the code path. Clicking any note plays the audio at the start of its earliest source segment. Notes generated from a transcript containing flagged segments exclude those segments, asserted. Do not build yet: No retention, export or deletion work yet. ### Phase 5 · Retention, export and actually deleting Build: Expire audio after a configured period while keeping transcripts and notes, and show the expiry date on every recording. Export a meeting as Markdown with timestamps plus a JSON bundle of segments, speakers and notes. Make deletion one operation covering audio, chunks, segments, speakers, notes and any search index, with a confirmation that names the meeting. Trap: 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. Trap: 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. Done when: Audio past the retention period is deleted while its transcript and notes survive intact, tested at the boundary. Searching for a phrase from a deleted meeting returns nothing, tested after deletion including the index. Export produces Markdown and a JSON bundle that reimport into an empty database with identical segment and note counts. Expiry runs at startup and writes a log line naming what it removed, even when nothing was. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins meetings on a platform, because doing it without every participant knowing is the thing this build refuses, and doing it properly means a platform integration with its own published rules. - Live transcription during the call, which needs streaming inference and a latency budget and is a different engineering problem to a file on disk. - Cross-meeting analytics such as talk time by person over a quarter, which turns a personal recorder into a monitoring tool pointed at colleagues. - Calendar and customer system integrations, which are the reliability work a subscription actually pays for. ### README must contain - That everyone in the recording must be told, that the consent note is required, and that there is no silent mode - The published hallucination findings for speech models, with the study and the date, before any usage instructions - That silence is removed before transcription and what the confidence thresholds are set to - That every note links to segments and that a note whose links do not resolve is discarded rather than shown - How long audio is kept, what deletion removes, and how to verify it with a search ===== AGENTS.md ===== # Agent instructions · Bluedot indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with a local web interface, nothing leaving the machine, ffmpeg for conversion, fixed length chunks written to disk as you record, A local Whisper-family model, run after voice activity detection, SQLite for segments and notes, audio as files beside it, Generated only with segment ids, and dropped if the ids do not resolve. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Bluedot 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture that survives the crash Recording writes to disk continuously, shows its state, and cannot start without a consent note. ### Steps 1. Write and seal chunks during capture The only defence against losing an hour is not holding an hour. Files: `src/capture/recorder.py`, `tests/test_crash_recovery.py` 2. Refuse to start without a consent note Friction here is the feature, not a bug to be smoothed off. Files: `src/capture/consent.py` 3. Ship a stitcher that runs on orphaned chunks Recovery has to work with no process alive to help it. Files: `src/capture/stitch.py` ### Done when - [ ] A killed process leaves recoverable sealed chunks - [ ] Recording cannot start without a consent note - [ ] The recording state is visible without hunting for it - [ ] Disk use per hour is measured and documented ### Watch 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. ## Phase 2 · 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. ### Steps 1. Detect speech before transcribing anything The model should never be asked what silence said. Files: `src/transcribe/vad.py`, `tests/test_silence.py` 2. Store the confidence scores with every segment You cannot mark a doubtful segment you did not measure. Files: `src/transcribe/segments.py` 3. Flag runaway repetition by compression ratio A loop is the model's tell and it reads as normal text. Files: `src/transcribe/flags.py` ### Done when - [ ] Silence produces no segments - [ ] Every segment stores its scores and model id - [ ] Flagged segments render marked and skip note generation - [ ] Clicking a segment plays the matching audio ### Watch 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. ## Phase 3 · Speakers are a guess with a name attached Speaker attribution is presented as a cluster somebody labelled, never as a fact the system knows. ### Steps 1. Keep cluster identity separate from display name The algorithm found a cluster; a person supplied a name. Files: `src/speakers/cluster.py` 2. Store and show attribution confidence A wrong attribution in notes is worse than no attribution. Files: `src/speakers/attribute.py`, `tests/test_attribution.py` 3. Make rename survive a re-clustering You will re-run diarisation and you will want the names back. Files: `src/speakers/rename.py` ### Done when - [ ] Clusters are labelled, not named, until a human renames them - [ ] Attribution confidence is stored and visible - [ ] Rename updates every segment and keeps the cluster id - [ ] Split and merge preserve applied names ### Watch 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. ## Phase 4 · 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. ### Steps 1. Number segments in the prompt and demand ids back A citation you did not ask for will not arrive. Files: `src/notes/generate.py`, `src/notes/prompt.py` 2. Resolve every id and drop the unresolvable An invented id is the model telling you it guessed. Files: `src/notes/verify.py`, `tests/test_note_sources.py` 3. Enforce the link with a database constraint Code paths get added later; constraints do not. Files: `src/db/schema.sql` ### Done when - [ ] Notes with unresolvable ids are dropped and counted - [ ] No note can be saved without a source segment - [ ] Clicking a note plays its earliest source - [ ] Flagged segments are excluded from note generation ### Watch 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. ## Not in this build - A meeting bot that joins calls. - Live transcription during the call. - Cross-meeting analytics about colleagues. - Calendar and customer system integrations. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recordings, segments, speakers and notes. Survives the audio, so back it up. DATABASE_PATH=./data/meetings.db # Required. Recordings and chunk directories. Deleted on expiry, rows retained. AUDIO_DIR=./data/audio # Required. Stored with every segment, because a different model gives different text. WHISPER_MODEL=a local model identifier # Required. After this, audio is deleted and the transcript stays. Shorter is a reasonable default. AUDIO_RETENTION_DAYS=30 # Required. Above this, a segment renders marked and is excluded from note generation. FLAG_NO_SPEECH_PROB=0.6 # Required. Above this, the segment is treated as runaway repetition rather than speech. FLAG_COMPRESSION_RATIO=2.4 # Required. How much audio a crash can cost. Lower means more files and less loss. CHUNK_SECONDS=30
You are building a lean indie version of Bluedot. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== README.md ===== # Bluedot · indie build 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. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a local web interface, nothing leaving the machine | a recording of a meeting is other people's words as well as yours | | Audio | ffmpeg for conversion, fixed length chunks written to disk as you record | a crash at minute 58 should cost seconds, not the meeting | | Transcription | A local Whisper-family model, run after voice activity detection | silence is where speech models invent things, so silence never reaches it | | Storage | SQLite for segments and notes, audio as files beside it | the transcript outlives the audio and the two have different retention | | Notes | Generated only with segment ids, and dropped if the ids do not resolve | a note you cannot trace to the recording is a rumour with a timestamp | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the recorder, the transcription pipeline and the local interface. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg** · free - Why: Every audio conversion and the chunk stitching go through it, including the recovery path. - Get it: Your package manager, or ffmpeg.org. - Verify: ffmpeg -version prints a version and the stitcher runs on a fixture - [ ] **A local Whisper-family model and its weights** · free, 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 - [ ] **A voice activity detector** · free - 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 - [ ] **How you tell people they are being recorded** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Bluedot, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Bluedot The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local meeting recorder that transcribes on your own machine and links every note back to the audio to replace Bluedot. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things decide this build, and both of them are about honesty rather than features. First, recording other people. This app records only when everyone in the room has been told, and it shows a recording state that is visible without looking for it. No silent capture, no bot that joins a call uninvited, no pulling another participant's audio out of a meeting platform in a way that platform has not published a way to do. The consent note is a field on the recording and the recorder refuses to start without it. If that feels like friction, it is the point. Second, the transcript is not a record of what was said, it is a model's best guess, and sometimes it is a fabrication. The audit published at FAccT 2024 as 'Careless Whisper: Speech-to-Text Hallucination Harms' found roughly one percent of transcriptions containing entire phrases or sentences that did not exist in the audio in any form, with 38 percent of those hallucinations carrying explicit harms such as invented violence or false authority, and they clustered in recordings with long non-vocal stretches, a common symptom of aphasia. A meeting recording is mostly silence and cross-talk, which is exactly that condition. So: cut silence before the model ever sees it, keep the per-segment confidence scores and show the weak ones as weak, and never let a note reach the summary without the segment ids it came from. Anyone reading a decision in these notes must be two clicks from hearing it said. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a local web interface, nothing leaving the machine · a recording of a meeting is other people's words as well as yours - Audio: ffmpeg for conversion, fixed length chunks written to disk as you record · a crash at minute 58 should cost seconds, not the meeting - Transcription: A local Whisper-family model, run after voice activity detection · silence is where speech models invent things, so silence never reaches it - Storage: SQLite for segments and notes, audio as files beside it · the transcript outlives the audio and the two have different retention - Notes: Generated only with segment ids, and dropped if the ids do not resolve · a note you cannot trace to the recording is a rumour with a timestamp ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that survives the crash Build: Record to fixed length chunks written and sealed as you go, stitching them on stop. Show a recording indicator that is visible from across the room. Require the consent note before the first chunk is written. Provide a recovery command that stitches an orphaned chunk directory into a recording after a crash. Trap: 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. Done when: Killing the process mid-recording leaves sealed chunks that the recovery command stitches into a playable file, tested by killing it. The recorder refuses to start without a consent note and the note is stored on the recording, asserted. The recording indicator is visible in the interface and in the system tray or title bar while capture is running. Disk use per hour of audio at the configured format is measured and written in the README rather than estimated. Do not build yet: No transcription, speakers, notes or search yet. ### Phase 2 · Transcription that admits what it is unsure about Build: Run voice activity detection first and transcribe only the speech regions, with a small overlap so a word on a boundary is not cut. Store avg_logprob, no_speech_prob and the compression ratio with every segment, along with the model identifier. Flag any segment past the configured thresholds and render it visibly marked rather than as ordinary text. Trap: 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. Trap: 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. Done when: A minute of room tone produces zero segments, tested with a silent fixture. Every segment row carries its three scores and the model identifier. A segment past the flag thresholds renders visibly marked in the interface and is excluded from note generation unless a human clears it. Clicking a segment plays the audio at its start time within a stated tolerance, checked by hand on a real recording. Do not build yet: No speaker attribution, notes or retention work yet. ### Phase 3 · Speakers are a guess with a name attached Build: Cluster segments into speakers and label them A, B, C until a human renames them. Store the attribution confidence per segment and show low confidence attributions differently. Renaming a cluster renames it everywhere while leaving the cluster identity intact, so a later correction of the clustering does not lose the names. Trap: 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. Done when: A transcript renders cluster labels until a human supplies names, and never invents a name from the audio. Attribution confidence is stored per segment and low confidence attributions are visually distinct, checked by review. Renaming a speaker updates every segment and leaves the cluster id unchanged, tested. Splitting or merging clusters is possible without losing the names already applied. Do not build yet: No notes, search, retention or export yet. ### Phase 4 · Notes that point back at the recording Build: Feed the model numbered segments and require it to return, for each note, the list of segment ids that support it. Resolve every returned id against the database, drop any note whose ids do not all resolve, and record what was dropped. Clicking a note in the interface plays the earliest supporting segment. Trap: 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. Done when: A note whose returned segment ids do not resolve is dropped and counted, tested with a fabricated id. Every saved note has at least one row in note_sources, enforced by a constraint rather than by the code path. Clicking any note plays the audio at the start of its earliest source segment. Notes generated from a transcript containing flagged segments exclude those segments, asserted. Do not build yet: No retention, export or deletion work yet. ### Phase 5 · Retention, export and actually deleting Build: Expire audio after a configured period while keeping transcripts and notes, and show the expiry date on every recording. Export a meeting as Markdown with timestamps plus a JSON bundle of segments, speakers and notes. Make deletion one operation covering audio, chunks, segments, speakers, notes and any search index, with a confirmation that names the meeting. Trap: 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. Trap: 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. Done when: Audio past the retention period is deleted while its transcript and notes survive intact, tested at the boundary. Searching for a phrase from a deleted meeting returns nothing, tested after deletion including the index. Export produces Markdown and a JSON bundle that reimport into an empty database with identical segment and note counts. Expiry runs at startup and writes a log line naming what it removed, even when nothing was. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins meetings on a platform, because doing it without every participant knowing is the thing this build refuses, and doing it properly means a platform integration with its own published rules. - Live transcription during the call, which needs streaming inference and a latency budget and is a different engineering problem to a file on disk. - Cross-meeting analytics such as talk time by person over a quarter, which turns a personal recorder into a monitoring tool pointed at colleagues. - Calendar and customer system integrations, which are the reliability work a subscription actually pays for. ### README must contain - That everyone in the recording must be told, that the consent note is required, and that there is no silent mode - The published hallucination findings for speech models, with the study and the date, before any usage instructions - That silence is removed before transcription and what the confidence thresholds are set to - That every note links to segments and that a note whose links do not resolve is discarded rather than shown - How long audio is kept, what deletion removes, and how to verify it with a search ===== AGENTS.md ===== # Agent instructions · Bluedot indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with a local web interface, nothing leaving the machine, ffmpeg for conversion, fixed length chunks written to disk as you record, A local Whisper-family model, run after voice activity detection, SQLite for segments and notes, audio as files beside it, Generated only with segment ids, and dropped if the ids do not resolve. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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. ===== BUILD_PLAN.md ===== # Build plan · Bluedot 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture that survives the crash Recording writes to disk continuously, shows its state, and cannot start without a consent note. ### Steps 1. Write and seal chunks during capture The only defence against losing an hour is not holding an hour. Files: `src/capture/recorder.py`, `tests/test_crash_recovery.py` 2. Refuse to start without a consent note Friction here is the feature, not a bug to be smoothed off. Files: `src/capture/consent.py` 3. Ship a stitcher that runs on orphaned chunks Recovery has to work with no process alive to help it. Files: `src/capture/stitch.py` ### Done when - [ ] A killed process leaves recoverable sealed chunks - [ ] Recording cannot start without a consent note - [ ] The recording state is visible without hunting for it - [ ] Disk use per hour is measured and documented ### Watch 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. ## Phase 2 · 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. ### Steps 1. Detect speech before transcribing anything The model should never be asked what silence said. Files: `src/transcribe/vad.py`, `tests/test_silence.py` 2. Store the confidence scores with every segment You cannot mark a doubtful segment you did not measure. Files: `src/transcribe/segments.py` 3. Flag runaway repetition by compression ratio A loop is the model's tell and it reads as normal text. Files: `src/transcribe/flags.py` ### Done when - [ ] Silence produces no segments - [ ] Every segment stores its scores and model id - [ ] Flagged segments render marked and skip note generation - [ ] Clicking a segment plays the matching audio ### Watch 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. ## Phase 3 · Speakers are a guess with a name attached Speaker attribution is presented as a cluster somebody labelled, never as a fact the system knows. ### Steps 1. Keep cluster identity separate from display name The algorithm found a cluster; a person supplied a name. Files: `src/speakers/cluster.py` 2. Store and show attribution confidence A wrong attribution in notes is worse than no attribution. Files: `src/speakers/attribute.py`, `tests/test_attribution.py` 3. Make rename survive a re-clustering You will re-run diarisation and you will want the names back. Files: `src/speakers/rename.py` ### Done when - [ ] Clusters are labelled, not named, until a human renames them - [ ] Attribution confidence is stored and visible - [ ] Rename updates every segment and keeps the cluster id - [ ] Split and merge preserve applied names ### Watch 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. ## Phase 4 · 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. ### Steps 1. Number segments in the prompt and demand ids back A citation you did not ask for will not arrive. Files: `src/notes/generate.py`, `src/notes/prompt.py` 2. Resolve every id and drop the unresolvable An invented id is the model telling you it guessed. Files: `src/notes/verify.py`, `tests/test_note_sources.py` 3. Enforce the link with a database constraint Code paths get added later; constraints do not. Files: `src/db/schema.sql` ### Done when - [ ] Notes with unresolvable ids are dropped and counted - [ ] No note can be saved without a source segment - [ ] Clicking a note plays its earliest source - [ ] Flagged segments are excluded from note generation ### Watch 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. ## Not in this build - A meeting bot that joins calls. - Live transcription during the call. - Cross-meeting analytics about colleagues. - Calendar and customer system integrations. ## After v1, if you want it - 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 ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recordings, segments, speakers and notes. Survives the audio, so back it up. DATABASE_PATH=./data/meetings.db # Required. Recordings and chunk directories. Deleted on expiry, rows retained. AUDIO_DIR=./data/audio # Required. Stored with every segment, because a different model gives different text. WHISPER_MODEL=a local model identifier # Required. After this, audio is deleted and the transcript stays. Shorter is a reasonable default. AUDIO_RETENTION_DAYS=30 # Required. Above this, a segment renders marked and is excluded from note generation. FLAG_NO_SPEECH_PROB=0.6 # Required. Above this, the segment is treated as runaway repetition rather than speech. FLAG_COMPRESSION_RATIO=2.4 # Required. How much audio a crash can cost. Lower means more files and less loss. CHUNK_SECONDS=30
You are building a production product version of Bluedot. Create the following project files first, then implement the application by following them. Keep the files updated as decisions change. Do not collapse this into a single README or prompt. ===== PRODUCT.md ===== # Bluedot · product brief ## Problem The visible meeting recorder loop is buildable, but a credible replacement needs more than the first screen. Bluedot earns its keep through capture, integrations, reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A meeting recorder that cannot start without a consent note, never asks a speech model what silence said, marks the segments it is unsure about, and refuses to save a note that does not point at the recording. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - FFmpeg - local Whisper model or optional API key in .env ## Explicit non-goals for v1 - 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 ## Success criteria - Recording is confirmed to refuse to start without a consent note - Silence is confirmed to produce no segments - Flagged segments are confirmed excluded from note generation - No note is confirmed savable without a resolvable source segment - A deleted meeting is confirmed unfindable by search ===== BRIEF.md ===== # Build brief · Bluedot The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local meeting recorder that transcribes on your own machine and links every note back to the audio to replace Bluedot. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things decide this build, and both of them are about honesty rather than features. First, recording other people. This app records only when everyone in the room has been told, and it shows a recording state that is visible without looking for it. No silent capture, no bot that joins a call uninvited, no pulling another participant's audio out of a meeting platform in a way that platform has not published a way to do. The consent note is a field on the recording and the recorder refuses to start without it. If that feels like friction, it is the point. Second, the transcript is not a record of what was said, it is a model's best guess, and sometimes it is a fabrication. The audit published at FAccT 2024 as 'Careless Whisper: Speech-to-Text Hallucination Harms' found roughly one percent of transcriptions containing entire phrases or sentences that did not exist in the audio in any form, with 38 percent of those hallucinations carrying explicit harms such as invented violence or false authority, and they clustered in recordings with long non-vocal stretches, a common symptom of aphasia. A meeting recording is mostly silence and cross-talk, which is exactly that condition. So: cut silence before the model ever sees it, keep the per-segment confidence scores and show the weak ones as weak, and never let a note reach the summary without the segment ids it came from. Anyone reading a decision in these notes must be two clicks from hearing it said. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a local web interface, nothing leaving the machine · a recording of a meeting is other people's words as well as yours - Audio: ffmpeg for conversion, fixed length chunks written to disk as you record · a crash at minute 58 should cost seconds, not the meeting - Transcription: A local Whisper-family model, run after voice activity detection · silence is where speech models invent things, so silence never reaches it - Storage: SQLite for segments and notes, audio as files beside it · the transcript outlives the audio and the two have different retention - Notes: Generated only with segment ids, and dropped if the ids do not resolve · a note you cannot trace to the recording is a rumour with a timestamp ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that survives the crash Build: Record to fixed length chunks written and sealed as you go, stitching them on stop. Show a recording indicator that is visible from across the room. Require the consent note before the first chunk is written. Provide a recovery command that stitches an orphaned chunk directory into a recording after a crash. Trap: 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. Done when: Killing the process mid-recording leaves sealed chunks that the recovery command stitches into a playable file, tested by killing it. The recorder refuses to start without a consent note and the note is stored on the recording, asserted. The recording indicator is visible in the interface and in the system tray or title bar while capture is running. Disk use per hour of audio at the configured format is measured and written in the README rather than estimated. Do not build yet: No transcription, speakers, notes or search yet. ### Phase 2 · Transcription that admits what it is unsure about Build: Run voice activity detection first and transcribe only the speech regions, with a small overlap so a word on a boundary is not cut. Store avg_logprob, no_speech_prob and the compression ratio with every segment, along with the model identifier. Flag any segment past the configured thresholds and render it visibly marked rather than as ordinary text. Trap: 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. Trap: 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. Done when: A minute of room tone produces zero segments, tested with a silent fixture. Every segment row carries its three scores and the model identifier. A segment past the flag thresholds renders visibly marked in the interface and is excluded from note generation unless a human clears it. Clicking a segment plays the audio at its start time within a stated tolerance, checked by hand on a real recording. Do not build yet: No speaker attribution, notes or retention work yet. ### Phase 3 · Speakers are a guess with a name attached Build: Cluster segments into speakers and label them A, B, C until a human renames them. Store the attribution confidence per segment and show low confidence attributions differently. Renaming a cluster renames it everywhere while leaving the cluster identity intact, so a later correction of the clustering does not lose the names. Trap: 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. Done when: A transcript renders cluster labels until a human supplies names, and never invents a name from the audio. Attribution confidence is stored per segment and low confidence attributions are visually distinct, checked by review. Renaming a speaker updates every segment and leaves the cluster id unchanged, tested. Splitting or merging clusters is possible without losing the names already applied. Do not build yet: No notes, search, retention or export yet. ### Phase 4 · Notes that point back at the recording Build: Feed the model numbered segments and require it to return, for each note, the list of segment ids that support it. Resolve every returned id against the database, drop any note whose ids do not all resolve, and record what was dropped. Clicking a note in the interface plays the earliest supporting segment. Trap: 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. Done when: A note whose returned segment ids do not resolve is dropped and counted, tested with a fabricated id. Every saved note has at least one row in note_sources, enforced by a constraint rather than by the code path. Clicking any note plays the audio at the start of its earliest source segment. Notes generated from a transcript containing flagged segments exclude those segments, asserted. Do not build yet: No retention, export or deletion work yet. ### Phase 5 · Retention, export and actually deleting Build: Expire audio after a configured period while keeping transcripts and notes, and show the expiry date on every recording. Export a meeting as Markdown with timestamps plus a JSON bundle of segments, speakers and notes. Make deletion one operation covering audio, chunks, segments, speakers, notes and any search index, with a confirmation that names the meeting. Trap: 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. Trap: 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. Done when: Audio past the retention period is deleted while its transcript and notes survive intact, tested at the boundary. Searching for a phrase from a deleted meeting returns nothing, tested after deletion including the index. Export produces Markdown and a JSON bundle that reimport into an empty database with identical segment and note counts. Expiry runs at startup and writes a log line naming what it removed, even when nothing was. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins meetings on a platform, because doing it without every participant knowing is the thing this build refuses, and doing it properly means a platform integration with its own published rules. - Live transcription during the call, which needs streaming inference and a latency budget and is a different engineering problem to a file on disk. - Cross-meeting analytics such as talk time by person over a quarter, which turns a personal recorder into a monitoring tool pointed at colleagues. - Calendar and customer system integrations, which are the reliability work a subscription actually pays for. ### README must contain - That everyone in the recording must be told, that the consent note is required, and that there is no silent mode - The published hallucination findings for speech models, with the study and the date, before any usage instructions - That silence is removed before transcription and what the confidence thresholds are set to - That every note links to segments and that a note whose links do not resolve is discarded rather than shown - How long audio is kept, what deletion removes, and how to verify it with a search ===== ARCHITECTURE.md ===== # Architecture · Bluedot ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a local web interface, nothing leaving the machine | a recording of a meeting is other people's words as well as yours | | Audio | ffmpeg for conversion, fixed length chunks written to disk as you record | a crash at minute 58 should cost seconds, not the meeting | | Transcription | A local Whisper-family model, run after voice activity detection | silence is where speech models invent things, so silence never reaches it | | Storage | SQLite for segments and notes, audio as files beside it | the transcript outlives the audio and the two have different retention | | Notes | Generated only with segment ids, and dropped if the ids do not resolve | a note you cannot trace to the recording is a rumour with a timestamp | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Chunked writing, sealing, stitching and recovery. | Any recorder, provided it never holds a meeting in memory. | | Speech detection | Finding the regions that contain voice. | Any detector, provided silence never reaches the model. | | Transcription | Segments, scores and flags. | Any local model, provided the scores are stored with the text. | | Attribution | Clusters, confidences and human names. | Any diariser, provided a label is never presented as a fact. | | Notes | Generation, id resolution and the source links. | Any model, provided a note without resolvable sources is dropped. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Recordings, segments, speakers and notes. Survives the audio, so back it up. - `AUDIO_DIR` · required · Recordings and chunk directories. Deleted on expiry, rows retained. - `WHISPER_MODEL` · required · Stored with every segment, because a different model gives different text. - `AUDIO_RETENTION_DAYS` · required · After this, audio is deleted and the transcript stays. Shorter is a reasonable default. - `FLAG_NO_SPEECH_PROB` · required · Above this, a segment renders marked and is excluded from note generation. - `FLAG_COMPRESSION_RATIO` · required · Above this, the segment is treated as runaway repetition rather than speech. - `CHUNK_SECONDS` · required · How much audio a crash can cost. Lower means more files and less loss. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it. ===== AGENTS.md ===== # Agent instructions · Bluedot product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with a local web interface, nothing leaving the machine, ffmpeg for conversion, fixed length chunks written to disk as you record, A local Whisper-family model, run after voice activity detection, SQLite for segments and notes, audio as files beside it, Generated only with segment ids, and dropped if the ids do not resolve. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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. ===== MILESTONES.md ===== # Delivery milestones · Bluedot Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture that survives the crash Recording writes to disk continuously, shows its state, and cannot start without a consent note. ### Steps 1. Write and seal chunks during capture The only defence against losing an hour is not holding an hour. Files: `src/capture/recorder.py`, `tests/test_crash_recovery.py` 2. Refuse to start without a consent note Friction here is the feature, not a bug to be smoothed off. Files: `src/capture/consent.py` 3. Ship a stitcher that runs on orphaned chunks Recovery has to work with no process alive to help it. Files: `src/capture/stitch.py` ### Done when - [ ] A killed process leaves recoverable sealed chunks - [ ] Recording cannot start without a consent note - [ ] The recording state is visible without hunting for it - [ ] Disk use per hour is measured and documented ### Watch 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. ## M2 · 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. ### Steps 1. Detect speech before transcribing anything The model should never be asked what silence said. Files: `src/transcribe/vad.py`, `tests/test_silence.py` 2. Store the confidence scores with every segment You cannot mark a doubtful segment you did not measure. Files: `src/transcribe/segments.py` 3. Flag runaway repetition by compression ratio A loop is the model's tell and it reads as normal text. Files: `src/transcribe/flags.py` ### Done when - [ ] Silence produces no segments - [ ] Every segment stores its scores and model id - [ ] Flagged segments render marked and skip note generation - [ ] Clicking a segment plays the matching audio ### Watch 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. ## M3 · Speakers are a guess with a name attached Speaker attribution is presented as a cluster somebody labelled, never as a fact the system knows. ### Steps 1. Keep cluster identity separate from display name The algorithm found a cluster; a person supplied a name. Files: `src/speakers/cluster.py` 2. Store and show attribution confidence A wrong attribution in notes is worse than no attribution. Files: `src/speakers/attribute.py`, `tests/test_attribution.py` 3. Make rename survive a re-clustering You will re-run diarisation and you will want the names back. Files: `src/speakers/rename.py` ### Done when - [ ] Clusters are labelled, not named, until a human renames them - [ ] Attribution confidence is stored and visible - [ ] Rename updates every segment and keeps the cluster id - [ ] Split and merge preserve applied names ### Watch 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. ## M4 · 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. ### Steps 1. Number segments in the prompt and demand ids back A citation you did not ask for will not arrive. Files: `src/notes/generate.py`, `src/notes/prompt.py` 2. Resolve every id and drop the unresolvable An invented id is the model telling you it guessed. Files: `src/notes/verify.py`, `tests/test_note_sources.py` 3. Enforce the link with a database constraint Code paths get added later; constraints do not. Files: `src/db/schema.sql` ### Done when - [ ] Notes with unresolvable ids are dropped and counted - [ ] No note can be saved without a source segment - [ ] Clicking a note plays its earliest source - [ ] Flagged segments are excluded from note generation ### Watch 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. ## M5 · Retention, export and actually deleting (production only) Audio expires on a schedule, everything exports, and deleting a meeting removes every trace of it. ### Steps 1. Expire audio, keep transcripts The recording stops being useful long before the notes do. Files: `src/retention/expire.py`, `tests/test_retention.py` 2. Make deletion one operation across every store Rows, files, chunks and index, or it is not deletion. Files: `src/recordings/delete.py`, `tests/test_delete.py` 3. Round trip the export into an empty database An export nobody has reimported is a hope. Files: `src/export/bundle.py` ### Done when - [ ] Audio expires and transcripts survive - [ ] A deleted meeting is unfindable by search - [ ] Export and reimport preserve segment and note counts - [ ] Expiry runs at startup and logs what it removed ### Watch 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. ===== OPERATIONS.md ===== # Operations · Bluedot ## Backup The database first, since transcripts and notes outlive the audio by design. Back up audio only if you have decided your retention period is longer than your backup retention, which is a decision worth writing down rather than discovering. ## Restore Restore the database and confirm the recording count, that every note still resolves to segments, and that a recording whose audio has expired still renders its transcript. A restore that brings back audio you had deliberately expired is a failure, not a success. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Proportion of segments flagged per recording, transcription time against audio duration, expiry job results, and disk used by the audio directory against the free space. A sudden jump in the flagged proportion usually means a microphone changed, not that the meeting got harder. ## Incident checklist The incident that matters is a note somebody acted on that nobody said. The segment ids on every note and the retained scores are what let you go back to the audio and settle it in a minute rather than argue about it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Recording is confirmed to refuse to start without a consent note - [ ] Silence is confirmed to produce no segments - [ ] Flagged segments are confirmed excluded from note generation - [ ] No note is confirmed savable without a resolvable source segment - [ ] A deleted meeting is confirmed unfindable by search ## Launch constraint Do not market omitted Bluedot capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered. ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recordings, segments, speakers and notes. Survives the audio, so back it up. DATABASE_PATH=./data/meetings.db # Required. Recordings and chunk directories. Deleted on expiry, rows retained. AUDIO_DIR=./data/audio # Required. Stored with every segment, because a different model gives different text. WHISPER_MODEL=a local model identifier # Required. After this, audio is deleted and the transcript stays. Shorter is a reasonable default. AUDIO_RETENTION_DAYS=30 # Required. Above this, a segment renders marked and is excluded from note generation. FLAG_NO_SPEECH_PROB=0.6 # Required. Above this, the segment is treated as runaway repetition rather than speech. FLAG_COMPRESSION_RATIO=2.4 # Required. How much audio a crash can cost. Lower means more files and less loss. CHUNK_SECONDS=30
# Bluedot · indie build 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. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a local web interface, nothing leaving the machine | a recording of a meeting is other people's words as well as yours | | Audio | ffmpeg for conversion, fixed length chunks written to disk as you record | a crash at minute 58 should cost seconds, not the meeting | | Transcription | A local Whisper-family model, run after voice activity detection | silence is where speech models invent things, so silence never reaches it | | Storage | SQLite for segments and notes, audio as files beside it | the transcript outlives the audio and the two have different retention | | Notes | Generated only with segment ids, and dropped if the ids do not resolve | a note you cannot trace to the recording is a rumour with a timestamp | ## Before you start Have every one of these ready. The plan assumes them from step one. - [ ] **Python 3.12 or newer** · free - Why: Runs the recorder, the transcription pipeline and the local interface. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **ffmpeg** · free - Why: Every audio conversion and the chunk stitching go through it, including the recovery path. - Get it: Your package manager, or ffmpeg.org. - Verify: ffmpeg -version prints a version and the stitcher runs on a fixture - [ ] **A local Whisper-family model and its weights** · free, 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 - [ ] **A voice activity detector** · free - 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 - [ ] **How you tell people they are being recorded** · free - 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 ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - 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 If one of those is essential to you, that is the reason to keep paying for Bluedot, and the README should say so rather than pretend.
# Build brief · Bluedot The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local meeting recorder that transcribes on your own machine and links every note back to the audio to replace Bluedot. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things decide this build, and both of them are about honesty rather than features. First, recording other people. This app records only when everyone in the room has been told, and it shows a recording state that is visible without looking for it. No silent capture, no bot that joins a call uninvited, no pulling another participant's audio out of a meeting platform in a way that platform has not published a way to do. The consent note is a field on the recording and the recorder refuses to start without it. If that feels like friction, it is the point. Second, the transcript is not a record of what was said, it is a model's best guess, and sometimes it is a fabrication. The audit published at FAccT 2024 as 'Careless Whisper: Speech-to-Text Hallucination Harms' found roughly one percent of transcriptions containing entire phrases or sentences that did not exist in the audio in any form, with 38 percent of those hallucinations carrying explicit harms such as invented violence or false authority, and they clustered in recordings with long non-vocal stretches, a common symptom of aphasia. A meeting recording is mostly silence and cross-talk, which is exactly that condition. So: cut silence before the model ever sees it, keep the per-segment confidence scores and show the weak ones as weak, and never let a note reach the summary without the segment ids it came from. Anyone reading a decision in these notes must be two clicks from hearing it said. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a local web interface, nothing leaving the machine · a recording of a meeting is other people's words as well as yours - Audio: ffmpeg for conversion, fixed length chunks written to disk as you record · a crash at minute 58 should cost seconds, not the meeting - Transcription: A local Whisper-family model, run after voice activity detection · silence is where speech models invent things, so silence never reaches it - Storage: SQLite for segments and notes, audio as files beside it · the transcript outlives the audio and the two have different retention - Notes: Generated only with segment ids, and dropped if the ids do not resolve · a note you cannot trace to the recording is a rumour with a timestamp ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that survives the crash Build: Record to fixed length chunks written and sealed as you go, stitching them on stop. Show a recording indicator that is visible from across the room. Require the consent note before the first chunk is written. Provide a recovery command that stitches an orphaned chunk directory into a recording after a crash. Trap: 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. Done when: Killing the process mid-recording leaves sealed chunks that the recovery command stitches into a playable file, tested by killing it. The recorder refuses to start without a consent note and the note is stored on the recording, asserted. The recording indicator is visible in the interface and in the system tray or title bar while capture is running. Disk use per hour of audio at the configured format is measured and written in the README rather than estimated. Do not build yet: No transcription, speakers, notes or search yet. ### Phase 2 · Transcription that admits what it is unsure about Build: Run voice activity detection first and transcribe only the speech regions, with a small overlap so a word on a boundary is not cut. Store avg_logprob, no_speech_prob and the compression ratio with every segment, along with the model identifier. Flag any segment past the configured thresholds and render it visibly marked rather than as ordinary text. Trap: 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. Trap: 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. Done when: A minute of room tone produces zero segments, tested with a silent fixture. Every segment row carries its three scores and the model identifier. A segment past the flag thresholds renders visibly marked in the interface and is excluded from note generation unless a human clears it. Clicking a segment plays the audio at its start time within a stated tolerance, checked by hand on a real recording. Do not build yet: No speaker attribution, notes or retention work yet. ### Phase 3 · Speakers are a guess with a name attached Build: Cluster segments into speakers and label them A, B, C until a human renames them. Store the attribution confidence per segment and show low confidence attributions differently. Renaming a cluster renames it everywhere while leaving the cluster identity intact, so a later correction of the clustering does not lose the names. Trap: 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. Done when: A transcript renders cluster labels until a human supplies names, and never invents a name from the audio. Attribution confidence is stored per segment and low confidence attributions are visually distinct, checked by review. Renaming a speaker updates every segment and leaves the cluster id unchanged, tested. Splitting or merging clusters is possible without losing the names already applied. Do not build yet: No notes, search, retention or export yet. ### Phase 4 · Notes that point back at the recording Build: Feed the model numbered segments and require it to return, for each note, the list of segment ids that support it. Resolve every returned id against the database, drop any note whose ids do not all resolve, and record what was dropped. Clicking a note in the interface plays the earliest supporting segment. Trap: 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. Done when: A note whose returned segment ids do not resolve is dropped and counted, tested with a fabricated id. Every saved note has at least one row in note_sources, enforced by a constraint rather than by the code path. Clicking any note plays the audio at the start of its earliest source segment. Notes generated from a transcript containing flagged segments exclude those segments, asserted. Do not build yet: No retention, export or deletion work yet. ### Phase 5 · Retention, export and actually deleting Build: Expire audio after a configured period while keeping transcripts and notes, and show the expiry date on every recording. Export a meeting as Markdown with timestamps plus a JSON bundle of segments, speakers and notes. Make deletion one operation covering audio, chunks, segments, speakers, notes and any search index, with a confirmation that names the meeting. Trap: 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. Trap: 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. Done when: Audio past the retention period is deleted while its transcript and notes survive intact, tested at the boundary. Searching for a phrase from a deleted meeting returns nothing, tested after deletion including the index. Export produces Markdown and a JSON bundle that reimport into an empty database with identical segment and note counts. Expiry runs at startup and writes a log line naming what it removed, even when nothing was. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins meetings on a platform, because doing it without every participant knowing is the thing this build refuses, and doing it properly means a platform integration with its own published rules. - Live transcription during the call, which needs streaming inference and a latency budget and is a different engineering problem to a file on disk. - Cross-meeting analytics such as talk time by person over a quarter, which turns a personal recorder into a monitoring tool pointed at colleagues. - Calendar and customer system integrations, which are the reliability work a subscription actually pays for. ### README must contain - That everyone in the recording must be told, that the consent note is required, and that there is no silent mode - The published hallucination findings for speech models, with the study and the date, before any usage instructions - That silence is removed before transcription and what the confidence thresholds are set to - That every note links to segments and that a note whose links do not resolve is discarded rather than shown - How long audio is kept, what deletion removes, and how to verify it with a search
# Agent instructions · Bluedot indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with a local web interface, nothing leaving the machine, ffmpeg for conversion, fixed length chunks written to disk as you record, A local Whisper-family model, run after voice activity detection, SQLite for segments and notes, audio as files beside it, Generated only with segment ids, and dropped if the ids do not resolve. Do not substitute. - Work one phase at a time, in order. Do not start a phase until every "Done when" item of the previous one passes. - Prefer the fewest moving parts that satisfy the step. No frameworks, services or dependencies the plan does not name. - Secrets live in `.env`, never in source or logs. Keep `.env.example` current when a variable is introduced. - Do not invent cryptography, security guarantees, APIs or compliance claims. - Add a focused test for every destructive, security-sensitive or data-loss path the plan names. - Run the project checks before declaring a phase complete, and record any deliberate shortcut in the README under "Tradeoffs". ## Known traps - 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. - 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. - 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. - 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.
# Build plan · Bluedot 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. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Capture that survives the crash Recording writes to disk continuously, shows its state, and cannot start without a consent note. ### Steps 1. Write and seal chunks during capture The only defence against losing an hour is not holding an hour. Files: `src/capture/recorder.py`, `tests/test_crash_recovery.py` 2. Refuse to start without a consent note Friction here is the feature, not a bug to be smoothed off. Files: `src/capture/consent.py` 3. Ship a stitcher that runs on orphaned chunks Recovery has to work with no process alive to help it. Files: `src/capture/stitch.py` ### Done when - [ ] A killed process leaves recoverable sealed chunks - [ ] Recording cannot start without a consent note - [ ] The recording state is visible without hunting for it - [ ] Disk use per hour is measured and documented ### Watch 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. ## Phase 2 · 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. ### Steps 1. Detect speech before transcribing anything The model should never be asked what silence said. Files: `src/transcribe/vad.py`, `tests/test_silence.py` 2. Store the confidence scores with every segment You cannot mark a doubtful segment you did not measure. Files: `src/transcribe/segments.py` 3. Flag runaway repetition by compression ratio A loop is the model's tell and it reads as normal text. Files: `src/transcribe/flags.py` ### Done when - [ ] Silence produces no segments - [ ] Every segment stores its scores and model id - [ ] Flagged segments render marked and skip note generation - [ ] Clicking a segment plays the matching audio ### Watch 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. ## Phase 3 · Speakers are a guess with a name attached Speaker attribution is presented as a cluster somebody labelled, never as a fact the system knows. ### Steps 1. Keep cluster identity separate from display name The algorithm found a cluster; a person supplied a name. Files: `src/speakers/cluster.py` 2. Store and show attribution confidence A wrong attribution in notes is worse than no attribution. Files: `src/speakers/attribute.py`, `tests/test_attribution.py` 3. Make rename survive a re-clustering You will re-run diarisation and you will want the names back. Files: `src/speakers/rename.py` ### Done when - [ ] Clusters are labelled, not named, until a human renames them - [ ] Attribution confidence is stored and visible - [ ] Rename updates every segment and keeps the cluster id - [ ] Split and merge preserve applied names ### Watch 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. ## Phase 4 · 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. ### Steps 1. Number segments in the prompt and demand ids back A citation you did not ask for will not arrive. Files: `src/notes/generate.py`, `src/notes/prompt.py` 2. Resolve every id and drop the unresolvable An invented id is the model telling you it guessed. Files: `src/notes/verify.py`, `tests/test_note_sources.py` 3. Enforce the link with a database constraint Code paths get added later; constraints do not. Files: `src/db/schema.sql` ### Done when - [ ] Notes with unresolvable ids are dropped and counted - [ ] No note can be saved without a source segment - [ ] Clicking a note plays its earliest source - [ ] Flagged segments are excluded from note generation ### Watch 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. ## Not in this build - A meeting bot that joins calls. - Live transcription during the call. - Cross-meeting analytics about colleagues. - Calendar and customer system integrations. ## After v1, if you want it - 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
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recordings, segments, speakers and notes. Survives the audio, so back it up. DATABASE_PATH=./data/meetings.db # Required. Recordings and chunk directories. Deleted on expiry, rows retained. AUDIO_DIR=./data/audio # Required. Stored with every segment, because a different model gives different text. WHISPER_MODEL=a local model identifier # Required. After this, audio is deleted and the transcript stays. Shorter is a reasonable default. AUDIO_RETENTION_DAYS=30 # Required. Above this, a segment renders marked and is excluded from note generation. FLAG_NO_SPEECH_PROB=0.6 # Required. Above this, the segment is treated as runaway repetition rather than speech. FLAG_COMPRESSION_RATIO=2.4 # Required. How much audio a crash can cost. Lower means more files and less loss. CHUNK_SECONDS=30
# Bluedot · product brief ## Problem The visible meeting recorder loop is buildable, but a credible replacement needs more than the first screen. Bluedot earns its keep through capture, integrations, reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A meeting recorder that cannot start without a consent note, never asks a speech model what silence said, marks the segments it is unsure about, and refuses to save a note that does not point at the recording. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - FFmpeg - local Whisper model or optional API key in .env ## Explicit non-goals for v1 - 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 ## Success criteria - Recording is confirmed to refuse to start without a consent note - Silence is confirmed to produce no segments - Flagged segments are confirmed excluded from note generation - No note is confirmed savable without a resolvable source segment - A deleted meeting is confirmed unfindable by search
# Build brief · Bluedot The one-shot brief this plan expands. `BUILD_PLAN.md` (or `MILESTONES.md`) is the same sequence broken into steps and checks; where the two disagree, the plan wins. Build me a local meeting recorder that transcribes on your own machine and links every note back to the audio to replace Bluedot. Build it in phases, in the order below. Do not write the whole thing in one pass. Finish a phase, run its "Done when" check, fix what fails, and only then start the next phase. ### Rule zero Two things decide this build, and both of them are about honesty rather than features. First, recording other people. This app records only when everyone in the room has been told, and it shows a recording state that is visible without looking for it. No silent capture, no bot that joins a call uninvited, no pulling another participant's audio out of a meeting platform in a way that platform has not published a way to do. The consent note is a field on the recording and the recorder refuses to start without it. If that feels like friction, it is the point. Second, the transcript is not a record of what was said, it is a model's best guess, and sometimes it is a fabrication. The audit published at FAccT 2024 as 'Careless Whisper: Speech-to-Text Hallucination Harms' found roughly one percent of transcriptions containing entire phrases or sentences that did not exist in the audio in any form, with 38 percent of those hallucinations carrying explicit harms such as invented violence or false authority, and they clustered in recordings with long non-vocal stretches, a common symptom of aphasia. A meeting recording is mostly silence and cross-talk, which is exactly that condition. So: cut silence before the model ever sees it, keep the per-segment confidence scores and show the weak ones as weak, and never let a note reach the summary without the segment ids it came from. Anyone reading a decision in these notes must be two clicks from hearing it said. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with a local web interface, nothing leaving the machine · a recording of a meeting is other people's words as well as yours - Audio: ffmpeg for conversion, fixed length chunks written to disk as you record · a crash at minute 58 should cost seconds, not the meeting - Transcription: A local Whisper-family model, run after voice activity detection · silence is where speech models invent things, so silence never reaches it - Storage: SQLite for segments and notes, audio as files beside it · the transcript outlives the audio and the two have different retention - Notes: Generated only with segment ids, and dropped if the ids do not resolve · a note you cannot trace to the recording is a rumour with a timestamp ### Data model (create this before Phase 1) - 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. ### Phase 1 · Capture that survives the crash Build: Record to fixed length chunks written and sealed as you go, stitching them on stop. Show a recording indicator that is visible from across the room. Require the consent note before the first chunk is written. Provide a recovery command that stitches an orphaned chunk directory into a recording after a crash. Trap: 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. Done when: Killing the process mid-recording leaves sealed chunks that the recovery command stitches into a playable file, tested by killing it. The recorder refuses to start without a consent note and the note is stored on the recording, asserted. The recording indicator is visible in the interface and in the system tray or title bar while capture is running. Disk use per hour of audio at the configured format is measured and written in the README rather than estimated. Do not build yet: No transcription, speakers, notes or search yet. ### Phase 2 · Transcription that admits what it is unsure about Build: Run voice activity detection first and transcribe only the speech regions, with a small overlap so a word on a boundary is not cut. Store avg_logprob, no_speech_prob and the compression ratio with every segment, along with the model identifier. Flag any segment past the configured thresholds and render it visibly marked rather than as ordinary text. Trap: 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. Trap: 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. Done when: A minute of room tone produces zero segments, tested with a silent fixture. Every segment row carries its three scores and the model identifier. A segment past the flag thresholds renders visibly marked in the interface and is excluded from note generation unless a human clears it. Clicking a segment plays the audio at its start time within a stated tolerance, checked by hand on a real recording. Do not build yet: No speaker attribution, notes or retention work yet. ### Phase 3 · Speakers are a guess with a name attached Build: Cluster segments into speakers and label them A, B, C until a human renames them. Store the attribution confidence per segment and show low confidence attributions differently. Renaming a cluster renames it everywhere while leaving the cluster identity intact, so a later correction of the clustering does not lose the names. Trap: 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. Done when: A transcript renders cluster labels until a human supplies names, and never invents a name from the audio. Attribution confidence is stored per segment and low confidence attributions are visually distinct, checked by review. Renaming a speaker updates every segment and leaves the cluster id unchanged, tested. Splitting or merging clusters is possible without losing the names already applied. Do not build yet: No notes, search, retention or export yet. ### Phase 4 · Notes that point back at the recording Build: Feed the model numbered segments and require it to return, for each note, the list of segment ids that support it. Resolve every returned id against the database, drop any note whose ids do not all resolve, and record what was dropped. Clicking a note in the interface plays the earliest supporting segment. Trap: 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. Done when: A note whose returned segment ids do not resolve is dropped and counted, tested with a fabricated id. Every saved note has at least one row in note_sources, enforced by a constraint rather than by the code path. Clicking any note plays the audio at the start of its earliest source segment. Notes generated from a transcript containing flagged segments exclude those segments, asserted. Do not build yet: No retention, export or deletion work yet. ### Phase 5 · Retention, export and actually deleting Build: Expire audio after a configured period while keeping transcripts and notes, and show the expiry date on every recording. Export a meeting as Markdown with timestamps plus a JSON bundle of segments, speakers and notes. Make deletion one operation covering audio, chunks, segments, speakers, notes and any search index, with a confirmation that names the meeting. Trap: 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. Trap: 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. Done when: Audio past the retention period is deleted while its transcript and notes survive intact, tested at the boundary. Searching for a phrase from a deleted meeting returns nothing, tested after deletion including the index. Export produces Markdown and a JSON bundle that reimport into an empty database with identical segment and note counts. Expiry runs at startup and writes a log line naming what it removed, even when nothing was. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins meetings on a platform, because doing it without every participant knowing is the thing this build refuses, and doing it properly means a platform integration with its own published rules. - Live transcription during the call, which needs streaming inference and a latency budget and is a different engineering problem to a file on disk. - Cross-meeting analytics such as talk time by person over a quarter, which turns a personal recorder into a monitoring tool pointed at colleagues. - Calendar and customer system integrations, which are the reliability work a subscription actually pays for. ### README must contain - That everyone in the recording must be told, that the consent note is required, and that there is no silent mode - The published hallucination findings for speech models, with the study and the date, before any usage instructions - That silence is removed before transcription and what the confidence thresholds are set to - That every note links to segments and that a note whose links do not resolve is discarded rather than shown - How long audio is kept, what deletion removes, and how to verify it with a search
# Architecture · Bluedot ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with a local web interface, nothing leaving the machine | a recording of a meeting is other people's words as well as yours | | Audio | ffmpeg for conversion, fixed length chunks written to disk as you record | a crash at minute 58 should cost seconds, not the meeting | | Transcription | A local Whisper-family model, run after voice activity detection | silence is where speech models invent things, so silence never reaches it | | Storage | SQLite for segments and notes, audio as files beside it | the transcript outlives the audio and the two have different retention | | Notes | Generated only with segment ids, and dropped if the ids do not resolve | a note you cannot trace to the recording is a rumour with a timestamp | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Capture | Chunked writing, sealing, stitching and recovery. | Any recorder, provided it never holds a meeting in memory. | | Speech detection | Finding the regions that contain voice. | Any detector, provided silence never reaches the model. | | Transcription | Segments, scores and flags. | Any local model, provided the scores are stored with the text. | | Attribution | Clusters, confidences and human names. | Any diariser, provided a label is never presented as a fact. | | Notes | Generation, id resolution and the source links. | Any model, provided a note without resolvable sources is dropped. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATABASE_PATH` · required · Recordings, segments, speakers and notes. Survives the audio, so back it up. - `AUDIO_DIR` · required · Recordings and chunk directories. Deleted on expiry, rows retained. - `WHISPER_MODEL` · required · Stored with every segment, because a different model gives different text. - `AUDIO_RETENTION_DAYS` · required · After this, audio is deleted and the transcript stays. Shorter is a reasonable default. - `FLAG_NO_SPEECH_PROB` · required · Above this, a segment renders marked and is excluded from note generation. - `FLAG_COMPRESSION_RATIO` · required · Above this, the segment is treated as runaway repetition rather than speech. - `CHUNK_SECONDS` · required · How much audio a crash can cost. Lower means more files and less loss. ## Production baseline - Security: least privilege, input validation at every boundary, secret redaction in logs, rate limits on abuse-prone paths, no invented security primitives. - Data: explicit schema and migrations, transactional writes where integrity matters, backup and restore procedures that have been exercised. - Integrations: adapters around third-party providers, idempotent webhook or job processing, bounded retries, timeouts. - Observability: structured logs with request or operation ids, an error-tracking hook, and health and readiness checks where a server exists. - Quality: unit tests for domain rules, integration tests at module boundaries, one end-to-end test of the critical path. ## Decision records For each dependency in the stack table, keep a short note: why it was chosen, its failure mode, and how it is replaced. Do not add infrastructure until a requirement in `PRODUCT.md` justifies it.
# Agent instructions · Bluedot product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with a local web interface, nothing leaving the machine, ffmpeg for conversion, fixed length chunks written to disk as you record, A local Whisper-family model, run after voice activity detection, SQLite for segments and notes, audio as files beside it, Generated only with segment ids, and dropped if the ids do not resolve. - Implement milestone by milestone from `MILESTONES.md`; keep each change reviewable and leave the application runnable at every commit. - Treat authentication, payments, encryption, imports, webhooks and destructive actions as high-risk boundaries when present. - Never invent cryptography or silently weaken a requirement to make a check pass. - Put every external service behind an interface with a deterministic fake for tests. - Add migrations and rollback or recovery notes for every persistent data change. - Log useful operational context without credentials, tokens, passwords or personal data. - Update documentation and run every check before completing a milestone. ## Known traps - 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. - 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. - 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. - 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. - 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.
# Delivery milestones · Bluedot Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Capture that survives the crash Recording writes to disk continuously, shows its state, and cannot start without a consent note. ### Steps 1. Write and seal chunks during capture The only defence against losing an hour is not holding an hour. Files: `src/capture/recorder.py`, `tests/test_crash_recovery.py` 2. Refuse to start without a consent note Friction here is the feature, not a bug to be smoothed off. Files: `src/capture/consent.py` 3. Ship a stitcher that runs on orphaned chunks Recovery has to work with no process alive to help it. Files: `src/capture/stitch.py` ### Done when - [ ] A killed process leaves recoverable sealed chunks - [ ] Recording cannot start without a consent note - [ ] The recording state is visible without hunting for it - [ ] Disk use per hour is measured and documented ### Watch 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. ## M2 · 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. ### Steps 1. Detect speech before transcribing anything The model should never be asked what silence said. Files: `src/transcribe/vad.py`, `tests/test_silence.py` 2. Store the confidence scores with every segment You cannot mark a doubtful segment you did not measure. Files: `src/transcribe/segments.py` 3. Flag runaway repetition by compression ratio A loop is the model's tell and it reads as normal text. Files: `src/transcribe/flags.py` ### Done when - [ ] Silence produces no segments - [ ] Every segment stores its scores and model id - [ ] Flagged segments render marked and skip note generation - [ ] Clicking a segment plays the matching audio ### Watch 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. ## M3 · Speakers are a guess with a name attached Speaker attribution is presented as a cluster somebody labelled, never as a fact the system knows. ### Steps 1. Keep cluster identity separate from display name The algorithm found a cluster; a person supplied a name. Files: `src/speakers/cluster.py` 2. Store and show attribution confidence A wrong attribution in notes is worse than no attribution. Files: `src/speakers/attribute.py`, `tests/test_attribution.py` 3. Make rename survive a re-clustering You will re-run diarisation and you will want the names back. Files: `src/speakers/rename.py` ### Done when - [ ] Clusters are labelled, not named, until a human renames them - [ ] Attribution confidence is stored and visible - [ ] Rename updates every segment and keeps the cluster id - [ ] Split and merge preserve applied names ### Watch 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. ## M4 · 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. ### Steps 1. Number segments in the prompt and demand ids back A citation you did not ask for will not arrive. Files: `src/notes/generate.py`, `src/notes/prompt.py` 2. Resolve every id and drop the unresolvable An invented id is the model telling you it guessed. Files: `src/notes/verify.py`, `tests/test_note_sources.py` 3. Enforce the link with a database constraint Code paths get added later; constraints do not. Files: `src/db/schema.sql` ### Done when - [ ] Notes with unresolvable ids are dropped and counted - [ ] No note can be saved without a source segment - [ ] Clicking a note plays its earliest source - [ ] Flagged segments are excluded from note generation ### Watch 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. ## M5 · Retention, export and actually deleting (production only) Audio expires on a schedule, everything exports, and deleting a meeting removes every trace of it. ### Steps 1. Expire audio, keep transcripts The recording stops being useful long before the notes do. Files: `src/retention/expire.py`, `tests/test_retention.py` 2. Make deletion one operation across every store Rows, files, chunks and index, or it is not deletion. Files: `src/recordings/delete.py`, `tests/test_delete.py` 3. Round trip the export into an empty database An export nobody has reimported is a hope. Files: `src/export/bundle.py` ### Done when - [ ] Audio expires and transcripts survive - [ ] A deleted meeting is unfindable by search - [ ] Export and reimport preserve segment and note counts - [ ] Expiry runs at startup and logs what it removed ### Watch 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.
# Operations · Bluedot ## Backup The database first, since transcripts and notes outlive the audio by design. Back up audio only if you have decided your retention period is longer than your backup retention, which is a decision worth writing down rather than discovering. ## Restore Restore the database and confirm the recording count, that every note still resolves to segments, and that a recording whose audio has expired still renders its transcript. A restore that brings back audio you had deliberately expired is a failure, not a success. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Proportion of segments flagged per recording, transcription time against audio duration, expiry job results, and disk used by the audio directory against the free space. A sudden jump in the flagged proportion usually means a microphone changed, not that the meeting got harder. ## Incident checklist The incident that matters is a note somebody acted on that nobody said. The segment ids on every note and the retained scores are what let you go back to the audio and settle it in a minute rather than argue about it. 1. Contain the issue without destroying evidence or user data. 2. Record the timeline and affected scope. 3. Rotate exposed secrets and revoke compromised sessions or credentials. 4. Restore from a verified backup when needed. 5. Document the root cause, the remediation and the regression test. ## Release gate - [ ] Recording is confirmed to refuse to start without a consent note - [ ] Silence is confirmed to produce no segments - [ ] Flagged segments are confirmed excluded from note generation - [ ] No note is confirmed savable without a resolvable source segment - [ ] A deleted meeting is confirmed unfindable by search ## Launch constraint Do not market omitted Bluedot capabilities as implemented. The non-goals in `PRODUCT.md` remain user-visible limitations until they are deliberately delivered.
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Recordings, segments, speakers and notes. Survives the audio, so back it up. DATABASE_PATH=./data/meetings.db # Required. Recordings and chunk directories. Deleted on expiry, rows retained. AUDIO_DIR=./data/audio # Required. Stored with every segment, because a different model gives different text. WHISPER_MODEL=a local model identifier # Required. After this, audio is deleted and the transcript stays. Shorter is a reasonable default. AUDIO_RETENTION_DAYS=30 # Required. Above this, a segment renders marked and is excluded from note generation. FLAG_NO_SPEECH_PROB=0.6 # Required. Above this, the segment is treated as runaway repetition rather than speech. FLAG_COMPRESSION_RATIO=2.4 # Required. How much audio a crash can cost. Lower means more files and less loss. CHUNK_SECONDS=30
$ choose a build depth, inspect the files, then open the complete pack in your agent
Bluedot: Customers pay for automatic capture, dependable speaker handling, search across calls, and notes arriving without manual file wrangling.
xcross-call team analytics
xmeeting-bot auto-join
xlive multi-speaker accuracy
xcalendar and CRM integrations
Bluedot pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | 5 meetings total; maximum 1 hour per recording. |
| basic | $18/user | $14/user | Unlimited audio-only meetings and storage; maximum 1 hour per recording. |
| pro | $25/user | $20/user | Unlimited audio and video meetings, unlimited duration, storage, and imports. |
| business | $39/user | $32/user | Unlimited meetings, duration, storage, and imports, plus CRM/ATS connections and SSO/SCIM. |
| flat rate | custom | — | Custom workspace-wide package; public numeric limits and price are not stated. |
free tier5 meetings total, with each recording capped at 1 hour.
billingmonthly + annual; annual discounts vary by tier
hidden costsEvery workspace is billed separately and added recorder seats are prorated. After cancellation, recordings can be restored for only 1 month before permanent deletion.
verified 2026-08-14 · source ↗
Vibecode Bluedot
Kinda. The core of Bluedot is buildable in a weekend with the prompt on this page, but there are real gaps: cross-call team analytics, meeting-bot auto-join. Read the honest list above before committing.
How much does Bluedot cost?
Bluedot's pricing is usage-based or varies by plan · The official pricing URL is recorded, but a stable current amount was not safely recoverable in this pass. Do not publish a number until rechecked..
What do I lose by replacing Bluedot?
Honestly: cross-call team analytics; meeting-bot auto-join; live multi-speaker accuracy; calendar and CRM integrations. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Bluedot?
Yes: whisper.cpp (Local speech-to-text engine suitable for private transcription.). Using prior art is also vibecoding; the prompt is for when you want it exactly your way.