Learn Levantine Arabic by talking. A voice tutor that speaks with you and explains letters, words and grammar on screen.
  • TypeScript 78.9%
  • CSS 12.7%
  • HTML 7.6%
  • Shell 0.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
cartopolis-agent 13e076e26c feat: see and steer the voices in a lesson, keep the screen on, go fullscreen
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.
2026-10-27 21:17:39 +00:00
deploy feat(deploy): midas deploys Dardasha after each landing 2026-10-27 21:09:09 +00:00
docs/mockups/lesson-voice docs(mockups): a lesson on a phone, kept awake, fullscreen, with more around the voice 2026-10-27 21:17:39 +00:00
src feat: see and steer the voices in a lesson, keep the screen on, go fullscreen 2026-10-27 21:17:39 +00:00
.gitignore feat(deploy): midas deploys Dardasha after each landing 2026-10-27 21:09:09 +00:00
bun.lock feat: a voice tutor for Levantine Arabic with a board 2026-10-27 17:36:36 +00:00
config.example.toml fix: transcribe the learner with gpt-4o-transcribe, prompted with the lesson's phrases 2026-10-27 19:11:08 +00:00
package.json feat: a voice tutor for Levantine Arabic with a board 2026-10-27 17:36:36 +00:00
README.md feat: see and steer the voices in a lesson, keep the screen on, go fullscreen 2026-10-27 21:17:39 +00:00
server.ts feat: see and steer the voices in a lesson, keep the screen on, go fullscreen 2026-10-27 21:17:39 +00:00
store.test.ts feat: the tutor remembers the learner across lessons 2026-10-27 20:18:07 +00:00
store.ts feat: the tutor remembers the learner across lessons 2026-10-27 20:18:07 +00:00
tsconfig.json feat: a voice tutor for Levantine Arabic with a board 2026-10-27 17:36:36 +00:00

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.request scope) 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 lose model.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