Case study
Ariha
A suite of fifteen voice and chat applications, built as one coordinated set so that a client could compare interaction models against one shared backend.
Overview
Ariha is a suite of fifteen voice and chat applications, built as one coordinated set so the client could compare interaction models against a shared backend rather than guess between them.
It covers real-time voice, a continuous listening mode, dedicated iOS and smartwatch clients, a shared canvas, group conversation, and a file-aware mode that calls external services through an agent layer.
The problem
The client wanted to know which voice interaction model people would actually use. Not a hypothesis, a comparison. Building one application and iterating on it would have taken a year to answer.
How it actually ran
The engagement opened as one small offer: secure the keys and set up a subdomain, against a brief describing “a service similar to Play AI but much more”. It closed thirty-three milestones later.
That is the honest shape of it. Nothing about the eventual product was in the original scope, and the client added each piece only after seeing the last one work. The list reads as a product being discovered rather than delivered: continuous conversation, realtime sessions, a canvas with export, file uploads, image generation, web search with moderation, a history page, sharing, a follow-up page, a Round Table group mode, a DeepThink page, and smartwatch subdomains.
Most of it was built in live sessions. Twenty-six calls are logged on the contract, several stacked back to back on the same day, and a good number of the milestones are explicitly the session itself rather than a deliverable. Working that way is unusual and it is why the scope could move as fast as it did: the decision, the build and the review happened in the same hour instead of across three days of messages.
I ended the contract in July, three months after the last feature landed.
Complexities tackled
Knowing when the user has stopped talking. Continuous listening has no send button, so the system has to decide when a pause means “your turn” rather than “thinking”. Too sensitive and it interrupts people mid-sentence; too slow and every exchange feels sluggish. Microphone sensitivity and background noise vary enormously between a phone, a laptop and a watch, so the silence detection thresholds are tuned per device class rather than set once globally.
Browser and iOS audio. Mobile browsers restrict audio playback in ways desktop does not: autoplay is blocked without a user gesture, and playback needs an unlocked audio context. Playback was rebuilt on AudioContext rather than plain audio elements to get predictable behaviour and lower latency, with iOS-specific handling on top.
Latency is the whole product. A voice assistant that pauses noticeably between hearing you and answering feels broken regardless of answer quality. The pipeline streams between stages rather than waiting for each to complete, so recognition, inference and synthesis overlap instead of queueing. Speech recognition runs locally rather than calling a cloud API, which removes a network round trip from the critical path.
Changing the model out from under a shipped product. The assistant ran on Llama 70B through a low-latency inference provider, chosen for speed. It was later swapped for Claude under a fixed token budget. Swapping a model is not a configuration change: prompts tuned against one model’s habits do not transfer, output length and formatting shift, and a token ceiling that was generous becomes a constraint the prompts have to be rewritten around. The prompt layer was reworked in the same milestone for exactly that reason.
Restraint in the model layer. The assistant can search the web, and early on it did so far too eagerly, costing seconds per exchange for no benefit. The system prompt was reworked to make search the exception rather than the reflex, with explicit criteria for when it is genuinely needed, and a moderation pass added around the search path in the API.
Citing sources so they can be checked. Answers that searched initially showed bare domains, which tells a reader almost nothing. The server was changed to return full source detail and the clients to show titles, which is the difference between a citation and a decoration.
Degrading on bad networks. Requests carry a fallback path for CORS and network failures, with error messaging aimed at the user rather than the console, because these clients run on phones and watches on unreliable connections.
Clients with no framework. The watch and iOS clients are plain HTML and JavaScript. On a watch, a framework bundle is a real cost, and there was nothing here a framework would have made easier.
Handing it over properly. Near the end the whole estate was migrated and transferred: the chat server moved off shared hosting onto an autoscaling platform, the front end moved off its previous host onto static hosting, every front end got its own custom domain, server SSH keys were rotated, and all repositories were transferred into the client’s own organisation. That last one matters more than it sounds. A project where the developer still owns the repositories is not finished, it is a dependency.
Stack
Python, FastAPI, Flask, Vosk for offline speech recognition, Llama 3 through Groq initially and then Claude for inference, gTTS for synthesis, AudioContext, Firebase for identity, nginx, Cloudflare, and Replit for the final hosting.
Client feedback
Verbatim from the public review left on the completed contract. Clients are not named because the contract record does not carry names.
Thank you
Working on something similar?
Tell me what you are building and what is in the way. I will tell you honestly whether I am the right person for it.