Last Updated: 16 February 2026
Engine Version: 2.1.0
Frontend Spec: V2 (FRONTEND_SPEC_V2.md)
Voice-to-Notes is a self-hosted audio transcription and note processing system. It watches a Google Drive folder for audio files (voice memos), transcribes them using Google's Gemini AI, structures the content, and outputs Obsidian-compatible markdown notes.
- Audio Transcription β Converts voice memos to text using Gemini AI
- Content Structuring β AI-powered extraction of key points, action items, and summaries
- Obsidian Integration β Native markdown with YAML frontmatter and wikilinks
- Dual Output System β Separates raw transcripts from structured notes
- Task Extraction β Automatically identifies and aggregates actionable items
- Tag-Based Routing β Copy notes to project folders based on tags
- Daily/Weekly Rollups β Automated summary generation
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Docker Compose Stack β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β β
β βββββββββββββββ βββββββββββββββ βββββββββββββββββββββββ β
β β Web UI β β Watcher β β Rclone Sync β β
β β (FastAPI) β β (V2 Engine)β β (Google Drive) β β
β β Port 8000 β β Auto-proc β β 5-min interval β β
β ββββββββ¬βββββββ ββββββββ¬βββββββ ββββββββββββ¬βββββββββββ β
β β β β β
β β β β β
β βΌ βΌ βΌ β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β Shared Volume: /data/gdrive β β
β β β β
β β π VoiceMemos/ β Input: Audio files from phone β β
β β π Obsidian/Workspace/ β Output: Obsidian vault β β
β β ββ VoiceNotes/ β β
β β ββ Inbox/ β Structured notes (source) β β
β β ββ Transcripts/ β Raw transcripts β β
β β ββ Tasks/ β Daily task aggregations β β
β β ββ Daily/ β Daily summaries β β
β β ββ Weekly/ β Weekly rollups β β
β β ββ Projects/ β Tag-routed copies β β
β βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
The standalone processing pipeline. No web framework dependencies.
| File | Purpose |
|---|---|
__init__.py |
Package exports, version 2.1.0 |
core.py |
Main process_audio() orchestrator β 6-step pipeline |
models.py |
Data classes: ProcessingResult, AudioMetadata, ExtractedTask, NoteStatus |
config.py |
Environment-driven configuration with DB override support |
ai.py |
GeminiClient for transcription and structuring |
audio.py |
FFmpeg-based compression and metadata extraction |
prompts.py |
AI prompt templates for all processing modes |
markdown.py |
Obsidian markdown generation with dual output |
titlegen.py |
AI-powered title generation |
registry.py |
SQLite tracking of processed files |
watcher.py |
Folder watcher daemon for auto-processing |
tasks.py |
Task extraction and daily aggregation |
rollups.py |
Daily/weekly summary generation |
routing.py |
Tag-based routing and project folder copying |
1. Metadata Extraction β Get duration, format, bitrate from audio
2. Audio Compression β FFmpeg compress to 48k for API efficiency
3. AI Transcription β Gemini transcribes audio to text
4. Content Structuring β Gemini extracts summary, key points, actions
5. Task Extraction β Parse TASK: markers, aggregate to daily file
6. Dual Output Save β Write to both Transcripts/ and Inbox/
FastAPI-based web interface for manual control and configuration.
| File | Purpose |
|---|---|
main.py |
FastAPI app with routes for upload, playback, settings |
processor.py |
Wrapper connecting web UI to V2 engine |
database.py |
SQLite models: Recording, Settings, ApiUsage |
api_keys.py |
API key management with round-robin rotation |
templates/ |
Jinja2 HTML templates |
| Route | Method | Description |
|---|---|---|
/ |
GET | Dashboard with recent recordings |
/upload |
POST | Manual file upload (uses V2 engine) |
/recording/{id} |
GET | View processed recording |
/keys |
GET | API key management page |
/storage |
GET | Storage statistics |
/activity |
GET | API usage activity log |
/settings |
GET/POST | System settings |
Cross-component utilities shared between web and engine.
| File | Purpose |
|---|---|
api_keys.py |
Centralized API key loading and model configuration |
| Service | Purpose | Port |
|---|---|---|
web |
FastAPI web interface | 8000 |
watcher |
Auto-processing daemon | β |
rclone |
Google Drive sync (5-min) | β |
# Required
GEMINI_API_KEYS=key1,key2,key3 # Comma-separated API keys
# Google Drive Paths (inside container)
AUDIO_INPUT_DIR=/data/gdrive/VoiceMemos
OBSIDIAN_VAULT_DIR=/data/gdrive/Obsidian/Workspace
# Optional Overrides
OBSIDIAN_NOTE_SUBDIR=VoiceNotes # Base folder for all output
GEMINI_MODEL=gemini-2.0-flash # Default AI model
# Watcher Tuning
STABILITY_SECONDS=10 # Wait before processing
SCAN_INTERVAL_SECONDS=5 # Polling frequencyAll subdirectories are configurable via EngineConfig:
inbox_subdir: str = "Inbox" # Structured notes
transcripts_subdir: str = "Transcripts" # Raw transcripts
tasks_subdir: str = "Tasks" # Daily task files
daily_subdir: str = "Daily" # Daily summaries
weekly_subdir: str = "Weekly" # Weekly rollups
projects_subdir: str = "Projects" # Tag-routed copiesDD_MM_YY_slug-title.md
Example: 14_02_26_brief-inquiry-regarding-clusters.md
---
type: voice-note
title: "Brief Inquiry Regarding Clusters"
date: 2026-02-14
source_file: "20260214_120000.m4a"
duration: "1m 23s"
mode: personal_note
status: inbox
has_tasks: false
transcript: "[[Transcripts/14_02_26_brief-inquiry-regarding-clusters]]"
tags: []
---
## Summary
AI-generated summary of the voice memo content.
## Key Points
- Main point extracted from audio
- Another important point
## Action Items
- [ ] Task identified from the recording
## Details
Extended content and context from the memo.---
type: transcript
title: "Brief Inquiry Regarding Clusters"
date: 2026-02-14
source_file: "20260214_120000.m4a"
duration: "1m 23s"
note: "[[Inbox/14_02_26_brief-inquiry-regarding-clusters]]"
---
## Transcript
Full verbatim transcription of the audio recording...---
type: task-rollup
date: 2026-02-14
---
## Tasks for 14 February 2026
### From: Brief Inquiry Regarding Clusters
- [ ] Task extracted from this note
### From: Another Voice Memo
- [ ] Another taskThe Inbox folder is permanent and never modified after creation. It serves as the canonical record of all processed voice memos.
When routing notes to project folders, files are copied, not moved. The Inbox always retains the original.
Every audio file produces two markdown files:
- Transcripts/ β Raw transcription for reference
- Inbox/ β Structured note for daily use
Cross-linked via Obsidian wikilinks in frontmatter.
No hardcoded paths. All directories and settings come from environment variables, with optional database overrides for runtime changes.
Multiple Gemini API keys are supported with automatic round-robin rotation and usage tracking per key.
recordings β Processed file records
id,filename,original_path,duration,transcription,structured_content,status,created_at,processed_at
settings β Key-value configuration store
key,value,created_at,updated_at
api_usage β Per-key usage tracking
id,api_key_masked,model_used,input_tokens,output_tokens,total_tokens,operation,success,error_message,created_at
processed_files β Watcher state tracking
id,file_path,file_hash,status,note_path,transcript_path,has_tasks,error,created_at,processed_at
| Operation | Model | Prompt |
|---|---|---|
transcribe() |
gemini-2.0-flash | Audio β text transcription |
structure() |
gemini-2.0-flash | Text β structured note |
generate_title() |
gemini-2.0-flash | Generate slug-friendly title |
extract_tasks() |
gemini-2.0-flash | Extract TASK: markers |
docker compose up -d# All services
docker compose logs -f
# Watcher only
docker compose logs -f watcher# Delete from registry
docker exec voice-to-notes-watcher python -c "
from engine.registry import FileRegistry
r = FileRegistry()
r._execute('DELETE FROM processed_files WHERE file_path LIKE ?', ('%filename%',))
"
# Restart watcher
docker compose restart watcherVisit http://localhost:8000/ and use the upload form.
- New default:
YYYY_MM_DD_HH_MM_slug.md(e.g.,2026_02_16_14_30_voice-note.md) - Previous:
DD_MM_YY_slug.md(e.g.,16_02_26_voice-note.md) - Rationale: Better time granularity and chronological sorting
- Configuration: Set
FILENAME_DATE_FORMATenv var to revert if needed
- New column:
ingested_atinprocessed_filestable - Purpose: Track actual ingestion time vs. filesystem modification time
- Sorting: All views now sort by
COALESCE(ingested_at, processed_at) DESC - Backward compatible: Falls back to
processed_atfor old entries - Impact: More predictable, deterministic file ordering
-- New column for tracking ingestion time
ALTER TABLE processed_files ADD COLUMN ingested_at TEXT;
-- Used in queries as:
SELECT * FROM processed_files
ORDER BY COALESCE(ingested_at, processed_at) DESC- Ingest Page: Complete redesign with dense table layout, inline project assignment
- Projects Page: Compact grid view, remove mode grouping, add tile actions
- Visual Language: Finder-style aesthetic, minimal whitespace, instant updates
- See:
FRONTEND_SPEC_V2.mdfor complete specification
| Version | Date | Changes |
|---|---|---|
| 2.1.0 | 2026-02-14 | Dual output system, task extraction, rollups, routing |
| 2.0.0 | 2026-02-13 | V2 engine with standalone pipeline, watcher service |
| 1.0.0 | 2026-02-12 | Initial release with web UI and manual processing |
- V2 Dashboard β New frontend for Inbox management, tagging, and task views
- Webhook Integration β Notify external services on new notes
- Multi-Vault Support β Route to different Obsidian vaults
- Voice Command Detection β Inline commands like "remind me" or "add to project X"