← All work

Jun – Sep 2026

Nava

Solo · design, Flutter, backend, DevOps

An AI caption editor for Persian video that I designed, built, and shipped, then checked against the first real installs.

At a glance

  • Production MVP in under a month
  • Audio-only API · video stays on the phone
  • On-device render · preview matches the file
  • About half used their free grant
First-version smoke test. Pick a clip, generate captions, export — the editor loop in under 40 seconds.

The problem

Persian creators put captions on Reels so more people watch, but the tools they have look cheap, feel heavy, or send the whole video to a server.

I didn't try to win on speech accuracy, because that part is already good enough. The real product is the editor: captions that follow the voice, Persian type that looks professional, and a short path from picking a video to exporting it.

How I think

Because I design and also write the code, I can change the UI and the pipeline on the same day, with no handoff and no waiting. That is how I made a production MVP in under a month. I kept the product small on purpose: one language, one platform, and one job, which is captions that look right and an export that matches the preview.

I also wanted it easy, so there is no onboarding and no sign-up. You pick a video and you start. If someone needs a tour, the editor is already too much.

Price is part of design too. The unit is minutes, since that is what the clip already shows, and the app takes minutes when it transcribes, not when it exports. Export is free and has no logo, paid minutes do not expire, and I sell packs instead of auto-renew subscriptions, so the cost is on the button before you press it.

Design

The video is the product, so the controls sit on the clip and users learn looks from a strip under the preview, not from a settings maze.

A simple RTL layout breaks the English part, so I set direction per line from the first strong letter.

Editor during playback. The clip stays in front, and the tools stay out of the way.
The cost is on the button before you generate. The editor shows minutes, and the pack sheet shows Toman.

Architecture

I didn't vibe-code a demo. I designed the architecture first, then I built it.

The phone extracts a 16 kHz mono WAV with a native plugin, not FFmpeg, and only that audio goes to the server. A FastAPI proxy holds the provider key, transcription comes back with word-level timestamps, and the phone draws captions and burns them into the video with Media3, so the clip never leaves the device.

This is why the system stays cheap and can grow:

  • Bandwidth. One minute of audio is about 1.5 MB. One minute of 1080p video is about 150 MB.
  • Privacy. The footage never goes to the cloud.
  • Cost. Speech is the only real bill. Export is on the phone, so it is free. Same audio hash hits a Postgres cache and skips the provider.

I changed the transcription vendor during the build without touching the client or the wallet, which is what a real proxy is for. Preview and export share one paint path, so if it looks right in the editor, the file matches. I wrote tests for sync, billing, and export, and CI runs analyze, tests, and lint. I didn't put it on CafeBazaar until generate and export were stable on a real phone.

FFmpegKit is archived, so I didn't want it in production. Native extract and native export keep the app smaller and the pipeline under my control.

Extract on the phone, upload audio only, transcribe, then render on the phone. The clip never hits a server.

Production

The API is thin on purpose: Flutter never sees provider keys, Postgres holds the minutes wallet and a cache of transcripts, and CafeBazaar purchases confirm on the server, not on the client. Rate limits stop people from making many free accounts.

This can scale without a rewrite, because the API is stateless and the heavy work is either on the phone or cached. I can run more API containers behind Caddy and the app does not change.

Production runs on Docker Compose on a VPS: API, Postgres 16, and Caddy with TLS. It is the same compose file as local, with a prod profile, live at api.getnava.ir. Redeploy is git pull and compose build, and Alembic runs on start so the schema follows the code.

iOS export is not in this build, on purpose. CafeBazaar is Android, so I shipped the loop that is live, not a platform I cannot sell yet.

First users

Shipping without a way to see the loop would be guessing. I designed Firebase events around a few questions: do people pick a clip, generate captions, export a file, and buy minutes, or do they drop in prepare, hit the paywall, or fail on a mid-range phone.

Each event sits on a real step, with params that help the decision: billed minutes, language, duration, whether the frame is vertical, the recipe and font on the export that actually ships, the pack SKU on a successful buy. Failures use a stable error kind, not Persian error text. I never send transcripts, file names, wallet IDs, or CafeBazaar purchase tokens. After the first pack, the user is marked as a buyer, so I can separate curiosity from revenue. Debug builds stay off unless I turn them on.

A new CafeBazaar listing does not produce a cohort you can read, so I bought a small batch of installs and followed those people through the funnel. The spend was for speed. I wanted targeted users in days, while changing the product is still cheap.

Of 106 first opens, 89 reached the editor and stayed. Export holds: 13 of 14 finished a file. A user came back four days after the free minutes ran out and captioned again within a minute of paying.

Try it

Nava is live. If you have CafeBazaar, open the listing and run one clip through generate and export. Outside the store, download the APK. Three minutes are free. There is no sign-up.

CafeBazaar listing. Persian-first, five screens, one job.

Outcome

Nava is live on CafeBazaar. I owned design, Flutter, native plugins, the API, billing, the VPS, and the analytics funnel. A small install campaign brought the first targeted users in days. Most of them reached the editor, and two bought minutes.