Skip to content
Agora-BuildPublic

About

Autonomous phone pick/call system: a CLI + daemon that drives a phone's calls, SMS, and scripted audio (ten-vad) over WiFi, with a USB sound-card audio bridge. macOS & Linux.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

DialF

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 --drop answers 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: voicemail on/off and raw mmi/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.

Install

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 | bash

Then 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 day

tail -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.

Upgrading

npm install -g @agora-build/dialf   # or re-run the curl installer
dialf service install [--user]      # repoint the service at the new binary, reload

The 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.

Build from source

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/bin

ten-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.

The phone app (DialF Phone)

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):

adb install dialf-phone-latest.apk     # or just open the .apk on the phone

Full UI/permissions reference: app/README.md.

Getting started (end-to-end)

Host (the computer running dialfd) and phone on the same WiFi. Minimal manual run:

  1. Install the CLI on the host (macOS/Linux): npm install -g @agora-build/dialf.
  2. Configure dialfd — create ~/.config/dialf/config.yaml with a shared_key and your sound-card devices (copy config.example.yaml; see Configuration).
  3. Start the daemon: dialf daemon (foreground), or install it as a service (above). On macOS use dialf service install --user if you need call recording.
  4. 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.
  5. Install & open DialF Phone (APK above). Grant phone/SMS/notification permissions, Allow the battery-optimization prompt, and Set it as the default dialer.
  6. 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.
  7. Verify on the host: dialf devices — the phone should appear.
  8. 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
  9. 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.)

The phone stays "disconnected"

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, dialfd uses avahi:

    OS Fix
    Ubuntu / Debian sudo apt install avahi-daemon avahi-utils && sudo systemctl enable --now avahi-daemon
    Arch sudo pacman -S avahi && sudo systemctl enable --now avahi-daemon
    Fedora sudo dnf install avahi-tools && sudo systemctl enable --now avahi-daemon
    NixOS services.avahi = { enable = true; openFirewall = true; publish = { enable = true; userServices = true; }; };

    Then restart dialfd — dialf service restart --user for a per-user service, sudo dialf service restart for the system one: dialfd advertises 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/udp
    firewalld (Fedora) sudo firewall-cmd --permanent --add-port=8765/tcp --add-service=mdns && sudo firewall-cmd --reload
    nftables (Arch) add tcp dport 8765 accept and udp dport 5353 accept to the input chain in /etc/nftables.conf
    macOS 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.

Commands

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).

Integrating DialF into your app

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 WAVs

What 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 to dialfd.
  • Its only protection is file permissions — whoever can open it can dial, text and run jobs.
  • One call at a time, and job.run answers only when the job ends. To cancel it, send job.cancel on 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) and server.manifest (which job steps this build supports).
  • The results are files. Every step reports t_start_ms/t_end_ms as 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.

Sharing a phone with another machine

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 4B2B1C

That 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 shell

Three 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.

No cable: wireless debugging

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: true

It 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.

How It Works

Two independent planes:

  • Control plane (WiFi): app ↔ dialfd over 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. dialfd owns 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.)

Scripted jobs (YAML)

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.wav

Reading 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.

Auto-answer inbound calls

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,+15559876

Notes: 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).

Sound-card bridge + recording

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 at audio.sample_rate / audio.channels — dialf never resamples or discards. (sox itself converts if you ask for a rate the card doesn't run; set sample_rate to 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 (when mix_recording: true): left = tx, right = rx, so the two voices stay separated. Swap with mix_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.

Audio tools (external, configurable)

dialfd shells out to whatever is available — no bound audio library:

  • Linux: arecord/aplay, or ffmpeg, or sox (rec/play)
  • macOS: ffmpeg or sox for capture; afplay/ffplay/play for 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.

Configuration

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 swaps

Paths — 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.

Export / import a setup

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 dialfd

export 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.

Development

cd server
cargo build
cargo test --workspace            # protocol, VAD, resample, tooling, job runner, formatters

Layout:

  • server/ — Rust workspace
    • crates/dialf/ — the dialf binary + 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 jobs
  • docs/ — INTEGRATION.md, PROTOCOL.md, HARDWARE.md, DEVICE_SHARING.md
  • config.example.yaml — sample daemon config (sound card + recording)

Release

Tag vX.Y.Z (via scripts/release.sh) triggers .github/workflows/release.yml.

License

MIT — see LICENSE.

About

Autonomous phone pick/call system: a CLI + daemon that drives a phone's calls, SMS, and scripted audio (ten-vad) over WiFi, with a USB sound-card audio bridge. macOS & Linux.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages