Build timeOS

KINDAback to the verdict

0%0 of 17 items done

Saved on this device only. Tick prerequisites first, then work the phases in order · do not start one until the checks above it pass.

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.

estimated effort multi-daythe files for this build are in the project pack

RuntimePython 3.12 with FastAPI, one process, served on localhostTranscriptionwhisper.cpp running locally against a downloaded modelAudioffmpeg for normalisation and segment extraction, originals never overwrittenStorageSQLite beside the audio directory, one row per decoder segmentNotesAn optional model call over the transcript, with every sentence bound to segments

Before step 1

Everything below is assumed from the first step. Tick each one when you actually have it, not when you plan to.

  1. 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

  2. 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

  3. 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

  4. 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

  5. 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.

VariableNeededExampleWhere the value comes from
DATABASE_PATHrequired./data/meetings.dbRecordings, segments, turns, notes and citations.
AUDIO_DIRrequired./data/audioOriginals and working copies. Sized for the retention you chose.
WHISPER_MODEL_PATHrequired./models/ggml-medium.binThe local model file. Its identifier is stored with every segment.
NO_SPEECH_THRESHOLDrequired0.6Segments above this are flagged and excluded from note generation.
SILENCE_DBrequired-35dBPassed to ffmpeg silencedetect. Tune it against your own recordings.
RETENTION_DAYSrequired90Audio and notes are deleted together at this age unless overridden per recording.
NOTES_MODEL_KEYsecretoptionalsk-...redactedOptional. Only note generation uses it; transcription never leaves the machine.
MONTHLY_COST_CEILINGrequired3.00Note generation stops here for the month. Transcription and playback continue.

The build, in order

  1. Audio in, audio kept

    A recording is imported or captured, hashed, and stored without being degraded on the way in.

    1. The original is the only thing that can settle a dispute about a note.

      Files app/ingest.pyapp/audio.py

      terminal
      ffmpeg -i input.m4a -ac 1 -ar 16000 -c:a pcm_s16le work.wav
    2. So a strange transcript can be traced to a conversion rather than argued about.

      Files app/ingest.pytests/test_ingest.py

    3. The 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 passes
    watch 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.
  2. A transcript that keeps its doubts

    whisper.cpp output is stored per segment with its confidence fields, and non-speech is never decoded.

    1. This is where speech models invent whole sentences.

      Files app/silence.pytests/test_silence.py

      terminal
      ffmpeg -i work.wav -af silencedetect=noise=-35dB:d=1.0 -f null -
    2. Text alone gives you nothing to sort the doubtful segments by.

      Files app/transcribe.pyapp/models.py

    3. Visibly, in the transcript, not in a log file nobody opens.

      Files app/flags.pytests/test_flags.py

    done when · tick each as it passes
    watch 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.
  3. Speakers are a guess you can correct

    Turns are proposed heuristically, labelled by a human, and the interface never pretends otherwise.

    1. A pause detector is honest about being a pause detector.

      Files app/turns.pytests/test_turns.py

    2. The difference between a guess and a correction has to survive into the export.

      Files app/models.py

    3. Otherwise nobody corrects anything after the third turn.

      Files app/routes/turns.py

    done when · tick each as it passes
    watch 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.
  4. Notes that cite the audio

    Every sentence of a generated note is bound to segments and plays back from the recording.

    1. A claim without a location in the audio is not a claim.

      Files app/notes.pyapp/schemas.py

    2. Models cite things that do not exist, cheerfully.

      Files app/notes.pytests/test_notes.py

    3. One 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 passes
    watch 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.
what this build does not replace
after v1, if you want it

Need the files? The project pack on the verdict page hands your agent the whole brief · more meeting notes.