Vibecode Jamie
track this build4 phases, 12 steps, beginner friendly0%The visible bot-free meeting notes loop is buildable, but a credible replacement needs more than the first screen. Jamie 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 Jamie. 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 ===== # Jamie · indie build A local transcription and notes pipeline whose hard problems are consent, getting long audio through a model without losing an hour's work to a crash, and producing notes where every claim can be traced back to the words that produced it. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process, bound to localhost | the recordings never leave the machine | | Audio | FFmpeg for normalisation, always to the format the model requires | wrong sample rate is the commonest first failure | | Transcription | whisper.cpp locally, chunked and resumable | a two hour file must survive a crash at ninety minutes | | Notes | An extract-then-merge pass with verbatim spans required | one pass over a whole transcript is where fabrication happens | | Storage | SQLite for meetings and segments, audio as files with a retention | the audio is the sensitive part and should not live forever | ## 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 pipeline and the local web interface. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg** · free - Why: Every input is normalised to the format the transcriber documents, and this is the tool that does it. - Get it: Your package manager, or the official builds. - Verify: ffmpeg -version prints a version and the conversion command runs - [ ] **whisper.cpp with a downloaded model** · free, after the model download - Why: Local transcription is the whole reason this is private, and the model file is a separate download from the binary. - Get it: Build from the repository and fetch a model with the provided script. - Verify: The command line transcribes a short 16 kHz mono WAV - [ ] **The recording law where you are** · free - Why: Whether you may record with one party's consent or need everyone's changes what this app is allowed to do, and it is a question with an actual answer. - Get it: Read your jurisdiction's wiretapping or interception statute, or its regulator's guidance, and write a paragraph. - Verify: The paragraph is in the README and the import gate reflects it - [ ] **A long recording you are allowed to use** · free - Why: Resume, chunk stitching and retention are only testable on something over an hour, and a two minute clip proves nothing. - Get it: Record yourself reading for ninety minutes, or use a public-domain recording with a known transcript. - Verify: It exceeds an hour and you know what it says ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A meeting bot that joins calls. - Live real-time transcription. - Calendar and CRM integrations. - Cross-meeting analytics on people. - calendar and CRM integrations - cross-call team analytics - recording recovery, mastering, hosting, and distribution - meeting-bot auto-join - live multi-speaker accuracy If one of those is essential to you, that is the reason to keep paying for Jamie, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Jamie 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 that turns a recording you were allowed to make into notes you can check against the audio to replace Jamie. 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 This tool records people. Before any of it, two rules. Consent is a legal question with a real answer where you live, and the answer is often that every participant must agree. California Penal Code section 632(a) punishes recording a confidential communication 'intentionally and without the consent of all parties' (checked against the legislature's own text), and a number of other jurisdictions take the same line. So the app asks who was in the room, records that consent was obtained, refuses to process a file without that acknowledgement, and never offers a hidden or silent capture mode. Look up your own jurisdiction and write the answer in the README. Second, notes must never invent. Every action item, decision and attributed statement carries a timestamp range and the verbatim words it came from, and anything the model produces without a span to point at is dropped rather than shown. A confident fabricated commitment in a meeting summary is worse than no summary, because somebody acts on it. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process, bound to localhost · the recordings never leave the machine - Audio: FFmpeg for normalisation, always to the format the model requires · wrong sample rate is the commonest first failure - Transcription: whisper.cpp locally, chunked and resumable · a two hour file must survive a crash at ninety minutes - Notes: An extract-then-merge pass with verbatim spans required · one pass over a whole transcript is where fabrication happens - Storage: SQLite for meetings and segments, audio as files with a retention · the audio is the sensitive part and should not live forever ### Data model (create this before Phase 1) - meetings(id, title, occurred_at, participants_json, consent_recorded_by, consent_recorded_at, source_filename, duration_s) - media(meeting_id, original_path, original_sha256, normalised_path, sample_rate, channels, codec, converted_at) - chunks(meeting_id, ordinal, start_s, end_s, overlap_s, state, attempts, transcript_path, finished_at) · state is pending, running, done or failed - segments(meeting_id, id, start_s, end_s, speaker_label, text) · speaker_label is anonymous until a human maps it - speakers(meeting_id, label, mapped_name, mapped_by, mapped_at) · empty until somebody says who is who - notes(meeting_id, kind, text, start_s, end_s, quote, confidence) · kind is decision, action or context, and a row without a quote is never written - retention(meeting_id, audio_delete_after, deleted_at) · the audio goes, the notes stay - Every notes row points at a span of segments, which is what makes the output checkable rather than plausible. ### Phase 1 · Consent, then audio in the one format that works Build: Import asks for the participant list and an explicit consent acknowledgement, stored with a timestamp, and refuses to continue without it. Then normalise the audio with FFmpeg: whisper.cpp's own README states its command line runs only with 16-bit WAV files and gives the conversion as 'ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le output.wav' (checked against the repository), so convert to 16 kHz mono 16-bit PCM and record what the source actually was. Trap: Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. Done when: Importing without the consent acknowledgement is refused and nothing is written except the refusal, tested. Any of a stereo MP3, an M4A and a 48 kHz WAV import and end up as 16 kHz mono 16-bit PCM, verified by probing the output file rather than by trusting the command. The original file's hash is recorded and the original is not modified. The participant list and the consent record are visible on the meeting page and included in every export. Do not build yet: No transcription, speakers, notes or export yet. ### Phase 2 · Long audio that survives a crash Build: Split the normalised audio into chunks with a small overlap, on silence where you can find it. Transcribe one chunk at a time, writing its transcript and marking it done before starting the next. On restart, resume from the first chunk that is not done. Stitch using the overlap to remove duplicated words at the seams. Trap: Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. Done when: Killing the process mid-run and restarting resumes from the first unfinished chunk with completed chunks untouched, tested by killing it. Chunk boundaries fall on silence where silence exists, and words duplicated in the overlap appear once in the stitched transcript, tested with a file containing a sentence spanning a boundary. Every segment carries start and end times that line up with the audio when spot-checked. A chunk that fails repeatedly is marked failed with its error and the rest of the meeting still transcribes. Do not build yet: No speaker labels, notes or export yet. ### Phase 3 · Speakers, without guessing who anyone is Build: Separate turns into anonymous labels. Present them in the interface with a sample of each voice's first lines so a human can map label to name. Store the mapping with who made it and when. Nothing downstream may infer a name from the content. Trap: Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. Done when: Transcripts and notes display anonymous labels until a mapping exists, and the mapping records who made it. The notes pipeline receives labels rather than names, verified by inspecting what is passed to the model. An unmapped label in an export appears as the label, never as a guess. Changing a mapping updates every rendered view without rewriting stored segments. Do not build yet: No notes extraction or export yet. ### Phase 4 · Notes that point at the words Build: Extract per chunk, not per meeting: ask for decisions, actions and open questions, each with a verbatim quote and the segment ids it spans. Drop any item whose quote does not appear in the transcript, checked by string comparison. Then merge across chunks, deduplicating items that quote overlapping spans. Trap: Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. Done when: Every notes row has a quote that is present verbatim in the transcript, enforced by a check that runs on write and is covered by a test with a deliberately fabricated item. Clicking an action item jumps to its timestamp in the audio. Items extracted twice across a chunk boundary are merged rather than duplicated. Running extraction twice on the same meeting produces the same set of items or reports the difference, rather than quietly changing the notes. Do not build yet: No export, retention or wipe yet. ### Phase 5 · Markdown out, audio gone Build: Export a meeting as Markdown containing participants, the consent record, the notes with timestamps and quotes, and optionally the full transcript. Delete the audio after a stated retention, logging the deletion, and keep the notes. Provide one command that erases a meeting entirely, including derived files. Trap: Keeping every recording forever because storage is cheap means a laptop holding two years of confidential conversations from people who agreed to one meeting being recorded, and the first time that matters is the worst possible time. Set a retention, delete on schedule, log each deletion, and make the notes stand alone without the audio. Done when: A Markdown export opens in any editor and contains participants, the consent record, notes with timestamps and quotes, and nothing that requires this app to read. Audio past its retention is deleted on the scheduled run and the deletion is logged with the meeting id. A meeting whose audio has been deleted still renders its notes and transcript. The wipe command removes the database rows, the audio, the normalised copies and the chunk transcripts, verified by searching the data directory afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - A meeting bot that joins calls, which means platform credentials, review processes and a participant list that did not agree to a fourth attendee. - Live real-time transcription, which is a different latency problem and a different model deployment entirely. - Calendar and CRM integrations, each of which is an OAuth flow and a permanent upkeep commitment. - Cross-meeting analytics on people, which is workplace surveillance wearing a productivity hat and is not built here. ### README must contain - That recording usually requires the consent of everyone present, with California Penal Code section 632 cited as an example, and that the reader must check their own jurisdiction - That import is refused without a recorded consent acknowledgement, and there is no silent capture mode - The audio format the transcriber requires and the exact FFmpeg command used to reach it - That every note carries a verbatim quote and a timestamp, and items without one are discarded - The audio retention period, where recordings live, and how to wipe a meeting completely ===== AGENTS.md ===== # Agent instructions · Jamie indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with FastAPI, one process, bound to localhost, FFmpeg for normalisation, always to the format the model requires, whisper.cpp locally, chunked and resumable, An extract-then-merge pass with verbatim spans required, SQLite for meetings and segments, audio as files with a retention. 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 - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. ===== BUILD_PLAN.md ===== # Build plan · Jamie A local transcription and notes pipeline whose hard problems are consent, getting long audio through a model without losing an hour's work to a crash, and producing notes where every claim can be traced back to the words that produced it. 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, then audio in the one format that works A file is accepted only with a consent record, and is converted to exactly what the transcriber wants. ### Steps 1. Make consent a gate, not a checkbox in the README The import path refuses without it, and the test proves the refusal. Files: `src/import.py`, `tests/test_consent_gate.py` 2. Normalise with the documented FFmpeg invocation Sample rate, channels and codec, then probe the result. Files: `src/audio.py`, `tests/test_normalise.py` 3. Record what the source was So a bad transcript can be traced to a bad input. Files: `src/media.py` ### Done when - [ ] Import without consent is refused - [ ] Three input formats all normalise to 16 kHz mono PCM, probed - [ ] The original hash is recorded and the file unmodified - [ ] Consent and participants appear in exports ### Watch out - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. ## Phase 2 · Long audio that survives a crash A two hour recording transcribes in resumable pieces, and a failure costs one piece. ### Steps 1. Persist each chunk before starting the next The difference between a resumable job and a gamble. Files: `src/transcribe.py`, `tests/test_resume.py` 2. Overlap and de-duplicate at the seams A sentence cut in half is a sentence transcribed twice. Files: `src/stitch.py` 3. Fail one chunk, not the meeting Ninety-five percent of a transcript is useful; nothing is not. Files: `src/transcribe.py` ### Done when - [ ] A kill mid-run resumes from the first unfinished chunk - [ ] Overlapping words appear once after stitching - [ ] Segment times line up with the audio on a spot check - [ ] A failed chunk does not fail the meeting ### Watch out - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. ## Phase 3 · Speakers, without guessing who anyone is Turns are separated and labelled anonymously until a person says who each label is. ### Steps 1. Keep labels anonymous through the pipeline Names enter only at render, from the human mapping. Files: `src/speakers.py`, `tests/test_no_name_inference.py` 2. Give the human enough to map with First lines per label beats a waveform nobody can read. Files: `src/templates/speakers.html` 3. Substitute at render, not in storage So fixing a mistaken mapping is one update, not a migration. Files: `src/render.py` ### Done when - [ ] Labels stay anonymous until mapped - [ ] The notes pipeline never receives names - [ ] Unmapped labels export as labels - [ ] Changing a mapping updates every view ### Watch out - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. ## Phase 4 · Notes that point at the words Every decision and action item carries the timestamped quote it came from. ### Steps 1. Require a verbatim quote per item And verify it against the transcript before writing the row. Files: `src/notes.py`, `tests/test_quote_grounding.py` 2. Extract per chunk, merge after Long-context summarisation is where invented commitments come from. Files: `src/notes.py` 3. Link every item to its timestamp A note you can hear is a note you can settle an argument with. Files: `src/templates/meeting.html` ### Done when - [ ] Every item's quote is present verbatim in the transcript - [ ] A fabricated item is dropped by the check - [ ] Boundary duplicates are merged - [ ] Items link to a playable timestamp ### Watch out - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. ## Not in this build - A meeting bot that joins calls. - Live real-time transcription. - Calendar and CRM integrations. - Cross-meeting analytics on people. ## After v1, if you want it - Search across meetings, which is easy once segments carry timestamps and speaker labels - A weekly digest of open action items, built from the notes table and nothing new - Importing a transcript from elsewhere, skipping phases one and two entirely ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Database, audio, normalised copies and chunk transcripts. All of it sensitive. DATA_DIR=./data # Required. The downloaded model file. Larger is slower and more accurate. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Chunk length. Smaller means less lost to a crash and more seams to stitch. CHUNK_SECONDS=600 # Required. Overlap used to remove duplicated words at boundaries. CHUNK_OVERLAP_SECONDS=5 # Required. After this the recording is deleted and the deletion is logged. Notes survive. AUDIO_RETENTION_DAYS=30 # Optional · secret. Only if you use a hosted model for extraction. Unset means local extraction and no audio or transcript leaves the machine. NOTES_API_KEY=sk-...redacted
You are building a lean indie version of Jamie. 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 ===== # Jamie · indie build A local transcription and notes pipeline whose hard problems are consent, getting long audio through a model without losing an hour's work to a crash, and producing notes where every claim can be traced back to the words that produced it. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process, bound to localhost | the recordings never leave the machine | | Audio | FFmpeg for normalisation, always to the format the model requires | wrong sample rate is the commonest first failure | | Transcription | whisper.cpp locally, chunked and resumable | a two hour file must survive a crash at ninety minutes | | Notes | An extract-then-merge pass with verbatim spans required | one pass over a whole transcript is where fabrication happens | | Storage | SQLite for meetings and segments, audio as files with a retention | the audio is the sensitive part and should not live forever | ## 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 pipeline and the local web interface. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg** · free - Why: Every input is normalised to the format the transcriber documents, and this is the tool that does it. - Get it: Your package manager, or the official builds. - Verify: ffmpeg -version prints a version and the conversion command runs - [ ] **whisper.cpp with a downloaded model** · free, after the model download - Why: Local transcription is the whole reason this is private, and the model file is a separate download from the binary. - Get it: Build from the repository and fetch a model with the provided script. - Verify: The command line transcribes a short 16 kHz mono WAV - [ ] **The recording law where you are** · free - Why: Whether you may record with one party's consent or need everyone's changes what this app is allowed to do, and it is a question with an actual answer. - Get it: Read your jurisdiction's wiretapping or interception statute, or its regulator's guidance, and write a paragraph. - Verify: The paragraph is in the README and the import gate reflects it - [ ] **A long recording you are allowed to use** · free - Why: Resume, chunk stitching and retention are only testable on something over an hour, and a two minute clip proves nothing. - Get it: Record yourself reading for ninety minutes, or use a public-domain recording with a known transcript. - Verify: It exceeds an hour and you know what it says ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A meeting bot that joins calls. - Live real-time transcription. - Calendar and CRM integrations. - Cross-meeting analytics on people. - calendar and CRM integrations - cross-call team analytics - recording recovery, mastering, hosting, and distribution - meeting-bot auto-join - live multi-speaker accuracy If one of those is essential to you, that is the reason to keep paying for Jamie, and the README should say so rather than pretend. ===== BRIEF.md ===== # Build brief · Jamie 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 that turns a recording you were allowed to make into notes you can check against the audio to replace Jamie. 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 This tool records people. Before any of it, two rules. Consent is a legal question with a real answer where you live, and the answer is often that every participant must agree. California Penal Code section 632(a) punishes recording a confidential communication 'intentionally and without the consent of all parties' (checked against the legislature's own text), and a number of other jurisdictions take the same line. So the app asks who was in the room, records that consent was obtained, refuses to process a file without that acknowledgement, and never offers a hidden or silent capture mode. Look up your own jurisdiction and write the answer in the README. Second, notes must never invent. Every action item, decision and attributed statement carries a timestamp range and the verbatim words it came from, and anything the model produces without a span to point at is dropped rather than shown. A confident fabricated commitment in a meeting summary is worse than no summary, because somebody acts on it. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process, bound to localhost · the recordings never leave the machine - Audio: FFmpeg for normalisation, always to the format the model requires · wrong sample rate is the commonest first failure - Transcription: whisper.cpp locally, chunked and resumable · a two hour file must survive a crash at ninety minutes - Notes: An extract-then-merge pass with verbatim spans required · one pass over a whole transcript is where fabrication happens - Storage: SQLite for meetings and segments, audio as files with a retention · the audio is the sensitive part and should not live forever ### Data model (create this before Phase 1) - meetings(id, title, occurred_at, participants_json, consent_recorded_by, consent_recorded_at, source_filename, duration_s) - media(meeting_id, original_path, original_sha256, normalised_path, sample_rate, channels, codec, converted_at) - chunks(meeting_id, ordinal, start_s, end_s, overlap_s, state, attempts, transcript_path, finished_at) · state is pending, running, done or failed - segments(meeting_id, id, start_s, end_s, speaker_label, text) · speaker_label is anonymous until a human maps it - speakers(meeting_id, label, mapped_name, mapped_by, mapped_at) · empty until somebody says who is who - notes(meeting_id, kind, text, start_s, end_s, quote, confidence) · kind is decision, action or context, and a row without a quote is never written - retention(meeting_id, audio_delete_after, deleted_at) · the audio goes, the notes stay - Every notes row points at a span of segments, which is what makes the output checkable rather than plausible. ### Phase 1 · Consent, then audio in the one format that works Build: Import asks for the participant list and an explicit consent acknowledgement, stored with a timestamp, and refuses to continue without it. Then normalise the audio with FFmpeg: whisper.cpp's own README states its command line runs only with 16-bit WAV files and gives the conversion as 'ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le output.wav' (checked against the repository), so convert to 16 kHz mono 16-bit PCM and record what the source actually was. Trap: Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. Done when: Importing without the consent acknowledgement is refused and nothing is written except the refusal, tested. Any of a stereo MP3, an M4A and a 48 kHz WAV import and end up as 16 kHz mono 16-bit PCM, verified by probing the output file rather than by trusting the command. The original file's hash is recorded and the original is not modified. The participant list and the consent record are visible on the meeting page and included in every export. Do not build yet: No transcription, speakers, notes or export yet. ### Phase 2 · Long audio that survives a crash Build: Split the normalised audio into chunks with a small overlap, on silence where you can find it. Transcribe one chunk at a time, writing its transcript and marking it done before starting the next. On restart, resume from the first chunk that is not done. Stitch using the overlap to remove duplicated words at the seams. Trap: Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. Done when: Killing the process mid-run and restarting resumes from the first unfinished chunk with completed chunks untouched, tested by killing it. Chunk boundaries fall on silence where silence exists, and words duplicated in the overlap appear once in the stitched transcript, tested with a file containing a sentence spanning a boundary. Every segment carries start and end times that line up with the audio when spot-checked. A chunk that fails repeatedly is marked failed with its error and the rest of the meeting still transcribes. Do not build yet: No speaker labels, notes or export yet. ### Phase 3 · Speakers, without guessing who anyone is Build: Separate turns into anonymous labels. Present them in the interface with a sample of each voice's first lines so a human can map label to name. Store the mapping with who made it and when. Nothing downstream may infer a name from the content. Trap: Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. Done when: Transcripts and notes display anonymous labels until a mapping exists, and the mapping records who made it. The notes pipeline receives labels rather than names, verified by inspecting what is passed to the model. An unmapped label in an export appears as the label, never as a guess. Changing a mapping updates every rendered view without rewriting stored segments. Do not build yet: No notes extraction or export yet. ### Phase 4 · Notes that point at the words Build: Extract per chunk, not per meeting: ask for decisions, actions and open questions, each with a verbatim quote and the segment ids it spans. Drop any item whose quote does not appear in the transcript, checked by string comparison. Then merge across chunks, deduplicating items that quote overlapping spans. Trap: Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. Done when: Every notes row has a quote that is present verbatim in the transcript, enforced by a check that runs on write and is covered by a test with a deliberately fabricated item. Clicking an action item jumps to its timestamp in the audio. Items extracted twice across a chunk boundary are merged rather than duplicated. Running extraction twice on the same meeting produces the same set of items or reports the difference, rather than quietly changing the notes. Do not build yet: No export, retention or wipe yet. ### Phase 5 · Markdown out, audio gone Build: Export a meeting as Markdown containing participants, the consent record, the notes with timestamps and quotes, and optionally the full transcript. Delete the audio after a stated retention, logging the deletion, and keep the notes. Provide one command that erases a meeting entirely, including derived files. Trap: Keeping every recording forever because storage is cheap means a laptop holding two years of confidential conversations from people who agreed to one meeting being recorded, and the first time that matters is the worst possible time. Set a retention, delete on schedule, log each deletion, and make the notes stand alone without the audio. Done when: A Markdown export opens in any editor and contains participants, the consent record, notes with timestamps and quotes, and nothing that requires this app to read. Audio past its retention is deleted on the scheduled run and the deletion is logged with the meeting id. A meeting whose audio has been deleted still renders its notes and transcript. The wipe command removes the database rows, the audio, the normalised copies and the chunk transcripts, verified by searching the data directory afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - A meeting bot that joins calls, which means platform credentials, review processes and a participant list that did not agree to a fourth attendee. - Live real-time transcription, which is a different latency problem and a different model deployment entirely. - Calendar and CRM integrations, each of which is an OAuth flow and a permanent upkeep commitment. - Cross-meeting analytics on people, which is workplace surveillance wearing a productivity hat and is not built here. ### README must contain - That recording usually requires the consent of everyone present, with California Penal Code section 632 cited as an example, and that the reader must check their own jurisdiction - That import is refused without a recorded consent acknowledgement, and there is no silent capture mode - The audio format the transcriber requires and the exact FFmpeg command used to reach it - That every note carries a verbatim quote and a timestamp, and items without one are discarded - The audio retention period, where recordings live, and how to wipe a meeting completely ===== AGENTS.md ===== # Agent instructions · Jamie indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with FastAPI, one process, bound to localhost, FFmpeg for normalisation, always to the format the model requires, whisper.cpp locally, chunked and resumable, An extract-then-merge pass with verbatim spans required, SQLite for meetings and segments, audio as files with a retention. 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 - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. ===== BUILD_PLAN.md ===== # Build plan · Jamie A local transcription and notes pipeline whose hard problems are consent, getting long audio through a model without losing an hour's work to a crash, and producing notes where every claim can be traced back to the words that produced it. 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, then audio in the one format that works A file is accepted only with a consent record, and is converted to exactly what the transcriber wants. ### Steps 1. Make consent a gate, not a checkbox in the README The import path refuses without it, and the test proves the refusal. Files: `src/import.py`, `tests/test_consent_gate.py` 2. Normalise with the documented FFmpeg invocation Sample rate, channels and codec, then probe the result. Files: `src/audio.py`, `tests/test_normalise.py` 3. Record what the source was So a bad transcript can be traced to a bad input. Files: `src/media.py` ### Done when - [ ] Import without consent is refused - [ ] Three input formats all normalise to 16 kHz mono PCM, probed - [ ] The original hash is recorded and the file unmodified - [ ] Consent and participants appear in exports ### Watch out - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. ## Phase 2 · Long audio that survives a crash A two hour recording transcribes in resumable pieces, and a failure costs one piece. ### Steps 1. Persist each chunk before starting the next The difference between a resumable job and a gamble. Files: `src/transcribe.py`, `tests/test_resume.py` 2. Overlap and de-duplicate at the seams A sentence cut in half is a sentence transcribed twice. Files: `src/stitch.py` 3. Fail one chunk, not the meeting Ninety-five percent of a transcript is useful; nothing is not. Files: `src/transcribe.py` ### Done when - [ ] A kill mid-run resumes from the first unfinished chunk - [ ] Overlapping words appear once after stitching - [ ] Segment times line up with the audio on a spot check - [ ] A failed chunk does not fail the meeting ### Watch out - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. ## Phase 3 · Speakers, without guessing who anyone is Turns are separated and labelled anonymously until a person says who each label is. ### Steps 1. Keep labels anonymous through the pipeline Names enter only at render, from the human mapping. Files: `src/speakers.py`, `tests/test_no_name_inference.py` 2. Give the human enough to map with First lines per label beats a waveform nobody can read. Files: `src/templates/speakers.html` 3. Substitute at render, not in storage So fixing a mistaken mapping is one update, not a migration. Files: `src/render.py` ### Done when - [ ] Labels stay anonymous until mapped - [ ] The notes pipeline never receives names - [ ] Unmapped labels export as labels - [ ] Changing a mapping updates every view ### Watch out - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. ## Phase 4 · Notes that point at the words Every decision and action item carries the timestamped quote it came from. ### Steps 1. Require a verbatim quote per item And verify it against the transcript before writing the row. Files: `src/notes.py`, `tests/test_quote_grounding.py` 2. Extract per chunk, merge after Long-context summarisation is where invented commitments come from. Files: `src/notes.py` 3. Link every item to its timestamp A note you can hear is a note you can settle an argument with. Files: `src/templates/meeting.html` ### Done when - [ ] Every item's quote is present verbatim in the transcript - [ ] A fabricated item is dropped by the check - [ ] Boundary duplicates are merged - [ ] Items link to a playable timestamp ### Watch out - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. ## Not in this build - A meeting bot that joins calls. - Live real-time transcription. - Calendar and CRM integrations. - Cross-meeting analytics on people. ## After v1, if you want it - Search across meetings, which is easy once segments carry timestamps and speaker labels - A weekly digest of open action items, built from the notes table and nothing new - Importing a transcript from elsewhere, skipping phases one and two entirely ===== .env.example ===== # Copy to .env and fill in. Never commit .env; this file documents it. # Required. Database, audio, normalised copies and chunk transcripts. All of it sensitive. DATA_DIR=./data # Required. The downloaded model file. Larger is slower and more accurate. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Chunk length. Smaller means less lost to a crash and more seams to stitch. CHUNK_SECONDS=600 # Required. Overlap used to remove duplicated words at boundaries. CHUNK_OVERLAP_SECONDS=5 # Required. After this the recording is deleted and the deletion is logged. Notes survive. AUDIO_RETENTION_DAYS=30 # Optional · secret. Only if you use a hosted model for extraction. Unset means local extraction and no audio or transcript leaves the machine. NOTES_API_KEY=sk-...redacted
You are building a production product version of Jamie. 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 ===== # Jamie · product brief ## Problem The visible bot-free meeting notes loop is buildable, but a credible replacement needs more than the first screen. Jamie 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 that will not process a recording without a consent record, survives a crash in the middle of a two hour file, and produces notes where every commitment can be played back. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - FFmpeg - local Whisper model or optional API key in .env ## Explicit non-goals for v1 - A meeting bot that joins calls. - Live real-time transcription. - Calendar and CRM integrations. - Cross-meeting analytics on people. - calendar and CRM integrations - cross-call team analytics - recording recovery, mastering, hosting, and distribution - meeting-bot auto-join - live multi-speaker accuracy ## Success criteria - Import without consent is confirmed refused - Three source formats are confirmed to normalise correctly, probed - A kill mid-transcription is confirmed to resume - A fabricated note item is confirmed discarded - The wipe is confirmed to leave nothing behind ===== BRIEF.md ===== # Build brief · Jamie 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 that turns a recording you were allowed to make into notes you can check against the audio to replace Jamie. 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 This tool records people. Before any of it, two rules. Consent is a legal question with a real answer where you live, and the answer is often that every participant must agree. California Penal Code section 632(a) punishes recording a confidential communication 'intentionally and without the consent of all parties' (checked against the legislature's own text), and a number of other jurisdictions take the same line. So the app asks who was in the room, records that consent was obtained, refuses to process a file without that acknowledgement, and never offers a hidden or silent capture mode. Look up your own jurisdiction and write the answer in the README. Second, notes must never invent. Every action item, decision and attributed statement carries a timestamp range and the verbatim words it came from, and anything the model produces without a span to point at is dropped rather than shown. A confident fabricated commitment in a meeting summary is worse than no summary, because somebody acts on it. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process, bound to localhost · the recordings never leave the machine - Audio: FFmpeg for normalisation, always to the format the model requires · wrong sample rate is the commonest first failure - Transcription: whisper.cpp locally, chunked and resumable · a two hour file must survive a crash at ninety minutes - Notes: An extract-then-merge pass with verbatim spans required · one pass over a whole transcript is where fabrication happens - Storage: SQLite for meetings and segments, audio as files with a retention · the audio is the sensitive part and should not live forever ### Data model (create this before Phase 1) - meetings(id, title, occurred_at, participants_json, consent_recorded_by, consent_recorded_at, source_filename, duration_s) - media(meeting_id, original_path, original_sha256, normalised_path, sample_rate, channels, codec, converted_at) - chunks(meeting_id, ordinal, start_s, end_s, overlap_s, state, attempts, transcript_path, finished_at) · state is pending, running, done or failed - segments(meeting_id, id, start_s, end_s, speaker_label, text) · speaker_label is anonymous until a human maps it - speakers(meeting_id, label, mapped_name, mapped_by, mapped_at) · empty until somebody says who is who - notes(meeting_id, kind, text, start_s, end_s, quote, confidence) · kind is decision, action or context, and a row without a quote is never written - retention(meeting_id, audio_delete_after, deleted_at) · the audio goes, the notes stay - Every notes row points at a span of segments, which is what makes the output checkable rather than plausible. ### Phase 1 · Consent, then audio in the one format that works Build: Import asks for the participant list and an explicit consent acknowledgement, stored with a timestamp, and refuses to continue without it. Then normalise the audio with FFmpeg: whisper.cpp's own README states its command line runs only with 16-bit WAV files and gives the conversion as 'ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le output.wav' (checked against the repository), so convert to 16 kHz mono 16-bit PCM and record what the source actually was. Trap: Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. Done when: Importing without the consent acknowledgement is refused and nothing is written except the refusal, tested. Any of a stereo MP3, an M4A and a 48 kHz WAV import and end up as 16 kHz mono 16-bit PCM, verified by probing the output file rather than by trusting the command. The original file's hash is recorded and the original is not modified. The participant list and the consent record are visible on the meeting page and included in every export. Do not build yet: No transcription, speakers, notes or export yet. ### Phase 2 · Long audio that survives a crash Build: Split the normalised audio into chunks with a small overlap, on silence where you can find it. Transcribe one chunk at a time, writing its transcript and marking it done before starting the next. On restart, resume from the first chunk that is not done. Stitch using the overlap to remove duplicated words at the seams. Trap: Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. Done when: Killing the process mid-run and restarting resumes from the first unfinished chunk with completed chunks untouched, tested by killing it. Chunk boundaries fall on silence where silence exists, and words duplicated in the overlap appear once in the stitched transcript, tested with a file containing a sentence spanning a boundary. Every segment carries start and end times that line up with the audio when spot-checked. A chunk that fails repeatedly is marked failed with its error and the rest of the meeting still transcribes. Do not build yet: No speaker labels, notes or export yet. ### Phase 3 · Speakers, without guessing who anyone is Build: Separate turns into anonymous labels. Present them in the interface with a sample of each voice's first lines so a human can map label to name. Store the mapping with who made it and when. Nothing downstream may infer a name from the content. Trap: Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. Done when: Transcripts and notes display anonymous labels until a mapping exists, and the mapping records who made it. The notes pipeline receives labels rather than names, verified by inspecting what is passed to the model. An unmapped label in an export appears as the label, never as a guess. Changing a mapping updates every rendered view without rewriting stored segments. Do not build yet: No notes extraction or export yet. ### Phase 4 · Notes that point at the words Build: Extract per chunk, not per meeting: ask for decisions, actions and open questions, each with a verbatim quote and the segment ids it spans. Drop any item whose quote does not appear in the transcript, checked by string comparison. Then merge across chunks, deduplicating items that quote overlapping spans. Trap: Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. Done when: Every notes row has a quote that is present verbatim in the transcript, enforced by a check that runs on write and is covered by a test with a deliberately fabricated item. Clicking an action item jumps to its timestamp in the audio. Items extracted twice across a chunk boundary are merged rather than duplicated. Running extraction twice on the same meeting produces the same set of items or reports the difference, rather than quietly changing the notes. Do not build yet: No export, retention or wipe yet. ### Phase 5 · Markdown out, audio gone Build: Export a meeting as Markdown containing participants, the consent record, the notes with timestamps and quotes, and optionally the full transcript. Delete the audio after a stated retention, logging the deletion, and keep the notes. Provide one command that erases a meeting entirely, including derived files. Trap: Keeping every recording forever because storage is cheap means a laptop holding two years of confidential conversations from people who agreed to one meeting being recorded, and the first time that matters is the worst possible time. Set a retention, delete on schedule, log each deletion, and make the notes stand alone without the audio. Done when: A Markdown export opens in any editor and contains participants, the consent record, notes with timestamps and quotes, and nothing that requires this app to read. Audio past its retention is deleted on the scheduled run and the deletion is logged with the meeting id. A meeting whose audio has been deleted still renders its notes and transcript. The wipe command removes the database rows, the audio, the normalised copies and the chunk transcripts, verified by searching the data directory afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - A meeting bot that joins calls, which means platform credentials, review processes and a participant list that did not agree to a fourth attendee. - Live real-time transcription, which is a different latency problem and a different model deployment entirely. - Calendar and CRM integrations, each of which is an OAuth flow and a permanent upkeep commitment. - Cross-meeting analytics on people, which is workplace surveillance wearing a productivity hat and is not built here. ### README must contain - That recording usually requires the consent of everyone present, with California Penal Code section 632 cited as an example, and that the reader must check their own jurisdiction - That import is refused without a recorded consent acknowledgement, and there is no silent capture mode - The audio format the transcriber requires and the exact FFmpeg command used to reach it - That every note carries a verbatim quote and a timestamp, and items without one are discarded - The audio retention period, where recordings live, and how to wipe a meeting completely ===== ARCHITECTURE.md ===== # Architecture · Jamie ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process, bound to localhost | the recordings never leave the machine | | Audio | FFmpeg for normalisation, always to the format the model requires | wrong sample rate is the commonest first failure | | Transcription | whisper.cpp locally, chunked and resumable | a two hour file must survive a crash at ninety minutes | | Notes | An extract-then-merge pass with verbatim spans required | one pass over a whole transcript is where fabrication happens | | Storage | SQLite for meetings and segments, audio as files with a retention | the audio is the sensitive part and should not live forever | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Consent | The import gate and the participant record. | Not swappable for a checkbox that does not block. | | Audio | Normalisation to the documented format. | Any tool, provided the output is probed rather than assumed. | | Transcribe | Chunking, resume and stitching. | Any engine, provided each chunk is persisted before the next starts. | | Speakers | Anonymous labels and human mapping. | Any separation method, provided names never come from content. | | Notes | Per-chunk extraction with verified quotes. | Any model, provided an item without a matching quote is discarded. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATA_DIR` · required · Database, audio, normalised copies and chunk transcripts. All of it sensitive. - `WHISPER_MODEL_PATH` · required · The downloaded model file. Larger is slower and more accurate. - `CHUNK_SECONDS` · required · Chunk length. Smaller means less lost to a crash and more seams to stitch. - `CHUNK_OVERLAP_SECONDS` · required · Overlap used to remove duplicated words at boundaries. - `AUDIO_RETENTION_DAYS` · required · After this the recording is deleted and the deletion is logged. Notes survive. - `NOTES_API_KEY` · optional, secret · Only if you use a hosted model for extraction. Unset means local extraction and no audio or transcript leaves the machine. ## 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 · Jamie product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with FastAPI, one process, bound to localhost, FFmpeg for normalisation, always to the format the model requires, whisper.cpp locally, chunked and resumable, An extract-then-merge pass with verbatim spans required, SQLite for meetings and segments, audio as files with a retention. - 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 - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. - Keeping every recording forever because storage is cheap means a laptop holding two years of confidential conversations from people who agreed to one meeting being recorded, and the first time that matters is the worst possible time. Set a retention, delete on schedule, log each deletion, and make the notes stand alone without the audio. ===== MILESTONES.md ===== # Delivery milestones · Jamie Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Consent, then audio in the one format that works A file is accepted only with a consent record, and is converted to exactly what the transcriber wants. ### Steps 1. Make consent a gate, not a checkbox in the README The import path refuses without it, and the test proves the refusal. Files: `src/import.py`, `tests/test_consent_gate.py` 2. Normalise with the documented FFmpeg invocation Sample rate, channels and codec, then probe the result. Files: `src/audio.py`, `tests/test_normalise.py` 3. Record what the source was So a bad transcript can be traced to a bad input. Files: `src/media.py` ### Done when - [ ] Import without consent is refused - [ ] Three input formats all normalise to 16 kHz mono PCM, probed - [ ] The original hash is recorded and the file unmodified - [ ] Consent and participants appear in exports ### Watch out - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. ## M2 · Long audio that survives a crash A two hour recording transcribes in resumable pieces, and a failure costs one piece. ### Steps 1. Persist each chunk before starting the next The difference between a resumable job and a gamble. Files: `src/transcribe.py`, `tests/test_resume.py` 2. Overlap and de-duplicate at the seams A sentence cut in half is a sentence transcribed twice. Files: `src/stitch.py` 3. Fail one chunk, not the meeting Ninety-five percent of a transcript is useful; nothing is not. Files: `src/transcribe.py` ### Done when - [ ] A kill mid-run resumes from the first unfinished chunk - [ ] Overlapping words appear once after stitching - [ ] Segment times line up with the audio on a spot check - [ ] A failed chunk does not fail the meeting ### Watch out - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. ## M3 · Speakers, without guessing who anyone is Turns are separated and labelled anonymously until a person says who each label is. ### Steps 1. Keep labels anonymous through the pipeline Names enter only at render, from the human mapping. Files: `src/speakers.py`, `tests/test_no_name_inference.py` 2. Give the human enough to map with First lines per label beats a waveform nobody can read. Files: `src/templates/speakers.html` 3. Substitute at render, not in storage So fixing a mistaken mapping is one update, not a migration. Files: `src/render.py` ### Done when - [ ] Labels stay anonymous until mapped - [ ] The notes pipeline never receives names - [ ] Unmapped labels export as labels - [ ] Changing a mapping updates every view ### Watch out - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. ## M4 · Notes that point at the words Every decision and action item carries the timestamped quote it came from. ### Steps 1. Require a verbatim quote per item And verify it against the transcript before writing the row. Files: `src/notes.py`, `tests/test_quote_grounding.py` 2. Extract per chunk, merge after Long-context summarisation is where invented commitments come from. Files: `src/notes.py` 3. Link every item to its timestamp A note you can hear is a note you can settle an argument with. Files: `src/templates/meeting.html` ### Done when - [ ] Every item's quote is present verbatim in the transcript - [ ] A fabricated item is dropped by the check - [ ] Boundary duplicates are merged - [ ] Items link to a playable timestamp ### Watch out - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. ## M5 · Markdown out, audio gone (production only) Notes leave in a plain format and the sensitive part does not linger. ### Steps 1. Export Markdown that stands alone Consent record included, because it is part of the record. Files: `src/export.py` 2. Delete audio on schedule and log it Retention that nobody runs is a promise, not a policy. Files: `src/retention.py`, `tests/test_retention.py` 3. Make the wipe reach every derived file Chunk transcripts and normalised copies are recordings too. Files: `src/wipe.py`, `tests/test_wipe.py` ### Done when - [ ] Markdown export is complete and standalone - [ ] Audio past retention is deleted and logged - [ ] Notes still render after the audio is gone - [ ] The wipe leaves nothing in the data directory ### Watch out - Keeping every recording forever because storage is cheap means a laptop holding two years of confidential conversations from people who agreed to one meeting being recorded, and the first time that matters is the worst possible time. Set a retention, delete on schedule, log each deletion, and make the notes stand alone without the audio. ===== OPERATIONS.md ===== # Operations · Jamie ## Backup The database and the notes, separately from the audio. Notes are small and worth keeping; recordings are large, sensitive and deliberately temporary, so backing them up quietly defeats the retention policy you wrote. ## Restore Restore the database and confirm a meeting whose audio is already deleted still renders its notes and transcript. That is the normal state for anything older than the retention, so it is the state the restore must handle. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Chunks stuck in running, meetings with failed chunks, the retention job's last successful run, and the count of notes rows rejected for an unverifiable quote. A rising rejection count means the extraction prompt has drifted. ## Incident checklist Someone disputing a note is answered by playing the timestamp, which is the entire reason the quote and span are stored. A recording that should not exist is answered by the wipe command, which is tested rather than described. 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 - [ ] Import without consent is confirmed refused - [ ] Three source formats are confirmed to normalise correctly, probed - [ ] A kill mid-transcription is confirmed to resume - [ ] A fabricated note item is confirmed discarded - [ ] The wipe is confirmed to leave nothing behind ## Launch constraint Do not market omitted Jamie 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. Database, audio, normalised copies and chunk transcripts. All of it sensitive. DATA_DIR=./data # Required. The downloaded model file. Larger is slower and more accurate. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Chunk length. Smaller means less lost to a crash and more seams to stitch. CHUNK_SECONDS=600 # Required. Overlap used to remove duplicated words at boundaries. CHUNK_OVERLAP_SECONDS=5 # Required. After this the recording is deleted and the deletion is logged. Notes survive. AUDIO_RETENTION_DAYS=30 # Optional · secret. Only if you use a hosted model for extraction. Unset means local extraction and no audio or transcript leaves the machine. NOTES_API_KEY=sk-...redacted
# Jamie · indie build A local transcription and notes pipeline whose hard problems are consent, getting long audio through a model without losing an hour's work to a crash, and producing notes where every claim can be traced back to the words that produced it. Estimated effort: **weekend**. Work `BUILD_PLAN.md` top to bottom · every phase ends in a check that has to pass before the next one starts. ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process, bound to localhost | the recordings never leave the machine | | Audio | FFmpeg for normalisation, always to the format the model requires | wrong sample rate is the commonest first failure | | Transcription | whisper.cpp locally, chunked and resumable | a two hour file must survive a crash at ninety minutes | | Notes | An extract-then-merge pass with verbatim spans required | one pass over a whole transcript is where fabrication happens | | Storage | SQLite for meetings and segments, audio as files with a retention | the audio is the sensitive part and should not live forever | ## 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 pipeline and the local web interface. - Get it: python.org, or your package manager. - Verify: python3 --version prints 3.12 or higher - [ ] **FFmpeg** · free - Why: Every input is normalised to the format the transcriber documents, and this is the tool that does it. - Get it: Your package manager, or the official builds. - Verify: ffmpeg -version prints a version and the conversion command runs - [ ] **whisper.cpp with a downloaded model** · free, after the model download - Why: Local transcription is the whole reason this is private, and the model file is a separate download from the binary. - Get it: Build from the repository and fetch a model with the provided script. - Verify: The command line transcribes a short 16 kHz mono WAV - [ ] **The recording law where you are** · free - Why: Whether you may record with one party's consent or need everyone's changes what this app is allowed to do, and it is a question with an actual answer. - Get it: Read your jurisdiction's wiretapping or interception statute, or its regulator's guidance, and write a paragraph. - Verify: The paragraph is in the README and the import gate reflects it - [ ] **A long recording you are allowed to use** · free - Why: Resume, chunk stitching and retention are only testable on something over an hour, and a two minute clip proves nothing. - Get it: Record yourself reading for ninety minutes, or use a public-domain recording with a known transcript. - Verify: It exceeds an hour and you know what it says ## Quick start Follow Phase 1 in `BUILD_PLAN.md`; it creates the project. Then copy `.env.example` to `.env` and fill in the values it documents. ## Honest limits This build deliberately does not replace: - A meeting bot that joins calls. - Live real-time transcription. - Calendar and CRM integrations. - Cross-meeting analytics on people. - calendar and CRM integrations - cross-call team analytics - recording recovery, mastering, hosting, and distribution - meeting-bot auto-join - live multi-speaker accuracy If one of those is essential to you, that is the reason to keep paying for Jamie, and the README should say so rather than pretend.
# Build brief · Jamie 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 that turns a recording you were allowed to make into notes you can check against the audio to replace Jamie. 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 This tool records people. Before any of it, two rules. Consent is a legal question with a real answer where you live, and the answer is often that every participant must agree. California Penal Code section 632(a) punishes recording a confidential communication 'intentionally and without the consent of all parties' (checked against the legislature's own text), and a number of other jurisdictions take the same line. So the app asks who was in the room, records that consent was obtained, refuses to process a file without that acknowledgement, and never offers a hidden or silent capture mode. Look up your own jurisdiction and write the answer in the README. Second, notes must never invent. Every action item, decision and attributed statement carries a timestamp range and the verbatim words it came from, and anything the model produces without a span to point at is dropped rather than shown. A confident fabricated commitment in a meeting summary is worse than no summary, because somebody acts on it. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process, bound to localhost · the recordings never leave the machine - Audio: FFmpeg for normalisation, always to the format the model requires · wrong sample rate is the commonest first failure - Transcription: whisper.cpp locally, chunked and resumable · a two hour file must survive a crash at ninety minutes - Notes: An extract-then-merge pass with verbatim spans required · one pass over a whole transcript is where fabrication happens - Storage: SQLite for meetings and segments, audio as files with a retention · the audio is the sensitive part and should not live forever ### Data model (create this before Phase 1) - meetings(id, title, occurred_at, participants_json, consent_recorded_by, consent_recorded_at, source_filename, duration_s) - media(meeting_id, original_path, original_sha256, normalised_path, sample_rate, channels, codec, converted_at) - chunks(meeting_id, ordinal, start_s, end_s, overlap_s, state, attempts, transcript_path, finished_at) · state is pending, running, done or failed - segments(meeting_id, id, start_s, end_s, speaker_label, text) · speaker_label is anonymous until a human maps it - speakers(meeting_id, label, mapped_name, mapped_by, mapped_at) · empty until somebody says who is who - notes(meeting_id, kind, text, start_s, end_s, quote, confidence) · kind is decision, action or context, and a row without a quote is never written - retention(meeting_id, audio_delete_after, deleted_at) · the audio goes, the notes stay - Every notes row points at a span of segments, which is what makes the output checkable rather than plausible. ### Phase 1 · Consent, then audio in the one format that works Build: Import asks for the participant list and an explicit consent acknowledgement, stored with a timestamp, and refuses to continue without it. Then normalise the audio with FFmpeg: whisper.cpp's own README states its command line runs only with 16-bit WAV files and gives the conversion as 'ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le output.wav' (checked against the repository), so convert to 16 kHz mono 16-bit PCM and record what the source actually was. Trap: Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. Done when: Importing without the consent acknowledgement is refused and nothing is written except the refusal, tested. Any of a stereo MP3, an M4A and a 48 kHz WAV import and end up as 16 kHz mono 16-bit PCM, verified by probing the output file rather than by trusting the command. The original file's hash is recorded and the original is not modified. The participant list and the consent record are visible on the meeting page and included in every export. Do not build yet: No transcription, speakers, notes or export yet. ### Phase 2 · Long audio that survives a crash Build: Split the normalised audio into chunks with a small overlap, on silence where you can find it. Transcribe one chunk at a time, writing its transcript and marking it done before starting the next. On restart, resume from the first chunk that is not done. Stitch using the overlap to remove duplicated words at the seams. Trap: Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. Done when: Killing the process mid-run and restarting resumes from the first unfinished chunk with completed chunks untouched, tested by killing it. Chunk boundaries fall on silence where silence exists, and words duplicated in the overlap appear once in the stitched transcript, tested with a file containing a sentence spanning a boundary. Every segment carries start and end times that line up with the audio when spot-checked. A chunk that fails repeatedly is marked failed with its error and the rest of the meeting still transcribes. Do not build yet: No speaker labels, notes or export yet. ### Phase 3 · Speakers, without guessing who anyone is Build: Separate turns into anonymous labels. Present them in the interface with a sample of each voice's first lines so a human can map label to name. Store the mapping with who made it and when. Nothing downstream may infer a name from the content. Trap: Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. Done when: Transcripts and notes display anonymous labels until a mapping exists, and the mapping records who made it. The notes pipeline receives labels rather than names, verified by inspecting what is passed to the model. An unmapped label in an export appears as the label, never as a guess. Changing a mapping updates every rendered view without rewriting stored segments. Do not build yet: No notes extraction or export yet. ### Phase 4 · Notes that point at the words Build: Extract per chunk, not per meeting: ask for decisions, actions and open questions, each with a verbatim quote and the segment ids it spans. Drop any item whose quote does not appear in the transcript, checked by string comparison. Then merge across chunks, deduplicating items that quote overlapping spans. Trap: Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. Done when: Every notes row has a quote that is present verbatim in the transcript, enforced by a check that runs on write and is covered by a test with a deliberately fabricated item. Clicking an action item jumps to its timestamp in the audio. Items extracted twice across a chunk boundary are merged rather than duplicated. Running extraction twice on the same meeting produces the same set of items or reports the difference, rather than quietly changing the notes. Do not build yet: No export, retention or wipe yet. ### Phase 5 · Markdown out, audio gone Build: Export a meeting as Markdown containing participants, the consent record, the notes with timestamps and quotes, and optionally the full transcript. Delete the audio after a stated retention, logging the deletion, and keep the notes. Provide one command that erases a meeting entirely, including derived files. Trap: Keeping every recording forever because storage is cheap means a laptop holding two years of confidential conversations from people who agreed to one meeting being recorded, and the first time that matters is the worst possible time. Set a retention, delete on schedule, log each deletion, and make the notes stand alone without the audio. Done when: A Markdown export opens in any editor and contains participants, the consent record, notes with timestamps and quotes, and nothing that requires this app to read. Audio past its retention is deleted on the scheduled run and the deletion is logged with the meeting id. A meeting whose audio has been deleted still renders its notes and transcript. The wipe command removes the database rows, the audio, the normalised copies and the chunk transcripts, verified by searching the data directory afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - A meeting bot that joins calls, which means platform credentials, review processes and a participant list that did not agree to a fourth attendee. - Live real-time transcription, which is a different latency problem and a different model deployment entirely. - Calendar and CRM integrations, each of which is an OAuth flow and a permanent upkeep commitment. - Cross-meeting analytics on people, which is workplace surveillance wearing a productivity hat and is not built here. ### README must contain - That recording usually requires the consent of everyone present, with California Penal Code section 632 cited as an example, and that the reader must check their own jurisdiction - That import is refused without a recorded consent acknowledgement, and there is no silent capture mode - The audio format the transcriber requires and the exact FFmpeg command used to reach it - That every note carries a verbatim quote and a timestamp, and items without one are discarded - The audio retention period, where recordings live, and how to wipe a meeting completely
# Agent instructions · Jamie indie build - Read `README.md` and `BUILD_PLAN.md` before writing code. The stack is fixed: Python 3.12 with FastAPI, one process, bound to localhost, FFmpeg for normalisation, always to the format the model requires, whisper.cpp locally, chunked and resumable, An extract-then-merge pass with verbatim spans required, SQLite for meetings and segments, audio as files with a retention. 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 - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not.
# Build plan · Jamie A local transcription and notes pipeline whose hard problems are consent, getting long audio through a model without losing an hour's work to a crash, and producing notes where every claim can be traced back to the words that produced it. 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, then audio in the one format that works A file is accepted only with a consent record, and is converted to exactly what the transcriber wants. ### Steps 1. Make consent a gate, not a checkbox in the README The import path refuses without it, and the test proves the refusal. Files: `src/import.py`, `tests/test_consent_gate.py` 2. Normalise with the documented FFmpeg invocation Sample rate, channels and codec, then probe the result. Files: `src/audio.py`, `tests/test_normalise.py` 3. Record what the source was So a bad transcript can be traced to a bad input. Files: `src/media.py` ### Done when - [ ] Import without consent is refused - [ ] Three input formats all normalise to 16 kHz mono PCM, probed - [ ] The original hash is recorded and the file unmodified - [ ] Consent and participants appear in exports ### Watch out - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. ## Phase 2 · Long audio that survives a crash A two hour recording transcribes in resumable pieces, and a failure costs one piece. ### Steps 1. Persist each chunk before starting the next The difference between a resumable job and a gamble. Files: `src/transcribe.py`, `tests/test_resume.py` 2. Overlap and de-duplicate at the seams A sentence cut in half is a sentence transcribed twice. Files: `src/stitch.py` 3. Fail one chunk, not the meeting Ninety-five percent of a transcript is useful; nothing is not. Files: `src/transcribe.py` ### Done when - [ ] A kill mid-run resumes from the first unfinished chunk - [ ] Overlapping words appear once after stitching - [ ] Segment times line up with the audio on a spot check - [ ] A failed chunk does not fail the meeting ### Watch out - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. ## Phase 3 · Speakers, without guessing who anyone is Turns are separated and labelled anonymously until a person says who each label is. ### Steps 1. Keep labels anonymous through the pipeline Names enter only at render, from the human mapping. Files: `src/speakers.py`, `tests/test_no_name_inference.py` 2. Give the human enough to map with First lines per label beats a waveform nobody can read. Files: `src/templates/speakers.html` 3. Substitute at render, not in storage So fixing a mistaken mapping is one update, not a migration. Files: `src/render.py` ### Done when - [ ] Labels stay anonymous until mapped - [ ] The notes pipeline never receives names - [ ] Unmapped labels export as labels - [ ] Changing a mapping updates every view ### Watch out - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. ## Phase 4 · Notes that point at the words Every decision and action item carries the timestamped quote it came from. ### Steps 1. Require a verbatim quote per item And verify it against the transcript before writing the row. Files: `src/notes.py`, `tests/test_quote_grounding.py` 2. Extract per chunk, merge after Long-context summarisation is where invented commitments come from. Files: `src/notes.py` 3. Link every item to its timestamp A note you can hear is a note you can settle an argument with. Files: `src/templates/meeting.html` ### Done when - [ ] Every item's quote is present verbatim in the transcript - [ ] A fabricated item is dropped by the check - [ ] Boundary duplicates are merged - [ ] Items link to a playable timestamp ### Watch out - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. ## Not in this build - A meeting bot that joins calls. - Live real-time transcription. - Calendar and CRM integrations. - Cross-meeting analytics on people. ## After v1, if you want it - Search across meetings, which is easy once segments carry timestamps and speaker labels - A weekly digest of open action items, built from the notes table and nothing new - Importing a transcript from elsewhere, skipping phases one and two entirely
# Copy to .env and fill in. Never commit .env; this file documents it. # Required. Database, audio, normalised copies and chunk transcripts. All of it sensitive. DATA_DIR=./data # Required. The downloaded model file. Larger is slower and more accurate. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Chunk length. Smaller means less lost to a crash and more seams to stitch. CHUNK_SECONDS=600 # Required. Overlap used to remove duplicated words at boundaries. CHUNK_OVERLAP_SECONDS=5 # Required. After this the recording is deleted and the deletion is logged. Notes survive. AUDIO_RETENTION_DAYS=30 # Optional · secret. Only if you use a hosted model for extraction. Unset means local extraction and no audio or transcript leaves the machine. NOTES_API_KEY=sk-...redacted
# Jamie · product brief ## Problem The visible bot-free meeting notes loop is buildable, but a credible replacement needs more than the first screen. Jamie 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 that will not process a recording without a consent record, survives a crash in the middle of a two hour file, and produces notes where every commitment can be played back. ## Target user A builder who needs a maintainable product foundation, not a one-off demo. ## Required capabilities - Python 3.12 - FFmpeg - local Whisper model or optional API key in .env ## Explicit non-goals for v1 - A meeting bot that joins calls. - Live real-time transcription. - Calendar and CRM integrations. - Cross-meeting analytics on people. - calendar and CRM integrations - cross-call team analytics - recording recovery, mastering, hosting, and distribution - meeting-bot auto-join - live multi-speaker accuracy ## Success criteria - Import without consent is confirmed refused - Three source formats are confirmed to normalise correctly, probed - A kill mid-transcription is confirmed to resume - A fabricated note item is confirmed discarded - The wipe is confirmed to leave nothing behind
# Build brief · Jamie 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 that turns a recording you were allowed to make into notes you can check against the audio to replace Jamie. 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 This tool records people. Before any of it, two rules. Consent is a legal question with a real answer where you live, and the answer is often that every participant must agree. California Penal Code section 632(a) punishes recording a confidential communication 'intentionally and without the consent of all parties' (checked against the legislature's own text), and a number of other jurisdictions take the same line. So the app asks who was in the room, records that consent was obtained, refuses to process a file without that acknowledgement, and never offers a hidden or silent capture mode. Look up your own jurisdiction and write the answer in the README. Second, notes must never invent. Every action item, decision and attributed statement carries a timestamp range and the verbatim words it came from, and anything the model produces without a span to point at is dropped rather than shown. A confident fabricated commitment in a meeting summary is worse than no summary, because somebody acts on it. ### Stack (fixed, do not substitute) - Runtime: Python 3.12 with FastAPI, one process, bound to localhost · the recordings never leave the machine - Audio: FFmpeg for normalisation, always to the format the model requires · wrong sample rate is the commonest first failure - Transcription: whisper.cpp locally, chunked and resumable · a two hour file must survive a crash at ninety minutes - Notes: An extract-then-merge pass with verbatim spans required · one pass over a whole transcript is where fabrication happens - Storage: SQLite for meetings and segments, audio as files with a retention · the audio is the sensitive part and should not live forever ### Data model (create this before Phase 1) - meetings(id, title, occurred_at, participants_json, consent_recorded_by, consent_recorded_at, source_filename, duration_s) - media(meeting_id, original_path, original_sha256, normalised_path, sample_rate, channels, codec, converted_at) - chunks(meeting_id, ordinal, start_s, end_s, overlap_s, state, attempts, transcript_path, finished_at) · state is pending, running, done or failed - segments(meeting_id, id, start_s, end_s, speaker_label, text) · speaker_label is anonymous until a human maps it - speakers(meeting_id, label, mapped_name, mapped_by, mapped_at) · empty until somebody says who is who - notes(meeting_id, kind, text, start_s, end_s, quote, confidence) · kind is decision, action or context, and a row without a quote is never written - retention(meeting_id, audio_delete_after, deleted_at) · the audio goes, the notes stay - Every notes row points at a span of segments, which is what makes the output checkable rather than plausible. ### Phase 1 · Consent, then audio in the one format that works Build: Import asks for the participant list and an explicit consent acknowledgement, stored with a timestamp, and refuses to continue without it. Then normalise the audio with FFmpeg: whisper.cpp's own README states its command line runs only with 16-bit WAV files and gives the conversion as 'ffmpeg -i input.mp3 -ar 16000 -ac 1 -c:a pcm_s16le output.wav' (checked against the repository), so convert to 16 kHz mono 16-bit PCM and record what the source actually was. Trap: Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. Done when: Importing without the consent acknowledgement is refused and nothing is written except the refusal, tested. Any of a stereo MP3, an M4A and a 48 kHz WAV import and end up as 16 kHz mono 16-bit PCM, verified by probing the output file rather than by trusting the command. The original file's hash is recorded and the original is not modified. The participant list and the consent record are visible on the meeting page and included in every export. Do not build yet: No transcription, speakers, notes or export yet. ### Phase 2 · Long audio that survives a crash Build: Split the normalised audio into chunks with a small overlap, on silence where you can find it. Transcribe one chunk at a time, writing its transcript and marking it done before starting the next. On restart, resume from the first chunk that is not done. Stitch using the overlap to remove duplicated words at the seams. Trap: Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. Done when: Killing the process mid-run and restarting resumes from the first unfinished chunk with completed chunks untouched, tested by killing it. Chunk boundaries fall on silence where silence exists, and words duplicated in the overlap appear once in the stitched transcript, tested with a file containing a sentence spanning a boundary. Every segment carries start and end times that line up with the audio when spot-checked. A chunk that fails repeatedly is marked failed with its error and the rest of the meeting still transcribes. Do not build yet: No speaker labels, notes or export yet. ### Phase 3 · Speakers, without guessing who anyone is Build: Separate turns into anonymous labels. Present them in the interface with a sample of each voice's first lines so a human can map label to name. Store the mapping with who made it and when. Nothing downstream may infer a name from the content. Trap: Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. Done when: Transcripts and notes display anonymous labels until a mapping exists, and the mapping records who made it. The notes pipeline receives labels rather than names, verified by inspecting what is passed to the model. An unmapped label in an export appears as the label, never as a guess. Changing a mapping updates every rendered view without rewriting stored segments. Do not build yet: No notes extraction or export yet. ### Phase 4 · Notes that point at the words Build: Extract per chunk, not per meeting: ask for decisions, actions and open questions, each with a verbatim quote and the segment ids it spans. Drop any item whose quote does not appear in the transcript, checked by string comparison. Then merge across chunks, deduplicating items that quote overlapping spans. Trap: Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. Done when: Every notes row has a quote that is present verbatim in the transcript, enforced by a check that runs on write and is covered by a test with a deliberately fabricated item. Clicking an action item jumps to its timestamp in the audio. Items extracted twice across a chunk boundary are merged rather than duplicated. Running extraction twice on the same meeting produces the same set of items or reports the difference, rather than quietly changing the notes. Do not build yet: No export, retention or wipe yet. ### Phase 5 · Markdown out, audio gone Build: Export a meeting as Markdown containing participants, the consent record, the notes with timestamps and quotes, and optionally the full transcript. Delete the audio after a stated retention, logging the deletion, and keep the notes. Provide one command that erases a meeting entirely, including derived files. Trap: Keeping every recording forever because storage is cheap means a laptop holding two years of confidential conversations from people who agreed to one meeting being recorded, and the first time that matters is the worst possible time. Set a retention, delete on schedule, log each deletion, and make the notes stand alone without the audio. Done when: A Markdown export opens in any editor and contains participants, the consent record, notes with timestamps and quotes, and nothing that requires this app to read. Audio past its retention is deleted on the scheduled run and the deletion is logged with the meeting id. A meeting whose audio has been deleted still renders its notes and transcript. The wipe command removes the database rows, the audio, the normalised copies and the chunk transcripts, verified by searching the data directory afterwards. Do not build yet: Nothing after this. ### Out of scope (and why) - A meeting bot that joins calls, which means platform credentials, review processes and a participant list that did not agree to a fourth attendee. - Live real-time transcription, which is a different latency problem and a different model deployment entirely. - Calendar and CRM integrations, each of which is an OAuth flow and a permanent upkeep commitment. - Cross-meeting analytics on people, which is workplace surveillance wearing a productivity hat and is not built here. ### README must contain - That recording usually requires the consent of everyone present, with California Penal Code section 632 cited as an example, and that the reader must check their own jurisdiction - That import is refused without a recorded consent acknowledgement, and there is no silent capture mode - The audio format the transcriber requires and the exact FFmpeg command used to reach it - That every note carries a verbatim quote and a timestamp, and items without one are discarded - The audio retention period, where recordings live, and how to wipe a meeting completely
# Architecture · Jamie ## Stack | Part | Choice | Why | | --- | --- | --- | | Runtime | Python 3.12 with FastAPI, one process, bound to localhost | the recordings never leave the machine | | Audio | FFmpeg for normalisation, always to the format the model requires | wrong sample rate is the commonest first failure | | Transcription | whisper.cpp locally, chunked and resumable | a two hour file must survive a crash at ninety minutes | | Notes | An extract-then-merge pass with verbatim spans required | one pass over a whole transcript is where fabrication happens | | Storage | SQLite for meetings and segments, audio as files with a retention | the audio is the sensitive part and should not live forever | ## Modules Each module has one owner concern and a documented way to replace it. | Module | Owns | How to replace it | | --- | --- | --- | | Consent | The import gate and the participant record. | Not swappable for a checkbox that does not block. | | Audio | Normalisation to the documented format. | Any tool, provided the output is probed rather than assumed. | | Transcribe | Chunking, resume and stitching. | Any engine, provided each chunk is persisted before the next starts. | | Speakers | Anonymous labels and human mapping. | Any separation method, provided names never come from content. | | Notes | Per-chunk extraction with verified quotes. | Any model, provided an item without a matching quote is discarded. | ## Configuration Every runtime setting is an environment variable documented in `.env.example`, validated at startup, with a safe local default wherever one exists. - `DATA_DIR` · required · Database, audio, normalised copies and chunk transcripts. All of it sensitive. - `WHISPER_MODEL_PATH` · required · The downloaded model file. Larger is slower and more accurate. - `CHUNK_SECONDS` · required · Chunk length. Smaller means less lost to a crash and more seams to stitch. - `CHUNK_OVERLAP_SECONDS` · required · Overlap used to remove duplicated words at boundaries. - `AUDIO_RETENTION_DAYS` · required · After this the recording is deleted and the deletion is logged. Notes survive. - `NOTES_API_KEY` · optional, secret · Only if you use a hosted model for extraction. Unset means local extraction and no audio or transcript leaves the machine. ## 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 · Jamie product build - Read `PRODUCT.md` and `ARCHITECTURE.md` before changing code. The stack is fixed: Python 3.12 with FastAPI, one process, bound to localhost, FFmpeg for normalisation, always to the format the model requires, whisper.cpp locally, chunked and resumable, An extract-then-merge pass with verbatim spans required, SQLite for meetings and segments, audio as files with a retention. - 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 - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. - Keeping every recording forever because storage is cheap means a laptop holding two years of confidential conversations from people who agreed to one meeting being recorded, and the first time that matters is the worst possible time. Set a retention, delete on schedule, log each deletion, and make the notes stand alone without the audio.
# Delivery milestones · Jamie Estimated effort: **weekend** for the indie phases; the production-only milestones add the trust and operability layer. ## M1 · Consent, then audio in the one format that works A file is accepted only with a consent record, and is converted to exactly what the transcriber wants. ### Steps 1. Make consent a gate, not a checkbox in the README The import path refuses without it, and the test proves the refusal. Files: `src/import.py`, `tests/test_consent_gate.py` 2. Normalise with the documented FFmpeg invocation Sample rate, channels and codec, then probe the result. Files: `src/audio.py`, `tests/test_normalise.py` 3. Record what the source was So a bad transcript can be traced to a bad input. Files: `src/media.py` ### Done when - [ ] Import without consent is refused - [ ] Three input formats all normalise to 16 kHz mono PCM, probed - [ ] The original hash is recorded and the file unmodified - [ ] Consent and participants appear in exports ### Watch out - Handing a 48 kHz stereo recording from a phone straight to the transcriber produces either an outright error or, worse, a transcript that is subtly wrong throughout with no indication why, and you spend an evening blaming the model. Normalise every input to the documented format first, store the original untouched, and log the conversion parameters against the meeting. ## M2 · Long audio that survives a crash A two hour recording transcribes in resumable pieces, and a failure costs one piece. ### Steps 1. Persist each chunk before starting the next The difference between a resumable job and a gamble. Files: `src/transcribe.py`, `tests/test_resume.py` 2. Overlap and de-duplicate at the seams A sentence cut in half is a sentence transcribed twice. Files: `src/stitch.py` 3. Fail one chunk, not the meeting Ninety-five percent of a transcript is useful; nothing is not. Files: `src/transcribe.py` ### Done when - [ ] A kill mid-run resumes from the first unfinished chunk - [ ] Overlapping words appear once after stitching - [ ] Segment times line up with the audio on a spot check - [ ] A failed chunk does not fail the meeting ### Watch out - Transcribing a two hour file in a single call means a crash, an out-of-memory kill or a closed laptop at the ninety minute mark throws away ninety minutes of compute, and the obvious retry throws away another ninety. Chunk it, persist each chunk's result before moving on, and make restart resume rather than start. ## M3 · Speakers, without guessing who anyone is Turns are separated and labelled anonymously until a person says who each label is. ### Steps 1. Keep labels anonymous through the pipeline Names enter only at render, from the human mapping. Files: `src/speakers.py`, `tests/test_no_name_inference.py` 2. Give the human enough to map with First lines per label beats a waveform nobody can read. Files: `src/templates/speakers.html` 3. Substitute at render, not in storage So fixing a mistaken mapping is one update, not a migration. Files: `src/render.py` ### Done when - [ ] Labels stay anonymous until mapped - [ ] The notes pipeline never receives names - [ ] Unmapped labels export as labels - [ ] Changing a mapping updates every view ### Watch out - Letting the summariser work out who is speaking from context puts a name against a commitment nobody made, and because the sentence reads naturally, the wrong person is chased for the wrong task a week later. Keep labels anonymous through the pipeline and substitute names only from the human mapping, at render time. ## M4 · Notes that point at the words Every decision and action item carries the timestamped quote it came from. ### Steps 1. Require a verbatim quote per item And verify it against the transcript before writing the row. Files: `src/notes.py`, `tests/test_quote_grounding.py` 2. Extract per chunk, merge after Long-context summarisation is where invented commitments come from. Files: `src/notes.py` 3. Link every item to its timestamp A note you can hear is a note you can settle an argument with. Files: `src/templates/meeting.html` ### Done when - [ ] Every item's quote is present verbatim in the transcript - [ ] A fabricated item is dropped by the check - [ ] Boundary duplicates are merged - [ ] Items link to a playable timestamp ### Watch out - Feeding a thirty thousand word transcript in one request and asking for a summary produces smooth, confident output containing action items nobody assigned, and there is no way to tell which ones by reading it. Extract per chunk with a required verbatim quote, verify the quote exists in the transcript, and discard the item if it does not. ## M5 · Markdown out, audio gone (production only) Notes leave in a plain format and the sensitive part does not linger. ### Steps 1. Export Markdown that stands alone Consent record included, because it is part of the record. Files: `src/export.py` 2. Delete audio on schedule and log it Retention that nobody runs is a promise, not a policy. Files: `src/retention.py`, `tests/test_retention.py` 3. Make the wipe reach every derived file Chunk transcripts and normalised copies are recordings too. Files: `src/wipe.py`, `tests/test_wipe.py` ### Done when - [ ] Markdown export is complete and standalone - [ ] Audio past retention is deleted and logged - [ ] Notes still render after the audio is gone - [ ] The wipe leaves nothing in the data directory ### Watch out - Keeping every recording forever because storage is cheap means a laptop holding two years of confidential conversations from people who agreed to one meeting being recorded, and the first time that matters is the worst possible time. Set a retention, delete on schedule, log each deletion, and make the notes stand alone without the audio.
# Operations · Jamie ## Backup The database and the notes, separately from the audio. Notes are small and worth keeping; recordings are large, sensitive and deliberately temporary, so backing them up quietly defeats the retention policy you wrote. ## Restore Restore the database and confirm a meeting whose audio is already deleted still renders its notes and transcript. That is the normal state for anything older than the retention, so it is the state the restore must handle. Do a restore drill before the first real user, and write the date here when it passes. ## Monitoring Chunks stuck in running, meetings with failed chunks, the retention job's last successful run, and the count of notes rows rejected for an unverifiable quote. A rising rejection count means the extraction prompt has drifted. ## Incident checklist Someone disputing a note is answered by playing the timestamp, which is the entire reason the quote and span are stored. A recording that should not exist is answered by the wipe command, which is tested rather than described. 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 - [ ] Import without consent is confirmed refused - [ ] Three source formats are confirmed to normalise correctly, probed - [ ] A kill mid-transcription is confirmed to resume - [ ] A fabricated note item is confirmed discarded - [ ] The wipe is confirmed to leave nothing behind ## Launch constraint Do not market omitted Jamie 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. Database, audio, normalised copies and chunk transcripts. All of it sensitive. DATA_DIR=./data # Required. The downloaded model file. Larger is slower and more accurate. WHISPER_MODEL_PATH=./models/ggml-base.en.bin # Required. Chunk length. Smaller means less lost to a crash and more seams to stitch. CHUNK_SECONDS=600 # Required. Overlap used to remove duplicated words at boundaries. CHUNK_OVERLAP_SECONDS=5 # Required. After this the recording is deleted and the deletion is logged. Notes survive. AUDIO_RETENTION_DAYS=30 # Optional · secret. Only if you use a hosted model for extraction. Unset means local extraction and no audio or transcript leaves the machine. NOTES_API_KEY=sk-...redacted
$ choose a build depth, inspect the files, then open the complete pack in your agent
Jamie: Customers pay for automatic capture, dependable speaker handling, search across calls, and notes arriving without manual file wrangling.
xcalendar and CRM integrations
xcross-call team analytics
xrecording recovery, mastering, hosting, and distribution
xmeeting-bot auto-join
xlive multi-speaker accuracy
Don't feel like building it? These folks already made it free.
all 5 free alternatives to Jamie →· no votes, no pay-to-list · just what's real
Jamie pricing
| plan | monthly | annual (per mo) | what you get |
|---|---|---|---|
| free | $0/user | $0/user | 10 meetings per month; maximum 30 minutes each; 1 custom template; 50 custom-vocabulary terms; unlimited storage. |
| plus | $28.84/user | $24.22/user | 20 meetings per month; maximum 2 hours each; 3 custom templates; 50 vocabulary terms; unlimited storage. |
| pro | $54.21/user | $44.98/user | Unlimited meetings; maximum 5 hours each; unlimited custom templates; 50 vocabulary terms; unlimited storage. |
| team | $44.98/user | $38.06/user | 2-10 seats; unlimited meetings; maximum 5 hours each; unlimited templates; 100 vocabulary terms; unlimited storage. |
| enterprise | custom | — | Starts at 10 seats; unlimited meetings; maximum 5 hours each; 250 vocabulary terms; custom security/support. |
free tier10 meetings per month, each capped at 30 minutes; 1 custom template and 50 vocabulary terms.
billingmonthly + annual (-about 17%); Enterprise annual contract; prices converted from EUR
hidden costsTeam starts at 2 seats and Enterprise at 10; VAT may be added, and the charged USD amount can vary with the card processor's exchange rate.
verified 2026-08-14 · source ↗
Vibecode Jamie
Kinda. The core of Jamie is buildable in a weekend with the prompt on this page, but there are real gaps: calendar and CRM integrations, cross-call team analytics. Read the honest list above before committing.
How much does Jamie cost?
Jamie'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 Jamie?
Honestly: calendar and CRM integrations; cross-call team analytics; recording recovery, mastering, hosting, and distribution; meeting-bot auto-join; live multi-speaker accuracy. If any of those are load-bearing for you, keep paying.
Is there an open-source alternative to Jamie?
Yes: Meetily (A local recorder and summarizer for Mac and Windows; no account, no meeting bot.) Anarlog (A Mac meeting notepad with local transcripts, local files and bring-your-own summaries.) OpenWhispr (Bot-free meeting capture, speaker labels and searchable notes in one cross-platform installer.) All 5 curated free alternatives are at vibecodeit.com/jamie/alternatives. The prompt is for when you want it exactly your way.