← Work

Case study

Prekucaj.rs · Serbian audio, turned into a document

People asked for the dictation inside my CRM without the CRM. Serbian speech-to-text is badly served, so I shipped it as its own product, and most of the work was getting a sixty-minute recording through a hobby-tier serverless plan.

Role
Design Engineer
Timeframe
2026
Focus
Serbian-first, Speech-to-text, Shipping constraints

Mine end to end: the product decision, the interface, the backend, the pricing stance and the legal pages. Live at prekucaj.rs. It launched a month before this was written, so there is nothing to claim about adoption yet, and this case study does not.

The live Prekucaj landing page: the headline over a waveform, with the record and upload controls beneath it

This one is live. Everything below is the real product at prekucaj.rs. You can open it and try it without an account.

01

Asked for, not decided

I was building dictation into a CRM for therapists. One of the people testing it is on a psychotherapy training programme, word got around there, and what came back was not feedback on the CRM. Several of them wanted the dictation without the CRM: no clients to track, no rooms to book, just recordings they had no way to turn into Serbian text.

That is the one signal you cannot manufacture: people asking for a piece of your product while declining the rest of it. Looking further turned up the same gap well outside therapy: lawyers with recorded meetings, translators, journalists with interviews.

The gap is real and structural rather than a matter of taste. Apple’s speech recognition has no Serbian locale, so a phone cannot do this natively at all. Serbian is also digraphic, and models return a mix of Cyrillic and Latin, so raw output is not something you can paste into a document and use.

prekucaj.rs
The Prekucaj landing page: record or drop a file, with a demo transcript panel beside it.
One job, stated in one line, with the control to do it directly underneath. No account needed for the first five minutes.
02

The constraint that shaped everything

The interesting design problem here was not the interface. It was that a sixty-minute recording had to survive a hobby-tier serverless plan.

A serverless function rejects a request body over 4.5 MB. An hour of phone audio is around 29 MB, and even five minutes would not fit. Posting the file to an API route, which is the obvious shape, fails for every recording anyone actually wants transcribed.

The obvious way

Anything longer than about five minutes fails

Shipped chosen

The 4.5 MB ceiling stops applying; the cap becomes storage, not the function

The limit is not a detail of one host; it is what a serverless request body costs anywhere. Moving the file out of the request is what made a sixty-minute recording possible on a hobby plan. The client still checks size and duration before it uploads, so a file that will be refused is refused immediately rather than after a two-minute wait.

So the browser asks for a signed URL, uploads straight to a private bucket, and the API route only ever receives a path. Two things follow from that:

  • The ceiling moves from the function to storage, which is 50 MB on the free plan. That is roughly an hour of phone audio at 64 kbps, and not an hour of a 128 kbps MP3, so the client checks both size and duration before it uploads, and refuses immediately rather than after a long wait.
  • The audio is deleted as soon as the text is back, including when the transcription is rejected. A recording of a legal meeting or a therapy session has no reason to outlive its transcript.

Waiting was never the bottleneck. Measured: 34 minutes of audio comes back as text in 45 seconds, about 2% of the recording’s length. File size was the whole problem, and it is the one nobody warns you about.

03

Counting things that must not be double-spent

Every account holds a balance of minutes. Two requests arriving together must not both be allowed to spend the last one.

The naive shape, read the balance then check it then subtract it, has a race in the middle of it, and a wallet that goes negative is a real bug, not a theoretical one. The fix is to make the check part of the write:

update public.wallets
set remaining_seconds = remaining_seconds - p_seconds
where user_id = p_user_id
  and remaining_seconds >= p_seconds
returning remaining_seconds into v_remaining;

If the row does not satisfy the condition, nothing is updated and nothing is returned, and the caller is told which of the two reasons applied: no wallet at all, or not enough left. One statement, no window in between.

Rate limits have the same shape of problem for a different reason. Counters in process memory are useless here, because every serverless invocation gets its own memory, so a limit of five per minute becomes five per minute per cold start. The counters live in Postgres instead.

Anonymous use is capped at five minutes, keyed on a hash of the IP rather than the address itself. A shared connection or a VPN can walk around that, which is precisely why the real wall is registration and a balance, not the IP check.

04

Refusing to show a price list

The product is free right now, and the pricing page says so instead of listing tiers that do not exist yet.

prekucaj.rs/cene
The pricing page: a note that Prekucaj is free for now, three cards explaining the free minutes, and a request for feedback.
Sixty minutes on sign-up, three more grants of sixty on request, and in exchange: rate the transcript and say what was wrong with it.

The trade is stated plainly, minutes for feedback, because at this stage the only question that matters is whether the transcript is good enough to use, and the only way to find out is to ask the person who just read it. Every transcription can be rated one to five with a comment, and those go to an admin view rather than into a testimonial.

A pricing page that invents tiers before anyone has paid is a page that has to be rewritten the moment reality arrives. This one says what is true and commits to one thing: minutes already granted stay granted when packages land.

05

The parts nobody photographs

Two things took longer than the interface and neither is visible.

Legal. Terms, privacy, cookies, an impressum and a complaints page, all in Serbian and all written against what the site actually does rather than copied from a template. Audio uploaded by a lawyer or a therapist is not ordinary content, and a tool that touches it without saying what it does with it does not deserve to be used. They are drafts pending a lawyer’s review before any real launch, and the site says so.

The phone. Most recordings are made on a phone, which means most people arrive on one. The landing page is not a desktop layout that survives being narrow; the control that does the job is the first thing under the headline at any width.

The Prekucaj landing page on a phone: headline, then the record and upload controls directly beneath it.
On a phone the record button is above the fold, because that is where the recording already is.
06

Outcome

It shipped, it works, and it has been live for about a month. That is the honest summary.

60 minLongest recording accepted, up from the ~5 min a request body allows
45 sTo transcribe 34 minutes of Serbian audio, measured
5 minFree without an account, before anything is asked of you

There are usage numbers, and they are the numbers of a product that launched a month ago to no announcement. Publishing them as evidence of anything would be dressing up a sample of a few dozen transcriptions as adoption, so they are not here. What can be said is that the thing runs, that people outside the original audience asked for it before it existed, and that it is open for anyone to test the claim themselves.

What I would do differently: I built the anonymous five-minute allowance before I had any idea whether the transcript quality would hold up on real Serbian audio. Quality was the risk; the funnel was not. Those two weeks belonged to listening tests across providers, and I did them afterwards.