Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ derives, entirely or substantially, from the functionality of the Software. Any
license notice or attribution required by the License must also include this
Commons Clause License Condition notice.

Software: Hermes Phone
Software: Dialtone
License: MIT
Licensor: JAN Labs

Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

Dialtone is a framework-agnostic VoIP/IVR service that connects any AI agent to a real phone number via Twilio. Your agent handles incoming calls, makes outbound calls, manages voicemails, and more — with pluggable STT, TTS, and LLM backends. Works on macOS, Linux, and Windows/WSL2.

> **Offline by default** — a fresh install runs a local LLM (Ollama, auto-sized to your Mac's RAM) plus local voice (mlx-whisper STT + Kokoro TTS on Apple Silicon), so you need **no API keys except Twilio**. Point `AGENT_PROVIDER` at Hermes Gateway or any cloud provider whenever you like.

---

## Built on Hermes Agent
Expand All @@ -29,7 +31,7 @@ HERMES_GATEWAY_TOKEN=your-hermes-key
HERMES_MODEL_OVERRIDE= # empty = use agent's default model
```

When `AGENT_PROVIDER` is set to `hermes-gateway` or left on `auto`, Dialtone automatically detects and connects to a running Hermes Gateway instance.
Set `AGENT_PROVIDER=hermes-gateway` (or `auto`, which tries Hermes first) and Dialtone detects and connects to a running Hermes Gateway instance.

**Not a Hermes user?** No problem — Dialtone works with any framework. Read on.

Expand Down Expand Up @@ -75,7 +77,7 @@ Dialtone is framework-agnostic. Plug in any AI agent or LLM:
| **Xiaomi MiMo** | `xiaomi` | MiMo 2.5 reasoning model, free tier available |
| **Any OpenAI-compat** | `openai-compat` | Set base URL + API key + model name |

**Auto-detection:** When `AGENT_PROVIDER=auto` (default), Dialtone tries Hermes Gateway first, then falls back to direct LLM calls, then a no-op fallback.
**Auto-detection:** When `AGENT_PROVIDER=auto`, Dialtone tries Hermes Gateway first, then falls back to direct LLM calls, then a no-op fallback. (The out-of-the-box default is `ollama` — fully offline, no API key.)

**How it works:** Each backend implements the same `AgentBackend` interface:
- `health_check()` — is the backend reachable?
Expand Down Expand Up @@ -144,10 +146,10 @@ Adding a new backend? Create a file in `agents/` and register it in the factory.
- Export: ZIP (audio + metadata + transcripts) or plain text

**Web Dashboard (port 5051)**
- Dark theme, mobile-friendly design
- Light & dark theme, mobile-friendly design
- Stats overview (total voicemails, recent activity)
- Voicemail manager with playback and transcription
- Outbound call form
- Phone dialpad for outbound calls
- Full settings panel — all `.env` options configurable via UI
- Provider discovery: browse and install STT/TTS/LLM providers
- Model discovery: fetch available models from Hermes, Ollama, LM Studio, OpenRouter
Expand Down Expand Up @@ -205,13 +207,13 @@ Install under WSL2 using the Linux instructions above. Access the web dashboard

## Configuration

All settings live in `~/.hermes/phone-agent/.env` and can be changed via the web dashboard (`/settings.html`) or the macOS native settings panel.
All settings live in `~/.hermes-phone/.env` and can be changed via the web dashboard (`/settings.html`) or the macOS native settings panel.

### Agent Backend

```bash
# Choose your backend (default: auto-detect)
AGENT_PROVIDER=auto # auto | hermes-gateway | openai | openrouter | ollama | lmstudio | xiaomi | openai-compat
# Choose your backend (default: ollama — fully offline, no API key)
AGENT_PROVIDER=ollama # ollama | hermes-gateway | openai | openrouter | lmstudio | xiaomi | openai-compat | auto

# Hermes Gateway
HERMES_GATEWAY_URL=http://127.0.0.1:8642
Expand Down
16 changes: 8 additions & 8 deletions menubar.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Hermes Phone — macOS Menu Bar App
Dialtone — macOS Menu Bar App

Single phone icon that changes color:
🟢 = server running
Expand Down Expand Up @@ -95,7 +95,7 @@ class PhoneMenuBar(rumps.App):
def __init__(self):
# Start with red icon (not running)
icon = ICON_RED if Path(ICON_RED).exists() else None
super().__init__(name="Hermes Phone", title="", icon=icon, quit_button=None)
super().__init__(name="Dialtone", title="", icon=icon, quit_button=None)
self.running = False
self.voicemails = []
self.health_data = {}
Expand Down Expand Up @@ -174,7 +174,7 @@ def start_server(self, _):
try:
r = requests.get(HEALTH_URL, timeout=2)
if r.status_code == 200:
rumps.notification("Hermes Phone", "", "Server already running")
rumps.notification("Dialtone", "", "Server already running")
return
except:
pass
Expand All @@ -188,11 +188,11 @@ def start_server(self, _):
stdout=log,
stderr=log,
)
rumps.notification("Hermes Phone", "", "Server starting...")
rumps.notification("Dialtone", "", "Server starting...")

def stop_server(self, _):
subprocess.run(["pkill", "-f", "server.py"], capture_output=True)
rumps.notification("Hermes Phone", "", "Server stopped")
rumps.notification("Dialtone", "", "Server stopped")

def restart_server(self, _):
self.stop_server(_)
Expand All @@ -213,11 +213,11 @@ def make_call(self, _):
try:
r = requests.post(CALL_URL, json={"to": response.text}, headers=api_headers(), timeout=10)
if r.status_code == 200:
rumps.notification("Hermes Phone", "", f"Calling {response.text}...")
rumps.notification("Dialtone", "", f"Calling {response.text}...")
else:
rumps.notification("Hermes Phone", "", f"Call failed: {r.json().get('error', 'unknown')}")
rumps.notification("Dialtone", "", f"Call failed: {r.json().get('error', 'unknown')}")
except Exception as e:
rumps.notification("Hermes Phone", "", f"Call failed: {e}")
rumps.notification("Dialtone", "", f"Call failed: {e}")

def open_settings(self, _):
"""Open native macOS settings window."""
Expand Down
8 changes: 4 additions & 4 deletions server.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""
Hermes Phone — AI-powered VoIP server for macOS.
Dialtone — AI-powered VoIP server for macOS.

Architecture:
Twilio (audio) → WebSocket → Deepgram (STT) → Hermes Agent (LLM + tools + memory) → TTS → Twilio
Expand Down Expand Up @@ -267,7 +267,7 @@ def get_auth_headers():
LOGIN_HTML = """<!DOCTYPE html>
<html lang="en"><head>
<meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>📞 Hermes Phone — Login</title>
<title>📞 Dialtone — Login</title>
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;background:#0a0a0a;color:#e0e0e0;min-height:100vh;display:flex;align-items:center;justify-content:center}
Expand All @@ -284,7 +284,7 @@ def get_auth_headers():
.hint{text-align:center;color:#555;font-size:12px;margin-top:16px}
</style></head><body>
<div class="login-card">
<h1>📞 Hermes Phone</h1>
<h1>📞 Dialtone</h1>
<div class="sub">Enter your dashboard token</div>
<div class="error" id="error">Invalid token</div>
<form onsubmit="return doLogin(event)">
Expand Down Expand Up @@ -1207,7 +1207,7 @@ def export_zip():
@dashboard_app.route("/export/transcripts", methods=["GET"])
def export_transcripts():
voicemails = load_voicemails()
lines = [f"Hermes Phone — Voicemail Transcripts", f"Exported: {datetime.now().isoformat()}", "=" * 50, ""]
lines = [f"Dialtone — Voicemail Transcripts", f"Exported: {datetime.now().isoformat()}", "=" * 50, ""]
for vm in voicemails:
caller = vm.get("from", "unknown").replace("+", "")
lines.append(f"From: {caller}")
Expand Down
Loading