Build timeOS
KINDAback to the verdict
A local meeting recorder whose hard problems are keeping the audio and the transcript joined so a claim can be checked, refusing to decode silence where speech models invent sentences, and being honest that speaker labels are guesses a human corrects.
Before step 1
Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.
- installfree
Why Runs the application and the ingest pipeline.
Get it python.org or your version manager. open ↗
Verify
python3 --version prints 3.12 or higher - installfree
Why Handles conversion and the silence detection that keeps non-speech out of the decoder.
Get it Your package manager, or a static build. open ↗
Verify
ffmpeg -filters | grep silencedetect returns a row - installfree, plus the disk for the model
Why Transcription runs locally, which is the whole reason this build exists rather than uploading a meeting recording.
Get it Build it and fetch a model; start with a medium-sized one and measure speed on your own machine before choosing.
Verify
A one-minute sample transcribes and prints segment timings - have readyfree
Why Every failure this build handles appears in a real meeting and in none of the clean test clips people usually try first.
Get it Record a genuine conversation with everyone's agreement, including a long pause and two people talking over each other.
Verify
It contains at least one silence over ten seconds and one overlap - decidefree
Why Deciding this after the build means deciding it from whatever the interface happened to make easy.
Get it Write down the wording you will use, when it is shown, and what you will check about local law.
Verify
The wording appears in the README and the timestamp gate is asserted in a test
Data model
Create these before the first phase that stores anything. Changing a table later is the expensive kind of change.
- recordings(id, title, started_at, ended_at, source, audio_path, sha256, duration_ms, sample_rate, notice_given_at) - segments(id, recording_id, ordinal, start_ms, end_ms, text, avg_logprob, no_speech_prob, compression_ratio, model_id, flagged_reason) · the decoder's own confidence fields are kept, not discarded - turns(id, recording_id, start_ms, end_ms, speaker_label, assigned_by) · assigned_by is heuristic or human, and the interface shows which - notes(id, recording_id, kind, body_md, model_id, prompt_version, created_at) · kind is summary, decisions or actions - note_claims(id, note_id, ordinal, text, segment_ids_json) · one row per sentence of a note and the segments behind it - actions(id, recording_id, text, owner_label, due_on, segment_id, state) · an action item with no segment is refused on insert - retention(recording_id, delete_audio_after_days, delete_all_after_days) · audio and notes expire together by default, because one without the other is unverifiable - There is no column anywhere holding a note sentence without a source segment. That constraint is what makes the rest of this honest.
Environment variables
These go in a .env file the app reads at startup. The pack's .env.example is this table as a file · copy it, never commit the filled-in version.
| Variable | Needed | Example | Where the value comes from |
|---|---|---|---|
DATABASE_PATH | required | ./data/meetings.db | Recordings, segments, turns, notes and citations. |
AUDIO_DIR | required | ./data/audio | Originals and working copies. Sized for the retention you chose. |
WHISPER_MODEL_PATH | required | ./models/ggml-medium.bin | The local model file. Its identifier is stored with every segment. |
NO_SPEECH_THRESHOLD | required | 0.6 | Segments above this are flagged and excluded from note generation. |
SILENCE_DB | required | -35dB | Passed to ffmpeg silencedetect. Tune it against your own recordings. |
RETENTION_DAYS | required | 90 | Audio and notes are deleted together at this age unless overridden per recording. |
NOTES_MODEL_KEYsecret | optional | sk-...redacted | Optional. Only note generation uses it; transcription never leaves the machine. |
MONTHLY_COST_CEILING | required | 3.00 | Note generation stops here for the month. Transcription and playback continue. |
The build, in order
Audio in, audio kept
A recording is imported or captured, hashed, and stored without being degraded on the way in.
The original is the only thing that can settle a dispute about a note.
Files
app/ingest.pyapp/audio.pyterminalffmpeg -i input.m4a -ac 1 -ar 16000 -c:a pcm_s16le work.wav
So a strange transcript can be traced to a conversion rather than argued about.
Files
app/ingest.pytests/test_ingest.pyThe other people in the call are told before this runs, not after.
Files
app/models.pyapp/routes/recordings.py
done when · tick each as it passeswatch out- Transcoding the upload in place to save disk means the one artefact that can settle an argument about what was said has been through a lossy encoder chosen by you, and nobody can tell afterwards which distortions came from the microphone and which from your ffmpeg flags. Keep the original file, work from a derived copy, and store the hash of both.
A transcript that keeps its doubts
whisper.cpp output is stored per segment with its confidence fields, and non-speech is never decoded.
This is where speech models invent whole sentences.
Files
app/silence.pytests/test_silence.pyterminalffmpeg -i work.wav -af silencedetect=noise=-35dB:d=1.0 -f null -
Text alone gives you nothing to sort the doubtful segments by.
Files
app/transcribe.pyapp/models.pyVisibly, in the transcript, not in a log file nobody opens.
Files
app/flags.pytests/test_flags.py
done when · tick each as it passeswatch out- Feeding the whole file to the decoder and keeping only the text throws away the three numbers that tell you which sentences to distrust, and the published research says the invented sentences appear precisely in the non-vocal stretches you just fed it. Cut silence out before decoding, keep every confidence field on the row, and mark flagged segments in the interface rather than in a log.
Speakers are a guess you can correct
Turns are proposed heuristically, labelled by a human, and the interface never pretends otherwise.
A pause detector is honest about being a pause detector.
Files
app/turns.pytests/test_turns.pyThe difference between a guess and a correction has to survive into the export.
Files
app/models.pyOtherwise nobody corrects anything after the third turn.
Files
app/routes/turns.py
done when · tick each as it passeswatch out- Presenting automatic speaker labels in the same typography as corrected ones means a quote gets attributed to the wrong person in the notes and then in an email, and nobody can tell from the interface that the attribution was a guess by a pause detector. Keep assigned_by on every turn and render the two states differently, including in exports.
Notes that cite the audio
Every sentence of a generated note is bound to segments and plays back from the recording.
A claim without a location in the audio is not a claim.
Files
app/notes.pyapp/schemas.pyModels cite things that do not exist, cheerfully.
Files
app/notes.pytests/test_notes.pyOne click from the claim to the audio, or the citation is decoration.
Files
app/templates/recording.htmlapp/static/player.js
done when · tick each as it passeswatch out- Asking for a summary as prose and storing it produces text that reads beautifully and cannot be checked, so the first time someone disputes an action item you are re-listening to forty minutes of audio to find out whether the model made it up. Require segment identifiers per sentence, validate them against the table, and drop any sentence that cannot be placed.
Expiry, export and a bill that cannot run awayproduct builder
Recordings expire on a schedule, leave in a readable format, and the optional model spend has a ceiling.
Notes that outlive their evidence are the thing this build avoids.
Files
app/retention.pytests/test_retention.pyAudio, Markdown transcript, notes with citations, segments as JSON.
Files
app/export.pyThe transcription is local and free, so the fallback is a good product.
Files
app/cost.pytests/test_cost.py
done when · tick each as it passeswatch out- Expiring the audio while keeping the notes leaves a file of confident statements about a conversation that can no longer be checked against anything, which is the exact failure the citation work in the previous phase existed to prevent. Expire them together by default, and make keeping notes past the audio an explicit per-recording choice that stamps the notes as unverifiable.
That is the whole plan for timeOS. What it deliberately does not cover is below · check the gaps before you call it a replacement.
- A meeting bot that joins calls on your behalf.
- Real-time transcription during a call.
- Voice prints and speaker identification across recordings.
- Calendar and CRM synchronisation.
- live multi-speaker accuracy
- calendar and CRM integrations
- cross-call team analytics
- meeting-bot auto-join
- A search across recordings that ranks by segment confidence as well as relevance
- A weekly digest of action items whose source segments were flagged, which is where the errors hide
- A second decoder pass on flagged segments only, comparing the two outputs before trusting either
Need the files? The project pack on the verdict page hands your agent the whole brief · more meeting notes.