Autonomous phone pick/call system. The dialf CLI commands a mobile app (the phone, on its
SIM) to make / answer / reject real calls and send/receive SMS over WiFi, while call audio is
bridged through a USB sound card on the host, where dialfd runs scripted audio
conversations with voice-activity detection (ten-vad).
dialf (CLI) ──▶ dialfd (host daemon) ──WiFi──▶ mobile app ── places/answers call on SIM
│
└─ USB sound card ◀──physical──▶ phone headset jack (all call audio)
- Make / answer / hang up / reject calls —
reject --dropanswers then instantly hangs up so callers can't reach voicemail (when the carrier won't disable it). - Auto-answer an allow-list (
autoanswer); dual-SIM aware (sims,call dial --sim). - Read the call log, send & receive SMS.
- Carrier controls:
voicemailon/off and rawmmi/USSD (reply captured headlessly). - Scripted audio conversations (YAML + ten-vad), call recording, runtime audio injection.
- Works while the phone is locked; runs on macOS & Linux, arm64 & x86_64.
Building DialF into your own product? See Integrating DialF into your app below.
See docs/PROTOCOL.md for the wire protocol + control API,
docs/HARDWARE.md for the sound-card bridge wiring + macOS
microphone/LaunchAgent setup, docs/DEVICE_SHARING.md for using a
phone attached to another machine over adb, and app/README.md for the
phone app.
Prebuilt binaries (macOS arm64/x86_64, Linux x86_64/aarch64) ship on GitHub Releases. Both
installers just install the dialf CLI (onnxruntime + ten-vad model bundled) — they don't
start a service; you launch dialfd separately.
npm install -g @agora-build/dialf # or: curl -fsSL https://dl.agora.build/dialf/install.sh | bashThen launch the daemon — pick one:
dialf daemon # run in the foreground
sudo dialf service install # system service at boot — SHARED across all users (launchd/systemd)
dialf service install --user # per-user service at login — ISOLATED to you (no sudo)Isolated vs shared, by install scope. --user is your own private daemon (per-user control
socket — /run/user/$UID/dialfd.sock on Linux, /tmp/dialfd-$UID.sock on macOS). A system
install (no --user, needs sudo) is a single shared daemon any login user can drive: it binds
a machine-wide socket (/run/dialf/dialfd.sock on Linux, /var/run/dialfd.sock on macOS) owned by
the auto-created dialf group, mode 0660. Grant a user access by adding them to the group
(sudo usermod -aG dialf <user>; macOS sudo dseditgroup -o edit -a <user> -t user dialf) — the
control socket lets its holder dial, hang up, SMS, and run jobs, so keep the group to trusted users.
The dialf CLI auto-picks your own daemon if you have one, else the shared one.
macOS + audio recording: a system (root) service can't access the microphone, so for
recording on macOS run the user service: dialf service install --user (details in
docs/HARDWARE.md). On Linux a system service records fine (no TCC gate).
macOS + tmux: where to run what. macOS decides microphone and Local Network permission by who started the process that needs them — and a process started from tmux (or screen, or over SSH) never gets them, silently. So:
| Run from a plain Terminal window | Run from anywhere, tmux included |
|---|---|
dialf service install/start/stop --user — starts the daemon, which records |
dialf run, devices, call, sms, play |
upgrading (npm install -g … then service install) |
dialf devices share … (the share runs in the daemon) |
the first adb command when no adb server is running — it starts the server that adb_share.autoconnect connects through |
every other adb command, once a server is running |
The right-hand commands are clients talking to the daemon over its local socket, which macOS
doesn't gate. If you started the adb server from tmux by accident, dialf devices shows
blocked_local_network; adb kill-server, then any adb command from Terminal, fixes it.
Manage the service (launchd on macOS / systemd on Linux):
dialf service status|stop|start|uninstall [--user](The curl installer is install-only by default; DIALF_SERVICE=system|user makes it also
install that service.)
Logs. dialfd writes a daily-rotated plain-text log (last 7 days kept), however it's
launched — foreground dialf daemon shows colored logs live and writes the file. The date in
the name is UTC, so the file rolls over at UTC midnight, not local midnight — use
date -u — outside UTC the plain $(date +%F) names the wrong day's file for part of
every day:
# macOS: ~/Library/Logs/dialfd.<UTC date>.log (system service: /var/log/dialfd.<UTC date>.log)
# Linux: ~/.local/state/dialf/dialfd.<UTC date>.log
tail -f ~/Library/Logs/dialfd.$(date -u +%F).log
ls -t ~/.local/state/dialf/dialfd.*.log | head -1 # Linux: the current file, any time of daytail -f (or -F) stays on the old file after a rollover — the new day's file has a
different name; re-run it with the new date to keep following.
npm install -g @agora-build/dialf # or re-run the curl installer
dialf service install [--user] # repoint the service at the new binary, reloadThe new binary installs at a versioned path, so re-run dialf service install after an
upgrade to point the service at it and reload (idempotent — no uninstall needed). On
macOS, the daemon is unsigned, so the OS re-prompts for the Microphone the first
time the upgraded daemon records — Allow it. (A silent empty rx.wav is the tell that
the mic grant is missing; see docs/HARDWARE.md. Do the upgrade from a plain Terminal, not tmux — see
macOS + tmux above — or the prompt never appears.) The phone reconnects
on its own a few seconds after the reload.
git submodule update --init --recursive # ten-vad lives at third_party/ten-vad
cd server
cargo build --release
cargo install --path crates/dialf # puts `dialf` in ~/.cargo/binten-vad is always compiled from source (the ONNX variant). build.rs auto-downloads the
matching onnxruntime into $CARGO_HOME/ten-vad-ort/ (one-time, needs network); set ORT_ROOT
(a dir with include/ + lib/) to use your own / build offline. The model loads via
$TEN_VAD_MODEL, defaulting to the submodule's src/onnx_model/ten-vad.onnx. Works on any
platform/arch, including Linux aarch64 / Raspberry Pi.
DialF Phone is the Android side — it is the phone DialF controls (on its own SIM). It
carries no call audio; it executes commands from dialfd and routes call audio to the
wired headset so the host's USB sound card becomes the call mic/earpiece.
- Default dialer (
ROLE_DIALER+InCallService) → programmatic dial / answer / hang up / reject. - Dual-SIM aware — per-call SIM selection, SIM list with the default tagged.
- SMS send + live inbox forwarding, call log, carrier voicemail on/off, raw MMI/USSD.
- Native foreground service — works while the phone is locked, auto-(re)starts on boot / power / network changes, and is Doze-exempt so commands run while it's asleep.
Install the APK (Android 9+, sideload — debug-signed):
- Newest build: https://dl.agora.build/dialf/dialf-phone-latest.apk
- All versions: https://github.com/Agora-Build/DialF/releases
adb install dialf-phone-latest.apk # or just open the .apk on the phoneFull UI/permissions reference: app/README.md.
Host (the computer running dialfd) and phone on the same WiFi. Minimal manual run:
- Install the CLI on the host (macOS/Linux):
npm install -g @agora-build/dialf. - Configure
dialfd— create~/.config/dialf/config.yamlwith ashared_keyand your sound-card devices (copyconfig.example.yaml; see Configuration). - Start the daemon:
dialf daemon(foreground), or install it as a service (above). On macOS usedialf service install --userif you need call recording. - Wire the sound card to the phone's headset jack (
docs/HARDWARE.md). Skip this if you only need call control / SMS and no audio. - Install & open DialF Phone (APK above). Grant phone/SMS/notification permissions, Allow the battery-optimization prompt, and Set it as the default dialer.
- Pair them: in the app enter the same shared key; leave the dialfd address blank to
auto-discover over mDNS (or pin
host:port). Tap Start service. - Verify on the host:
dialf devices— the phone should appear. - Drive it:
dialf sims <device> # which SIMs are in the phone dialf call dial <device> +15551234 # place a call dialf sms send <device> +15551234 "hi" # send a text dialf call list <device> --human # read the call log
- Run a scripted call (needs the audio bridge):
dialf run server/jobs/outbound-call.yaml --device <device>.
(<device> is the id from dialf devices; omit --device/<device> when exactly one phone
is connected.)
When no phone is connected, dialf devices --human (or plain dialf devices in a terminal) prints
what on the host is keeping it out, with the fix for your OS. The same diagnosis is logged when
dialfd starts. The host needs two things dialf cannot grant itself:
-
An mDNS responder, so the phone can discover
dialfd. macOS has Bonjour built in. On Linux,dialfduses avahi:OS Fix Ubuntu / Debian sudo apt install avahi-daemon avahi-utils && sudo systemctl enable --now avahi-daemonArch sudo pacman -S avahi && sudo systemctl enable --now avahi-daemonFedora sudo dnf install avahi-tools && sudo systemctl enable --now avahi-daemonNixOS services.avahi = { enable = true; openFirewall = true; publish = { enable = true; userServices = true; }; };Then restart dialfd —
dialf service restart --userfor a per-user service,sudo dialf service restartfor the system one:dialfdadvertises only when it starts. -
A firewall that lets the phone in on TCP 8765 (
ws_bind) and UDP 5353 (mDNS):Firewall Fix NixOS (on by default) networking.firewall.allowedTCPPorts = [ 8765 ];(the avahi block above opens 5353)ufw (Ubuntu) sudo ufw allow 8765/tcp && sudo ufw allow 5353/udpfirewalld (Fedora) sudo firewall-cmd --permanent --add-port=8765/tcp --add-service=mdns && sudo firewall-cmd --reloadnftables (Arch) add tcp dport 8765 acceptandudp dport 5353 acceptto the input chain in/etc/nftables.confmacOS application firewall sudo /usr/libexec/ApplicationFirewall/socketfilterfw --add <dialf> --unblockapp <dialf>(the path is versioned, so repeat after upgrades)
Beyond the host: a guest network or a router with client/AP isolation blocks phone-to-computer
traffic. To skip discovery entirely, type <host LAN IP>:8765 into the app's dialfd address
field.
Start the daemon (or install it as a service, above), then drive it with dialf:
dialf daemon # run dialfd (control socket + WS + mDNS)
dialf devices # list connected phones (add --human for a readable list)
dialf sims <device> # list SIMs (slot/number/carrier, default tagged)
dialf call dial <device> <number> [--sim N] # place a call (default SIM if --sim omitted)
dialf call answer <device> # answer the ringing call
dialf call hangup <device> # end the active call
dialf call reject <device> [--drop] # decline ringing call (--drop = answer+hangup)
dialf call list <device> [--human] # read the call log (JSON, or --human)
dialf sms send <device> <to> <body> # send a text
dialf sms list <device> [--human] # read recent texts (JSON, or --human)
dialf voicemail off <device> [--sim N] # disable carrier voicemail
dialf voicemail on <device> [--number <vm#>] [--sim N] # re-enable
dialf mmi <device> <code> [--sim N] # (advanced) raw MMI/USSD code, returns the reply
dialf run <job.yaml> [--device <id>] [--name <label>] # run a scripted job once
dialf run <job.yaml> --autoanswer <numbers> # serve: answer those numbers with this job (Ctrl-C reverts)
dialf play <file> # inject audio out the sound card
dialf manifest # which job steps this build implements
dialf --version # CLI + running daemon (dialfd) versions
dialf devices share --list # adb devices attached to this host
dialf devices share --target <serial> [--token] [--expire-after N] [--forward-port P]
dialf devices share --status | --stop # what's shared / close it
dialf devices connect <host> --token <dvs_…> # reach a token-protected share (runs until Ctrl-C)<device> is the id the phone registered as (see dialf devices). dialf talks to dialfd
over a local control socket, so it must run on the same host. --human formats
times/numbers/durations; omit it for JSON (scriptable).
Use the control socket. It is the same interface the dialf CLI uses, so anything the CLI
can do, your program can do — with structured JSON in and out instead of text to parse.
| Approach | Good for | Why not always |
|---|---|---|
| Control socket (recommended) | services, test harnesses, eval pipelines | needs a few lines of client code |
Run the dialf CLI from your code |
shell scripts, cron | output is written for people; no cancel, no streaming |
autoanswer: in config.yaml |
a fixed inbound behaviour, no code at all | static: one job per number |
The socket speaks one JSON object per line each way. This is a complete client:
import json, os, socket
sock = socket.socket(socket.AF_UNIX)
sock.connect(f"/tmp/dialfd-{os.getuid()}.sock") # the per-user daemon on macOS
lines = sock.makefile("rb")
def call(op, **fields):
sock.sendall(json.dumps({"id": "1", "op": op, **fields}).encode() + b"\n")
resp = json.loads(lines.readline())
if not resp.get("ok"):
raise RuntimeError(resp.get("error"))
return resp.get("data")
print(call("server.info")) # {'version': '0.3.10', 'microphone': 'authorized', ...}
result = call("job.run", name="demo", steps=[
{"type": "call.dial", "number": "+15551234"},
{"type": "call.wait_answered", "timeout_ms": 30000},
{"type": "audio.play", "file": "/abs/path/prompt.wav"},
{"type": "audio.wait_for_speech", "end_timeout_ms": 30000},
{"type": "call.hangup"},
])
# result["steps"]: each step's timing and end_reason; result["recording"]: the rx/tx/mix WAVsWhat to know before you build on it:
- It is local. A Unix socket on the machine running
dialfd, because the call audio is a cable into that machine. A remote product runs a small service next todialfd. - Its only protection is file permissions — whoever can open it can dial, text and run jobs.
- One call at a time, and
job.runanswers only when the job ends. To cancel it, sendjob.cancelon a second connection: the first one is busy waiting. - Check at startup, not mid-call:
server.info(version, and on macOS whether the microphone is granted) andserver.manifest(which job steps this build supports). - The results are files. Every step reports
t_start_ms/t_end_msas offsets into the recordings, so you can cut the audio per step without matching clocks.
The full guide — finding the socket in every install mode, inbound calls, every op and error,
deployment: docs/INTEGRATION.md. Field reference:
docs/PROTOCOL.md.
The phone is plugged into machine A; you want adb on machine B. dialf devices share
exposes A's adb server so B can run adb shell, install, logcat, push/pull as if the
phone were plugged into B. Useful on a LAN or a Netbird/Tailscale/VPN overlay.
# on A — what can I share?
dialf devices share --list
# 4B2B1C device Pixel 9 Pro
# on A — share it
dialf devices share --target 4B2B1CThat prints where to connect, when the share expires, and a warning if it's open. Then on B:
export ADB_SERVER_SOCKET=tcp:<A-address>:5939
adb devices # the phone, as if it were local
adb shellThree things to know before you use it:
The default share is open. It binds 0.0.0.0:5939, so anyone who can reach that port can
install apps, read /sdcard and open a shell on the phone. That's fine on an overlay or a
trusted LAN — the network is the boundary — and not on one you don't control. Add --token
for a one-time secret (printed once, never stored); B then uses dialf devices connect
instead, because adb has no way to authenticate by itself. Or bind loopback and reach it
with ssh -L, which is the only option that also encrypts the session.
Shares expire — one hour by default, --expire-after 0 to disable (it warns). Expiry
closes in-flight connections too, not just the port.
Sharing never guesses. With no --target and no --all it errors and lists what's
attached, so a host that gains a second phone won't start sharing it silently. Scope is
enforced per connection, not advertised: a device you didn't share can't be reached even with
a valid token, and adb kill-server from B can't stop A's adb server.
scrcpy, flutter run and Android Studio need one extra flag. They tunnel through
adb forward, which opens its socket on the sharing host, so the tool on the other machine
finds nothing to connect to. Share the range scrcpy picks from, then on B:
# on A
dialf devices share --target <serial> --forward-port 27183-27199
# on B
export ADB_SERVER_SOCKET=tcp:<A-address>:5939 # scrcpy has no -H flag
scrcpy --tunnel-host=<A-address>ADB_SERVER_SOCKET points adb-based tools at the share; without it they talk to B's own adb
server and report no devices. Plain adb needs neither.
Full walkthrough, including the Netbird setup and the security trade-offs:
docs/DEVICE_SHARING.md.
The phone app reports its wireless-debugging port to dialfd, so dialf devices shows where
adb can reach it — no reading the port off the phone's screen, which changes every time
wireless debugging is switched on:
pixel-9-pro-9657 Pixel 9 Pro 192.168.100.179 seen 1s ago idle
adb: 192.168.100.179:41195 connected
When adb connected the phone by itself (it does after pairing), the line ends with
(adb -s <name>) — the serial to pass to adb -s or dialf devices share --target.
Turn on adb_share.autoconnect and dialfd keeps the phone connected in this host's adb server,
reconnecting after drops and port changes — so adb, scrcpy and dialf devices share all work
with no cable at all:
adb_share:
autoconnect: trueIt is independent of sharing: useful on its own for local adb, and a share started later
simply finds the phone already there. You pair each host with the phone once (Wireless
debugging → Pair device with pairing code, then adb pair <ip>:<port>); dialf devices says so
when that's what's missing. Setup, every state, and the caveats (a share by ip:port does not
follow a port change): docs/DEVICE_SHARING.md.
macOS: connecting needs Local Network permission for whichever process owns the adb
server. Start the service (or the adb server) from a plain Terminal window, not tmux — under
tmux macOS never grants it, and adb connect fails with No route to host even though the
network is fine. dialf devices reports this as blocked_local_network.
Two independent planes:
- Control plane (WiFi): app ↔
dialfdover WebSocket (mDNS discovery + shared key) — dial / answer / reject / SMS / call-log / heartbeat. No audio. The app runs a native foreground service, so it works while the phone is locked and resumes on reboot. - Audio plane (physical): phone headset jack ↔ USB sound card on the host.
dialfdowns the audio engine, VAD, recording, and the YAML job runner. (Android blocks apps from capturing cellular-call audio, so audio is bridged physically — never over WiFi.)
A job is a list of steps run in order. Examples in server/jobs/:
sample.yaml (answer + two-turn exchange), outbound-call.yaml (dial → greet → Q&A → SMS →
hangup), inbound-call.yaml (auto-answer conversation), live-call-pilot.yaml
(answer → play → wait → text → hangup), record-only.yaml (record the sound card only — no
call; any app's audio), blackhole.yaml (evaluate an AI agent over BlackHole virtual audio — no
phone, no card; audio-only jobs run with no device connected).
- type: call.dial # also: call.answer, call.hangup
number: "5551234"
- type: call.wait_answered # block until the callee actually answers
timeout_ms: 30000
- type: audio.play
file: samples/prompt-en-1.wav
- type: audio.wait_for_speech
end_timeout_ms: 45000 # hard cap waiting for the turn to end
silence_duration_ms: 3000 # trailing silence that marks end-of-turn
onset_duration_ms: 100 # sustained voice needed to count as speech (debounces noise)
- type: sms.send { to: "5551234", body: "thanks!" }
- type: wait { ms: 1000 }
- type: log { message: "done" }call.wait_answered waits for the outbound call to reach the answered (active) state, so
prompts play only after a real answer (not on a fixed timer). audio.wait_for_speech captures
from the card → resamples to 16 kHz → runs ten-vad per 256-sample hop; speech onset (a
continuous onset_duration_ms voiced run, so noise/echo doesn't false-trigger) followed by
silence_duration_ms of non-speech ends the turn (end_timeout_ms is the overall cap).
Interrupting the far end. audio.wait_for_speech_start is the mirror image: it returns
while the other side is still talking, so the next audio.play cuts in rather than replies.
A timeout here is not a failure — the step reports it and the job carries on.
- type: audio.wait_for_speech_start
timeout_ms: 15000 # give up if it never starts speaking
wait_after_start_ms: 2000 # let it talk this long, then return mid-sentence
- type: audio.play # lands on top of it
file: samples/interrupt.wavReading the result. dialf run reports each step with t_start_ms/t_end_ms relative
to the start of the recording, an end_reason, and any id you put on the step — so a
recording can be segmented step by step without correlating clocks. Alongside them the result
carries the recording paths and the call's disposition (answer latency, duration, how it
ended). dialf manifest prints which steps this build implements, for callers that generate
jobs. Full shapes in docs/PROTOCOL.md.
dialfd can answer incoming calls and run a job in response. In auto-answer mode the daemon
answers the call itself, so the job is just the conversation — call.dial /
call.wait_answered / call.answer are skipped (with a warning) if present, so a job can be
shared with normal outbound use (see server/jobs/inbound-call.yaml). Two ways to wire which
numbers it answers:
# Persistent — in config.yaml. Number → optional job path; null = answer only.
autoanswer:
"+15551234": jobs/inbound-call.yaml # answer, then run this job
"+15559876": # answer only (no job)# Ad-hoc — no config edit, no file change. Foreground "serve": answers these numbers with
# this job (overriding config.yaml) and reverts when you press Ctrl-C.
dialf run server/jobs/inbound-call.yaml --autoanswer +15551234,+15559876Notes: one phone + one sound card, so DialF runs one call at a time — a second call that
arrives while one is in progress is skipped and logged (it rings/goes to voicemail; only a
brief call-waiting beep, if any, bleeds into the active recording). For the same reason only
one dialf run --autoanswer session may run at a time; a second is refused. If the
configured job file fails to load, the call is still answered (plain answer, logged).
A USB sound card bridges the phone and host: card output → phone mic (inject prompts),
card input ← phone earpiece (capture the far end). A recorded job writes (paths returned
by dialf run):
<job>-rx.wav— captured from the card (the phone / far end), written exactly as the capture tool delivers it ataudio.sample_rate/audio.channels— dialf never resamples or discards. (sox itself converts if you ask for a rate the card doesn't run; setsample_rateto the card's own rate to skip that.)<job>-tx.wav— audio injected into the card (our prompts), same rate/channels as rx so the two legs share one clock<job>-mix.wav— stereo (whenmix_recording: true): left = tx, right = rx, so the two voices stay separated. Swap withmix_channels: rx_tx.
Recording is full-duplex on a single clock — rx records continuously for the whole job
(including during playback and wait gaps), tx carries each prompt at its true offset, and
all three files are the same length and sample-aligned. That makes them usable for latency
measurement (cross-correlate tx vs rx); see docs/HARDWARE.md.
List ALSA cards with arecord -l (Linux). On macOS, capturing needs Microphone permission
for the host app; Linux/ALSA has no such gate.
dialfd shells out to whatever is available — no bound audio library:
- Linux:
arecord/aplay, orffmpeg, orsox(rec/play) - macOS:
ffmpegorsoxfor capture;afplay/ffplay/playfor playback
Auto-detected via PATH; override with audio.capture_cmd / audio.playback_cmd. Capture
must emit raw little-endian s16 PCM on stdout, interleaved at audio.channels (mono by
default). Speech detection always runs on 16 kHz mono — the daemon derives that internally
from the call channel, so recording fidelity and VAD are independent.
dialfd reads ~/.config/dialf/config.yaml (override with dialf daemon --config <path>, or
bake it into the service with dialf service install --config <path> — --config is a daemon
option; client commands like dialf devices/run don't take it):
shared_key: change-me # must match the app's shared key
ws_bind: "0.0.0.0:8765" # phone WebSocket server bind
instance_name: dialfd # mDNS instance name
autoanswer: # inbound routing: number → optional job path
"+15551234": jobs/inbound-call.yaml # answer, then run this job
"+15559876": # answer only
audio:
capture_device: "plughw:1,0" # macOS: the CoreAudio device name, e.g. "USB Audio Device"
playback_device: "plughw:1,0"
record_dir: /var/lib/dialf/recordings
mix_recording: true
mix_channels: tx_rx # mix.wav: left=tx / right=rx (default); rx_tx swapsPaths — absolute or relative. File paths in the config (autoanswer job paths, record_dir)
may be absolute, or relative — in which case they resolve against the config file's own
directory, not the daemon's working directory (the service runs with cwd=/). Relative paths
inside a job file (e.g. audio.play) resolve against that job file's directory. So a config,
its jobs, and its recordings can live in one folder and move together. dialf daemon --config and
dialf run <job> both accept absolute or relative paths (relative to your shell's CWD). An explicit
--config that doesn't exist is an error (rather than silently falling back to defaults). macOS and
Linux behave identically.
The app's shared key / device id / optional fixed dialfd address are set in its UI — see
app/README.md.
Move a working setup (job scripts + audio samples + the config) to another machine as one zip:
dialf export ~/Dev/myEval # -> myEval.dialf.zip; prints the zip's contents
dialf import ~/Dev/myEval [--override] # an unzipped bundle folder; installs config + restarts dialfdexport bundles the folder's YAML scripts and the samples they reference, plus the active
config.yaml (--config to pick another; a config.yaml inside the folder wins by default) —
recordings are excluded (per the config's audio.record_dir). Paths in the bundled config
are rewritten relative so the zip is self-contained; jobs/samples referenced from outside the
folder are pulled in under scripts//samples/.
The bundled config stays byte-faithful to yours — same fields, order, quoting, and comments;
only the path values are rewritten (relative, so the bundle is position-independent). That
means a bundle can also be activated without dialf import: just unzip its contents
straight into ~/.config/dialf/ and restart dialfd — everything resolves relative to the
config's own directory.
import takes a bundle folder or a bundle .zip — a zip is extracted flat into the
current folder (the zip's own top-level folder, if any, is stripped; existing files are
never overwritten; password-protected zips are refused with instructions). It validates the
bundle and installs its config.yaml to the default config path with paths rewritten to
point at that folder — the folder stays in place as the live setup. An existing config is
only replaced after a [y/N] confirmation — Enter keeps it — or pass --override to replace
directly without the prompt. Either way the old config is kept as a timestamped backup
(config.yaml.bak.YYYYMMDD-HHMMSS) — earlier backups are never overwritten. It then restarts the installed dialfd service (or tells you to restart a
foreground dialf daemon / use sudo for a system service) and confirms via the daemon which
config is now active. --no-restart skips that. Host-specific settings generally travel as-is
(except the SoX CoreAudio-to-ALSA migration described below) — check the printed warnings for
tools/devices (audio.capture_cmd, device names) that don't exist on the new machine.
On a terminal, import runs an interactive host precheck instead of just warning: it
prompts to replace a missing/empty/change-me shared key, detects this machine's audio devices
(system_profiler on macOS, /proc/asound on Linux), and lets you pick replacements for device
names that don't exist here. It also confirms record_dir, translates SoX commands from
CoreAudio to ALSA when importing a macOS config on Linux, fixes a pinned tool path that moved
(e.g. /opt/homebrew vs /usr/local sox), and offers to install a missing tool (brew install sox on macOS; apt-get/dnf/pacman/zypper on Linux). Missing BlackHole virtual devices get
the matching brew install blackhole-* hint. Enter always keeps the current value; piped/non-TTY
runs skip the prompts and warn when the shared key is still a placeholder.
cd server
cargo build
cargo test --workspace # protocol, VAD, resample, tooling, job runner, formattersLayout:
server/— Rust workspacecrates/dialf/— thedialfbinary + library (CLI, protocol, audio engine, jobs)crates/ten-vad-sys/— FFI bindings to ten-vad (built from source)jobs/— sample jobs
app/— Flutter + Kotlin phone app (app/README.md)samples/— ready-to-use voice prompts for the sample jobsdocs/—INTEGRATION.md,PROTOCOL.md,HARDWARE.md,DEVICE_SHARING.mdconfig.example.yaml— sample daemon config (sound card + recording)
Tag vX.Y.Z (via scripts/release.sh) triggers .github/workflows/release.yml.
MIT — see LICENSE.