claude-phone-local 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +108 -0
- package/Dockerfile +84 -0
- package/LICENSE +22 -0
- package/README.md +231 -0
- package/claude-api-server/package-lock.json +832 -0
- package/claude-api-server/package.json +20 -0
- package/claude-api-server/server.js +540 -0
- package/claude-api-server/structured.js +208 -0
- package/cli/README.md +231 -0
- package/cli/bin/check-publish-safety.js +53 -0
- package/cli/bin/claude-phone.js +23 -0
- package/cli/bin/cli-main.js +284 -0
- package/cli/bin/postinstall.js +20 -0
- package/cli/lib/commands/api-server.js +77 -0
- package/cli/lib/commands/backup.js +69 -0
- package/cli/lib/commands/config/path.js +30 -0
- package/cli/lib/commands/config/reset.js +73 -0
- package/cli/lib/commands/config/show.js +86 -0
- package/cli/lib/commands/device/add.js +143 -0
- package/cli/lib/commands/device/list.js +55 -0
- package/cli/lib/commands/device/remove.js +83 -0
- package/cli/lib/commands/doctor.js +422 -0
- package/cli/lib/commands/logs.js +186 -0
- package/cli/lib/commands/restore.js +172 -0
- package/cli/lib/commands/setup.js +1246 -0
- package/cli/lib/commands/start.js +346 -0
- package/cli/lib/commands/status.js +139 -0
- package/cli/lib/commands/stop.js +101 -0
- package/cli/lib/commands/uninstall.js +183 -0
- package/cli/lib/commands/update.js +205 -0
- package/cli/lib/config.js +113 -0
- package/cli/lib/docker.js +384 -0
- package/cli/lib/mcp-register.js +108 -0
- package/cli/lib/network.js +109 -0
- package/cli/lib/platform.js +85 -0
- package/cli/lib/port-check.js +135 -0
- package/cli/lib/prereqs/checks/compose.js +147 -0
- package/cli/lib/prereqs/checks/disk.js +89 -0
- package/cli/lib/prereqs/checks/docker.js +155 -0
- package/cli/lib/prereqs/checks/network.js +78 -0
- package/cli/lib/prereqs/checks/node.js +95 -0
- package/cli/lib/prereqs/installers/docker-desktop.js +248 -0
- package/cli/lib/prereqs/installers/docker.js +229 -0
- package/cli/lib/prereqs/installers/node.js +254 -0
- package/cli/lib/prereqs/platform.js +175 -0
- package/cli/lib/prereqs/utils/execute.js +208 -0
- package/cli/lib/prereqs/utils/rollback.js +223 -0
- package/cli/lib/prereqs/utils/sudo.js +177 -0
- package/cli/lib/prereqs.js +228 -0
- package/cli/lib/prerequisites.js +115 -0
- package/cli/lib/process-manager.js +176 -0
- package/cli/lib/utils.js +78 -0
- package/cli/lib/validators.js +219 -0
- package/cli/lib/voice-downloader.js +79 -0
- package/docker/entrypoint.sh +110 -0
- package/docker/supervisord.conf +78 -0
- package/docker-compose.yml +40 -0
- package/docs/CLAUDE-CODE-SKILL.md +45 -0
- package/docs/LANGUAGES.md +113 -0
- package/docs/MCP-SERVER.md +111 -0
- package/docs/TROUBLESHOOTING.md +288 -0
- package/mcp-server/index.js +159 -0
- package/mcp-server/package-lock.json +1201 -0
- package/mcp-server/package.json +9 -0
- package/package.json +73 -0
- package/stt-local/Dockerfile +12 -0
- package/stt-local/server.py +45 -0
- package/tts-local/Dockerfile +9 -0
- package/tts-local/server.py +83 -0
- package/voice-app/API-QUERY-CONTRACT.md +238 -0
- package/voice-app/DEPLOYMENT.md +176 -0
- package/voice-app/Dockerfile +20 -0
- package/voice-app/README-OUTBOUND.md +316 -0
- package/voice-app/config/devices.json.example +18 -0
- package/voice-app/index.js +273 -0
- package/voice-app/lib/audio-fork.js +465 -0
- package/voice-app/lib/claude-bridge.js +113 -0
- package/voice-app/lib/connection-retry.js +58 -0
- package/voice-app/lib/conversation-loop.js +490 -0
- package/voice-app/lib/device-registry.js +171 -0
- package/voice-app/lib/http-server.js +242 -0
- package/voice-app/lib/logger.js +35 -0
- package/voice-app/lib/multi-registrar.js +133 -0
- package/voice-app/lib/outbound-handler.js +299 -0
- package/voice-app/lib/outbound-routes.js +417 -0
- package/voice-app/lib/outbound-session.js +324 -0
- package/voice-app/lib/query-routes.js +484 -0
- package/voice-app/lib/registrar.js +144 -0
- package/voice-app/lib/sip-handler.js +490 -0
- package/voice-app/lib/tts-service.js +205 -0
- package/voice-app/lib/whisper-client.js +140 -0
- package/voice-app/package.json +38 -0
- package/voice-app/static/gotit-beep.wav +0 -0
- package/voice-app/static/hold-music.wav +0 -0
- package/voice-app/static/ready-beep.wav +0 -0
- package/voice-app/test/freeswitch-retry.test.js +100 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Bootstraps persistent state in /data, then hands off to supervisord.
|
|
3
|
+
# Everything here is idempotent: it only fetches what is missing, so a restart
|
|
4
|
+
# with a populated ./data volume starts instantly and never re-downloads.
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
log() { echo "[bootstrap] $*"; }
|
|
8
|
+
|
|
9
|
+
: "${EXTERNAL_IP:?must be set to this host LAN IP}"
|
|
10
|
+
export DRACHTIO_SECRET="${DRACHTIO_SECRET:-cymru}"
|
|
11
|
+
export RTP_START="${RTP_START:-30000}"
|
|
12
|
+
export RTP_END="${RTP_END:-30100}"
|
|
13
|
+
# FreeSWITCH binds sofia to the container IP (mrf.xml sip-ip = $${local_ip_v4}),
|
|
14
|
+
# never to loopback. drachtio lives in this same network namespace, so it must
|
|
15
|
+
# be pointed at that same address - advertising 127.0.0.1 would send it to a
|
|
16
|
+
# port nothing is listening on. RTP still advertises EXTERNAL_IP for the SBC.
|
|
17
|
+
FS_SIP_IP="$(hostname -i | awk '{print $1}')"
|
|
18
|
+
export FS_SIP_IP
|
|
19
|
+
log "FreeSWITCH SIP will advertise ${FS_SIP_IP}; RTP will advertise ${EXTERNAL_IP}"
|
|
20
|
+
|
|
21
|
+
export PIPER_VOICES_DIR=/data/voices
|
|
22
|
+
export HF_HOME=/data/models/huggingface
|
|
23
|
+
|
|
24
|
+
mkdir -p /data/voices /data/models/huggingface /data/audio /data/config
|
|
25
|
+
|
|
26
|
+
# --- Piper voices -----------------------------------------------------------
|
|
27
|
+
# VOICES is a space-separated list of Piper voice names. Each is resolved to its
|
|
28
|
+
# path in the rhasspy/piper-voices repo from the language prefix.
|
|
29
|
+
PIPER_BASE="https://huggingface.co/rhasspy/piper-voices/resolve/main"
|
|
30
|
+
VOICES="${PIPER_VOICES:-en_US-lessac-medium hi_IN-priyamvada-medium mr_IN-google-medium}"
|
|
31
|
+
|
|
32
|
+
voice_repo_path() {
|
|
33
|
+
# en_US-lessac-medium -> en/en_US/lessac/medium/en_US-lessac-medium
|
|
34
|
+
local name="$1"
|
|
35
|
+
local locale="${name%%-*}" # en_US
|
|
36
|
+
local rest="${name#*-}" # lessac-medium
|
|
37
|
+
local speaker="${rest%%-*}" # lessac
|
|
38
|
+
local quality="${rest##*-}" # medium
|
|
39
|
+
local lang="${locale%%_*}" # en
|
|
40
|
+
echo "${lang}/${locale}/${speaker}/${quality}/${name}"
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
for v in $VOICES; do
|
|
44
|
+
if [ -f "/data/voices/${v}.onnx" ]; then
|
|
45
|
+
log "voice ${v} already present"
|
|
46
|
+
continue
|
|
47
|
+
fi
|
|
48
|
+
path="$(voice_repo_path "$v")"
|
|
49
|
+
log "downloading voice ${v} ..."
|
|
50
|
+
if curl -fsSL --retry 3 --retry-delay 2 "${PIPER_BASE}/${path}.onnx" -o "/data/voices/${v}.onnx.part" \
|
|
51
|
+
&& curl -fsSL --retry 3 --retry-delay 2 "${PIPER_BASE}/${path}.onnx.json" -o "/data/voices/${v}.onnx.json"; then
|
|
52
|
+
mv "/data/voices/${v}.onnx.part" "/data/voices/${v}.onnx"
|
|
53
|
+
log "voice ${v} ready"
|
|
54
|
+
else
|
|
55
|
+
rm -f "/data/voices/${v}.onnx.part"
|
|
56
|
+
log "WARNING: could not download voice ${v} - it will be unavailable"
|
|
57
|
+
fi
|
|
58
|
+
done
|
|
59
|
+
|
|
60
|
+
# --- Whisper model ----------------------------------------------------------
|
|
61
|
+
# faster-whisper pulls the model on first use into HF_HOME, which lives on the
|
|
62
|
+
# /data volume, so it survives restarts. Pre-fetch it so the first call is fast.
|
|
63
|
+
if [ "${PREFETCH_WHISPER:-1}" = "1" ]; then
|
|
64
|
+
log "ensuring Whisper model '${WHISPER_MODEL:-medium}' is cached (first run may take a while) ..."
|
|
65
|
+
/opt/venv/bin/python - <<'PY' || log "WARNING: Whisper prefetch failed; it will download on first call"
|
|
66
|
+
import os
|
|
67
|
+
from faster_whisper import WhisperModel
|
|
68
|
+
WhisperModel(
|
|
69
|
+
os.environ.get("WHISPER_MODEL", "medium"),
|
|
70
|
+
device=os.environ.get("WHISPER_DEVICE", "cpu"),
|
|
71
|
+
compute_type=os.environ.get("WHISPER_COMPUTE_TYPE", "int8"),
|
|
72
|
+
)
|
|
73
|
+
print("[bootstrap] whisper model cached")
|
|
74
|
+
PY
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# --- Device config ----------------------------------------------------------
|
|
78
|
+
# Seed /data/config/devices.json once, then never touch it again - it holds the
|
|
79
|
+
# user's SIP credentials and is theirs to edit.
|
|
80
|
+
if [ ! -f /data/config/devices.json ]; then
|
|
81
|
+
if [ -n "${SIP_EXTENSION:-}" ]; then
|
|
82
|
+
log "generating devices.json for extension ${SIP_EXTENSION}"
|
|
83
|
+
/usr/bin/node -e '
|
|
84
|
+
const fs = require("fs");
|
|
85
|
+
const ext = process.env.SIP_EXTENSION;
|
|
86
|
+
fs.writeFileSync("/data/config/devices.json", JSON.stringify({
|
|
87
|
+
[ext]: {
|
|
88
|
+
name: process.env.DEVICE_NAME || "Maya",
|
|
89
|
+
extension: ext,
|
|
90
|
+
authId: process.env.SIP_AUTH_ID || ext,
|
|
91
|
+
password: process.env.SIP_PASSWORD || "",
|
|
92
|
+
voice: process.env.PIPER_VOICE || "en_US-lessac-medium",
|
|
93
|
+
language: process.env.STT_LANGUAGE || "auto",
|
|
94
|
+
prompt: process.env.DEVICE_PROMPT ||
|
|
95
|
+
"You are " + (process.env.DEVICE_NAME || "Maya") +
|
|
96
|
+
", a helpful AI assistant speaking to the user over the phone. " +
|
|
97
|
+
"ALWAYS reply in the same language the user just used, using that " +
|
|
98
|
+
"language native script. Keep voice responses under 40 words."
|
|
99
|
+
}
|
|
100
|
+
}, null, 2) + "\n");
|
|
101
|
+
'
|
|
102
|
+
else
|
|
103
|
+
log "SIP_EXTENSION not set - skipping devices.json generation"
|
|
104
|
+
fi
|
|
105
|
+
else
|
|
106
|
+
log "devices.json already present (left untouched)"
|
|
107
|
+
fi
|
|
108
|
+
|
|
109
|
+
log "bootstrap complete - starting services"
|
|
110
|
+
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
; claude-phone-local - all five services in one container.
|
|
2
|
+
; Everything talks over 127.0.0.1 here, which is why the unified image needs no
|
|
3
|
+
; Docker DNS names and sidesteps the bridge-networking issues of the split setup.
|
|
4
|
+
[supervisord]
|
|
5
|
+
nodaemon=true
|
|
6
|
+
logfile=/dev/null
|
|
7
|
+
logfile_maxbytes=0
|
|
8
|
+
pidfile=/run/supervisord.pid
|
|
9
|
+
user=root
|
|
10
|
+
|
|
11
|
+
[program:freeswitch]
|
|
12
|
+
command=/entrypoint.sh freeswitch
|
|
13
|
+
--sip-port 5080
|
|
14
|
+
--rtp-range-start %(ENV_RTP_START)s
|
|
15
|
+
--rtp-range-end %(ENV_RTP_END)s
|
|
16
|
+
--ext-rtp-ip %(ENV_EXTERNAL_IP)s
|
|
17
|
+
--ext-sip-ip %(ENV_FS_SIP_IP)s
|
|
18
|
+
--advertise-external-ip
|
|
19
|
+
-- -nc -nf
|
|
20
|
+
priority=10
|
|
21
|
+
autorestart=true
|
|
22
|
+
startsecs=5
|
|
23
|
+
stdout_logfile=/dev/fd/1
|
|
24
|
+
stdout_logfile_maxbytes=0
|
|
25
|
+
stderr_logfile=/dev/fd/2
|
|
26
|
+
stderr_logfile_maxbytes=0
|
|
27
|
+
stdout_events_enabled=true
|
|
28
|
+
|
|
29
|
+
[program:drachtio]
|
|
30
|
+
command=/usr/local/bin/drachtio
|
|
31
|
+
--contact sip:*:5070;transport=tcp,udp
|
|
32
|
+
--external-ip %(ENV_EXTERNAL_IP)s
|
|
33
|
+
--secret %(ENV_DRACHTIO_SECRET)s
|
|
34
|
+
--port 9022
|
|
35
|
+
--loglevel info
|
|
36
|
+
priority=10
|
|
37
|
+
autorestart=true
|
|
38
|
+
startsecs=3
|
|
39
|
+
stdout_logfile=/dev/fd/1
|
|
40
|
+
stdout_logfile_maxbytes=0
|
|
41
|
+
stderr_logfile=/dev/fd/2
|
|
42
|
+
stderr_logfile_maxbytes=0
|
|
43
|
+
|
|
44
|
+
[program:stt]
|
|
45
|
+
command=/opt/venv/bin/uvicorn server:app --host 127.0.0.1 --port 9001
|
|
46
|
+
directory=/opt/stt
|
|
47
|
+
environment=HF_HOME="/data/models/huggingface",HOME="/root"
|
|
48
|
+
priority=20
|
|
49
|
+
autorestart=true
|
|
50
|
+
; the first run downloads the Whisper model, which can take several minutes
|
|
51
|
+
startsecs=10
|
|
52
|
+
stdout_logfile=/dev/fd/1
|
|
53
|
+
stdout_logfile_maxbytes=0
|
|
54
|
+
stderr_logfile=/dev/fd/2
|
|
55
|
+
stderr_logfile_maxbytes=0
|
|
56
|
+
|
|
57
|
+
[program:tts]
|
|
58
|
+
command=/opt/venv/bin/uvicorn server:app --host 127.0.0.1 --port 9002
|
|
59
|
+
directory=/opt/tts
|
|
60
|
+
environment=PIPER_VOICES_DIR="/data/voices"
|
|
61
|
+
priority=20
|
|
62
|
+
autorestart=true
|
|
63
|
+
startsecs=5
|
|
64
|
+
stdout_logfile=/dev/fd/1
|
|
65
|
+
stdout_logfile_maxbytes=0
|
|
66
|
+
stderr_logfile=/dev/fd/2
|
|
67
|
+
stderr_logfile_maxbytes=0
|
|
68
|
+
|
|
69
|
+
[program:voice-app]
|
|
70
|
+
command=/usr/bin/node index.js
|
|
71
|
+
directory=/app
|
|
72
|
+
priority=30
|
|
73
|
+
autorestart=true
|
|
74
|
+
startsecs=5
|
|
75
|
+
stdout_logfile=/dev/fd/1
|
|
76
|
+
stdout_logfile_maxbytes=0
|
|
77
|
+
stderr_logfile=/dev/fd/2
|
|
78
|
+
stderr_logfile_maxbytes=0
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# claude-phone-local - one container, one volume.
|
|
2
|
+
#
|
|
3
|
+
# All five services (drachtio, FreeSWITCH, voice-app, STT, TTS) run inside a
|
|
4
|
+
# single image and talk over 127.0.0.1, so there is no container DNS to get
|
|
5
|
+
# wrong. Everything that must survive a restart lives in ./data:
|
|
6
|
+
#
|
|
7
|
+
# ./data/voices Piper voice models (downloaded on first run)
|
|
8
|
+
# ./data/models Whisper model cache (downloaded on first run)
|
|
9
|
+
# ./data/config devices.json (generated once, then yours to edit)
|
|
10
|
+
# ./data/audio generated speech (scratch)
|
|
11
|
+
#
|
|
12
|
+
# The previous five-container layout is preserved in docker-compose.multi.yml.
|
|
13
|
+
#
|
|
14
|
+
# EXTERNAL_IP in .env must be this PC's LAN IP (run `ipconfig` / `ip addr`) --
|
|
15
|
+
# it is the address 3CX sends SIP and RTP to.
|
|
16
|
+
|
|
17
|
+
services:
|
|
18
|
+
claude-phone:
|
|
19
|
+
build: .
|
|
20
|
+
image: claude-phone-local:latest
|
|
21
|
+
container_name: claude-phone
|
|
22
|
+
restart: unless-stopped
|
|
23
|
+
env_file:
|
|
24
|
+
- .env
|
|
25
|
+
environment:
|
|
26
|
+
# Everything is in-process now, so all hosts are loopback.
|
|
27
|
+
- DRACHTIO_HOST=127.0.0.1
|
|
28
|
+
- FREESWITCH_HOST=127.0.0.1
|
|
29
|
+
- STT_LOCAL_URL=http://127.0.0.1:9001
|
|
30
|
+
- TTS_LOCAL_URL=http://127.0.0.1:9002
|
|
31
|
+
- AUDIO_BASE_URL=http://127.0.0.1:3000
|
|
32
|
+
- RTP_START=30000
|
|
33
|
+
- RTP_END=30100
|
|
34
|
+
ports:
|
|
35
|
+
- "3000:3000" # HTTP API + audio file serving
|
|
36
|
+
- "5070:5070/udp" # drachtio SIP
|
|
37
|
+
- "5070:5070/tcp"
|
|
38
|
+
- "30000-30100:30000-30100/udp" # RTP
|
|
39
|
+
volumes:
|
|
40
|
+
- ./data:/data
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Claude Code Call Skill — superseded
|
|
2
|
+
|
|
3
|
+
> **This approach has been replaced by the [MCP server](MCP-SERVER.md).**
|
|
4
|
+
|
|
5
|
+
This document used to describe a Python-based Claude Code *skill* that wrapped
|
|
6
|
+
the outbound-call API, so you could say "call me when the backup finishes".
|
|
7
|
+
|
|
8
|
+
That capability now ships as a proper **MCP server** (`mcp-server/`), which is
|
|
9
|
+
better in every way that matters here:
|
|
10
|
+
|
|
11
|
+
| | Old skill | MCP server |
|
|
12
|
+
|---|---|---|
|
|
13
|
+
| Integration | Python script invoked by a skill | Native MCP tools |
|
|
14
|
+
| Discovery | Claude had to be told it existed | Advertised via `tools/list` |
|
|
15
|
+
| Errors | Raised as script output | Returned as tool errors Claude can act on |
|
|
16
|
+
| Install | Copy files into a skills directory | `claude mcp add` |
|
|
17
|
+
| Status/hangup | Manual HTTP calls | `call_status`, `hangup` tools |
|
|
18
|
+
|
|
19
|
+
## Use this instead
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
cd mcp-server && npm install
|
|
23
|
+
|
|
24
|
+
claude mcp add claude-phone --scope user \
|
|
25
|
+
--env PHONE_DEFAULT_TO=17510 \
|
|
26
|
+
-- node /absolute/path/to/mcp-server/index.js
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Restart Claude Code, then just ask:
|
|
30
|
+
|
|
31
|
+
> "Run the migration and call me if anything needs a decision."
|
|
32
|
+
|
|
33
|
+
Full documentation: **[MCP-SERVER.md](MCP-SERVER.md)**.
|
|
34
|
+
|
|
35
|
+
## If you still want the old skill
|
|
36
|
+
|
|
37
|
+
It remains in git history. To recover it:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
git log --oneline -- docs/CLAUDE-CODE-SKILL.md
|
|
41
|
+
git show <commit>:docs/CLAUDE-CODE-SKILL.md > old-skill.md
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
The underlying HTTP API it called (`POST /api/outbound-call`) is unchanged and
|
|
45
|
+
still supported — see [../voice-app/README-OUTBOUND.md](../voice-app/README-OUTBOUND.md).
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Languages — English, Hindi, Marathi
|
|
2
|
+
|
|
3
|
+
Speech is fully offline in all three. Whisper detects the language of each
|
|
4
|
+
utterance, and the reply comes back in that same language with a matching voice.
|
|
5
|
+
|
|
6
|
+
## How it works
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
you speak
|
|
10
|
+
→ faster-whisper transcribes AND reports the language code (hi / mr / en)
|
|
11
|
+
→ the code picks a Piper voice from LANG_VOICE_MAP
|
|
12
|
+
→ Claude is instructed to reply in the same language
|
|
13
|
+
→ Piper speaks it in that voice
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Detection is **per utterance**, so you can switch language mid-call and she
|
|
17
|
+
follows.
|
|
18
|
+
|
|
19
|
+
## Configuration
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
STT_LANGUAGE=auto # detect per utterance; or pin to en | hi | mr
|
|
23
|
+
SUPPORTED_LANGS=en,hi,mr # languages we will answer in
|
|
24
|
+
LANG_VOICE_MAP={"en":"en_US-lessac-medium","hi":"hi_IN-priyamvada-medium","mr":"mr_IN-google-medium"}
|
|
25
|
+
WHISPER_MODEL=medium # see accuracy below
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
`SUPPORTED_LANGS` is a safety net, not decoration. Whisper can return any of
|
|
29
|
+
~99 codes but only the installed voices exist on disk. A code outside this list
|
|
30
|
+
falls back to the device voice, so a misdetection — `ne` for Marathi is very
|
|
31
|
+
plausible, both are Devanagari — costs you the wrong accent rather than a 404
|
|
32
|
+
mid-call.
|
|
33
|
+
|
|
34
|
+
## Voices
|
|
35
|
+
|
|
36
|
+
Downloaded automatically on first run into `data/voices/`:
|
|
37
|
+
|
|
38
|
+
| Language | Voice | Size |
|
|
39
|
+
|---|---|---|
|
|
40
|
+
| English | `en_US-lessac-medium` | 61 MB |
|
|
41
|
+
| Hindi | `hi_IN-priyamvada-medium` | 61 MB |
|
|
42
|
+
| Marathi | `mr_IN-google-medium` | 73 MB |
|
|
43
|
+
|
|
44
|
+
Change the set with `PIPER_VOICES` (space-separated) in `.env`; the entrypoint
|
|
45
|
+
resolves each name to its path in the
|
|
46
|
+
[rhasspy/piper-voices](https://huggingface.co/rhasspy/piper-voices) repo.
|
|
47
|
+
|
|
48
|
+
Other English voices worth trying: `en_US-amy-medium`, `en_US-kristin-medium`,
|
|
49
|
+
`en_US-hfc_female-medium`, `en_US-ryan-high`.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
curl http://127.0.0.1:9002/voices # what is actually installed
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Accuracy — read this before judging Marathi
|
|
56
|
+
|
|
57
|
+
Measured on this stack:
|
|
58
|
+
|
|
59
|
+
| Language | `small` | `medium` |
|
|
60
|
+
|---|---|---|
|
|
61
|
+
| English | good | very good |
|
|
62
|
+
| Hindi | fair | **very good** |
|
|
63
|
+
| Marathi | poor | **fair** |
|
|
64
|
+
|
|
65
|
+
Whisper's Marathi training data is thin compared with Hindi. Detection is
|
|
66
|
+
reliable in both; it is the *words* that suffer in Marathi.
|
|
67
|
+
|
|
68
|
+
`large-v3` improves Marathi noticeably but is ~3 GB and materially slower per
|
|
69
|
+
turn on CPU — a real cost on a live call, where every second is dead air.
|
|
70
|
+
|
|
71
|
+
## The speed/accuracy trade-off
|
|
72
|
+
|
|
73
|
+
`WHISPER_MODEL` is the single biggest latency lever:
|
|
74
|
+
|
|
75
|
+
| Model | Transcription | Notes |
|
|
76
|
+
|---|---|---|
|
|
77
|
+
| `small` | ~4s | fine for English, weak on Marathi |
|
|
78
|
+
| `medium` | ~14s | needed for solid Hindi/Marathi |
|
|
79
|
+
| `large-v3` | ~40s+ | best accuracy, painful on a phone call |
|
|
80
|
+
|
|
81
|
+
If you mostly speak English, `small` makes calls feel dramatically snappier.
|
|
82
|
+
Keep `medium` while testing the Indian languages.
|
|
83
|
+
|
|
84
|
+
## Making her reply in the right language
|
|
85
|
+
|
|
86
|
+
Transcription alone is not enough — Claude has to be told, or she will answer a
|
|
87
|
+
Hindi question in English. The device prompt in `data/config/devices.json`
|
|
88
|
+
handles it:
|
|
89
|
+
|
|
90
|
+
> ALWAYS reply in the same language the user just used, using that language's
|
|
91
|
+
> native script (Devanagari for Hindi and Marathi). Never transliterate.
|
|
92
|
+
|
|
93
|
+
The "never transliterate" clause matters: Piper's Hindi and Marathi voices read
|
|
94
|
+
**Devanagari**. Hand them romanised Hindi ("aap kaise hain") and you get an
|
|
95
|
+
English speaker mangling syllables.
|
|
96
|
+
|
|
97
|
+
## Ending a call
|
|
98
|
+
|
|
99
|
+
Goodbye phrases are recognised in all three languages, including
|
|
100
|
+
`बंद करो`, `फोन बंद करो`, `ठेवतो`, `अलविदा`, `बाय`, plus romanised forms
|
|
101
|
+
(`band karo`, `alvida`). See `goodbyePhrases` in `voice-app/lib/sip-handler.js`.
|
|
102
|
+
|
|
103
|
+
## Adding another language
|
|
104
|
+
|
|
105
|
+
1. Find a voice in the [Piper catalogue](https://huggingface.co/rhasspy/piper-voices)
|
|
106
|
+
(56 languages available).
|
|
107
|
+
2. Add its name to `PIPER_VOICES` in `.env`.
|
|
108
|
+
3. Add the Whisper code → voice mapping to `LANG_VOICE_MAP`.
|
|
109
|
+
4. Add the code to `SUPPORTED_LANGS`.
|
|
110
|
+
5. Add goodbye phrases for it in `sip-handler.js`.
|
|
111
|
+
6. `docker compose restart claude-phone` — the entrypoint downloads it on boot.
|
|
112
|
+
|
|
113
|
+
No image rebuild is needed; voices live on the `./data` volume.
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# MCP Server — let Claude phone you
|
|
2
|
+
|
|
3
|
+
Gives a Claude Code session a phone. It can ring you mid-task, say why, and —
|
|
4
|
+
in conversation mode — talk the decision through before carrying on.
|
|
5
|
+
|
|
6
|
+
Useful when Claude is doing something long and hits a fork in the road: a
|
|
7
|
+
migration that needs a call, a deploy that wants confirmation, a test suite that
|
|
8
|
+
went red in a way worth discussing.
|
|
9
|
+
|
|
10
|
+
## How it fits
|
|
11
|
+
|
|
12
|
+
The MCP server does **no SIP work**. It is a thin wrapper over the voice-app
|
|
13
|
+
HTTP API that already existed:
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Claude Code session
|
|
17
|
+
│ MCP (stdio)
|
|
18
|
+
▼
|
|
19
|
+
mcp-server/index.js
|
|
20
|
+
│ HTTP
|
|
21
|
+
▼
|
|
22
|
+
voice-app /api/outbound-call → drachtio → FreeSWITCH → 3CX → your phone
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
cd mcp-server && npm install
|
|
29
|
+
|
|
30
|
+
claude mcp add claude-phone --scope user \
|
|
31
|
+
--env VOICE_APP_URL=http://127.0.0.1:3000 \
|
|
32
|
+
--env PHONE_DEFAULT_TO=17510 \
|
|
33
|
+
-- node /absolute/path/to/mcp-server/index.js
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Then **restart Claude Code** and confirm:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
claude mcp list
|
|
40
|
+
# claude-phone: node …/mcp-server/index.js - ✔ Connected
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
| Env | Purpose |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `VOICE_APP_URL` | Where voice-app listens (default `http://127.0.0.1:3000`) |
|
|
46
|
+
| `PHONE_DEFAULT_TO` | Number/extension to ring when `to` is omitted |
|
|
47
|
+
|
|
48
|
+
## Tools
|
|
49
|
+
|
|
50
|
+
### `call_me`
|
|
51
|
+
|
|
52
|
+
Rings you and speaks a message.
|
|
53
|
+
|
|
54
|
+
| Arg | Required | Notes |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| `message` | yes | What to say. One or two spoken sentences, no markdown. |
|
|
57
|
+
| `mode` | no | `announce` (say it, hang up) or `converse` (stay on the line) |
|
|
58
|
+
| `to` | no | Defaults to `PHONE_DEFAULT_TO` |
|
|
59
|
+
| `context` | no | Background so the voice agent can field follow-ups |
|
|
60
|
+
|
|
61
|
+
### `call_status`
|
|
62
|
+
|
|
63
|
+
Poll a call by `callId` — whether it is still up and how it ended.
|
|
64
|
+
|
|
65
|
+
### `hangup`
|
|
66
|
+
|
|
67
|
+
End a call that is still in progress.
|
|
68
|
+
|
|
69
|
+
## Using it
|
|
70
|
+
|
|
71
|
+
Just ask in plain language:
|
|
72
|
+
|
|
73
|
+
> "Run the migration, and call me if anything needs a decision."
|
|
74
|
+
|
|
75
|
+
> "This test suite takes 20 minutes — phone me when it finishes with a summary."
|
|
76
|
+
|
|
77
|
+
Claude picks the tool itself. For a back-and-forth, ask for conversation mode:
|
|
78
|
+
|
|
79
|
+
> "Call me and talk me through the options — I want to answer, not just listen."
|
|
80
|
+
|
|
81
|
+
## Writing a good message
|
|
82
|
+
|
|
83
|
+
It is **spoken aloud**, so:
|
|
84
|
+
|
|
85
|
+
- Keep it to one or two sentences.
|
|
86
|
+
- No markdown, code blocks, paths, or long identifiers — they sound terrible.
|
|
87
|
+
- Lead with the decision, not the backstory:
|
|
88
|
+
*"The migration is ready but will drop two columns — should I continue?"*
|
|
89
|
+
- Use `context` for the detail. It never gets read out, but the voice agent can
|
|
90
|
+
draw on it when you ask a follow-up question.
|
|
91
|
+
|
|
92
|
+
## Notes and limits
|
|
93
|
+
|
|
94
|
+
- **voice-app must be running** (`docker compose up -d`) — the MCP server only
|
|
95
|
+
proxies to it.
|
|
96
|
+
- **`announce` does not wait.** It returns as soon as the call starts. Poll
|
|
97
|
+
`call_status` if you need the outcome.
|
|
98
|
+
- **If you do not answer**, the call ends on the ring timeout
|
|
99
|
+
(`OUTBOUND_RING_TIMEOUT`, default 30s) and `call_status` reports it. Claude
|
|
100
|
+
should carry on rather than block.
|
|
101
|
+
- Errors come back as tool failures with the HTTP status, so Claude can react
|
|
102
|
+
instead of crashing.
|
|
103
|
+
|
|
104
|
+
## Troubleshooting
|
|
105
|
+
|
|
106
|
+
| Symptom | Check |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `✘ Failed to connect` in `claude mcp list` | `cd mcp-server && npm install`; use an absolute path in the registration |
|
|
109
|
+
| `claude-phone: 500 …` | Is voice-app up? `curl http://127.0.0.1:3000/api/devices` |
|
|
110
|
+
| Phone never rings | SIP registration — `docker logs claude-phone \| grep MULTI-REGISTRAR` |
|
|
111
|
+
| Rings, then silence | Same RTP/SDP checks as inbound: [TROUBLESHOOTING.md](TROUBLESHOOTING.md) |
|