- TypeScript 78.9%
- CSS 12.7%
- HTML 7.6%
- Shell 0.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
A lesson was a microphone button and a line of state text: nothing showed whether the microphone heard you or where the tutor was in what it said, and whether the screen was being kept on was invisible, so a refusal looked like the feature not working. - Round the big button, bars move with the tutor's voice and rings grow with the learner's (Web Audio analysers on both streams). Tapping it cuts the tutor off while it talks, pauses otherwise. - A live line shows what is being said. The tutor's transcript arrives before its audio, so it is let out at about the speaking pace and the card's words light up as they come by (speech.ts: matching by spelling). - Saying a card back marks each word right, near or missing, with what was heard; a show_correction marks the words it is about and shows on the board. - Card words are tappable: the tutor says that word again. - Quick buttons follow the moment: again/slower/meaning while the tutor talks, again/I don't know/skip on the learner's turn. - Settings under ⋯, kept in localStorage: keep the screen on (the top bar says whether the lock holds; it is retried after a tap), fullscreen, talk open or by a held button (turn detection off, the page commits the turn), the tutor's pace (output speed, applied between turns), the live line. - A web manifest and the apple-mobile-web-app meta, so an iPhone, which cannot go fullscreen from a page, can open Dardasha from the home screen without bars. The links are added from the script: Bun's HTML bundler tries to resolve server routes as files and fails the build. |
||
| deploy | ||
| docs/mockups/lesson-voice | ||
| src | ||
| .gitignore | ||
| bun.lock | ||
| config.example.toml | ||
| package.json | ||
| README.md | ||
| server.ts | ||
| store.test.ts | ||
| store.ts | ||
| tsconfig.json | ||
Dardasha · دردشة
Learn Levantine Arabic by talking, or Dutch the other way round. A voice tutor that speaks with you and explains on screen.
Run
bun install
bun server.ts --create-admin <name> # the first account; asks for its password
bun dev
Open http://127.0.0.1:4410 and sign in. As the admin, open Beheer to set
the OpenAI key and make an account for each learner; then pick what you want
to learn and a situation to start from. Use headphones, or the tutor hears
itself. Settings other than the key can go in ~/.config/dardasha/config.toml
(see config.example.toml).
Accounts
Using Dardasha needs an account, and nobody signs up: the admin makes every
account, since every lesson is paid for with the admin's key. The admin page
also disables an account (which signs it out everywhere), gives it a new
password, or makes it an admin; the last enabled admin stays one. Accounts,
sign-ins and the key are kept in one SQLite file,
~/.local/share/dardasha/dardasha.db by default (database in the config,
or DARDASHA_DB). Passwords are stored as argon2id hashes; a sign-in is an
HttpOnly cookie that lasts 30 days from its last use.
The OpenAI key
The admin sets it on the admin page; it is kept on the server and never shown
again, only its last four characters. Dardasha makes one call to OpenAI: it
starts a realtime conversation (POST /v1/realtime/calls). Give it a key that
can do that and nothing else:
- a restricted key with Model capabilities set to Request (the
Realtime API falls under it, as the
model.requestscope) and every other permission set to None. Set Model capabilities to Request as a whole first: narrowed down from there it shows "Mixed" and can losemodel.request, and the first lesson then fails with "missing scopes"; - in an OpenAI project of its own with a monthly budget, so what lessons cost is apart from anything else and cannot run away.
The key stays on the server: the page never sees it. The server listens on loopback only and answers only its own hostname, because anyone who can reach it spends the key.
Two directions
- Levantine Arabic (for a Dutch speaker): explanations in Dutch, Arabic as spoken in Damascus (Shami), with ق as a glottal stop and the everyday words a Damascene uses (قدّيش, شو, هلّق, بدّي) rather than standard Arabic.
- Dutch (for a speaker of Levantine Arabic): explanations in Shami, the page in Arabic and right to left, everyday Dutch as it is spoken in the Netherlands.
You can talk to the tutor in either language, or both mixed.
How a lesson goes
Practical first: you pick a situation (the market, a taxi, a café, the supermarket, the GP …) or a free conversation, and the tutor teaches the phrases that situation needs, a few at a time. Letters of the Arabic alphabet, or hard Dutch sounds (ui, g, eu …), come in passing, from words you already know.
You talk at least half the time. A new phrase you first only hear, then say after the tutor; practice varies (how do you say …, answer a question, finish a sentence, pick one of two). Get something wrong and the tutor says what was off, shows the correction, and has you try again. After six to eight phrases it plays the whole situation through with you as a short role play.
The tutor remembers you. When you tell it something about yourself (work, family, a trip coming up) or it notices what you keep finding hard, it writes a short dated note, shown as a quiet line in the conversation. The next lesson starts from those notes: a free conversation opens with a follow-up on one ("you were going to Damascus in May, how are the preparations?"), and examples come from your own life. The start screen lists what it remembers, and each note can be forgotten there; nobody else sees them.
A lesson starts with a minute's review of phrases from earlier days, the ones you got wrong first. When you stop, the phrases of today are listed to read back.
While it talks, it puts what it teaches on the board: a sentence word by word, a word, a letter with its four shapes, a Dutch sound with example words, a table for a pattern. The newest card is shown large; earlier ones become tiles below it, and tapping one brings it back. Tap a word on a card and the tutor says it again.
You see who is talking. Round the big button, warm bars move with the tutor's voice and green rings grow with yours, so you can tell the microphone hears you. A line under the board shows what is being said, live: the tutor's words let out at about the pace it speaks (close, not word-exact, as the text comes before the sound), and the card's words light up as it says them. When you say a card's phrase back, each word turns green when it was heard right and warm when it was near or missing, with "I heard: …" under it; a correction from the tutor marks the words it was about, and shows on the board as well as in the conversation. This is matching by spelling against the transcript, which is prompted with the lesson's phrases, so it is a hint: the tutor's ear decides.
The buttons follow the moment: while the tutor talks, again, slower and what it means; on your turn, again, I don't know, and skip. Tapping the big button cuts the tutor off while it talks, and pauses otherwise.
Under ⋯ in a lesson:
- Keep the screen on (on by default): a locked phone stops the microphone. The top bar says whether the browser allows it; a battery saver can refuse.
- Fullscreen: no browser bars. On an iPhone a page cannot go fullscreen; add Dardasha to the home screen instead (Share → Add to Home Screen) and it opens without them.
- Talk: open (just talk; the tutor answers after a silence) or by button (hold it down while you speak, for noisy places).
- Tutor's pace: calm, normal or brisk.
- Live line: on or off.
These are kept in the browser.
On a phone the conversation folds into a sheet at the bottom; with the live line on it is a tab, as what is said is already in view. Tap it to open. A phone needs the page served over https for the microphone.
On the server
Dardasha runs at https://dardasha.garage44.eu/ on the garage44.eu VPS: main's
files in /data/dardasha/app, run by bun as the dardasha user
under systemd (deploy/dardasha.service), listening on the docker0 gateway
(172.17.0.1:4410) where only Caddy reaches it. Caddy terminates TLS:
dardasha.garage44.eu {
reverse_proxy 172.17.0.1:4410
}
DARDASHA_PUBLIC_URL names the public address: the server then answers that
hostname and marks its cookie Secure.
midas deploys it, like any project of the workspace: after a landing on
main the hub builds deploy/build.sh on a node, which gathers the files the
server runs into deploy/out, starts that copy once as dardasha against a
scratch database, publishes it to /data/dardasha/app and writes
/data/dardasha/restart.stamp, which deploy/dardasha-reload.path restarts
the service on. midas deploy dardasha does it by hand. The recipe is the
hub's (/etc/midas/hub.conf, projects."jeroen/dardasha".deploy). The first
install:
sudo useradd --system --home-dir /data/dardasha --shell /usr/sbin/nologin dardasha
sudo install -d -o dardasha -g dardasha -m 750 /data/dardasha/data
sudo cp deploy/dardasha.service deploy/dardasha-reload.* /etc/systemd/system/
sudo systemctl enable --now dardasha-reload.path && sudo systemctl enable dardasha
midas deploy dardasha
cd /data/dardasha/app && sudo -u dardasha DARDASHA_DB=/data/dardasha/data/dardasha.db bun server.ts --create-admin <name>
How it works
The page talks to OpenAI's realtime model (gpt-realtime-2.1) over WebRTC;
server.ts only answers the page's offer with the session for the chosen
direction and situation. The directions, situations and the page's words in
both languages are in src/lessons.ts; the tutor's instructions and board
tools in src/tutor.ts; the page runs the tool calls (src/board.ts) and
answers them, so the tutor carries on talking.
bun run typecheck
bun test