diff --git a/LICENSE b/LICENSE index c81a53e..c92664c 100644 --- a/LICENSE +++ b/LICENSE @@ -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 diff --git a/README.md b/README.md index d2b8ffc..6181bb9 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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? @@ -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 @@ -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 diff --git a/menubar.py b/menubar.py index 9ebf169..5526d77 100644 --- a/menubar.py +++ b/menubar.py @@ -1,5 +1,5 @@ """ -Hermes Phone — macOS Menu Bar App +Dialtone — macOS Menu Bar App Single phone icon that changes color: 🟢 = server running @@ -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 = {} @@ -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 @@ -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(_) @@ -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.""" diff --git a/server.py b/server.py index c530d35..b1d83e6 100644 --- a/server.py +++ b/server.py @@ -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 @@ -267,7 +267,7 @@ def get_auth_headers(): LOGIN_HTML = """ -📞 Hermes Phone — Login +📞 Dialtone — Login
-

📞 Hermes Phone

+

📞 Dialtone

Enter your dashboard token
Invalid token
@@ -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}")