This is a submission for the Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle
What I Built
SabiBook is a sales and debt book you talk to. It's for small traders in Nigeria who sell on credit.
Picture a woman with a provisions stall selling rice, garri, Indomie and eggs. Half her regulars don't pay in full. Emeka takes two cartons of Indomie today and brings the rest on Friday. The record of who owes what lives in an exercise book, a WhatsApp chat with herself, or her head. On a busy Saturday she skips entries because customers are waiting. A week later nobody is sure whether Emeka paid the second half.
That notebook is where her money leaks.
With SabiBook she holds one big button and says, in Pidgin or English:
"I sell two carton of Indomie give Emeka, na twenty thousand. Him pay fifteen thousand, e go bring the rest tomorrow."
A few seconds later a draft card shows up: Sale, Emeka, 2 cartons of Indomie, ₦20,000 total, ₦15,000 paid, ₦5,000 still owed. Every field is editable. She taps Save to my book, and Emeka appears on the Debtors screen with ₦5,000 next to his name. When it's time to collect, SabiBook drafts a short polite reminder and opens WhatsApp with it.
There's also a ledger grouped by day, payments recorded by voice or by tapping an amount, and a receipt link for any saved entry. It's a PWA, so it installs to a phone's home screen.
Who would pay? Traders with ten to a hundred credit customers. If it recovers one forgotten debt a month, a small naira subscription costs less than it saves. Side hustlers too. Think of someone selling perfume to colleagues on the side, who wants to know if it makes money without opening a spreadsheet. The hosted version would be the business.
To be clear: SabiBook isn't deployed and has no users yet. Everything here runs on my machine.
Demo
[DEMO VIDEO: https://youtu.be/YRmcgkBkveQ]
Code
The code is at https://github.com/dev-lawrence/sabibook, under the MIT licence.
To run it fully locally, install Ollama and Speaches, pull gemma4:e4b, run pnpm exec convex dev for the local Convex backend, and run pnpm dev for the app. The README lists the env vars to set in between. The repo also has a Docker Compose file, but I have not tested it end to end. I ran the native setup.
How I Built It
Two open weight models do the AI work. Whisper large v3 turbo handles speech, served by Speaches, which puts an OpenAI style /v1/audio/transcriptions endpoint over faster-whisper. Gemma 4 E4B (gemma4:e4b) on Ollama reads the transcript and fills in a JSON schema: entry type, customer, items, total, paid, and how many days ago it happened. Gemma also writes the reminders.
The app is Next.js with Convex as the backend. A voice note goes like this:
- The phone uploads the audio to Convex, and a background action sends it to Speaches. The trader's saved customer names go along as Whisper's prompt, so "Chiamaka" comes back spelled the way she saved it.
- The transcript goes to Gemma with the same names, so it reuses Mama Ngozi instead of creating her twice. Gemma returns JSON. If the server has no structured output, the code falls back to asking for JSON in the prompt.
- Zod checks the JSON against the schema.
- Plain TypeScript checks the numbers against what was actually said.
- The draft appears on screen. Nothing goes into the book until she taps Save.
Step 4 is the part I care most about. A model that guesses a number in a ledger is worse than no app, so Gemma does no arithmetic:
-
Balance is computed.
balance = total - paid, in code. - Every amount must be heard. The code pulls every number out of the transcript, spoken ("forty five thousand"), shortened ("45k") or written ("₦45,000"). If Gemma returns an amount that isn't one of those, or a product or sum of them, the field is flagged "I did not hear this".
- Dropped scales are allowed. Traders say "forty five thousand, she paid thirty five" and mean 35,000. The trade-off is that the check can't catch a mix-up between 10 and 10,000 in that case, so the draft card is the backstop.
- Items must add up. If they don't, the draft says so.
- A credit sale needs a name. A debt with no name can't be collected.
- Unknown stays empty. If she never said how much was paid, the field is null and the Save button tells her why.
Dates work the same way. Gemma returns daysAgo, and code turns it into a date using the phone's local day.
How well it does
I wrote 40 test sentences the way traders talk: 25 English, 10 Pidgin and 5 mixed with Yoruba, Hausa or Igbo words. Some are hard on purpose. pnpm eval:extract scores each field separately. For gemma4:e4b:
| type | date | customer | total | paid | balance | items | flags | exact |
|---|---|---|---|---|---|---|---|---|
| 100.0% | 97.5% | 100.0% | 97.5% | 90.0% | 95.0% | 100.0% | 92.5% | 87.5% |
"Exact" means all eight fields right. By language that was 84.0% for English (21/25), 100.0% for Pidgin (10/10) and 80.0% for mixed (4/5). Median latency was 2506ms and p90 was 10002ms on my machine.
Five cases missed:
- en-14. "Chidi took two bags of garri on credit for thirty six thousand, we will settle later." Gemma set paid to 0 and balance to ₦36,000. My label says paid is unknown, so the expected "missing" flag didn't fire. Gemma's reading is defensible, and I may change the label.
- en-18. Cement, no price agreed. Gemma left the total empty and the draft flagged it, but it set paid to 0 instead of null.
- en-20. This one worries me. The trader lists items worth 80,000 and then says "total sixty thousand". Gemma changed total and paid to ₦80,000, so the "items don't add up" flag never fired. Since 80,000 is a sum of heard amounts, the grounding check let it through.
- en-22. Chidi paid ₦15,000 for a ₦12,000 carton of Peak milk. Gemma capped paid at ₦12,000, so the overpaid flag didn't fire and the ₦3,000 change owed to him vanished.
- mix-01. "Lana, Iya Bisi ra ten litres of epo pupa..." "Lana" is the Yoruba word for yesterday, written without accents (the standard spelling is "à ná"). My label dates the sale 12 October. Gemma dated it today, 13 October, and missed the word.
The fix for en-20 and en-22 probably belongs in code: when a total was spoken, compare it with the model's total.
Limits
- I tested with Whisper large v3 turbo only. There's an optional speech track for Nigerian languages (NaijaVox, via
ASR_MODEL_NAIJA), but it isn't part of the setup I tested. - Yoruba, Hausa and Igbo are beta.
- I haven't run the speech accuracy eval. The harness is in the repo, but I have no figures to quote.
- One sentence makes one entry, and debts are tracked per customer, not per sale.
Why Does Open Innovation Matter?
A trader's book is private. It says who is behind on payments and how much cash sits in the shop on a Friday.
With open weights, that data can stay where the trader chooses. Gemma, Whisper, Speaches, Ollama and the Convex backend all run on one machine. A cooperative could run it on a cheap server, and the customer list never leaves that box.
Inference is four env vars: ASR_BASE_URL, ASR_MODEL, LLM_BASE_URL and LLM_MODEL. Replacing gemma4:e4b is one line, and the eval takes a list of models, so I can compare candidates on the same 40 cases before switching. When Gemma got en-20 wrong, I could rerun it locally as often as I liked for nothing.
Then there's cost. A busy trader can record a lot of sales in a day, and the margin on a single crate of eggs is thin. On a server I pay for, each sentence costs hardware and electricity, and no vendor's price change can alter that.
A closed API could probably parse "him pay fifteen thousand" just fine. That isn't my argument. This app needed the records off third party servers and a per-sentence cost I set myself, and open weights were the simplest way to get both.
Prize Categories
- Overall winner
- Best Use of Gemma
Top comments (0)