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
markdownOne 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)
- User opens the app, clicks Record.
- App captures the mic and the system audio output (the other meeting participants) simultaneously into a single audio file on disk.
- User has the meeting. Side chatter, jokes, tangents - all captured.
- User clicks Stop.
- App uploads the audio to Gemini via the Files API, asks for a structured JSON summary with off-topic content filtered out.
- App renders the summary: TLDR, decisions, action items, topics, per-speaker contributions, talk-time chart, Mermaid diagrams of discussion flow.
- User can edit speaker labels (Speaker 1 → Nitin) and export the summary as Markdown or PDF.
- 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)
| Layer | Choice | Why |
|---|---|---|
| Desktop shell | Electron 31+ | Fastest path to a working POC; TS on both sides |
| Language | TypeScript (strict) | One language across main + renderer + shared types |
| Build | electron-vite | Single config for main, preload, renderer |
| UI framework | React 18 + Vite | Already in the repo (frontend/) - repurpose |
| Styling | Tailwind + shadcn/ui | Pretty defaults, zero design work |
| Data fetching | TanStack Query | Already in the repo; works fine over IPC wrappers |
| State | Local React state + Query cache | No Redux/Zustand needed for POC |
| Database | SQLite via better-sqlite3 | Local, fast, sync API, no server |
| Audio capture | ffmpeg spawned from main, two PulseAudio/PipeWire inputs | 20 lines of shell beats native audio libs |
| LLM | Google Gemini 2.5 Flash (@google/generative-ai) | Audio-in, JSON-out, Hindi+English native, generous free tier |
| Charts | Recharts | Speaker talk-time bar chart |
| Diagrams | Mermaid.js (rendered in renderer) | Gemini emits Mermaid code; we render it |
| Export | Markdown + md-to-pdf | Lightweight |
| Logging | electron-log | File logs at ~/.config/<app>/logs/ |
| Secret storage | electron-store (encrypted) | Gemini API key, persisted |
| Validation | zod | IPC inputs + Gemini response schema |
| Packaging | electron-builder → AppImage + .deb | Linux-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_droppedso 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.
| Lap | Spec | Scope | Output |
|---|---|---|---|
| 1 | 01-backend-spec.md | Electron main process: lifecycle, IPC, audio capture, SQLite, Gemini, config, logging, smoke test | A working backend that can record + summarize via a CLI smoke script |
| 2 | 02-frontend-spec.md | Renderer: React UI, record/stop button, meetings list, summary view with charts + Mermaid, export | A clickable desktop app driving the lap-1 backend |
| 3 | 03-packaging-spec.md | electron-builder config, app icon, AppImage + .deb, autostart, first-run UX | A 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
| Risk | Mitigation |
|---|---|
| PipeWire monitor source missing on user's machine | Detect at startup with pactl list short sources; show clear error |
| ffmpeg not installed | Detect with ffmpeg -version; document install in README; consider bundling later |
| 1-hour audio > Gemini inline upload limit | Always use Files API (handles large files) |
| Gemini Hindi quality lower than English | Acceptable for POC; could swap in Deepgram Nova-3 in lap 4 |
| Speaker diarization accuracy with 5+ people | Gemini does basic labeling; pyannote sidecar is a future upgrade |
| API key leakage from renderer | Key never crosses IPC - renderer only sees has_key: boolean |
Open questions (to settle before lap 2):
- Do we poll
meetings:getfor 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).