Mindlap
← All projects

Desktop App

Stand Log

A Linux-first AI meeting companion for engineering teams - records calls, transcribes, and turns every standup into searchable, structured notes.

Want the codebase? Get the Git repo emailed to you.

About this project

Stand Log is a Linux-first take on Granola, a native desktop meeting and notes companion built to run first-class on Linux, where most polished meeting tools don't ship. It captures both microphone and system audio simultaneously via PipeWire and ffmpeg, so every standup, design review, and pairing call is recorded end-to-end without a browser tab or screen-share workaround.

Under the hood it's an Electron app with a TypeScript main process and a local SQLite store, keeping recordings on-device by default. Audio is sent to the Gemini Files API and returned as structured JSON summaries persisted alongside each meeting, so you record, transcribe, and summarize automatically, then revisit clean, searchable notes for any past conversation.

Demo

At a glance

8.1h

Agent hours

49M

Tokens

3

Laps

29

Stories

49M

tokens

Codex · 94%

Others · 6%

Tokens by stage

Tech Spec

10M

Implement

26M

Code Review

5M

Fix

7M

Merge

1M

Implement Direct

0M

Pipeline

Stage

Runs

Tokens

Duration

Tech Spec

40

10M

2.8h

Implement

42

26M

3h

Code Review

32

5M

1.3h

Fix

10

7M

0.8h

Merge

5

1M

0.2h

Implement Direct

1

0M

0h

Engines used

Codex

Tokens

46M

Runs

56

Agent hours

5.9h

Success

100%

Others

Tokens

3M

Runs

74

Agent hours

2.2h

Success

81.1%

Agent team

engineering_manager

69 runs · 4.1h

66 passed · 3 failed

developer

55 runs · 4.1h

50 passed · 5 failed

product_manager

2 runs · 0.1h

2 passed · 0 failed

Artifacts

Feature Spec

markdown

One master spec covering what we're building, why, and how. Detailed per-lap specs (backend, frontend, packaging) live alongside this file.

1. What I am building

A Linux desktop application that records a meeting locally (microphone + system audio), and after the meeting ends, produces a clean, structured summary using Google Gemini - filtering out off-topic chatter and presenting the relevant discussion as TLDR, decisions, action items, per-speaker thoughts, and visual diagrams.

The app is the Linux equivalent of Granola, which doesn't ship a Linux build. It is a personal POC - single user, single machine, no cloud sync, no auth.

2. Why

  • Granola has no Linux desktop app.
  • Existing OSS options (Meetily, Hyprnote, anarlog, muesli) are macOS/Windows first; Linux support is partial or "coming soon".
  • I want bilingual (English + Hindi + code-switched) transcription, which Gemini handles natively without extra tooling.
  • I want post-meeting summaries (not realtime) - simpler to build, fits POC scope.

3. Core use case (single golden path)

  1. User opens the app, clicks Record.
  2. App captures the mic and the system audio output (the other meeting participants) simultaneously into a single audio file on disk.
  3. User has the meeting. Side chatter, jokes, tangents - all captured.
  4. User clicks Stop.
  5. App uploads the audio to Gemini via the Files API, asks for a structured JSON summary with off-topic content filtered out.
  6. App renders the summary: TLDR, decisions, action items, topics, per-speaker contributions, talk-time chart, Mermaid diagrams of discussion flow.
  7. User can edit speaker labels (Speaker 1 → Nitin) and export the summary as Markdown or PDF.
  8. Past meetings are listed in a sidebar; user can revisit, re-summarize, or delete.

Non-goals for v1: realtime transcription, multi-user, cloud sync, auth, calendar integration, automatic meeting detection.

4. Tech stack (final, POC-optimised)

LayerChoiceWhy
Desktop shellElectron 31+Fastest path to a working POC; TS on both sides
LanguageTypeScript (strict)One language across main + renderer + shared types
Buildelectron-viteSingle config for main, preload, renderer
UI frameworkReact 18 + ViteAlready in the repo (frontend/) - repurpose
StylingTailwind + shadcn/uiPretty defaults, zero design work
Data fetchingTanStack QueryAlready in the repo; works fine over IPC wrappers
StateLocal React state + Query cacheNo Redux/Zustand needed for POC
DatabaseSQLite via better-sqlite3Local, fast, sync API, no server
Audio captureffmpeg spawned from main, two PulseAudio/PipeWire inputs20 lines of shell beats native audio libs
LLMGoogle Gemini 2.5 Flash (@google/generative-ai)Audio-in, JSON-out, Hindi+English native, generous free tier
ChartsRechartsSpeaker talk-time bar chart
DiagramsMermaid.js (rendered in renderer)Gemini emits Mermaid code; we render it
ExportMarkdown + md-to-pdfLightweight
Loggingelectron-logFile logs at ~/.config/<app>/logs/
Secret storageelectron-store (encrypted)Gemini API key, persisted
ValidationzodIPC inputs + Gemini response schema
Packagingelectron-builder → AppImage + .debLinux-first

Only paid key needed: Gemini. Free tier covers POC use.

5. Architecture (desktop = full-stack in one binary)

┌────────────────────────────────────────────────────────┐
│                    Electron App (one process tree)          │
│  ┌────────────────────┐         ┌──────────────────────┐   │
│  │   Renderer (UI)    │  IPC    │   Main (Backend)      │   │
│  │   React + Tailwind │ ◄───► │   Node.js + TS        │   │
│  │   Chromium window  │  via    │   - SQLite (local)    │   │
│  │   Sandboxed        │ preload │   - ffmpeg child proc │   │
│  │                    │ bridge  │   - Gemini SDK        │   │
│  └────────────────────┘         │   - File I/O          │   │
│                                  └───────────┬───────────┘   │
└────────────────────────────────────────┼──────────────┘
                          ┌────────────────────┼───────────────────┐
                          ▼                    ▼                     ▼
                    SQLite file        Recordings folder      Gemini API
                    meetings.db        ~/.../recordings/      (cloud)
  • Renderer (frontend) = React app, sandboxed, no OS access. Talks to main via window.api.* calls defined in preload.
  • Main (backend) = Node.js, full OS access. Owns audio, DB, Gemini key.
  • IPC = in-process messaging. No HTTP, no CORS. Every channel validated with zod.
  • Storage = SQLite for structured data, flat files for audio recordings.

The mental model:

  • Renderer ≈ browser frontend
  • Main ≈ Node/Express backend
  • IPC ≈ REST, but in-process
  • SQLite file ≈ Postgres, but local

6. Target repo structure (after refactor)

stand-log/
├── package.json                      # root - Electron + scripts
├── electron.vite.config.ts           # builds main + preload + renderer
├── tsconfig.json
├── electron-builder.yml              # AppImage + .deb config (lap 3)
├── .env / .env.example               # GEMINI_API_KEY for dev
│
├── electron/
│   ├── main/                         # ===== Backend (lap 1) =====
│   │   ├── index.ts                  # app lifecycle, window, IPC wiring
│   │   ├── ipc/                      # IPC handlers (one file per domain)
│   │   ├── services/                 # audio, gemini, db, storage, config, logger
│   │   └── schemas/                  # zod schemas for IPC + Gemini JSON
│   └── preload/
│       └── index.ts                  # contextBridge → exposes `window.api`
│
├── src/                              # ===== Frontend / Renderer (lap 2) =====
│   ├── main.tsx                      # React entrypoint
│   ├── App.tsx
│   ├── pages/                        # RecordPage, MeetingsList, MeetingDetail
│   ├── components/                   # SpeakerCard, MermaidDiagram, TalkTimeChart
│   ├── api/                          # thin IPC wrappers (mirrors backend API)
│   └── lib/
│
├── shared/
│   └── types.ts                      # Meeting, Summary, etc. - used by both
│
├── scripts/
│   └── smoke.ts                      # end-to-end smoke test
│
└── docs/                             # specs (gitignored, local only)
    ├── 00-overview.md                # this file
    ├── 01-backend-spec.md            # lap 1 - main process
    ├── 02-frontend-spec.md           # lap 2 - renderer (TBD)
    └── 03-packaging-spec.md          # lap 3 - distribution (TBD)

The current backend/ (FastAPI) and frontend/ (Vite React) folders from the POC boilerplate will be replaced: the Python backend is removed, frontend/ is repurposed (or recreated) as src/ for the renderer.

7. Data model

Two SQLite tables - full details in 01-backend-spec.md §4.

  • meetings - one row per recording. Fields: id, title, started_at, ended_at, duration_ms, audio_path, status (recording → recorded → summarizing → done | failed), error.
  • summaries - one row per meeting, stores the full Gemini JSON payload validated by zod. Cascade-deletes with the meeting.

8. The summary JSON (what Gemini returns)

{
  meeting_title: string,
  tldr: string,
  key_decisions: string[],
  action_items: { owner: string, task: string, due: string | null }[],
  topics: { name: string, summary: string, speakers_involved: string[] }[],
  per_speaker: Record<string, {
    main_points: string[],
    questions_raised: string[],
    sentiment: 'positive' | 'neutral' | 'negative' | 'mixed'
  }>,
  off_topic_dropped: string[],
  mermaid_diagrams: { title: string, code: string }[],
  speaker_contribution: { speaker: string, talk_time_pct: number, topics_count: number }[]
}

Gemini is forced to return this shape via responseSchema in the SDK - no markdown parsing, no regex extraction.

9. How "ignore off-topic" works

There is no audio-level filtering. The filtering happens in the summarization prompt sent to Gemini:

"You are a meeting note-taker. From the transcript below, extract only: decisions made, action items with owners, key discussion points, and open questions. Ignore greetings, small talk, jokes, side tangents, and off-topic remarks. Move anything off-topic into off_topic_dropped so the user can verify nothing important was missed."

The raw audio is preserved on disk (source of truth). The summary is a derived, prompt-driven view - re-runnable any time.

10. Execution plan - three laps

Each lap = one spec, broken into stories with parallel/sequential dependency graphs. Stories are sized so an agent can complete one in a single run with a Definition-of-Done checklist.

LapSpecScopeOutput
101-backend-spec.mdElectron main process: lifecycle, IPC, audio capture, SQLite, Gemini, config, logging, smoke testA working backend that can record + summarize via a CLI smoke script
202-frontend-spec.mdRenderer: React UI, record/stop button, meetings list, summary view with charts + Mermaid, exportA clickable desktop app driving the lap-1 backend
303-packaging-spec.mdelectron-builder config, app icon, AppImage + .deb, autostart, first-run UXA distributable Linux binary

Lap 1 status: spec written (01-backend-spec.md), 10 stories defined, parallel batches identified.

11. Out of scope (explicitly)

  • macOS / Windows builds (Linux-only for now; cross-platform is later).
  • Realtime / streaming transcription.
  • Speaker identification by real name (we keep Speaker 1..N from Gemini; user can rename in UI).
  • Auto-detect when a Zoom/Meet/Teams call starts.
  • Calendar integration.
  • Cloud sync / multi-device.
  • Authentication, multi-user, RBAC.
  • Migrations framework - schema is recreated idempotently.
  • Push to Granola's cloud (their API exists but we don't need it).

12. Risks & open questions

RiskMitigation
PipeWire monitor source missing on user's machineDetect at startup with pactl list short sources; show clear error
ffmpeg not installedDetect with ffmpeg -version; document install in README; consider bundling later
1-hour audio > Gemini inline upload limitAlways use Files API (handles large files)
Gemini Hindi quality lower than EnglishAcceptable for POC; could swap in Deepgram Nova-3 in lap 4
Speaker diarization accuracy with 5+ peopleGemini does basic labeling; pyannote sidecar is a future upgrade
API key leakage from rendererKey never crosses IPC - renderer only sees has_key: boolean

Open questions (to settle before lap 2):

  • Do we poll meetings:get for summarize progress, or add an event channel? (Default: poll.)
  • Keep raw transcript separately or embed in summary JSON? (Default: embed.)
  • WebM/Opus vs WAV for recordings? (Default: WebM/Opus - smaller.)

13. Definition of "done" for the whole POC

  • Single-click record on a Linux desktop.
  • 30-minute meeting in mixed English + Hindi produces a summary in <90s after Stop.
  • Summary correctly drops obvious off-topic chatter (manual eyeball check).
  • Per-speaker breakdown for at least 3 speakers.
  • One Mermaid diagram + one talk-time chart render in the UI.
  • Export to Markdown works.
  • AppImage launches on a fresh Ubuntu 24.04 VM without dev tooling installed (lap 3).

Laps & stories

mindlap
AboutFAQPrivacyTerms

© 2026 mindlap