DiaryBot: Building a Private Diary with Telegram, Whisper, OCR, and Ollama
I wanted a diary that was easy to use in the moment.
Opening a notes app, finding the right document, and deciding how much structure to add creates just enough friction to make the habit fail. Telegram is already on my phone, so I built a private diary bot around the simplest possible interaction:
- Send a text message.
- Send a voice message.
- Send a screenshot or photo.
- Let the system do the filing, transcription, and organization.
The result is DiaryBot, a local-first .NET 10 application that stores the durable diary as Markdown and keeps the original audio and images alongside it. The current release is v1.2.0, and it is ready to run on an on-prem Docker host.
What is ready now
DiaryBot can now be used as a practical private diary, not just a prototype:
- Text messages become diary entries.
- Voice and audio messages are saved, transcribed locally, and summarized.
- Screenshots and photos are saved, OCR is extracted locally, and readable text is summarized.
- Each entry keeps the cleaned transcription, raw transcription when useful, LLM summary, model names, source links, and context metadata.
/showsends today's Markdown file, or a specific date when supplied./searchfinds matching dates without sending large diary excerpts back to Telegram./backup,/getbackup, and/restore latestsupport owner-controlled backup and recovery./helpshows the deployed version and all supported commands.- Rolling local logs make it easier to diagnose processing and deployment issues.
The experience
The interaction is deliberately small.
When a message arrives, the bot immediately confirms that it has been persisted:
Saved to DB, awaiting LLM processing.
That message means the capture is safe in the local SQLite inbox. The slower work can now happen independently.
When processing finishes, the bot reports the diary timestamp and elapsed processing time:
Diary entry added 20:51 - 26 Aug 2026 (13s)
The same flow works for typed messages, voice messages, and screenshots. Typed messages go directly to enrichment. Voice messages are downloaded, preserved, converted temporarily when necessary, and transcribed locally with whisper.cpp. Screenshots and photos are preserved, passed through Tesseract OCR, and then enriched through the same LLM path.
The current deployment uses English as the explicit transcription language and the quantized large-v3-turbo-q5_0 model, which improved accuracy over the earlier tiny model without requiring the full multi-gigabyte large-v3 model.
What gets written
Each day becomes one Markdown file:
data/diary/
└── 2026/
└── 08/
├── 26.md
├── audio/
│ └── 201333-msg-17.oga
├── images/
│ └── 201455-msg-18.png
└── metadata/
├── 17.json
└── 18.json
An entry contains the cleaned transcript, the raw transcription when it differs, the LLM summary, structured context, and a link to the original audio or image:
## 20:13
**Source:** Voice
**Audio:** [201333-msg-17.oga](26/audio/201333-msg-17.oga)
**Transcription (cleaned):**
Hello this is a test
**Transcription (raw):**
Hello this is a text
_Transcription model: ggml-large-v3-turbo-q5_0.bin_
**Summary:**
_Enrichment model: qwen3:8b_
Initial test entry.
### Context
- Mood: Neutral (5/10)
- Energy: 5/10
- Activities: Testing
The raw transcription is kept separate from the cleaned or summarized content. That distinction matters: the LLM can improve readability, but it should not become the only copy of what was originally said.
The model name is stored with the enrichment result as well. If I switch models later, old entries still show which model produced their summaries.
The architecture
DiaryBot is a single ASP.NET 10 Core host containing the API and background workers.
The important design decision is the durable inbox. Telegram updates are written to SQLite before media download, transcription, Ollama, or Markdown work begins.
That gives the system a clear recovery point. A process restart does not mean losing the message or starting from an unknown state.
Crash-safe by design
The processor records stages:
Persisted -> MediaDownloaded -> Transcribed -> Enriched -> MarkdownWritten -> Completed
Each stage is independently persisted. If Whisper fails, the original audio remains available. If Ollama is offline, the raw content can still be written when unenriched Markdown is enabled, and enrichment is retried later.
Markdown writes use deterministic entry IDs, markers, per-day locks, chronological insertion, and atomic replacement. Before writing, DiaryBot inspects the existing file. This prevents a crash between writing the file and updating SQLite from creating duplicate entries.
The application also protects against Telegram redelivery with unique constraints on both the Telegram update ID and the chat/message identity.
Useful Telegram commands
/start
/help
/status
/today
/show
/show 2026-08-26
/search meeting
/search 2026 meeting
/retry
/summarize
/backup
/getbackup
/restore latest
The /help command prints the available commands and the deployed DiaryBot version, which makes it easy to confirm which release is running after an on-prem update.
It also prints the configured models: the Whisper provider and model file, OCR language, and Ollama model. That is useful because transcription and summary quality can change when models change.
The /show command sends today's Markdown file directly back to Telegram, while /show YYYY-MM-DD fetches an older date. This is useful when the diary is stored on a server but the phone is the main interface.
The /search command is deliberately compact: /search keyword or /search 2026 keyword returns only the newest 25 matching dates, each with a /show YYYY-MM-DD command. It does not return snippets or whole entries, which keeps Telegram responses short and avoids exposing more diary text than requested.
Backups can also be handled from Telegram. /backup creates a server-side archive and replies with the file name, size, and file count. It does not send the zip by default, because the archive grows as the diary grows. /getbackup is the explicit command for sending the latest archive over Telegram when the file is small enough. /restore latest queues the newest archive for restore and restarts the service so the database is replaced before it is opened.
Why local-first?
The diary is personal data, so the default deployment keeps the important processing on infrastructure I control:
- SQLite stores the operational state.
- Markdown is the human-readable source of truth.
- Original audio is preserved locally.
- Original images are preserved locally.
- Whisper runs locally for transcription.
- Tesseract runs locally for OCR.
- Ollama runs locally for enrichment.
- Docker keeps the runtime reproducible.
Telegram is used as the capture interface, not as the permanent diary store.
The operational API is intended for a trusted network or a protected reverse proxy. Telegram commands are restricted to the configured owner account, and the bot token remains outside the repository in local environment configuration.
Running it in production
The application runs with Docker Compose and persistent bind mounts:
docker compose up -d --build
The important directories live outside the container:
./data/diary Markdown, audio, metadata
./data/database SQLite database
./backups Backup archives and restore marker
./logs Rolling activity logs
./models whisper.cpp models
./whisper whisper.cpp executable and libraries
The rolling logs are for operational visibility, not diary reading. They record command names, update IDs, entry IDs, stages, timings, retries, and failures, but they avoid logging diary text, transcripts, OCR text, and tokens.
The production runbook also covers a practical Telegram constraint: only one running DiaryBot instance should use the real bot token. If a local test container and production container both poll the same token, they can race for updates. Local or staging containers should use TELEGRAM_ENABLED=false or a separate test bot.
Health checks are exposed at:
/health
/health/live
/health/ready
The complete on-prem deployment procedure is documented in production-deployment.md.
For upgrades, the important operational rule is to avoid running two containers against the same Telegram bot token. Stop the existing production container first, pull or build the new image, then start one instance with the real token. The /help command should show the expected version after the restart.
The practical limits
A long recording is possible, but performance depends on the hardware, Whisper model, and audio size. The current setup processes audio synchronously per entry, so a 25-minute recording may take several minutes on CPU-only hardware. The V3 Turbo model generally improves recognition quality at the cost of more CPU, memory, and start-up load than the tiny model.
Telegram's standard Bot API also has a download-size limit for bots. Compressed voice messages are usually much smaller than high-bitrate audio attachments, but this is worth checking for long recordings.
The system is designed to fail visibly and recoverably:
- The audio is saved before transcription.
- A failed stage is recorded.
- Retry timing uses exponential backoff.
- The original data is not replaced by a failed transformation.
- The diary remains readable even when enrichment is unavailable.
Why this feels finished
The project combines a surprisingly small user interface with a fairly serious backend.
From the phone, it feels like sending a message to myself. Underneath, it is a durable event pipeline with explicit processing stages, local AI, reproducible storage, and recovery behaviour.
The most useful part is not the summary. It is the reduction in friction. A thought can be captured in a few seconds, and the system takes care of turning that capture into a dated, inspectable diary entry without hiding the original words.
That is the kind of automation I want from personal software: quiet, local, recoverable, and useful every day. DiaryBot is now at the point where the interface is small enough to keep using and the backend is explicit enough to trust.



Comments
Post a Comment