Vibecode Circleback
track this build4 phases, 12 steps, beginner friendly0%The visible meeting notes loop is buildable, but a credible replacement needs more than the first screen. Circleback 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 Circleback. 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 ===== # Circleback · indie build A local meeting notes pipeline whose hard problems are a speech model that invents text during silence, speaker labels that are wrong often enough to matter, and notes that must be traceable to audio because people act on them. Estimated effort: **multi-day**. 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 with a small local web interface and a background worker | transcription is a long job and the interface should stay usable while it runs | | Audio | FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else | every downstream model expects it and half the failures are a container nobody normalised | | Transcription | A local speech model with voice activity detection as a pre-pass | long non-vocal stretches are the documented predictor of invented text | | Diarisation | A maintained open pipeline, with its labels treated as suggestions | the best open results on distant-microphone meeting audio still misattribute a large share of speech | | Notes | A model pass that must return segment identifiers with every claim | an uncited sentence is deleted, which is the only defence against a confident summary | ## 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 interface and the transcription worker. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg** · free - Why: Every input is normalised to single-channel sixteen kilohertz before anything else touches it, and this is where half the format problems disappear. - Get it: Your package manager, or ffmpeg.org. - Verify: ffmpeg -version prints a version and a test file converts - [ ] **A local speech recognition model with voice activity detection** · free, at the cost of disk and time - Why: Running it locally keeps recordings of colleagues on your machine, and the detection pre-pass is the mitigation for the documented failure mode. - Get it: A maintained local implementation with a bundled detector and per-segment scores exposed. - Verify: A file with silence in the middle produces no segments for it - [ ] **A maintained speaker diarisation pipeline** · free, with an account for the model where required - Why: Attribution is a separate model from transcription, and the maintained pipelines publish their error rates so you can state them honestly. - Get it: A current open pipeline with published benchmark numbers, accepting its licence terms. - Verify: A two-speaker recording produces two labels with sensible boundaries - [ ] **Your consent and retention policy** · free - Why: Both are legal questions before they are product questions, and the build enforces whatever you write down here. - Get it: Decide what participants are told, how the telling is recorded, and how long audio and transcripts are kept. - Verify: Both appear in the README and are enforced by code rather than by intention ## 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 bot that joins calls automatically. - Live transcription during the meeting. - Cross-meeting analytics about named people. - Calendar and CRM 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 Circleback, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Circleback 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 private meeting notes pipeline where every line of the notes points at the second of audio it came from to replace Circleback. 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 rules, and they both exist because this tool produces evidence about what people said. Recording is regulated and the rules follow the participants, not the host. California Penal Code section 632 makes it an offence to record a confidential communication without the consent of all parties, with a fine of up to 2,500 dollars per violation, and roughly a dozen US states require all-party consent for at least some categories of recording, with authoritative lists disagreeing on the exact membership. In the UK the Information Commissioner's October 2023 guidance on monitoring workers says you must identify a lawful basis before you start, that audio recording is more intrusive than visual and needs greater justification, and that a recording capability should be off by default. A transcript will also incidentally capture health, religion or trade union membership, which needs its own condition. So the recorder starts stopped, consent is announced and recorded as a row before audio is kept, and there is no hidden mode and no bot that joins a call uninvited. Check your own jurisdiction; the citations above are a starting point, not advice. Second, the transcript is not what was said, and the notes are not the transcript. The peer- reviewed FAccT 2024 study of Whisper found hallucinated content in about 1.4 percent of transcriptions, most strongly predicted by long non-vocal stretches inside a segment, and OpenAI's own model card says the predictions 'may include texts that are not actually spoken in the audio input'. So every note and every action item stores the segment identifiers it came from, anything the model produces without a citable segment is dropped rather than shown, and the audio for any line is one click away. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface and a background worker · transcription is a long job and the interface should stay usable while it runs - Audio: FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else · every downstream model expects it and half the failures are a container nobody normalised - Transcription: A local speech model with voice activity detection as a pre-pass · long non-vocal stretches are the documented predictor of invented text - Diarisation: A maintained open pipeline, with its labels treated as suggestions · the best open results on distant-microphone meeting audio still misattribute a large share of speech - Notes: A model pass that must return segment identifiers with every claim · an uncited sentence is deleted, which is the only defence against a confident summary ### Data model (create this before Phase 1) - meetings(id, title, started_at, duration_s, source_path, normalised_path, consent_recorded_at, consent_note, retention_until, state) - segments(id, meeting_id, start_ms, end_ms, text, avg_logprob, no_speech_prob, compression_ratio, flagged_reason) · one row per transcribed segment with the numbers that predict a bad one - speech_regions(meeting_id, start_ms, end_ms) · from voice activity detection, before transcription touches the audio - speakers(id, meeting_id, label, display_name, confirmed_by_user) · a guess until somebody confirms it - segment_speakers(segment_id, speaker_id, overlap_ratio) · attribution kept separately from the words - notes(id, meeting_id, kind, text, segment_ids_json, model_id, created_at) · kind is summary, decision or action; the segment list is mandatory - actions(id, note_id, owner_speaker_id, due_text, confidence, accepted_by_user) · nothing is assigned to a person until a human accepts it - Nothing in the notes table can exist without segment identifiers, and the schema enforces it rather than the prompt. ### Phase 1 · Consent first, audio second Build: Importing or recording a meeting requires a consent note naming who was told and how, stored as a row with a timestamp before the audio file is kept. Normalise with FFmpeg to single-channel sixteen kilohertz. Set a retention date at import. There is no automatic joining of calls. Trap: Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. Done when: Audio cannot be stored without a consent row, enforced by the schema and tested by attempting it. Every meeting has a retention date set at import and a job deletes the audio when it passes, tested with a backdated row. Normalisation produces single-channel sixteen kilohertz output for every supported input, tested with three container formats. The application has no code path that joins a call or records without an explicit start. Do not build yet: No transcription, diarisation, notes or search yet. ### Phase 2 · A transcript that does not invent Build: Run voice activity detection first and transcribe speech regions rather than the whole file, because long non-vocal stretches are what the research found predicts invented text. Turn off conditioning on previous text to break repetition loops, accepting the documented cost that wording is less consistent across window boundaries. Store the average log probability, the no-speech probability and the compression ratio for every segment, and flag the ones that look wrong. Remember that the silence threshold is combined with the log probability, so raising it alone will not suppress a confidently invented segment. Trap: Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. Done when: A file with two minutes of silence in the middle produces no segments for that period, tested with a constructed file. A segment repeating the same phrase is flagged by its compression ratio and shown as doubtful, tested. Every segment stores its log probability, no-speech probability and compression ratio. Transcribing the same file twice produces the same segment boundaries, given fixed settings, tested. Do not build yet: No diarisation, notes or search yet. ### Phase 3 · Speaker labels that admit what they are Build: Run a maintained diarisation pipeline and attach speakers to segments with an overlap ratio. Present labels as unnamed speakers until somebody names them. Every segment plays its own audio from the transcript view. Show the error rate you should expect for your microphone setup. Trap: Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. Done when: Speakers appear as unnamed labels until named by the user, and the naming is stored as confirmed. Every segment in the transcript plays its own audio range, tested by clicking one. Overlapping speech produces a segment attributed to more than one speaker with ratios rather than an arbitrary winner. The interface states, next to the speaker list, that these labels are estimates and roughly how often they are wrong for this kind of audio. Do not build yet: No notes, actions or search yet. ### Phase 4 · Notes that cite the transcript Build: Send the transcript with segment identifiers and require the model to return each claim with the identifiers supporting it. Drop any returned item whose identifiers are missing or do not exist. Action items carry a suggested owner and a confidence, and nothing is assigned to a person until the user accepts it. Show each note with its citations expanded in place. Trap: A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. Done when: A note returned without valid segment identifiers is dropped and counted, tested with a fake model returning uncited claims. Every displayed note expands to the segment text and plays the audio. Action items are unassigned until accepted, and accepting one is recorded with a timestamp. Regenerating notes for a meeting creates a new set rather than overwriting the accepted items. Do not build yet: No search, export or redaction yet. ### Phase 5 · Search, redaction and leaving Build: Search across transcripts and notes with results linked to the segment and its audio. Delete a meeting and everything derived from it in one operation, including audio, segments, notes and search entries. Redact a passage so the text is replaced and the audio range is muted in place. Export a meeting as Markdown plus a JSON file containing the segments and citations. Trap: Deleting a meeting row and leaving the derived rows behind means the transcript is still searchable and the audio is still on disk, which turns a deletion request into a false statement that you honoured it. Make deletion one operation over every table plus the files, and test it by searching for a distinctive phrase afterwards. Done when: Deleting a meeting removes audio, segments, speakers, notes, actions and search entries, verified by searching for a phrase from it afterwards and finding nothing. Redacting a passage replaces the text and mutes that audio range in the stored file, tested by listening. Export produces Markdown a person can read and JSON containing segments and citations, and the JSON reimports into an empty database. Every search result links to the segment and plays it. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins calls automatically, which is the feature most likely to record someone who was never asked and which every platform gates differently. - Live transcription during the meeting, which is a different latency problem and makes the silence handling harder rather than easier. - Cross-meeting analytics about people, such as talk time by named participant, which turns notes into workplace monitoring with speaker labels that are often wrong. - Calendar and CRM integrations, which are most of the paid product and none of the hard problems here. ### README must contain - That consent is recorded before audio is kept, with the California and UK citations that motivated it and a note to check your own jurisdiction - That speech models can produce text nobody said, the published rate and condition, and what this build does about it - That speaker labels are estimates and roughly how often they are wrong on distant-microphone audio - That conditioning on previous text is disabled, and the wording inconsistency that costs across window boundaries - How deletion and redaction work, exactly what each removes, and how to verify it afterwards ===== AGENTS.md ===== # Agent instructions · Circleback indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with a small local web interface and a background worker, FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else, A local speech model with voice activity detection as a pre-pass, A maintained open pipeline, with its labels treated as suggestions, A model pass that must return segment identifiers with every claim. 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 - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. ===== BUILD_PLAN.md ===== # Build plan · Circleback A local meeting notes pipeline whose hard problems are a speech model that invents text during silence, speaker labels that are wrong often enough to matter, and notes that must be traceable to audio because people act on them. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Consent first, audio second No audio is retained until a consent record exists, and every recording states its retention. ### Steps 1. Make consent a row the audio depends on Not a checkbox that can be added afterwards. Files: `src/meetings.py`, `src/db/schema.sql` 2. Normalise every input through FFmpeg first One sample rate and one channel removes a category of later failures. Files: `src/audio.py`, `tests/test_audio.py` 3. Set retention at import and enforce it Recordings of colleagues should have an expiry date. Files: `src/retention.py` ### Done when - [ ] Audio without a consent row is refused - [ ] Retention is set at import and enforced by a job - [ ] Three container formats normalise correctly - [ ] No code path records or joins without an explicit start ### Watch out - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. ## Phase 2 · A transcript that does not invent Silence produces nothing, repetition loops are caught, and doubtful segments are marked. ### Steps 1. Detect speech before transcribing The pre-pass removes the exact condition that produces invented text. Files: `src/vad.py`, `tests/test_silence.py` 2. Break the repetition loop at the setting level And state the trade-off in the README rather than hiding it. Files: `src/transcribe.py` 3. Keep the per-segment scores and flag on them A doubtful segment should look doubtful in the interface. Files: `src/transcribe.py`, `src/templates/transcript.html` ### Done when - [ ] Silence produces no segments - [ ] A repetition loop is flagged automatically - [ ] Every segment stores its three scores - [ ] Two runs of one file agree on boundaries ### Watch out - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. ## Phase 3 · Speaker labels that admit what they are Attribution is a suggestion the user confirms, and every label can be checked against the audio. ### Steps 1. Keep labels anonymous until confirmed A name is a claim about who said something. Files: `src/diarise.py`, `src/templates/speakers.html` 2. Store overlap ratios rather than a winner People talk over each other and the transcript should show it. Files: `src/diarise.py` 3. Make every segment playable from the transcript Checking a line should take one click. Files: `src/templates/transcript.html` ### Done when - [ ] Speakers are unnamed until a user names them - [ ] Every segment plays its own audio - [ ] Overlaps produce ratios, not a single winner - [ ] The interface states that labels are estimates ### Watch out - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. ## Phase 4 · Notes that cite the transcript Every summary line, decision and action item points at the segments it came from, or it is not shown. ### Steps 1. Require and verify segment identifiers Verify they exist, because a model will invent an identifier too. Files: `src/notes.py`, `tests/test_citations.py` 2. Drop uncited claims rather than caveating them A caveat is read as confidence by the third reader. Files: `src/notes.py` 3. Leave owners unassigned until a human accepts Assigning work from a guess about who spoke is how this goes wrong. Files: `src/actions.py`, `src/templates/notes.html` ### Done when - [ ] Uncited claims are dropped and counted - [ ] Every note expands to its segments and audio - [ ] Actions are unassigned until accepted - [ ] Regenerating notes preserves accepted items ### Watch out - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. ## Not in this build - A bot that joins calls automatically. - Live transcription during the meeting. - Cross-meeting analytics about named people. - Calendar and CRM integrations. ## After v1, if you want it - A glossary of names and jargon supplied to the transcriber, which fixes more errors than a bigger model does - Topic segmentation within a meeting, once citations are trusted enough to hang it on - A weekly file of open action items, generated locally and written to disk rather than emailed ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Meetings, segments, speakers and notes. Contains what colleagues said, so treat it accordingly. DATABASE_PATH=./data/meetings.db # Required. Normalised audio. Deleted at the retention date along with the original. AUDIO_DIR=./data/audio # Required. Default retention set at import. Can be shortened per meeting, never silently extended. RETENTION_DAYS=90 # Required. Stored per meeting, because a different model gives a different transcript. ASR_MODEL=a local speech model identifier # Required. Used only for the notes pass. If hosted, the transcript leaves the machine and the README must say so. NOTES_MODEL=a local or hosted model identifier # Required. Non-vocal stretches longer than this are cut before transcription. VAD_MIN_SILENCE_MS=2000 # Required. Above this a segment is flagged as probably repeating rather than transcribing. FLAG_COMPRESSION_RATIO=2.4
You are building a lean indie version of Circleback. 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 ===== # Circleback · indie build A local meeting notes pipeline whose hard problems are a speech model that invents text during silence, speaker labels that are wrong often enough to matter, and notes that must be traceable to audio because people act on them. Estimated effort: **multi-day**. 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 with a small local web interface and a background worker | transcription is a long job and the interface should stay usable while it runs | | Audio | FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else | every downstream model expects it and half the failures are a container nobody normalised | | Transcription | A local speech model with voice activity detection as a pre-pass | long non-vocal stretches are the documented predictor of invented text | | Diarisation | A maintained open pipeline, with its labels treated as suggestions | the best open results on distant-microphone meeting audio still misattribute a large share of speech | | Notes | A model pass that must return segment identifiers with every claim | an uncited sentence is deleted, which is the only defence against a confident summary | ## 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 interface and the transcription worker. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg** · free - Why: Every input is normalised to single-channel sixteen kilohertz before anything else touches it, and this is where half the format problems disappear. - Get it: Your package manager, or ffmpeg.org. - Verify: ffmpeg -version prints a version and a test file converts - [ ] **A local speech recognition model with voice activity detection** · free, at the cost of disk and time - Why: Running it locally keeps recordings of colleagues on your machine, and the detection pre-pass is the mitigation for the documented failure mode. - Get it: A maintained local implementation with a bundled detector and per-segment scores exposed. - Verify: A file with silence in the middle produces no segments for it - [ ] **A maintained speaker diarisation pipeline** · free, with an account for the model where required - Why: Attribution is a separate model from transcription, and the maintained pipelines publish their error rates so you can state them honestly. - Get it: A current open pipeline with published benchmark numbers, accepting its licence terms. - Verify: A two-speaker recording produces two labels with sensible boundaries - [ ] **Your consent and retention policy** · free - Why: Both are legal questions before they are product questions, and the build enforces whatever you write down here. - Get it: Decide what participants are told, how the telling is recorded, and how long audio and transcripts are kept. - Verify: Both appear in the README and are enforced by code rather than by intention ## 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 bot that joins calls automatically. - Live transcription during the meeting. - Cross-meeting analytics about named people. - Calendar and CRM 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 Circleback, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Circleback 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 private meeting notes pipeline where every line of the notes points at the second of audio it came from to replace Circleback. 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 rules, and they both exist because this tool produces evidence about what people said. Recording is regulated and the rules follow the participants, not the host. California Penal Code section 632 makes it an offence to record a confidential communication without the consent of all parties, with a fine of up to 2,500 dollars per violation, and roughly a dozen US states require all-party consent for at least some categories of recording, with authoritative lists disagreeing on the exact membership. In the UK the Information Commissioner's October 2023 guidance on monitoring workers says you must identify a lawful basis before you start, that audio recording is more intrusive than visual and needs greater justification, and that a recording capability should be off by default. A transcript will also incidentally capture health, religion or trade union membership, which needs its own condition. So the recorder starts stopped, consent is announced and recorded as a row before audio is kept, and there is no hidden mode and no bot that joins a call uninvited. Check your own jurisdiction; the citations above are a starting point, not advice. Second, the transcript is not what was said, and the notes are not the transcript. The peer- reviewed FAccT 2024 study of Whisper found hallucinated content in about 1.4 percent of transcriptions, most strongly predicted by long non-vocal stretches inside a segment, and OpenAI's own model card says the predictions 'may include texts that are not actually spoken in the audio input'. So every note and every action item stores the segment identifiers it came from, anything the model produces without a citable segment is dropped rather than shown, and the audio for any line is one click away. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface and a background worker · transcription is a long job and the interface should stay usable while it runs - Audio: FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else · every downstream model expects it and half the failures are a container nobody normalised - Transcription: A local speech model with voice activity detection as a pre-pass · long non-vocal stretches are the documented predictor of invented text - Diarisation: A maintained open pipeline, with its labels treated as suggestions · the best open results on distant-microphone meeting audio still misattribute a large share of speech - Notes: A model pass that must return segment identifiers with every claim · an uncited sentence is deleted, which is the only defence against a confident summary ### Data model (create this before Phase 1) - meetings(id, title, started_at, duration_s, source_path, normalised_path, consent_recorded_at, consent_note, retention_until, state) - segments(id, meeting_id, start_ms, end_ms, text, avg_logprob, no_speech_prob, compression_ratio, flagged_reason) · one row per transcribed segment with the numbers that predict a bad one - speech_regions(meeting_id, start_ms, end_ms) · from voice activity detection, before transcription touches the audio - speakers(id, meeting_id, label, display_name, confirmed_by_user) · a guess until somebody confirms it - segment_speakers(segment_id, speaker_id, overlap_ratio) · attribution kept separately from the words - notes(id, meeting_id, kind, text, segment_ids_json, model_id, created_at) · kind is summary, decision or action; the segment list is mandatory - actions(id, note_id, owner_speaker_id, due_text, confidence, accepted_by_user) · nothing is assigned to a person until a human accepts it - Nothing in the notes table can exist without segment identifiers, and the schema enforces it rather than the prompt. ### Phase 1 · Consent first, audio second Build: Importing or recording a meeting requires a consent note naming who was told and how, stored as a row with a timestamp before the audio file is kept. Normalise with FFmpeg to single-channel sixteen kilohertz. Set a retention date at import. There is no automatic joining of calls. Trap: Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. Done when: Audio cannot be stored without a consent row, enforced by the schema and tested by attempting it. Every meeting has a retention date set at import and a job deletes the audio when it passes, tested with a backdated row. Normalisation produces single-channel sixteen kilohertz output for every supported input, tested with three container formats. The application has no code path that joins a call or records without an explicit start. Do not build yet: No transcription, diarisation, notes or search yet. ### Phase 2 · A transcript that does not invent Build: Run voice activity detection first and transcribe speech regions rather than the whole file, because long non-vocal stretches are what the research found predicts invented text. Turn off conditioning on previous text to break repetition loops, accepting the documented cost that wording is less consistent across window boundaries. Store the average log probability, the no-speech probability and the compression ratio for every segment, and flag the ones that look wrong. Remember that the silence threshold is combined with the log probability, so raising it alone will not suppress a confidently invented segment. Trap: Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. Done when: A file with two minutes of silence in the middle produces no segments for that period, tested with a constructed file. A segment repeating the same phrase is flagged by its compression ratio and shown as doubtful, tested. Every segment stores its log probability, no-speech probability and compression ratio. Transcribing the same file twice produces the same segment boundaries, given fixed settings, tested. Do not build yet: No diarisation, notes or search yet. ### Phase 3 · Speaker labels that admit what they are Build: Run a maintained diarisation pipeline and attach speakers to segments with an overlap ratio. Present labels as unnamed speakers until somebody names them. Every segment plays its own audio from the transcript view. Show the error rate you should expect for your microphone setup. Trap: Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. Done when: Speakers appear as unnamed labels until named by the user, and the naming is stored as confirmed. Every segment in the transcript plays its own audio range, tested by clicking one. Overlapping speech produces a segment attributed to more than one speaker with ratios rather than an arbitrary winner. The interface states, next to the speaker list, that these labels are estimates and roughly how often they are wrong for this kind of audio. Do not build yet: No notes, actions or search yet. ### Phase 4 · Notes that cite the transcript Build: Send the transcript with segment identifiers and require the model to return each claim with the identifiers supporting it. Drop any returned item whose identifiers are missing or do not exist. Action items carry a suggested owner and a confidence, and nothing is assigned to a person until the user accepts it. Show each note with its citations expanded in place. Trap: A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. Done when: A note returned without valid segment identifiers is dropped and counted, tested with a fake model returning uncited claims. Every displayed note expands to the segment text and plays the audio. Action items are unassigned until accepted, and accepting one is recorded with a timestamp. Regenerating notes for a meeting creates a new set rather than overwriting the accepted items. Do not build yet: No search, export or redaction yet. ### Phase 5 · Search, redaction and leaving Build: Search across transcripts and notes with results linked to the segment and its audio. Delete a meeting and everything derived from it in one operation, including audio, segments, notes and search entries. Redact a passage so the text is replaced and the audio range is muted in place. Export a meeting as Markdown plus a JSON file containing the segments and citations. Trap: Deleting a meeting row and leaving the derived rows behind means the transcript is still searchable and the audio is still on disk, which turns a deletion request into a false statement that you honoured it. Make deletion one operation over every table plus the files, and test it by searching for a distinctive phrase afterwards. Done when: Deleting a meeting removes audio, segments, speakers, notes, actions and search entries, verified by searching for a phrase from it afterwards and finding nothing. Redacting a passage replaces the text and mutes that audio range in the stored file, tested by listening. Export produces Markdown a person can read and JSON containing segments and citations, and the JSON reimports into an empty database. Every search result links to the segment and plays it. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins calls automatically, which is the feature most likely to record someone who was never asked and which every platform gates differently. - Live transcription during the meeting, which is a different latency problem and makes the silence handling harder rather than easier. - Cross-meeting analytics about people, such as talk time by named participant, which turns notes into workplace monitoring with speaker labels that are often wrong. - Calendar and CRM integrations, which are most of the paid product and none of the hard problems here. ### README must contain - That consent is recorded before audio is kept, with the California and UK citations that motivated it and a note to check your own jurisdiction - That speech models can produce text nobody said, the published rate and condition, and what this build does about it - That speaker labels are estimates and roughly how often they are wrong on distant-microphone audio - That conditioning on previous text is disabled, and the wording inconsistency that costs across window boundaries - How deletion and redaction work, exactly what each removes, and how to verify it afterwards ===== AGENTS.md ===== # Agent instructions · Circleback indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with a small local web interface and a background worker, FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else, A local speech model with voice activity detection as a pre-pass, A maintained open pipeline, with its labels treated as suggestions, A model pass that must return segment identifiers with every claim. 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 - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. ===== BUILD_PLAN.md ===== # Build plan · Circleback A local meeting notes pipeline whose hard problems are a speech model that invents text during silence, speaker labels that are wrong often enough to matter, and notes that must be traceable to audio because people act on them. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Consent first, audio second No audio is retained until a consent record exists, and every recording states its retention. ### Steps 1. Make consent a row the audio depends on Not a checkbox that can be added afterwards. Files: `src/meetings.py`, `src/db/schema.sql` 2. Normalise every input through FFmpeg first One sample rate and one channel removes a category of later failures. Files: `src/audio.py`, `tests/test_audio.py` 3. Set retention at import and enforce it Recordings of colleagues should have an expiry date. Files: `src/retention.py` ### Done when - [ ] Audio without a consent row is refused - [ ] Retention is set at import and enforced by a job - [ ] Three container formats normalise correctly - [ ] No code path records or joins without an explicit start ### Watch out - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. ## Phase 2 · A transcript that does not invent Silence produces nothing, repetition loops are caught, and doubtful segments are marked. ### Steps 1. Detect speech before transcribing The pre-pass removes the exact condition that produces invented text. Files: `src/vad.py`, `tests/test_silence.py` 2. Break the repetition loop at the setting level And state the trade-off in the README rather than hiding it. Files: `src/transcribe.py` 3. Keep the per-segment scores and flag on them A doubtful segment should look doubtful in the interface. Files: `src/transcribe.py`, `src/templates/transcript.html` ### Done when - [ ] Silence produces no segments - [ ] A repetition loop is flagged automatically - [ ] Every segment stores its three scores - [ ] Two runs of one file agree on boundaries ### Watch out - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. ## Phase 3 · Speaker labels that admit what they are Attribution is a suggestion the user confirms, and every label can be checked against the audio. ### Steps 1. Keep labels anonymous until confirmed A name is a claim about who said something. Files: `src/diarise.py`, `src/templates/speakers.html` 2. Store overlap ratios rather than a winner People talk over each other and the transcript should show it. Files: `src/diarise.py` 3. Make every segment playable from the transcript Checking a line should take one click. Files: `src/templates/transcript.html` ### Done when - [ ] Speakers are unnamed until a user names them - [ ] Every segment plays its own audio - [ ] Overlaps produce ratios, not a single winner - [ ] The interface states that labels are estimates ### Watch out - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. ## Phase 4 · Notes that cite the transcript Every summary line, decision and action item points at the segments it came from, or it is not shown. ### Steps 1. Require and verify segment identifiers Verify they exist, because a model will invent an identifier too. Files: `src/notes.py`, `tests/test_citations.py` 2. Drop uncited claims rather than caveating them A caveat is read as confidence by the third reader. Files: `src/notes.py` 3. Leave owners unassigned until a human accepts Assigning work from a guess about who spoke is how this goes wrong. Files: `src/actions.py`, `src/templates/notes.html` ### Done when - [ ] Uncited claims are dropped and counted - [ ] Every note expands to its segments and audio - [ ] Actions are unassigned until accepted - [ ] Regenerating notes preserves accepted items ### Watch out - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. ## Not in this build - A bot that joins calls automatically. - Live transcription during the meeting. - Cross-meeting analytics about named people. - Calendar and CRM integrations. ## After v1, if you want it - A glossary of names and jargon supplied to the transcriber, which fixes more errors than a bigger model does - Topic segmentation within a meeting, once citations are trusted enough to hang it on - A weekly file of open action items, generated locally and written to disk rather than emailed ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Meetings, segments, speakers and notes. Contains what colleagues said, so treat it accordingly. DATABASE_PATH=./data/meetings.db # Required. Normalised audio. Deleted at the retention date along with the original. AUDIO_DIR=./data/audio # Required. Default retention set at import. Can be shortened per meeting, never silently extended. RETENTION_DAYS=90 # Required. Stored per meeting, because a different model gives a different transcript. ASR_MODEL=a local speech model identifier # Required. Used only for the notes pass. If hosted, the transcript leaves the machine and the README must say so. NOTES_MODEL=a local or hosted model identifier # Required. Non-vocal stretches longer than this are cut before transcription. VAD_MIN_SILENCE_MS=2000 # Required. Above this a segment is flagged as probably repeating rather than transcribing. FLAG_COMPRESSION_RATIO=2.4
You are building a production product version of Circleback. 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 ===== # Circleback · product brief ## Problem The visible meeting notes loop is buildable, but a credible replacement needs more than the first screen. Circleback earns its keep through capture, integrations, reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A notes pipeline where nothing is recorded without consent, silence produces silence, speaker names are confirmed rather than assumed, and every line of the notes can be played back as audio. ## 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 bot that joins calls automatically. - Live transcription during the meeting. - Cross-meeting analytics about named people. - Calendar and CRM integrations. - cross-call team analytics - meeting-bot auto-join - live multi-speaker accuracy - calendar and CRM integrations ## Success criteria - Audio without a consent row is confirmed refused - Two minutes of silence is confirmed to produce no segments - Speaker labels are confirmed unnamed until a user names them - Uncited notes are confirmed dropped rather than displayed - A deleted meeting is confirmed unfindable with no audio on disk ===== BRIEF.md ===== # Build brief · Circleback 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 private meeting notes pipeline where every line of the notes points at the second of audio it came from to replace Circleback. 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 rules, and they both exist because this tool produces evidence about what people said. Recording is regulated and the rules follow the participants, not the host. California Penal Code section 632 makes it an offence to record a confidential communication without the consent of all parties, with a fine of up to 2,500 dollars per violation, and roughly a dozen US states require all-party consent for at least some categories of recording, with authoritative lists disagreeing on the exact membership. In the UK the Information Commissioner's October 2023 guidance on monitoring workers says you must identify a lawful basis before you start, that audio recording is more intrusive than visual and needs greater justification, and that a recording capability should be off by default. A transcript will also incidentally capture health, religion or trade union membership, which needs its own condition. So the recorder starts stopped, consent is announced and recorded as a row before audio is kept, and there is no hidden mode and no bot that joins a call uninvited. Check your own jurisdiction; the citations above are a starting point, not advice. Second, the transcript is not what was said, and the notes are not the transcript. The peer- reviewed FAccT 2024 study of Whisper found hallucinated content in about 1.4 percent of transcriptions, most strongly predicted by long non-vocal stretches inside a segment, and OpenAI's own model card says the predictions 'may include texts that are not actually spoken in the audio input'. So every note and every action item stores the segment identifiers it came from, anything the model produces without a citable segment is dropped rather than shown, and the audio for any line is one click away. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface and a background worker · transcription is a long job and the interface should stay usable while it runs - Audio: FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else · every downstream model expects it and half the failures are a container nobody normalised - Transcription: A local speech model with voice activity detection as a pre-pass · long non-vocal stretches are the documented predictor of invented text - Diarisation: A maintained open pipeline, with its labels treated as suggestions · the best open results on distant-microphone meeting audio still misattribute a large share of speech - Notes: A model pass that must return segment identifiers with every claim · an uncited sentence is deleted, which is the only defence against a confident summary ### Data model (create this before Phase 1) - meetings(id, title, started_at, duration_s, source_path, normalised_path, consent_recorded_at, consent_note, retention_until, state) - segments(id, meeting_id, start_ms, end_ms, text, avg_logprob, no_speech_prob, compression_ratio, flagged_reason) · one row per transcribed segment with the numbers that predict a bad one - speech_regions(meeting_id, start_ms, end_ms) · from voice activity detection, before transcription touches the audio - speakers(id, meeting_id, label, display_name, confirmed_by_user) · a guess until somebody confirms it - segment_speakers(segment_id, speaker_id, overlap_ratio) · attribution kept separately from the words - notes(id, meeting_id, kind, text, segment_ids_json, model_id, created_at) · kind is summary, decision or action; the segment list is mandatory - actions(id, note_id, owner_speaker_id, due_text, confidence, accepted_by_user) · nothing is assigned to a person until a human accepts it - Nothing in the notes table can exist without segment identifiers, and the schema enforces it rather than the prompt. ### Phase 1 · Consent first, audio second Build: Importing or recording a meeting requires a consent note naming who was told and how, stored as a row with a timestamp before the audio file is kept. Normalise with FFmpeg to single-channel sixteen kilohertz. Set a retention date at import. There is no automatic joining of calls. Trap: Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. Done when: Audio cannot be stored without a consent row, enforced by the schema and tested by attempting it. Every meeting has a retention date set at import and a job deletes the audio when it passes, tested with a backdated row. Normalisation produces single-channel sixteen kilohertz output for every supported input, tested with three container formats. The application has no code path that joins a call or records without an explicit start. Do not build yet: No transcription, diarisation, notes or search yet. ### Phase 2 · A transcript that does not invent Build: Run voice activity detection first and transcribe speech regions rather than the whole file, because long non-vocal stretches are what the research found predicts invented text. Turn off conditioning on previous text to break repetition loops, accepting the documented cost that wording is less consistent across window boundaries. Store the average log probability, the no-speech probability and the compression ratio for every segment, and flag the ones that look wrong. Remember that the silence threshold is combined with the log probability, so raising it alone will not suppress a confidently invented segment. Trap: Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. Done when: A file with two minutes of silence in the middle produces no segments for that period, tested with a constructed file. A segment repeating the same phrase is flagged by its compression ratio and shown as doubtful, tested. Every segment stores its log probability, no-speech probability and compression ratio. Transcribing the same file twice produces the same segment boundaries, given fixed settings, tested. Do not build yet: No diarisation, notes or search yet. ### Phase 3 · Speaker labels that admit what they are Build: Run a maintained diarisation pipeline and attach speakers to segments with an overlap ratio. Present labels as unnamed speakers until somebody names them. Every segment plays its own audio from the transcript view. Show the error rate you should expect for your microphone setup. Trap: Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. Done when: Speakers appear as unnamed labels until named by the user, and the naming is stored as confirmed. Every segment in the transcript plays its own audio range, tested by clicking one. Overlapping speech produces a segment attributed to more than one speaker with ratios rather than an arbitrary winner. The interface states, next to the speaker list, that these labels are estimates and roughly how often they are wrong for this kind of audio. Do not build yet: No notes, actions or search yet. ### Phase 4 · Notes that cite the transcript Build: Send the transcript with segment identifiers and require the model to return each claim with the identifiers supporting it. Drop any returned item whose identifiers are missing or do not exist. Action items carry a suggested owner and a confidence, and nothing is assigned to a person until the user accepts it. Show each note with its citations expanded in place. Trap: A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. Done when: A note returned without valid segment identifiers is dropped and counted, tested with a fake model returning uncited claims. Every displayed note expands to the segment text and plays the audio. Action items are unassigned until accepted, and accepting one is recorded with a timestamp. Regenerating notes for a meeting creates a new set rather than overwriting the accepted items. Do not build yet: No search, export or redaction yet. ### Phase 5 · Search, redaction and leaving Build: Search across transcripts and notes with results linked to the segment and its audio. Delete a meeting and everything derived from it in one operation, including audio, segments, notes and search entries. Redact a passage so the text is replaced and the audio range is muted in place. Export a meeting as Markdown plus a JSON file containing the segments and citations. Trap: Deleting a meeting row and leaving the derived rows behind means the transcript is still searchable and the audio is still on disk, which turns a deletion request into a false statement that you honoured it. Make deletion one operation over every table plus the files, and test it by searching for a distinctive phrase afterwards. Done when: Deleting a meeting removes audio, segments, speakers, notes, actions and search entries, verified by searching for a phrase from it afterwards and finding nothing. Redacting a passage replaces the text and mutes that audio range in the stored file, tested by listening. Export produces Markdown a person can read and JSON containing segments and citations, and the JSON reimports into an empty database. Every search result links to the segment and plays it. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins calls automatically, which is the feature most likely to record someone who was never asked and which every platform gates differently. - Live transcription during the meeting, which is a different latency problem and makes the silence handling harder rather than easier. - Cross-meeting analytics about people, such as talk time by named participant, which turns notes into workplace monitoring with speaker labels that are often wrong. - Calendar and CRM integrations, which are most of the paid product and none of the hard problems here. ### README must contain - That consent is recorded before audio is kept, with the California and UK citations that motivated it and a note to check your own jurisdiction - That speech models can produce text nobody said, the published rate and condition, and what this build does about it - That speaker labels are estimates and roughly how often they are wrong on distant-microphone audio - That conditioning on previous text is disabled, and the wording inconsistency that costs across window boundaries - How deletion and redaction work, exactly what each removes, and how to verify it afterwards ===== ARCHITECTURE.md ===== # Architecture · Circleback ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small local web interface and a background worker | transcription is a long job and the interface should stay usable while it runs | | Audio | FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else | every downstream model expects it and half the failures are a container nobody normalised | | Transcription | A local speech model with voice activity detection as a pre-pass | long non-vocal stretches are the documented predictor of invented text | | Diarisation | A maintained open pipeline, with its labels treated as suggestions | the best open results on distant-microphone meeting audio still misattribute a large share of speech | | Notes | A model pass that must return segment identifiers with every claim | an uncited sentence is deleted, which is the only defence against a confident summary | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Intake | Consent, normalisation and retention. | Any capture, provided consent precedes storage. | | Transcription | Speech detection, settings and per-segment scores. | Any engine that exposes confidence scores; one that does not is a downgrade. | | Diarisation | Labels, overlap ratios and confirmation. | Any pipeline, provided labels stay unnamed until confirmed. | | Notes | Citation-enforced extraction. | Any model, provided uncited output is dropped by the code. | | Lifecycle | Search, redaction, deletion and export. | Any implementation, provided deletion is verified by searching afterwards. | ## 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 · Meetings, segments, speakers and notes. Contains what colleagues said, so treat it accordingly. - `AUDIO_DIR` · required · Normalised audio. Deleted at the retention date along with the original. - `RETENTION_DAYS` · required · Default retention set at import. Can be shortened per meeting, never silently extended. - `ASR_MODEL` · required · Stored per meeting, because a different model gives a different transcript. - `NOTES_MODEL` · required · Used only for the notes pass. If hosted, the transcript leaves the machine and the README must say so. - `VAD_MIN_SILENCE_MS` · required · Non-vocal stretches longer than this are cut before transcription. - `FLAG_COMPRESSION_RATIO` · required · Above this a segment is flagged as probably repeating rather than transcribing. ## 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 · Circleback product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python with a small local web interface and a background worker, FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else, A local speech model with voice activity detection as a pre-pass, A maintained open pipeline, with its labels treated as suggestions, A model pass that must return segment identifiers with every claim. - 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 - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. - Deleting a meeting row and leaving the derived rows behind means the transcript is still searchable and the audio is still on disk, which turns a deletion request into a false statement that you honoured it. Make deletion one operation over every table plus the files, and test it by searching for a distinctive phrase afterwards. ===== MILESTONES.md ===== # Delivery milestones · Circleback Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Consent first, audio second No audio is retained until a consent record exists, and every recording states its retention. ### Steps 1. Make consent a row the audio depends on Not a checkbox that can be added afterwards. Files: `src/meetings.py`, `src/db/schema.sql` 2. Normalise every input through FFmpeg first One sample rate and one channel removes a category of later failures. Files: `src/audio.py`, `tests/test_audio.py` 3. Set retention at import and enforce it Recordings of colleagues should have an expiry date. Files: `src/retention.py` ### Done when - [ ] Audio without a consent row is refused - [ ] Retention is set at import and enforced by a job - [ ] Three container formats normalise correctly - [ ] No code path records or joins without an explicit start ### Watch out - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. ## M2 · A transcript that does not invent Silence produces nothing, repetition loops are caught, and doubtful segments are marked. ### Steps 1. Detect speech before transcribing The pre-pass removes the exact condition that produces invented text. Files: `src/vad.py`, `tests/test_silence.py` 2. Break the repetition loop at the setting level And state the trade-off in the README rather than hiding it. Files: `src/transcribe.py` 3. Keep the per-segment scores and flag on them A doubtful segment should look doubtful in the interface. Files: `src/transcribe.py`, `src/templates/transcript.html` ### Done when - [ ] Silence produces no segments - [ ] A repetition loop is flagged automatically - [ ] Every segment stores its three scores - [ ] Two runs of one file agree on boundaries ### Watch out - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. ## M3 · Speaker labels that admit what they are Attribution is a suggestion the user confirms, and every label can be checked against the audio. ### Steps 1. Keep labels anonymous until confirmed A name is a claim about who said something. Files: `src/diarise.py`, `src/templates/speakers.html` 2. Store overlap ratios rather than a winner People talk over each other and the transcript should show it. Files: `src/diarise.py` 3. Make every segment playable from the transcript Checking a line should take one click. Files: `src/templates/transcript.html` ### Done when - [ ] Speakers are unnamed until a user names them - [ ] Every segment plays its own audio - [ ] Overlaps produce ratios, not a single winner - [ ] The interface states that labels are estimates ### Watch out - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. ## M4 · Notes that cite the transcript Every summary line, decision and action item points at the segments it came from, or it is not shown. ### Steps 1. Require and verify segment identifiers Verify they exist, because a model will invent an identifier too. Files: `src/notes.py`, `tests/test_citations.py` 2. Drop uncited claims rather than caveating them A caveat is read as confidence by the third reader. Files: `src/notes.py` 3. Leave owners unassigned until a human accepts Assigning work from a guess about who spoke is how this goes wrong. Files: `src/actions.py`, `src/templates/notes.html` ### Done when - [ ] Uncited claims are dropped and counted - [ ] Every note expands to its segments and audio - [ ] Actions are unassigned until accepted - [ ] Regenerating notes preserves accepted items ### Watch out - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. ## M5 · Search, redaction and leaving (production only) Meetings are findable, a participant can be removed, and everything exports as files. ### Steps 1. Make deletion one operation across every table and the files Then prove it by searching for the phrase afterwards. Files: `src/delete.py`, `tests/test_delete.py` 2. Redact text and audio together Redacted text over intact audio is not redaction. Files: `src/redact.py` 3. Export Markdown for people and JSON for machines With the citations intact in the JSON. Files: `src/export.py` ### Done when - [ ] A deleted meeting leaves nothing findable and no audio - [ ] Redaction changes both the text and the audio - [ ] Export produces readable Markdown and reimportable JSON - [ ] Every search result links to playable audio ### Watch out - Deleting a meeting row and leaving the derived rows behind means the transcript is still searchable and the audio is still on disk, which turns a deletion request into a false statement that you honoured it. Make deletion one operation over every table plus the files, and test it by searching for a distinctive phrase afterwards. ===== OPERATIONS.md ===== # Operations · Circleback ## Backup The database and audio together, encrypted, with a short retention. This is the rare build where keeping fewer backups for less time is the better operational choice, because the contents are recordings of other people. ## Restore Restore, confirm meeting counts and that retention deletion still runs against the restored rows. A restore that resurrects audio past its retention date has undone a deletion you promised. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Flagged segments per meeting, uncited claims dropped per notes run, transcription failures, and meetings past their retention date that still have audio on disk. The last one is the one that matters. ## Incident checklist The incident here is a note that was wrong and got acted on. The citations and the per-segment scores are what let you show whether the model invented it, the diariser misattributed it, or somebody actually said 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 - [ ] Audio without a consent row is confirmed refused - [ ] Two minutes of silence is confirmed to produce no segments - [ ] Speaker labels are confirmed unnamed until a user names them - [ ] Uncited notes are confirmed dropped rather than displayed - [ ] A deleted meeting is confirmed unfindable with no audio on disk ## Launch constraint Do not market omitted Circleback 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. Meetings, segments, speakers and notes. Contains what colleagues said, so treat it accordingly. DATABASE_PATH=./data/meetings.db # Required. Normalised audio. Deleted at the retention date along with the original. AUDIO_DIR=./data/audio # Required. Default retention set at import. Can be shortened per meeting, never silently extended. RETENTION_DAYS=90 # Required. Stored per meeting, because a different model gives a different transcript. ASR_MODEL=a local speech model identifier # Required. Used only for the notes pass. If hosted, the transcript leaves the machine and the README must say so. NOTES_MODEL=a local or hosted model identifier # Required. Non-vocal stretches longer than this are cut before transcription. VAD_MIN_SILENCE_MS=2000 # Required. Above this a segment is flagged as probably repeating rather than transcribing. FLAG_COMPRESSION_RATIO=2.4
# Circleback · indie build A local meeting notes pipeline whose hard problems are a speech model that invents text during silence, speaker labels that are wrong often enough to matter, and notes that must be traceable to audio because people act on them. Estimated effort: **multi-day**. 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 with a small local web interface and a background worker | transcription is a long job and the interface should stay usable while it runs | | Audio | FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else | every downstream model expects it and half the failures are a container nobody normalised | | Transcription | A local speech model with voice activity detection as a pre-pass | long non-vocal stretches are the documented predictor of invented text | | Diarisation | A maintained open pipeline, with its labels treated as suggestions | the best open results on distant-microphone meeting audio still misattribute a large share of speech | | Notes | A model pass that must return segment identifiers with every claim | an uncited sentence is deleted, which is the only defence against a confident summary | ## 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 interface and the transcription worker. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg** · free - Why: Every input is normalised to single-channel sixteen kilohertz before anything else touches it, and this is where half the format problems disappear. - Get it: Your package manager, or ffmpeg.org. - Verify: ffmpeg -version prints a version and a test file converts - [ ] **A local speech recognition model with voice activity detection** · free, at the cost of disk and time - Why: Running it locally keeps recordings of colleagues on your machine, and the detection pre-pass is the mitigation for the documented failure mode. - Get it: A maintained local implementation with a bundled detector and per-segment scores exposed. - Verify: A file with silence in the middle produces no segments for it - [ ] **A maintained speaker diarisation pipeline** · free, with an account for the model where required - Why: Attribution is a separate model from transcription, and the maintained pipelines publish their error rates so you can state them honestly. - Get it: A current open pipeline with published benchmark numbers, accepting its licence terms. - Verify: A two-speaker recording produces two labels with sensible boundaries - [ ] **Your consent and retention policy** · free - Why: Both are legal questions before they are product questions, and the build enforces whatever you write down here. - Get it: Decide what participants are told, how the telling is recorded, and how long audio and transcripts are kept. - Verify: Both appear in the README and are enforced by code rather than by intention ## 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 bot that joins calls automatically. - Live transcription during the meeting. - Cross-meeting analytics about named people. - Calendar and CRM 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 Circleback, and the README should say so rather than pretend.
# Build brief · Circleback 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 private meeting notes pipeline where every line of the notes points at the second of audio it came from to replace Circleback. 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 rules, and they both exist because this tool produces evidence about what people said. Recording is regulated and the rules follow the participants, not the host. California Penal Code section 632 makes it an offence to record a confidential communication without the consent of all parties, with a fine of up to 2,500 dollars per violation, and roughly a dozen US states require all-party consent for at least some categories of recording, with authoritative lists disagreeing on the exact membership. In the UK the Information Commissioner's October 2023 guidance on monitoring workers says you must identify a lawful basis before you start, that audio recording is more intrusive than visual and needs greater justification, and that a recording capability should be off by default. A transcript will also incidentally capture health, religion or trade union membership, which needs its own condition. So the recorder starts stopped, consent is announced and recorded as a row before audio is kept, and there is no hidden mode and no bot that joins a call uninvited. Check your own jurisdiction; the citations above are a starting point, not advice. Second, the transcript is not what was said, and the notes are not the transcript. The peer- reviewed FAccT 2024 study of Whisper found hallucinated content in about 1.4 percent of transcriptions, most strongly predicted by long non-vocal stretches inside a segment, and OpenAI's own model card says the predictions 'may include texts that are not actually spoken in the audio input'. So every note and every action item stores the segment identifiers it came from, anything the model produces without a citable segment is dropped rather than shown, and the audio for any line is one click away. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface and a background worker · transcription is a long job and the interface should stay usable while it runs - Audio: FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else · every downstream model expects it and half the failures are a container nobody normalised - Transcription: A local speech model with voice activity detection as a pre-pass · long non-vocal stretches are the documented predictor of invented text - Diarisation: A maintained open pipeline, with its labels treated as suggestions · the best open results on distant-microphone meeting audio still misattribute a large share of speech - Notes: A model pass that must return segment identifiers with every claim · an uncited sentence is deleted, which is the only defence against a confident summary ### Data model (create this before Phase 1) - meetings(id, title, started_at, duration_s, source_path, normalised_path, consent_recorded_at, consent_note, retention_until, state) - segments(id, meeting_id, start_ms, end_ms, text, avg_logprob, no_speech_prob, compression_ratio, flagged_reason) · one row per transcribed segment with the numbers that predict a bad one - speech_regions(meeting_id, start_ms, end_ms) · from voice activity detection, before transcription touches the audio - speakers(id, meeting_id, label, display_name, confirmed_by_user) · a guess until somebody confirms it - segment_speakers(segment_id, speaker_id, overlap_ratio) · attribution kept separately from the words - notes(id, meeting_id, kind, text, segment_ids_json, model_id, created_at) · kind is summary, decision or action; the segment list is mandatory - actions(id, note_id, owner_speaker_id, due_text, confidence, accepted_by_user) · nothing is assigned to a person until a human accepts it - Nothing in the notes table can exist without segment identifiers, and the schema enforces it rather than the prompt. ### Phase 1 · Consent first, audio second Build: Importing or recording a meeting requires a consent note naming who was told and how, stored as a row with a timestamp before the audio file is kept. Normalise with FFmpeg to single-channel sixteen kilohertz. Set a retention date at import. There is no automatic joining of calls. Trap: Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. Done when: Audio cannot be stored without a consent row, enforced by the schema and tested by attempting it. Every meeting has a retention date set at import and a job deletes the audio when it passes, tested with a backdated row. Normalisation produces single-channel sixteen kilohertz output for every supported input, tested with three container formats. The application has no code path that joins a call or records without an explicit start. Do not build yet: No transcription, diarisation, notes or search yet. ### Phase 2 · A transcript that does not invent Build: Run voice activity detection first and transcribe speech regions rather than the whole file, because long non-vocal stretches are what the research found predicts invented text. Turn off conditioning on previous text to break repetition loops, accepting the documented cost that wording is less consistent across window boundaries. Store the average log probability, the no-speech probability and the compression ratio for every segment, and flag the ones that look wrong. Remember that the silence threshold is combined with the log probability, so raising it alone will not suppress a confidently invented segment. Trap: Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. Done when: A file with two minutes of silence in the middle produces no segments for that period, tested with a constructed file. A segment repeating the same phrase is flagged by its compression ratio and shown as doubtful, tested. Every segment stores its log probability, no-speech probability and compression ratio. Transcribing the same file twice produces the same segment boundaries, given fixed settings, tested. Do not build yet: No diarisation, notes or search yet. ### Phase 3 · Speaker labels that admit what they are Build: Run a maintained diarisation pipeline and attach speakers to segments with an overlap ratio. Present labels as unnamed speakers until somebody names them. Every segment plays its own audio from the transcript view. Show the error rate you should expect for your microphone setup. Trap: Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. Done when: Speakers appear as unnamed labels until named by the user, and the naming is stored as confirmed. Every segment in the transcript plays its own audio range, tested by clicking one. Overlapping speech produces a segment attributed to more than one speaker with ratios rather than an arbitrary winner. The interface states, next to the speaker list, that these labels are estimates and roughly how often they are wrong for this kind of audio. Do not build yet: No notes, actions or search yet. ### Phase 4 · Notes that cite the transcript Build: Send the transcript with segment identifiers and require the model to return each claim with the identifiers supporting it. Drop any returned item whose identifiers are missing or do not exist. Action items carry a suggested owner and a confidence, and nothing is assigned to a person until the user accepts it. Show each note with its citations expanded in place. Trap: A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. Done when: A note returned without valid segment identifiers is dropped and counted, tested with a fake model returning uncited claims. Every displayed note expands to the segment text and plays the audio. Action items are unassigned until accepted, and accepting one is recorded with a timestamp. Regenerating notes for a meeting creates a new set rather than overwriting the accepted items. Do not build yet: No search, export or redaction yet. ### Phase 5 · Search, redaction and leaving Build: Search across transcripts and notes with results linked to the segment and its audio. Delete a meeting and everything derived from it in one operation, including audio, segments, notes and search entries. Redact a passage so the text is replaced and the audio range is muted in place. Export a meeting as Markdown plus a JSON file containing the segments and citations. Trap: Deleting a meeting row and leaving the derived rows behind means the transcript is still searchable and the audio is still on disk, which turns a deletion request into a false statement that you honoured it. Make deletion one operation over every table plus the files, and test it by searching for a distinctive phrase afterwards. Done when: Deleting a meeting removes audio, segments, speakers, notes, actions and search entries, verified by searching for a phrase from it afterwards and finding nothing. Redacting a passage replaces the text and mutes that audio range in the stored file, tested by listening. Export produces Markdown a person can read and JSON containing segments and citations, and the JSON reimports into an empty database. Every search result links to the segment and plays it. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins calls automatically, which is the feature most likely to record someone who was never asked and which every platform gates differently. - Live transcription during the meeting, which is a different latency problem and makes the silence handling harder rather than easier. - Cross-meeting analytics about people, such as talk time by named participant, which turns notes into workplace monitoring with speaker labels that are often wrong. - Calendar and CRM integrations, which are most of the paid product and none of the hard problems here. ### README must contain - That consent is recorded before audio is kept, with the California and UK citations that motivated it and a note to check your own jurisdiction - That speech models can produce text nobody said, the published rate and condition, and what this build does about it - That speaker labels are estimates and roughly how often they are wrong on distant-microphone audio - That conditioning on previous text is disabled, and the wording inconsistency that costs across window boundaries - How deletion and redaction work, exactly what each removes, and how to verify it afterwards
# Agent instructions · Circleback indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python with a small local web interface and a background worker, FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else, A local speech model with voice activity detection as a pre-pass, A maintained open pipeline, with its labels treated as suggestions, A model pass that must return segment identifiers with every claim. 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 - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat.
# Build plan · Circleback A local meeting notes pipeline whose hard problems are a speech model that invents text during silence, speaker labels that are wrong often enough to matter, and notes that must be traceable to audio because people act on them. Phases are in dependency order. Each ends in a "Done when" list; treat an unticked item as a blocker, not a note. ## Phase 1 · Consent first, audio second No audio is retained until a consent record exists, and every recording states its retention. ### Steps 1. Make consent a row the audio depends on Not a checkbox that can be added afterwards. Files: `src/meetings.py`, `src/db/schema.sql` 2. Normalise every input through FFmpeg first One sample rate and one channel removes a category of later failures. Files: `src/audio.py`, `tests/test_audio.py` 3. Set retention at import and enforce it Recordings of colleagues should have an expiry date. Files: `src/retention.py` ### Done when - [ ] Audio without a consent row is refused - [ ] Retention is set at import and enforced by a job - [ ] Three container formats normalise correctly - [ ] No code path records or joins without an explicit start ### Watch out - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. ## Phase 2 · A transcript that does not invent Silence produces nothing, repetition loops are caught, and doubtful segments are marked. ### Steps 1. Detect speech before transcribing The pre-pass removes the exact condition that produces invented text. Files: `src/vad.py`, `tests/test_silence.py` 2. Break the repetition loop at the setting level And state the trade-off in the README rather than hiding it. Files: `src/transcribe.py` 3. Keep the per-segment scores and flag on them A doubtful segment should look doubtful in the interface. Files: `src/transcribe.py`, `src/templates/transcript.html` ### Done when - [ ] Silence produces no segments - [ ] A repetition loop is flagged automatically - [ ] Every segment stores its three scores - [ ] Two runs of one file agree on boundaries ### Watch out - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. ## Phase 3 · Speaker labels that admit what they are Attribution is a suggestion the user confirms, and every label can be checked against the audio. ### Steps 1. Keep labels anonymous until confirmed A name is a claim about who said something. Files: `src/diarise.py`, `src/templates/speakers.html` 2. Store overlap ratios rather than a winner People talk over each other and the transcript should show it. Files: `src/diarise.py` 3. Make every segment playable from the transcript Checking a line should take one click. Files: `src/templates/transcript.html` ### Done when - [ ] Speakers are unnamed until a user names them - [ ] Every segment plays its own audio - [ ] Overlaps produce ratios, not a single winner - [ ] The interface states that labels are estimates ### Watch out - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. ## Phase 4 · Notes that cite the transcript Every summary line, decision and action item points at the segments it came from, or it is not shown. ### Steps 1. Require and verify segment identifiers Verify they exist, because a model will invent an identifier too. Files: `src/notes.py`, `tests/test_citations.py` 2. Drop uncited claims rather than caveating them A caveat is read as confidence by the third reader. Files: `src/notes.py` 3. Leave owners unassigned until a human accepts Assigning work from a guess about who spoke is how this goes wrong. Files: `src/actions.py`, `src/templates/notes.html` ### Done when - [ ] Uncited claims are dropped and counted - [ ] Every note expands to its segments and audio - [ ] Actions are unassigned until accepted - [ ] Regenerating notes preserves accepted items ### Watch out - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. ## Not in this build - A bot that joins calls automatically. - Live transcription during the meeting. - Cross-meeting analytics about named people. - Calendar and CRM integrations. ## After v1, if you want it - A glossary of names and jargon supplied to the transcriber, which fixes more errors than a bigger model does - Topic segmentation within a meeting, once citations are trusted enough to hang it on - A weekly file of open action items, generated locally and written to disk rather than emailed
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Meetings, segments, speakers and notes. Contains what colleagues said, so treat it accordingly. DATABASE_PATH=./data/meetings.db # Required. Normalised audio. Deleted at the retention date along with the original. AUDIO_DIR=./data/audio # Required. Default retention set at import. Can be shortened per meeting, never silently extended. RETENTION_DAYS=90 # Required. Stored per meeting, because a different model gives a different transcript. ASR_MODEL=a local speech model identifier # Required. Used only for the notes pass. If hosted, the transcript leaves the machine and the README must say so. NOTES_MODEL=a local or hosted model identifier # Required. Non-vocal stretches longer than this are cut before transcription. VAD_MIN_SILENCE_MS=2000 # Required. Above this a segment is flagged as probably repeating rather than transcribing. FLAG_COMPRESSION_RATIO=2.4
# Circleback · product brief ## Problem The visible meeting notes loop is buildable, but a credible replacement needs more than the first screen. Circleback earns its keep through capture, integrations, reliability, so expect a weekend or multi-day build and a narrower personal scope. ## Product outcome A notes pipeline where nothing is recorded without consent, silence produces silence, speaker names are confirmed rather than assumed, and every line of the notes can be played back as audio. ## 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 bot that joins calls automatically. - Live transcription during the meeting. - Cross-meeting analytics about named people. - Calendar and CRM integrations. - cross-call team analytics - meeting-bot auto-join - live multi-speaker accuracy - calendar and CRM integrations ## Success criteria - Audio without a consent row is confirmed refused - Two minutes of silence is confirmed to produce no segments - Speaker labels are confirmed unnamed until a user names them - Uncited notes are confirmed dropped rather than displayed - A deleted meeting is confirmed unfindable with no audio on disk
# Build brief · Circleback 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 private meeting notes pipeline where every line of the notes points at the second of audio it came from to replace Circleback. 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 rules, and they both exist because this tool produces evidence about what people said. Recording is regulated and the rules follow the participants, not the host. California Penal Code section 632 makes it an offence to record a confidential communication without the consent of all parties, with a fine of up to 2,500 dollars per violation, and roughly a dozen US states require all-party consent for at least some categories of recording, with authoritative lists disagreeing on the exact membership. In the UK the Information Commissioner's October 2023 guidance on monitoring workers says you must identify a lawful basis before you start, that audio recording is more intrusive than visual and needs greater justification, and that a recording capability should be off by default. A transcript will also incidentally capture health, religion or trade union membership, which needs its own condition. So the recorder starts stopped, consent is announced and recorded as a row before audio is kept, and there is no hidden mode and no bot that joins a call uninvited. Check your own jurisdiction; the citations above are a starting point, not advice. Second, the transcript is not what was said, and the notes are not the transcript. The peer- reviewed FAccT 2024 study of Whisper found hallucinated content in about 1.4 percent of transcriptions, most strongly predicted by long non-vocal stretches inside a segment, and OpenAI's own model card says the predictions 'may include texts that are not actually spoken in the audio input'. So every note and every action item stores the segment identifiers it came from, anything the model produces without a citable segment is dropped rather than shown, and the audio for any line is one click away. ### Stack (fixed, do not substitute) - Runtime: Python with a small local web interface and a background worker · transcription is a long job and the interface should stay usable while it runs - Audio: FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else · every downstream model expects it and half the failures are a container nobody normalised - Transcription: A local speech model with voice activity detection as a pre-pass · long non-vocal stretches are the documented predictor of invented text - Diarisation: A maintained open pipeline, with its labels treated as suggestions · the best open results on distant-microphone meeting audio still misattribute a large share of speech - Notes: A model pass that must return segment identifiers with every claim · an uncited sentence is deleted, which is the only defence against a confident summary ### Data model (create this before Phase 1) - meetings(id, title, started_at, duration_s, source_path, normalised_path, consent_recorded_at, consent_note, retention_until, state) - segments(id, meeting_id, start_ms, end_ms, text, avg_logprob, no_speech_prob, compression_ratio, flagged_reason) · one row per transcribed segment with the numbers that predict a bad one - speech_regions(meeting_id, start_ms, end_ms) · from voice activity detection, before transcription touches the audio - speakers(id, meeting_id, label, display_name, confirmed_by_user) · a guess until somebody confirms it - segment_speakers(segment_id, speaker_id, overlap_ratio) · attribution kept separately from the words - notes(id, meeting_id, kind, text, segment_ids_json, model_id, created_at) · kind is summary, decision or action; the segment list is mandatory - actions(id, note_id, owner_speaker_id, due_text, confidence, accepted_by_user) · nothing is assigned to a person until a human accepts it - Nothing in the notes table can exist without segment identifiers, and the schema enforces it rather than the prompt. ### Phase 1 · Consent first, audio second Build: Importing or recording a meeting requires a consent note naming who was told and how, stored as a row with a timestamp before the audio file is kept. Normalise with FFmpeg to single-channel sixteen kilohertz. Set a retention date at import. There is no automatic joining of calls. Trap: Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. Done when: Audio cannot be stored without a consent row, enforced by the schema and tested by attempting it. Every meeting has a retention date set at import and a job deletes the audio when it passes, tested with a backdated row. Normalisation produces single-channel sixteen kilohertz output for every supported input, tested with three container formats. The application has no code path that joins a call or records without an explicit start. Do not build yet: No transcription, diarisation, notes or search yet. ### Phase 2 · A transcript that does not invent Build: Run voice activity detection first and transcribe speech regions rather than the whole file, because long non-vocal stretches are what the research found predicts invented text. Turn off conditioning on previous text to break repetition loops, accepting the documented cost that wording is less consistent across window boundaries. Store the average log probability, the no-speech probability and the compression ratio for every segment, and flag the ones that look wrong. Remember that the silence threshold is combined with the log probability, so raising it alone will not suppress a confidently invented segment. Trap: Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. Done when: A file with two minutes of silence in the middle produces no segments for that period, tested with a constructed file. A segment repeating the same phrase is flagged by its compression ratio and shown as doubtful, tested. Every segment stores its log probability, no-speech probability and compression ratio. Transcribing the same file twice produces the same segment boundaries, given fixed settings, tested. Do not build yet: No diarisation, notes or search yet. ### Phase 3 · Speaker labels that admit what they are Build: Run a maintained diarisation pipeline and attach speakers to segments with an overlap ratio. Present labels as unnamed speakers until somebody names them. Every segment plays its own audio from the transcript view. Show the error rate you should expect for your microphone setup. Trap: Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. Done when: Speakers appear as unnamed labels until named by the user, and the naming is stored as confirmed. Every segment in the transcript plays its own audio range, tested by clicking one. Overlapping speech produces a segment attributed to more than one speaker with ratios rather than an arbitrary winner. The interface states, next to the speaker list, that these labels are estimates and roughly how often they are wrong for this kind of audio. Do not build yet: No notes, actions or search yet. ### Phase 4 · Notes that cite the transcript Build: Send the transcript with segment identifiers and require the model to return each claim with the identifiers supporting it. Drop any returned item whose identifiers are missing or do not exist. Action items carry a suggested owner and a confidence, and nothing is assigned to a person until the user accepts it. Show each note with its citations expanded in place. Trap: A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. Done when: A note returned without valid segment identifiers is dropped and counted, tested with a fake model returning uncited claims. Every displayed note expands to the segment text and plays the audio. Action items are unassigned until accepted, and accepting one is recorded with a timestamp. Regenerating notes for a meeting creates a new set rather than overwriting the accepted items. Do not build yet: No search, export or redaction yet. ### Phase 5 · Search, redaction and leaving Build: Search across transcripts and notes with results linked to the segment and its audio. Delete a meeting and everything derived from it in one operation, including audio, segments, notes and search entries. Redact a passage so the text is replaced and the audio range is muted in place. Export a meeting as Markdown plus a JSON file containing the segments and citations. Trap: Deleting a meeting row and leaving the derived rows behind means the transcript is still searchable and the audio is still on disk, which turns a deletion request into a false statement that you honoured it. Make deletion one operation over every table plus the files, and test it by searching for a distinctive phrase afterwards. Done when: Deleting a meeting removes audio, segments, speakers, notes, actions and search entries, verified by searching for a phrase from it afterwards and finding nothing. Redacting a passage replaces the text and mutes that audio range in the stored file, tested by listening. Export produces Markdown a person can read and JSON containing segments and citations, and the JSON reimports into an empty database. Every search result links to the segment and plays it. Do not build yet: Nothing after this. ### Out of scope (and why) - A bot that joins calls automatically, which is the feature most likely to record someone who was never asked and which every platform gates differently. - Live transcription during the meeting, which is a different latency problem and makes the silence handling harder rather than easier. - Cross-meeting analytics about people, such as talk time by named participant, which turns notes into workplace monitoring with speaker labels that are often wrong. - Calendar and CRM integrations, which are most of the paid product and none of the hard problems here. ### README must contain - That consent is recorded before audio is kept, with the California and UK citations that motivated it and a note to check your own jurisdiction - That speech models can produce text nobody said, the published rate and condition, and what this build does about it - That speaker labels are estimates and roughly how often they are wrong on distant-microphone audio - That conditioning on previous text is disabled, and the wording inconsistency that costs across window boundaries - How deletion and redaction work, exactly what each removes, and how to verify it afterwards
# Architecture · Circleback ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python with a small local web interface and a background worker | transcription is a long job and the interface should stay usable while it runs | | Audio | FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else | every downstream model expects it and half the failures are a container nobody normalised | | Transcription | A local speech model with voice activity detection as a pre-pass | long non-vocal stretches are the documented predictor of invented text | | Diarisation | A maintained open pipeline, with its labels treated as suggestions | the best open results on distant-microphone meeting audio still misattribute a large share of speech | | Notes | A model pass that must return segment identifiers with every claim | an uncited sentence is deleted, which is the only defence against a confident summary | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Intake | Consent, normalisation and retention. | Any capture, provided consent precedes storage. | | Transcription | Speech detection, settings and per-segment scores. | Any engine that exposes confidence scores; one that does not is a downgrade. | | Diarisation | Labels, overlap ratios and confirmation. | Any pipeline, provided labels stay unnamed until confirmed. | | Notes | Citation-enforced extraction. | Any model, provided uncited output is dropped by the code. | | Lifecycle | Search, redaction, deletion and export. | Any implementation, provided deletion is verified by searching afterwards. | ## 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 · Meetings, segments, speakers and notes. Contains what colleagues said, so treat it accordingly. - `AUDIO_DIR` · required · Normalised audio. Deleted at the retention date along with the original. - `RETENTION_DAYS` · required · Default retention set at import. Can be shortened per meeting, never silently extended. - `ASR_MODEL` · required · Stored per meeting, because a different model gives a different transcript. - `NOTES_MODEL` · required · Used only for the notes pass. If hosted, the transcript leaves the machine and the README must say so. - `VAD_MIN_SILENCE_MS` · required · Non-vocal stretches longer than this are cut before transcription. - `FLAG_COMPRESSION_RATIO` · required · Above this a segment is flagged as probably repeating rather than transcribing. ## 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 · Circleback product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python with a small local web interface and a background worker, FFmpeg to normalise everything to single-channel sixteen kilohertz before anything else, A local speech model with voice activity detection as a pre-pass, A maintained open pipeline, with its labels treated as suggestions, A model pass that must return segment identifiers with every claim. - 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 - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. - Deleting a meeting row and leaving the derived rows behind means the transcript is still searchable and the audio is still on disk, which turns a deletion request into a false statement that you honoured it. Make deletion one operation over every table plus the files, and test it by searching for a distinctive phrase afterwards.
# Delivery milestones · Circleback Estimated effort: **multi-day** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Consent first, audio second No audio is retained until a consent record exists, and every recording states its retention. ### Steps 1. Make consent a row the audio depends on Not a checkbox that can be added afterwards. Files: `src/meetings.py`, `src/db/schema.sql` 2. Normalise every input through FFmpeg first One sample rate and one channel removes a category of later failures. Files: `src/audio.py`, `tests/test_audio.py` 3. Set retention at import and enforce it Recordings of colleagues should have an expiry date. Files: `src/retention.py` ### Done when - [ ] Audio without a consent row is refused - [ ] Retention is set at import and enforced by a job - [ ] Three container formats normalise correctly - [ ] No code path records or joins without an explicit start ### Watch out - Building capture first and consent as a checkbox later means the first real recordings in the database have no record of whether anyone was told, and the one meeting you later need to justify is one of those. Make the consent row a foreign key the audio path depends on, so the sequence cannot be done in the other order. ## M2 · A transcript that does not invent Silence produces nothing, repetition loops are caught, and doubtful segments are marked. ### Steps 1. Detect speech before transcribing The pre-pass removes the exact condition that produces invented text. Files: `src/vad.py`, `tests/test_silence.py` 2. Break the repetition loop at the setting level And state the trade-off in the README rather than hiding it. Files: `src/transcribe.py` 3. Keep the per-segment scores and flag on them A doubtful segment should look doubtful in the interface. Files: `src/transcribe.py`, `src/templates/transcript.html` ### Done when - [ ] Silence produces no segments - [ ] A repetition loop is flagged automatically - [ ] Every segment stores its three scores - [ ] Two runs of one file agree on boundaries ### Watch out - Feeding an hour of audio straight in and trusting the output puts plausible sentences into the transcript during the four minutes when somebody was screen sharing in silence, and they read exactly like the rest, which is why the published work found harms in over a third of the hallucinations it collected. Cut to speech regions first, keep the per-segment scores, and show flagged segments differently. ## M3 · Speaker labels that admit what they are Attribution is a suggestion the user confirms, and every label can be checked against the audio. ### Steps 1. Keep labels anonymous until confirmed A name is a claim about who said something. Files: `src/diarise.py`, `src/templates/speakers.html` 2. Store overlap ratios rather than a winner People talk over each other and the transcript should show it. Files: `src/diarise.py` 3. Make every segment playable from the transcript Checking a line should take one click. Files: `src/templates/transcript.html` ### Done when - [ ] Speakers are unnamed until a user names them - [ ] Every segment plays its own audio - [ ] Overlaps produce ratios, not a single winner - [ ] The interface states that labels are estimates ### Watch out - Presenting diarisation output as names in the notes produces sentences like a named person committing to a deadline they never mentioned, and on distant-microphone meeting audio the best open pipelines still misattribute around a fifth of speech under a strict scoring protocol. Keep labels anonymous until confirmed, store the overlap ratio, and make every attributed line playable. ## M4 · Notes that cite the transcript Every summary line, decision and action item points at the segments it came from, or it is not shown. ### Steps 1. Require and verify segment identifiers Verify they exist, because a model will invent an identifier too. Files: `src/notes.py`, `tests/test_citations.py` 2. Drop uncited claims rather than caveating them A caveat is read as confidence by the third reader. Files: `src/notes.py` 3. Leave owners unassigned until a human accepts Assigning work from a guess about who spoke is how this goes wrong. Files: `src/actions.py`, `src/templates/notes.html` ### Done when - [ ] Uncited claims are dropped and counted - [ ] Every note expands to its segments and audio - [ ] Actions are unassigned until accepted - [ ] Regenerating notes preserves accepted items ### Watch out - A summary produced from the whole transcript in one pass reads beautifully and contains a decision nobody made, because the model resolved an ambiguous exchange into a conclusion, and the person who reads the notes was not in the meeting and cannot tell. Require identifiers, verify they exist, and delete anything that arrives without them rather than showing it with a caveat. ## M5 · Search, redaction and leaving (production only) Meetings are findable, a participant can be removed, and everything exports as files. ### Steps 1. Make deletion one operation across every table and the files Then prove it by searching for the phrase afterwards. Files: `src/delete.py`, `tests/test_delete.py` 2. Redact text and audio together Redacted text over intact audio is not redaction. Files: `src/redact.py` 3. Export Markdown for people and JSON for machines With the citations intact in the JSON. Files: `src/export.py` ### Done when - [ ] A deleted meeting leaves nothing findable and no audio - [ ] Redaction changes both the text and the audio - [ ] Export produces readable Markdown and reimportable JSON - [ ] Every search result links to playable audio ### Watch out - Deleting a meeting row and leaving the derived rows behind means the transcript is still searchable and the audio is still on disk, which turns a deletion request into a false statement that you honoured it. Make deletion one operation over every table plus the files, and test it by searching for a distinctive phrase afterwards.
# Operations · Circleback ## Backup The database and audio together, encrypted, with a short retention. This is the rare build where keeping fewer backups for less time is the better operational choice, because the contents are recordings of other people. ## Restore Restore, confirm meeting counts and that retention deletion still runs against the restored rows. A restore that resurrects audio past its retention date has undone a deletion you promised. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Flagged segments per meeting, uncited claims dropped per notes run, transcription failures, and meetings past their retention date that still have audio on disk. The last one is the one that matters. ## Incident checklist The incident here is a note that was wrong and got acted on. The citations and the per-segment scores are what let you show whether the model invented it, the diariser misattributed it, or somebody actually said 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 - [ ] Audio without a consent row is confirmed refused - [ ] Two minutes of silence is confirmed to produce no segments - [ ] Speaker labels are confirmed unnamed until a user names them - [ ] Uncited notes are confirmed dropped rather than displayed - [ ] A deleted meeting is confirmed unfindable with no audio on disk ## Launch constraint Do not market omitted Circleback 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. Meetings, segments, speakers and notes. Contains what colleagues said, so treat it accordingly. DATABASE_PATH=./data/meetings.db # Required. Normalised audio. Deleted at the retention date along with the original. AUDIO_DIR=./data/audio # Required. Default retention set at import. Can be shortened per meeting, never silently extended. RETENTION_DAYS=90 # Required. Stored per meeting, because a different model gives a different transcript. ASR_MODEL=a local speech model identifier # Required. Used only for the notes pass. If hosted, the transcript leaves the machine and the README must say so. NOTES_MODEL=a local or hosted model identifier # Required. Non-vocal stretches longer than this are cut before transcription. VAD_MIN_SILENCE_MS=2000 # Required. Above this a segment is flagged as probably repeating rather than transcribing. FLAG_COMPRESSION_RATIO=2.4
$ choose a build depth, inspect the files, then open the complete pack in your agent
Circleback: 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
Circleback pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| individual | — | $20.83/user | Unlimited meetings, imports, and automations; 100+ languages. |
| team | — | $25/user | Unlimited meetings, imports, and automations, plus team sharing, centralized billing, settings, and retention controls. |
| enterprise | custom | — | Custom security, administration, and support package; no public numeric limits or price. |
free tierno free tier
billingmonthly + annual; only annual numeric prices were publicly verifiable; free trial offered with no public duration
hidden costsMonthly prices are shown only in the interactive purchase flow and were not exposed in the public rendered pricing page.
verified 2026-08-14 · source ↗
Vibecode Circleback
Kinda. The core of Circleback 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 Circleback cost?
Circleback'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 Circleback?
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 Circleback?
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.