BabelDuck: a self-hosted speaking app whose voice input has one vendor
Beginner-friendly AI conversation practice application
At a glance
- What is it?
- BabelDuck is a Next 14 application for oral practice with an AI, built around one idea: never interrupt the conversation to ask for a correction. Suggestions come back as a branch you can drop, while speech to text is currently coupled to one provider and the environment file says so in plain words.
- Who is it for?
- BabelDuck fits a language learner who wants correction without losing the thread of a conversation, because the sub-conversation model is a better answer than a modal that interrupts, and the quick commands keep the correction one click away. It does not fit anyone who wants provider choice on speech today, since transcription is hard-wired to one endpoint and the environment file names the roadmap item that would change that.
- Can I use it commercially?
- Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
- Is it still maintained?
- Activity is slowing. The repository last received commits 6 months ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 4, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Voice input is wired to one vendor and the env file admits it
The most honest document in this repository is `.env.example`. Its comments are the author's own notes about unfinished work, and the first one concerns speech to text:
# another unfortunate thing is that the stt feature is coupled with siliconflow for now,
# so we need to provide the api url and api key as well, you can get the key from https://cloud.siliconflow.cn/account/akThe next lines say that decoupling from SiliconFlow has been on the roadmap and that the two variables can go once it is done. The default value makes the coupling concrete: `STT_API_URL` points at `https://api.siliconflow.cn/v1/audio/transcriptions`, and `SILICONFLOW_API_KEY` is empty for you to fill.
So the voice input feature has one provider baked into the configuration, and the README's claim of voice input and voice response should be read in that light.
One OpenAI-shaped env block carries the multi-service claim
The same file repeats the pattern for the chat side, in words worth quoting:
# unforunately, the implementation of input handlers is coupled with openai at this time,
# so openai or openai-compatible services are necessary here (json mode required)Three variables follow: `OPENAI_CHAT_COMPLETION_URL`, `OPENAI_API_KEY` and `OPENAI_MODEL_NAME`. JSON mode is called out as required, which is a constraint on whatever service you point it at, not just on OpenAI.
The dependency list matches that picture. There is one provider package, `@ai-sdk/openai` at a 0.0.x range, next to the `ai` package at 3.x, which is enough to serve any OpenAI-compatible endpoint but not enough to make provider switching a first-class feature. A script named `extract-models.js` sits at the repository root, which is how you would fill in `OPENAI_MODEL_NAME` for a service you did not write.
The feature list's promise of integrating multiple LLM services is therefore real in the sense that any compatible endpoint works, and thin in the sense that the code path is one.
Three component libraries share one dependency list
The manifest carries several component systems at once. The Radix primitives appear individually, dialog, popover and slot, alongside `class-variance-authority` and `tailwind-merge`, and the root holds a `components.json`, which is the shadcn/ui configuration file. That combination is the standard shadcn stack.
Next to it sit `primereact` at 10.x, a separate component library with its own styling model, and `cmdk`, a command palette, plus `react-hot-toast`, `react-tooltip`, `react-switch`, `react-loader-spinner`, `react-icons` and `lucide-react`. Two icon libraries and two tooltip approaches in one app is a choice someone made deliberately at some point and has to live with at another.
State is handled by Redux Toolkit with React Redux, and Immer appears twice, once as the library and once as the `use-immer` hook. It is a lot of machinery for a chat interface, and it explains why the image is worth measuring before you deploy it.
The manifest is still called babel-fish at version 0.1.0
The package manifest names the project `babel-fish`, at version 0.1.0, with `"private": true`. The repository, the image and the port all say BabelDuck, so the manifest kept the earlier name and nobody renamed it.
The version is the other loose end. There are no GitHub releases, no tags to compare against, and the last push is dated 2026-04-07, which is the only version reference that means anything for this project. A private manifest at 0.1.0 alongside a published container image means the image tag is the release, and `latest` is the only handle you get.
There is also no `license` field in the manifest, even though the repository root has a `LICENSE` file and the README links to it, and the licence value returned by the repository API is empty. So the licence is reachable only by opening the file.
Recording and speech out go to different places
The voice stack is split across three dependencies that say more about the design than the feature list does. `extendable-media-recorder` records from the browser, and `extendable-media-recorder-wav-encoder` encodes the result as WAV, which is the format a transcription endpoint expects.
Output is a different story: `microsoft-cognitiveservices-speech-sdk` is in the dependency list, so the spoken response appears to come from Microsoft speech services while the spoken input is posted to SiliconFlow. Two vendors for one conversation, with the transcription side the one the env file admits is temporary.
The rest of the stack is unremarkable and modern: Next 14.2.12 on React 18, TypeScript 5, Tailwind 3.4 with PostCSS, and `@radix-ui` plus `cmdk` for the panel behaviour. One development dependency is unusual in a good way, `dpdm`, a detector for circular imports, which suggests someone hit that class of bug and added a tool for it.
Docker picks one of three lockfiles and runs as uid 1001
The image is a standard three stage Next build, but the dependency stage tries three package managers in order and refuses to continue without one:
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fiThe repository ships `package-lock.json`, so the npm branch is what runs. The runtime stage creates a `nodejs` group and a `nextjs` user, both with uid 1001, and chowns the `.next` directory so the prerender cache is writable by the unprivileged user.
Next's own telemetry switches are present but commented out in both the build and runtime stages, so telemetry stays on unless you disable it. Running it yourself is one command, with the port mapping in the README:
docker run -d --name babel-duck --env-file .env -p 9000:9000 orenoid/babel-duck:latestSub-conversations are the answer to a bad suggestion
The product idea is in how corrections are delivered. Instead of interrupting, quick commands ask for a grammar check, a translation or a refinement and the answer arrives without the conversation moving on. If the suggestion needs discussing, you open a sub-conversation about it and return to the original when you are done.
That is a tree of conversations rather than a list, and it is why each conversation can also carry its own preference settings. The rest of the feature set supports the same goal of lowering the cost of speaking: multiple conversations, custom system prompts, streaming responses, a multilingual interface with browser language detection, built-in tutorials, and data kept locally.
The privacy claim deserves the plain reading. The feature list says data is stored locally, and the deployment is a container you run on your own machine, but every turn still goes to whichever provider you configured, so the conversation content leaves the container for that request.
Editorial conclusion
BabelDuck fits a language learner who wants correction without losing the thread of a conversation, because the sub-conversation model is a better answer than a modal that interrupts, and the quick commands keep the correction one click away. It does not fit anyone who wants provider choice on speech today, since transcription is hard-wired to one endpoint and the environment file names the roadmap item that would change that. Before you run it, read the coupling notes in the env template, decide whether your OpenAI-compatible service supports JSON mode, and note that there are no tagged releases and the last push is dated 2026-04-07.
Frequently asked questions
How do I run BabelDuck?
With Docker: install Docker, clone the repository, rename `.env.example` to `.env` and fill in the OpenAI-compatible URL, key and model name, then run `docker run -d --name babel-duck --env-file .env -p 9000:9000 orenoid/babel-duck:latest` and open http://localhost:9000.
Which AI services does BabelDuck support?
Any OpenAI-compatible chat completion service, configured through `OPENAI_CHAT_COMPLETION_URL`, `OPENAI_API_KEY` and `OPENAI_MODEL_NAME`, with JSON mode required. Speech to text is currently coupled to SiliconFlow through `STT_API_URL` and `SILICONFLOW_API_KEY`, and the env file says that coupling is on the roadmap to remove.
Does BabelDuck keep my conversations on my own machine?
The feature list states that data is stored locally for privacy and security, and the app is deployed as one container you run on port 9000. Each turn still reaches whichever provider you configured, so the request content goes to that service.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/orenoid-babelduck)