Pahlavon Numonjonov Case study
tekstlybot
← All work

tekstlybot

Media bot: compress video and photos, extract audio, make GIFs, turn speech into text. A job queue with workers, inside 256 MB of heap.

Role
Solo — design, build, run
Timeline
2026 — present
Stack
Java · ffmpeg · whisper.cpp · SQLite
Status
Running

The problem

People send each other video that will not upload, voice notes nobody wants to listen to, and clips that need to be a GIF. Every one of those is a job for ffmpeg — and almost nobody is going to install ffmpeg.

The first version did the obvious thing: run the conversion right there in the update handler. It worked until two people used it at once. A ten-minute transcription blocked the thread that receives updates, and the bot stopped answering everyone — including people who only wanted to say hello.

Constraints and tradeoffs

The update thread must never do work

Receiving and processing are separated by a queue. The handler validates, enqueues and answers immediately; workers pull jobs and do the slow part. This is the whole architecture, and it is the only decision that actually mattered.

256 MB of heap, on purpose

The target is the cheapest VPS that exists. That rules out loading media into memory: every operation streams through temporary files, and the worker count is small and fixed. The constraint is enforced in the launcher (-Xmx256m) so that a change which breaks it fails here rather than on the server at 3 a.m.

Two transcription engines behind one interface

WhisperCppTranscriber runs locally — free, private, slow on weak hardware. ApiTranscriber is fast and costs money. Both implement Transcriber and the choice is config, not code, so the same build runs on a laptop and on a server with a budget.

What I traded away

  • Telegram's 20 MB download limit stays. Working around it means running a local Bot API server — a second piece of infrastructure to operate, for a minority of files. The bot explains the limit instead of pretending.
  • No distributed queue. The queue is in-process; a restart loses in-flight jobs. For a personal-scale bot, Redis would be more moving parts than the problem deserves.
  • No progress bars. Editing a message every second to animate a percentage burns rate limit for decoration. One “working on it”, then the result.

Architecture

MediaBot never blocks JobQueue bounded Worker 1 Worker 2 Worker n ffmpeg Transcriber whisper.cpp | API BotGateway sends the result
The green path is the reply going back out — the only time a worker touches Telegram.

Screens and states

[video 48 MB] → queued, position 2 → compressing… → done: 11 MB (h264, crf 28)

Compression. The user gets a position immediately, not silence.

[voice 9:41] → transcribing (whisper.cpp) → 1 480 words → sent as a .txt file

Transcription. Long results go as a file — a wall of text in a chat is unreadable.

[video 74 MB] ✗ Telegram caps bot downloads at 20 MB. Send a shorter clip or a link.

Limit reached. Explaining the platform limit beats a generic failure.

rate limit: 4 jobs / 10 min → next job in 3m 12s work/ cleaned: 6 files, 214 MB

Housekeeping. Temporary files are deleted after every job; the disk is the real limit.

Results and lessons

256 MB
heap ceiling, held under concurrent jobs
0
requests blocked by someone else's long job
2
transcription engines swapped by config alone

What I would tell myself at the start

  • A queue is not premature optimisation for a bot. It is the difference between a toy and something two people can use at the same time. I should have started there.
  • Delete temporary files in a finally. The disk filled up first, well before memory ever became a concern. Cleanup on the happy path only is not cleanup.
  • Put the memory ceiling in the launcher. A limit that only exists on the production server is a limit you discover by breaking production.
  • Name the platform limit in the error text. Support questions dropped to nearly nothing once the message said why, not just failed.