Repository navigation
Database
The app uses SQLite via rusqlite (bundled — no system SQLite dependency). The database file notetaker.db is created in the working directory of the Tauri process.
| Column | Type | Notes |
|---|---|---|
id |
INTEGER PK AUTOINCREMENT | |
date |
TEXT NOT NULL |
toLocaleString() of recording end time |
title |
TEXT NOT NULL | Auto-generated: Meeting <date>
|
raw_transcript |
TEXT NOT NULL | Plain text from WhisperX |
markdown_summary |
TEXT NOT NULL | Markdown/fallback text from the LangGraph workflow |
speakers |
TEXT | JSON array of speaker names (nullable) |
tags |
TEXT | JSON array of tag strings from LLM (nullable) |
structured_summary |
TEXT | Full structured JSON from LangGraph (nullable) |
transcript_segments |
TEXT | Versioned transcript evidence JSON (nullable) |
schema_version |
INTEGER | Structured transcript/summary schema version, defaults to 1 for old rows |
CREATE VIRTUAL TABLE meetings_fts
USING fts5(title, raw_transcript, markdown_summary, content='meetings', content_rowid='id');The content= option links the FTS index to the meetings table without duplicating data. Three triggers (INSERT, UPDATE, DELETE) keep the index in sync automatically.
On every startup, lib.rs attempts additive migrations for existing databases:
for sql in &[
"ALTER TABLE meetings ADD COLUMN speakers TEXT",
"ALTER TABLE meetings ADD COLUMN tags TEXT",
"ALTER TABLE meetings ADD COLUMN structured_summary TEXT",
"ALTER TABLE meetings ADD COLUMN transcript_segments TEXT",
"ALTER TABLE meetings ADD COLUMN schema_version INTEGER DEFAULT 1",
] {
if let Err(e) = conn.execute(sql, []) {
if !e.to_string().contains("duplicate column name") {
eprintln!("[DB Migration] {}: {}", sql, e);
}
}
}SQLite does not support IF NOT EXISTS on ALTER TABLE. The duplicate column name error is treated as a no-op. Any other error is logged to stderr but does not crash the app.
| Command | Parameters | Returns |
|---|---|---|
save_meeting |
date, title, raw_transcript, markdown_summary, speakers?, tags?, structured_summary?, transcript_segments?, schema_version? | inserted row id |
get_meetings |
— |
Meeting[] ordered by id DESC |
search_meetings |
query: string |
Meeting[] via FTS5 MATCH, falls back to get_meetings if query is empty |
search_meetings appends * to the query for prefix search — typing "intel" matches "intelligence". Special characters are stripped before the query is passed to SQLite.
let fts_query = format!("{}*", query.trim().replace('"', ""));The sidebar search in the UI debounces the input by 300ms before calling search_meetings.
{
"schema_version": 2,
"metadata": {"title": "Meeting Summary", "date": "2026-06-20", "tags": ["release"]},
"tldr": "One sentence summary.",
"participants": [
{"name": "Alice", "role": "PM", "engagement_level": "high"}
],
"metrics": [
{"label": "Latency", "value": "68.428s", "trend": "flat"}
],
"key_decisions": [
{
"decision": "Ship the Windows sidecar fix",
"rationale": "The packaged app needs the PyInstaller binary",
"owner": "Igor",
"evidence_segment_ids": ["seg_000001"],
"evidence_quote": "Ship the Windows sidecar fix",
"confidence": 0.95,
"inference": false
}
],
"action_items": [
{
"task": "Update the README and wiki",
"assignee": "Igor",
"due_date": null,
"priority": "Medium",
"status": "open",
"evidence_segment_ids": ["seg_000002"],
"evidence_quote": "Update the README and wiki",
"confidence": 0.9,
"inference": false
}
],
"summary_points": ["Important detail from the meeting."],
"risks": [],
"open_questions": [],
"unresolved_topics": []
}This object is serialized as a JSON string and stored in the structured_summary column. The frontend parses it on load.
If the LLM returns invalid summary JSON, the Python engine stores a transcript-derived fallback summary with summary_generation_warning instead of storing the old hard-failure TL;DR.