TreeMap
A free, open-source disk-space visualizer. Squarified treemap, duplicate hunting, disk-full forecasting, and cleanup that only ever moves files to the Trash.
翻译准备中
Dictation with a live preview: see what you say while you say it. Hold Right Ctrl, watch the words appear, release to paste.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗See what you say while you say it.
Ciao is a dictation app that shows your words live, as you speak, in a small floating card at the bottom of the screen. When you let go of the key, the text is pasted into the app you were typing in.
Other dictation tools show nothing until you stop talking. You only find out that "don't" became "do" after the text has landed in your prompt. With Ciao you watch the transcript form in real time and catch the one word that flips the meaning while you are still talking. It is built for talking to coding agents (T3 Code, Claude Code, Codex) in Russian and English mixed with technical terms.
Live preview. New words fade in bright and settle to grey. On a real microphone they appear about 0.8 s after they are spoken (median), and that holds steady over multi-minute dictations. The final text arrives about 0.6 s after you release the key.
low. The level ranges from
minimal to xhigh: lower shows words sooner, higher is more accurate, and the
price is the same. It is hidden by default. To see and change it, go to
Settings → For developers → Show delay.Push to talk. Hold Right Ctrl (Right Option on a Mac), speak, release, and the text is pasted.
A pill between dictations. A small Ciao pill stays at the bottom of the screen; the card unfolds out of it when you start and folds back when the text is in. Click it to dictate hands-free (click Stop or the red dot on the card to finish), drag it anywhere. The card then opens where the pill is, on that screen, rather than on the screen under the pointer. Turn it off in Settings → Behavior → Pill on screen; then the card simply appears and disappears.
Pick the microphone. Settings → Microphone lists the connected ones; the system's default is used until you choose one, and while the chosen one isn't connected (Ciao goes back to it once it is). Test shows a level bar and the name of the microphone actually heard. If the microphone in use is unplugged in the middle of a dictation, the recording carries on from the one to use now (with none left, what was said until then is still transcribed and pasted); a microphone plugged in or out between dictations is picked up at once, even while the wake word keeps the mic open. The choice is kept by the microphone's name: Windows may rename a USB one plugged into another port ("2- USB Audio Device"), then choose it again. Desktop only: on Android the phone chooses (a headset when one is connected).
Sounds when it listens. A soft rising two-note sound when Ciao starts listening, a falling one when it stops (Settings → Behavior → Start and stop sounds, on by default; desktop only: on Android the bubble buzzes instead). The start sound plays at once, except with a trigger like Right Ctrl (0.15 s later, so a Ctrl+C makes none) or when short presses are dropped (0.35 s); the mic audio for the next 0.28 s is silenced so the sound isn't in the recording, unless you are speaking already (after the wake word, or when you started before the sound).
Music pauses while you dictate, if you turn on Settings → Behavior → Pause music while you dictate (off by default). Once a recording has gone on for 0.35 s (1 s with a trigger like Right Ctrl, which a shortcut can still turn into Ctrl+C until then), the players that are playing pause, so a shortcut leaves the music alone. A short tap starts hands-free mode, so it pauses the music too, unless A short press starts hands-free mode is off: then the tap is dropped before the music pauses. When the recording ends, or Ciao quits, those of them still paused play on. A player you started or stopped yourself meanwhile is left as it is.
dbus-send.T3 mode for dictating prompts into T3 Code, in Settings → T3 Code. It is for hands-free dictations: a short press (unless A short press starts hands-free mode is off), the wake word, or a click on the pill. A hold works as without T3 mode: the text goes where your cursor is, with no T3 Code brought forward, no prefix and no check. So does a dictation started in an AI chat: ChatGPT, Claude, Gemini (and Google AI Studio), Perplexity, Grok, DeepSeek or Microsoft Copilot open in the browser in front, or the ChatGPT or Claude desktop app. It stays there (T3 Code isn't brought forward, no prefix, no Enter) and the card shows that chat's icon instead of T3 Code's. Ciao knows the chat by the page's address, read through accessibility from the page that has focus (Windows: Chromium browsers and Firefox; macOS: Safari and Chromium browsers, with the Accessibility permission; Linux: only with desktop accessibility on), or else by the window title. A ChatGPT conversation is titled by its subject alone, so there only the address tells: with focus in the address bar, or on Linux without accessibility, only its start page is known. macOS and Wayland give no window titles, so there it is the address or nothing. For a hands-free dictation anywhere else:
The card stays out of your way.
Pastes where you finish. The text goes into the window that is active when you stop the dictation, so you can start talking, go to the window the text is for, and stop there. If that window loses focus while the text is being finished, nothing is typed anywhere and the text is left on the clipboard. After a successful paste, the clipboard is restored 1.5 s later, unless something else was copied meanwhile (in T3 mode, see above).
Text that didn't land stays in reach. When the text went to the clipboard instead, when T3 mode couldn't see it reach the chat box, or when it was pasted while nothing editable had focus (a click had left focus on a page or a list), the card stays with the text in a text box instead of folding away: select any of it with the mouse (a double click selects all of it) and drag the selection into another app's field; Copy puts it on the clipboard. The box is read-only: the card never takes the keyboard, which stays with the app you dictate into. It stays until you close it, use it, or start the next dictation (a click on its icon starts one, as the pill would; a dictation that comes to nothing, like a brushed trigger or a silent hold, gives the card back, while Esc lets it go). It doesn't turn see-through under the pointer. Not when Paste text right away is off: then the clipboard is where the text is meant to go. The "nothing editable" check counts only in windows where Ciao has read a field before (Chromium apps show theirs only after a first read; the first paste into a window makes that read) and not in terminals, which take a paste without showing a field.
Learns the words you correct. When you fix a misheard word in the pasted text ("постхок" → "PostHog", "клод код" → "Claude Code"), Ciao offers to add it to the terms, in a small card under its own name; one click and recognition spells it right from then on (and on your other devices, with sync). For that it reads the focused field of the window it pasted into, for two minutes after the paste and only while that window is in front: after you type in it, and as you press Enter (a chat app sends and clears the field then). If the text went to the clipboard instead (the window changed, or Paste text right away is off), the field you paste it into yourself with Ctrl+V (⌘V) within a minute is watched the same way. Only words that sound like what was heard and look like a term (Latin letters in Russian speech, capitals, digits) are offered; rewrites, added sentences and typo fixes are not. The text is compared on your computer and never stored or sent. Fixing a dictation in the history (Edit) offers the corrected words too. Turn it off in Settings → Behavior → Suggest new words.
Paste the last transcript again with Alt+Shift+Z (Option+Shift+Z on a Mac).
Nothing is lost.
History. Click the tray icon to see every dictation. You can play it, copy it, delete it, or transcribe it again:
gpt-transcribe or gemini-3.5-transcribe).Paragraphs and lists, live. The card lays the text out while you speak, and the pasted text has the same layout:
Context (Settings tab). Describe what you usually talk about and list the
terms that must be spelled exactly (T3 Code, WebSocket, …). This noticeably
improves product names and identifiers.
Sync through Google (Settings → Sync → Sign in with Google, on every platform and on Android). The terms, the context and the API keys become the same on every device you sign in on. See Sync.
English or Russian interface (Settings → Appearance → Language). The default is Russian if the system is in Russian and English otherwise (also for an existing install, the first time it starts with this setting); a change applies right away.
Light, dark or system theme (Settings → Appearance). System is the default and follows the system live.
Cost meter. Shows how many cents the current dictation costs. You can turn it off in Settings.
Usage (the Usage tab of the history window, Usage on Android's history
screen): what dictation cost and how much you said over the last 24 hours, 7,
30 or 90 days or all time, after T3 Code's Usage page. Totals (cost since the
period's first day, minutes, dictations, the average per day with dictation), a
column per day (per hour for 24 hours) split into OpenAI and Gemini, switchable
between cost and minutes, and each model's share. It is Ciao's estimate at list
prices (minutes recorded × the model's price, src/core/cost.ts), not the
providers' bill, and counts the dictations of this device's history only.
Gemini or OpenAI (Settings → Who transcribes on the desktop and on
Android). Ciao doesn't recognize speech itself: it sends the audio to one of
these services, on your own key and your bill, and the card lists them side by
side with their price per hour of speech. Gemini is the default for a new install.
An Android install where no service was ever picked moves to Gemini with the
update if it has a Gemini key (one with only an OpenAI key stays on OpenAI); the
desktop keeps the service it had. With Google's Gemini
(gemini-3.5-transcribe-live while you speak, gemini-3.5-transcribe for the
file), live transcription costs about half as much. Gemini transcribes verbatim:
its smart mode, which dropped "um" and applied spoken corrections, also dropped
whole sentences now and then, and is gone since 0.9.16.
Proxy (Settings → Who transcribes → Proxy, on every platform and on
Android). Requests to OpenAI and Gemini, live and file, go through an HTTP proxy
(http://user:password@host:port), for when Gemini isn't available in your
country. Paste it, press Save, and Check (run on its own after a save) says
whether Gemini answers through it, or that the proxy refused the login or can't be
reached; Remove goes back to direct. The proxy in use shows without its
password. Sync, updates and telemetry go direct. The proxy syncs through Google
along with the API keys (and not while Sync API keys is off). A proxy that
can't be reached fails a dictation as when offline ("Couldn't reach …"), and the
audio is kept.
gpt-live-transcribe and gpt-transcribe, or a
Gemini API key from Google AI Studio (a
project with billing on; new accounts prepay at least $5), or a Vertex AI key:
in a Google Cloud project with billing on, enable the Vertex AI API, give a
service account the Vertex AI User role and create an API key bound to it and
limited to the Vertex AI API (gcloud services api-keys create --api-target=service=aiplatform.googleapis.com --service-account=…). Vertex
refuses a key not bound to a service account, and a key that may also call the
Gemini API is taken for an AI Studio key.Pricing is per minute of audio:
| live | re-transcribing the file | |
|---|---|---|
OpenAI (gpt-live-transcribe, gpt-transcribe) | $0.017/min, about $1 per hour of talking | $0.0045/min |
Gemini (gemini-3.5-transcribe-live, gemini-3.5-transcribe) | about $0.009/min | about $0.005/min |
Gemini bills per token; the per-minute figures are Google's own estimate for speech. Idle time costs nothing.
Download Ciao-Setup-<version>.exe from the
latest release and run it. It
installs for your user only (no admin prompt) into %LOCALAPPDATA%\Programs\Ciao
and starts Ciao. If you already run a copy you built yourself, quit it first
(tray → Quit); otherwise the new one hands over to it and exits.
Ciao starts with Windows by default. The toggle is in Settings.
Ciao checks GitHub Releases for a newer version 15 s after it starts and every
4 hours after that. When there is one, an Update to X button shows in the
window's title bar and in the tray menu. Click it: the new version downloads,
Ciao quits, installs it silently and starts again. Settings, the key and the
history in %APPDATA%\Ciao stay. Nothing is downloaded until you click, and a
failed check in the background shows nothing.
To check by hand, go to Settings → Updates → Check.
On Linux the AppImage updates itself the same way; the .deb asks for your
password to install the new package. On macOS the check works the same, but the
button opens the release page instead: see macOS.
A portable copy (the release/win-unpacked folder from npm run dist:win) is
updated the same way. The update installs Ciao into %LOCALAPPDATA%\Programs\Ciao,
and from then on that copy runs and starts with Windows. You can delete the old
folder. A dev run (electron .) never updates.
Download from the latest release:
Ciao-<version>-arm64.dmg for Apple Silicon (M1 and later), Ciao-<version>-x64.dmg
for Intel. Open it and drag Ciao into Applications.
xattr -dr com.apple.quarantine /Applications/Ciao.app in Terminal once.What is different on macOS for now:
.dmg and drag Ciao into Applications over
the old one. Because the app is unsigned, macOS then stops honouring the
Accessibility permission even though the switch still shows on: remove Ciao from
that list with −, press Settings → Permissions → Open settings to put it
back, and turn it on again.You need Node.js 22+, and the .NET 8 SDK for the Windows app. The Windows build runs on Windows, Linux or macOS.
git clone https://github.com/iliyasone/ciao.git
cd ciao
npm install
npm run build:native # the input helper → build/win-input/Ciao.Input.exe
npm run build:kws # the wake-word detector and model (~50 MB) → build/kws
npm run dist:win # a portable folder → release/win-unpacked/Ciao.exe
npm run dist:win:installer # the installer → release/Ciao-Setup-<version>.exe
dist:win needs nothing else. dist:win:installer needs Wine on Linux and macOS
(NSIS uses it for the uninstaller). On Windows it needs nothing extra.
The macOS app builds on a Mac only, with Xcode's command line tools
(xcode-select --install) instead of .NET:
npm run build:native:mac # the input helper → build/mac-input/Ciao.Input
npm run dist:mac # disk images → release/Ciao-<version>-{arm64,x64}.dmg
The Linux app is built on Linux, without Wine or .NET:
npm run build:kws:linux # the wake-word detector and FFI for Linux → build/kws
npm run dist:linux # → release/Ciao-<version>.AppImage and .deb
There the input helper is src/linux-input, plain Node run by
Electron, which speaks the same protocol as the Windows one.
Download Ciao-<version>.AppImage (any distribution) or Ciao-<version>.deb
(Debian, Ubuntu) from the
latest release.
chmod +x Ciao-*.AppImage && ./Ciao-*.AppImage # or: sudo apt install ./Ciao-*.deb
sudo usermod -aG input $USER # then sign out and back in
Ciao reads the keyboard and mouse from /dev/input, which works the same on X11
and Wayland, so you need to be in the input group. Without it Ciao says so
at start and only the wake word starts a dictation.
How it differs from Windows:
Keys are not hidden from other apps. The app in front sees the trigger key too, so the default is Right Ctrl alone. A middle-click trigger is off by default, because the middle click would also paste the selection under the pointer. During a dictation Esc is held back from the app in front on X11, but on Wayland it gets through.
X11 works fully: Ciao pastes with Ctrl+V (Ctrl+Shift+V in terminals) and only into the window that was in front when you finished.
Wayland gives no app a way to see which window is active, so the text is
pasted into whatever is in front when you finish, always with Ctrl+V. To type
it, Ciao needs a virtual keyboard through /dev/uinput; allow that once:
echo 'KERNEL=="uinput", GROUP="input", MODE="0660", OPTIONS+="static_node=uinput"' | sudo tee /etc/udev/rules.d/60-ciao-uinput.rules
sudo udevadm control --reload && sudo udevadm trigger /dev/uinput
Without it the text stays on the clipboard.
The Ciao windows run through XWayland on Wayland, so the card can stay at the
bottom of the screen above everything. On a Wayland desktop Ciao restarts
itself once at launch with --ozone-platform=x11; pass --ozone-platform=…
yourself to choose otherwise.
Learning corrected words reads the field through AT-SPI, the desktop's
accessibility bus. GTK and Qt apps answer it; Chromium, Electron and Firefox
only once accessibility is switched on, for example with GNOME's
gsettings set org.gnome.desktop.interface toolkit-accessibility true (or a
screen reader running). Where nothing answers, Ciao just offers no words. On
Wayland there is no window to go by, so Ciao stays with the app that was in
front at its first look and doesn't read Enter.
The tray icon needs AppIndicator support (on GNOME, the AppIndicator and
KStatusNotifierItem Support extension). Start when you sign in writes
~/.config/autostart/ciao.desktop.
Ciao takes the OpenAI key from the OPENAI_API_KEY environment variable and the
Gemini key from GEMINI_API_KEY. If that is not set, it reads openai-key.txt or
gemini-key.txt in the data folder. Saving a key in
Settings writes that file; the key field is under the service in use in
Settings → Who transcribes.
Set them in Settings → Keys.
On macOS, Option is Alt and Command is Win in these names, and the Fn key is
Fn.
In config.json these are:
triggers — +-separated modifiers (Ctrl, Alt, Shift, Win), then a
.NET Keys
name or MButton / XButton1 / XButton2.pasteLastHotkey — an
Electron accelerator.The Android app works like Wispr Flow: whenever the keyboard is open over a text field, the Ciao icon floats above it. Tap it and speak. The card at the top of the screen shows your words live, the same way as on the desktop (bright new words, paragraphs and lists). Tap the icon again, or say "ciao ciao" ("чао-чао"), and the text is typed into the field. It works with any keyboard; Ciao doesn't replace yours.
gpt-transcribe, or gemini-3.5-transcribe) transcribes the audio
from where the live text left off (with Gemini; with OpenAI, all of it). A long
Gemini dictation moves to a new connection before the old one runs out, as on
the desktop.
If that fails too (still offline), the card keeps the recording with a
Transcribe again button until you dismiss it.Ciao-<version>.apk from the
latest release on the phone
and open it. Allow installing apps from that source if Android asks.Terms are one per line, as on the desktop, and Sign in with Google syncs them, the context and the API keys (OpenAI and Gemini) with your other devices (Sync). Signing in needs Google Play services. The service picked stays per device, as on the desktop.
Open history lists every dictation by day, newest first, with the minutes dictated today and in total and a search, as the desktop's history window. Each shows how it went (pasted, on the clipboard, not transcribed, cancelled) and every text it has, labelled (live, with paragraphs, whole recording, live again); tap a text to copy it. Its buttons: Listen, Copy, More accurate (the file model), Live (the streaming model, at 4× with Gemini), Via OpenAI / Gemini with keys for both, Try with … after a failed retry, and Delete. A dictation that failed, or that you cancelled, stays there with its recording, so a dictation lost to a dropped connection can be transcribed again. The recordings of the newest 200 dictations are kept (about 3 MB a minute); older ones keep only their text. Entries from Ciao 0.8.3 keep their text as the live one. The history stays on the phone: it isn't synced.
Usage, at the top of the history, is the desktop's Usage tab for the phone's
dictations: the same periods, totals, chart and models (Usage.kt, a port of
src/core/usage.ts, and UsageActivity.kt).
While you dictate, a Ciao is listening notification shows with Done and Cancel: the dictation goes on with the screen off or locked, and you can finish it from the shade or the lock screen. With the shade pulled down, the bubble and the card step aside so they don't cover it. If Android kills Ciao mid-dictation (out of memory, say), what was recorded is kept: when Ciao comes back it transcribes it into the history and says so in a notification. Android 13+ asks once to allow notifications; without them the dictation still goes on with the screen off, just without the buttons.
The settings match the desktop's: Languages (OpenAI), Insert the text right away (off: the clipboard only), Restore the clipboard, Pause music while you dictate (Ciao takes the audio focus while recording: players pause and play on by themselves afterwards), Anonymous usage counts, the interface language and the theme (As the system, light, dark), and under For developers the delay level. Paste last is a quick-settings tile: add it once (pull the shade down twice, then the pencil); tapping it closes the shade and puts the last dictation into the field you were typing in, or onto the clipboard. What the desktop has and Android doesn't: the wake word, and what a phone has no use for (hotkeys, the card going see-through under the pointer, the pill between dictations, starting at sign-in: the service starts by itself).
The APK from GitHub updates itself from the same releases as the desktop app. Each time you open the Ciao screen (at most every 4 hours) it checks for a newer version; when there is one, a card at the top offers Update to X and What's new. To check by hand, tap Check for updates at the bottom of the screen.
Update downloads the APK and hands it to Android, which asks you to confirm and installs it over the old one; your settings stay. The first time, Android asks you to let Ciao install apps (Install unknown apps); turn it on and come back, and the update continues. Nothing is downloaded until you tap Update. Android only accepts an update signed with the same key, so a tampered APK is refused. Installing a newer APK by hand works too.
Sign in with Google (Settings → Sync on the desktop, Sync with Google on
Android) and your terms, context and API keys are the same on every device signed
in with that account: on a new device, signing in is all the setup. There is no Ciao server: they're kept in one file,
ciao-sync.json, in the hidden app folder of your own Google Drive. Ciao asks
only for that folder (drive.appdata, which can't see any other file in your
Drive) and your email address, to show which account is signed in. Google's page
lists Drive access as a box to tick, unticked at first: tick it, or Ciao says it
has no access.
OPENAI_API_KEY / GEMINI_API_KEY is never synced, only one saved in Settings.sync-state.json (desktop) or its preferences (Android).Privacy policy: sayciao.vercel.app/privacy.html.
The Google Cloud project is ciao-510802. The desktop app gets its OAuth client
at build time from CIAO_GOOGLE_CLIENT_ID and CIAO_GOOGLE_CLIENT_SECRET
(GitHub secrets in CI); built without them, it has no Sync card. The Android
app needs nothing: Google knows it by its package name and signing key, so a
build signed with another key can't sign in.
Everything is under %APPDATA%\Ciao on Windows, ~/Library/Application Support/Ciao
on macOS and ~/.config/Ciao on Linux:
state.json — whether Ciao is recording right now, and hands-free:
{"recording": true, "handsFree": true, "pid": 1234}, written whole whenever that
changes and set to not recording when Ciao quits. Other apps may read it
(TrackpadClicks makes a single click stop a hands-free dictation).config.json — settings. Most of them are edited in the app. An edit to the file
while Ciao runs is picked up within 2 s, instead of being overwritten by Ciao's next save.openai-key.txt, gemini-key.txt — the API keys.history\<id>\ — one folder per dictation, holding audio.wav and
entry.json (transcripts, timings, cost, where it was pasted). The <id>
starts with the UTC start time, YYYYMMDD-HHMMSS. The Folder button in the
history window opens it.ciao.log — the app log, with the warnings and errors of its windows (a crash or hang of one too).telemetry-id — the random install id for anonymous usage counts.google-account.json — the Google account for sync: its email and
refresh token, encrypted with the system keychain where there is one.sync-state.json — every term change this device knows, for merging.Recordings and transcripts stay on your machine. Audio leaves it only to be transcribed by OpenAI or Google, whichever you chose.
Ciao sends anonymous usage counts to PostHog (EU cloud), so
we can see how many people use it. Two events (Android sends the same ones, with
os android, trigger bubble and no target_app):
app_started: app version, OS version, CPU architecture, the transcription
service (provider: openai or gemini), and whether its API key is set and the
wake word and paragraph layout are on.dictation, once per dictation kept in the history:
outcome (pasted, clipboard, empty, failed, cancelled) and
transcribed_by (live; file when the live transcript failed and the saved
audio was sent instead; none when nothing was transcribed, as on Esc);trigger (the key or button that started it, such as
RControlKey or MButton, wake_word, or pill for a click on it), ended_by (release, press,
stop_phrase, escape, mic_error) and hands_free;provider (openai or gemini);duration_s, voice_onset_ms (not for wake-word starts, whose
audio begins with speech), first_text_ms, final_after_release_ms,
ready_after_release_ms (from the stop until the text was pasted or put on the
clipboard, or Ciao gave up with none: how long you waited, including the saved
audio sent again when the live transcript failed; not for cancelled dictations, nor
on the desktop for a microphone failure before the stop) and cost_usd;target_app, the kind of app it was meant for, from a
closed list in src/core/apps.ts (t3code, vscode,
terminal, browser, telegram, …). Any program not on the list is sent as
other, never by name. When the text could not be pasted,
paste_miss says why: focus-changed, no-target, auto-paste-off,
no-permission (macOS: Ciao has no Accessibility permission), not-t3,
no-field and not-in-field (T3 mode: T3 Code wasn't in front, no chat box was
focused, or the text didn't show up in it), or no-helper / timeout / when the input helper
failed. On and there is also (the app
in front instead, same list); on also and
(true when you switched to another
virtual desktop; absent when Windows can't tell). is true when
the window in front at the end was another one than at the start (the text
then goes to the one at the end), unless it is T3 Code that T3 mode brought
forward.Each event carries a random install id from telemetry-id in the data folder. It is
not derived from your machine or accounts. No text, audio, window titles, prompts,
terms or API keys are ever sent. PostHog keeps no person profiles for these events, does
not look up a location from your IP address, and the project discards IP addresses.
To turn it off, use Settings → Behavior → Anonymous usage stats (Настройки → Поведение → Анонимная статистика in Russian), or set
CIAO_TELEMETRY=0. An unpackaged dev run (electron .) sends nothing unless
CIAO_TELEMETRY=1. CIAO_POSTHOG_KEY and CIAO_POSTHOG_HOST
point it at your own PostHog project.
Every release ships all the apps under one version, the one in package.json
(the Android build reads it from there too):
main and wait for CI to pass.main, run npm version 0.5.0 -m "Release %s" && git push --follow-tags.
npm version bumps package.json and package-lock.json, commits
"Release 0.5.0" and tags v0.5.0; pushing the tag starts the release.release.yml creates a draft GitHub release, then
four jobs upload into it at the same time:
| Job | Runner | Files |
|---|---|---|
windows | Windows | Ciao-Setup-<version>.exe, its .blockmap, latest.yml |
mac | macOS | Ciao-<version>-{arm64,x64}.dmg, .zips, their .blockmaps, latest-mac.yml |
linux | Ubuntu | Ciao-<version>.AppImage, Ciao-<version>.deb, latest-linux.yml |
android | Ubuntu | Ciao-<version>.apk |
Installed desktop copies check latest.yml / latest-mac.yml / latest-linux.yml
of the latest published release, so the draft is published only after all four jobs
succeed.
If one fails, rerun the failed jobs: they fill the same draft.
v + the package.json version, or the workflow fails.CIAO_KEYSTORE_BASE64, CIAO_KEYSTORE_PASSWORD, CIAO_KEY_ALIAS,
CIAO_KEY_PASSWORD). Every release must be signed with that key, or Android
refuses to install it over the previous one.ci.yml builds the same installer, disk images,
AppImage, .deb and APK on every PR and attaches them to the run as artifacts. On
macOS it also checks that the input helper answers and that the packaged app is
signed and starts.
While Ciao is running, run Ciao.exe --replay=C:\path\to\clip.wav. The clip plays
through the live pipeline in real time: it shows in the card and is saved to the
history, but nothing is pasted. Make a compatible clip with:
ffmpeg -i input.m4a -ar 24000 -ac 1 -sample_fmt s16 clip.wav
mic ─► AudioWorklet (24 kHz PCM16, 40 ms chunks) overlay renderer
│
▼
main process ──► history/<id>/audio.wav written first, always
│
└──► OpenAI Realtime, gpt-live-transcribe, or Gemini Live,
gemini-3.5-transcribe-live (WebSocket, opened in advance)
│ transcript deltas ──► overlay card (live text)
│ on key release: commit ──► final transcript
▼
Ciao.Input ──► Ctrl+V (⌘V) into the window that was active at the start
gpt-transcribe (or gemini-3.5-transcribe) instead.Ciao.Input is a tiny native helper: .NET on Windows, Swift on macOS. It
installs a low-level keyboard and mouse hook (an event tap on macOS), so it can
see Right Ctrl being held and swallow Esc (the focused app never receives it).
It also reports the foreground window and injects Ctrl+V. It talks to the
Electron app over stdin/stdout, one JSON object per line; both helpers speak the
same protocol (src/main/input.ts).src/core/ — platform-neutral TypeScript with no Electron or Node imports: the
live transcription sessions (realtime.ts for OpenAI, gemini.ts for Gemini,
providers.ts for which models each uses, proxy.ts for reading a proxy address),
PCM/WAV helpers, prices, usage.ts (the Usage tab's sums), shared types,
and corrections.ts, which finds the words worth learning in a corrected text. It is
meant to be reused as-is by other shells (a T3 Code integration, mobile).
src/main/ — the Electron main process:
dictation.ts — the hotkey state machine and the pipeline;history.ts — storage and crash recovery;transcribe.ts — live and file transcription; proxy.ts — the proxy its
requests go through;paste.ts — clipboard-preserving paste;input.ts — talks to the keyboard and paste helper of the system;corrections.ts — watches the field a dictation went into for corrections;overlayWindow.ts — the window of the pill and the card;updater.ts — updates from GitHub Releases;sync.ts — Google sign-in and syncing through Drive; the merging is in
src/core/sync.ts.src/renderer/ — React 19 + Tailwind 4:
overlay/ — the pill, the live card, the word suggestions and microphone capture;history/ — the history, usage and settings window;mic.ts — the connected microphones and opening the one chosen in Settings.src/preload/ — the IPC bridge exposed to the renderer as window.ciao.
native/win-input/ — the Windows keyboard and paste helper (C#); it also reads
the focused field through UI Automation (FieldReader.cs) and pauses the music
through the system media sessions (Media.cs).
src/linux-input/ — the Linux one, with the same protocol: evdev for the keys,
X11 (libX11/libXtst through koffi) for the active window, bringing an app forward
(_NET_ACTIVE_WINDOW), Ctrl+V and Enter, uinput for
Ctrl+V on Wayland, AT-SPI (libatspi) for the focused field, and MPRIS over D-Bus
for pausing the music (media.ts). Electron runs it as plain Node.
npm run typecheck checks the desktop app. For Android you need JDK 17 and the
Android SDK (platform 35); cd android && ./gradlew assembleRelease builds
app/build/outputs/apk/release/Ciao-<version>-release.apk, and
./gradlew testReleaseUnitTest runs the tests. Without
android/keystore.properties (storeFile, storePassword, keyAlias,
keyPassword) the APK is signed with your debug key.
MIT
@ or / menu, dialog or command palette takes the
Enter, and a T3 Code window narrower than 640 px takes it as a new line (such a
window doesn't put focus back into the chat box either). ciao.log says
whether the box gave the text up (sent, not-sent, or enter when it couldn't
tell). On Linux, Ciao sees T3 Code's chat box only with desktop accessibility on
(see Linux); otherwise it never sends. It doesn't send there while you
hold a modifier key either. Not on Wayland;T3 Code doesn't tell other apps which chat is open (its window is always titled "T3 Code"), so Ciao goes by the window in front. On a Wayland desktop, where no app can tell which window is in front, it pastes without that check.
unknownfocus-changednot-t3switched_to_appfocus-changedtarget_closedtarget_on_other_desktoptarget_movednative/mac-input/ — the macOS one (Swift), same protocol: an event tap for
the keys, the frontmost app, bringing an app forward (Launch Services), ⌘V and
Return, the Accessibility API for the focused field, and
AppleScript to pause Music and Spotify.
assets/icon.svg — the icon. scripts/build-icons.sh renders the PNG, ICO and
tray icons from it; don't edit those by hand.
site/ — the landing page, sayciao.vercel.app, and
privacy.html, the privacy policy Google's sign-in screen links to: static
HTML, deployed with vercel deploy --prod from site/. Its card
demo is also the GIF at the top of this README: node site/record-demo.mjs
re-records assets/demo-*.gif.
android/ — the Android app (Kotlin; its dependencies are OkHttp and Google
Play services' sign-in):
CiaoService.kt — the accessibility service: the icon over the keyboard
(Spring.kt moves it), the dictation pipeline, typing into the field;CardView.kt — the live card;RecordingService.kt — the foreground service and notification of a
running dictation; Recovery.kt — transcribes dictations cut off by a kill;History.kt, HistoryActivity.kt — the history, stored as on the desktop
(src/main/history.ts), one folder per dictation, and its screen;
Usage.kt, UsageActivity.kt — the Usage screen, as src/core/usage.ts
and src/renderer/history/UsagePanel.tsx;
Telemetry.kt — the usage counts of src/main/telemetry.ts; Ui.kt — the
interface language and theme; PasteLastTile.kt — Paste last;
Retry.kt — its More accurate and Live, as retry in src/main/main.ts
and transcribeLive in src/main/transcribe.ts; Player.kt plays a recording;Realtime.kt, Gemini.kt, FileTranscriber.kt — the same OpenAI and
Gemini calls as src/core/realtime.ts, src/core/gemini.ts and
src/main/transcribe.ts; Proxy.kt — the port of src/core/proxy.ts and
src/main/proxy.ts, whose test holds the TypeScript outputs; Revision.kt — the port of src/core/revision.ts,
whose test holds its TypeScript outputs;Updater.kt — updates from GitHub Releases through Android's package
installer, like src/main/updater.ts;LiveLayout.kt, VoiceCommands.kt — ports of src/core/liveLayout.ts and
src/core/voiceCommands.ts. Their unit test holds the TypeScript outputs, so
change both together;Sync.kt — the port of src/core/sync.ts, with the same kind of test;
GoogleSync.kt — sign-in and the Drive calls of src/main/sync.ts;Corrections.kt — the port of src/core/corrections.ts, with the same kind
of test; SuggestionView.kt — the card offering a corrected word for the terms.