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
package/package.json
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "claude-phone-local",
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Local/offline fork of NetworkChuck's claude-phone: talk to Claude Code over 3CX/SIP with faster-whisper STT + Piper TTS in one Docker container.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"claude-phone": "cli/bin/claude-phone.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli/bin",
|
|
11
|
+
"cli/lib",
|
|
12
|
+
"cli/README.md",
|
|
13
|
+
"voice-app",
|
|
14
|
+
"claude-api-server",
|
|
15
|
+
"stt-local",
|
|
16
|
+
"tts-local",
|
|
17
|
+
"Dockerfile",
|
|
18
|
+
"mcp-server",
|
|
19
|
+
"docker",
|
|
20
|
+
"docker-compose.yml",
|
|
21
|
+
".env.example",
|
|
22
|
+
"README.md",
|
|
23
|
+
"docs",
|
|
24
|
+
"!voice-app/config/devices.json",
|
|
25
|
+
"!voice-app/audio/**",
|
|
26
|
+
"!voice-app/audio-temp/**",
|
|
27
|
+
"!tts-local/voices/**",
|
|
28
|
+
"!**/.env",
|
|
29
|
+
"!**/node_modules/**"
|
|
30
|
+
],
|
|
31
|
+
"scripts": {
|
|
32
|
+
"postinstall": "node ./cli/bin/postinstall.js || true",
|
|
33
|
+
"prepublishOnly": "node ./cli/bin/check-publish-safety.js"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"claude",
|
|
37
|
+
"claude-code",
|
|
38
|
+
"voice",
|
|
39
|
+
"sip",
|
|
40
|
+
"3cx",
|
|
41
|
+
"ai",
|
|
42
|
+
"whisper",
|
|
43
|
+
"piper",
|
|
44
|
+
"offline",
|
|
45
|
+
"self-hosted",
|
|
46
|
+
"hindi",
|
|
47
|
+
"marathi",
|
|
48
|
+
"multilingual",
|
|
49
|
+
"voip",
|
|
50
|
+
"freeswitch",
|
|
51
|
+
"drachtio"
|
|
52
|
+
],
|
|
53
|
+
"author": "",
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"commander": "^12.1.0",
|
|
57
|
+
"inquirer": "^9.2.15",
|
|
58
|
+
"chalk": "^5.3.0",
|
|
59
|
+
"ora": "^8.0.1",
|
|
60
|
+
"axios": "^1.6.7",
|
|
61
|
+
"@modelcontextprotocol/sdk": "^1.0.0"
|
|
62
|
+
},
|
|
63
|
+
"engines": {
|
|
64
|
+
"node": ">=18.0.0"
|
|
65
|
+
},
|
|
66
|
+
"publishConfig": {
|
|
67
|
+
"access": "public"
|
|
68
|
+
},
|
|
69
|
+
"repository": {
|
|
70
|
+
"type": "git",
|
|
71
|
+
"url": "git+https://github.com/theNetworkChuck/claude-phone.git"
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
FROM python:3.11-slim
|
|
2
|
+
|
|
3
|
+
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
|
|
4
|
+
&& rm -rf /var/lib/apt/lists/*
|
|
5
|
+
|
|
6
|
+
WORKDIR /app
|
|
7
|
+
RUN pip install --no-cache-dir fastapi uvicorn[standard] python-multipart faster-whisper
|
|
8
|
+
|
|
9
|
+
COPY server.py /app/server.py
|
|
10
|
+
|
|
11
|
+
EXPOSE 9001
|
|
12
|
+
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "9001"]
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local speech-to-text sidecar for claude-phone.
|
|
3
|
+
Wraps faster-whisper behind a tiny HTTP API so voice-app (Node) can call it
|
|
4
|
+
the same way it used to call OpenAI's Whisper API — except fully offline.
|
|
5
|
+
|
|
6
|
+
POST /transcribe multipart form: file=<wav bytes>, language=en|hi|mr|auto
|
|
7
|
+
-> {"text": "...", "language": "hi"}
|
|
8
|
+
language=auto (or empty) lets Whisper detect it and the
|
|
9
|
+
detected code comes back so the caller can pick a matching
|
|
10
|
+
TTS voice.
|
|
11
|
+
GET /health -> {"status": "ok", "model": "small"}
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import io
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI, UploadFile, Form
|
|
18
|
+
from faster_whisper import WhisperModel
|
|
19
|
+
|
|
20
|
+
MODEL_SIZE = os.environ.get("WHISPER_MODEL", "small")
|
|
21
|
+
DEVICE = os.environ.get("WHISPER_DEVICE", "cpu")
|
|
22
|
+
COMPUTE_TYPE = os.environ.get("WHISPER_COMPUTE_TYPE", "int8")
|
|
23
|
+
|
|
24
|
+
app = FastAPI(title="claude-phone local STT")
|
|
25
|
+
|
|
26
|
+
print(f"[stt-local] loading faster-whisper '{MODEL_SIZE}' ({DEVICE}/{COMPUTE_TYPE})...")
|
|
27
|
+
model = WhisperModel(MODEL_SIZE, device=DEVICE, compute_type=COMPUTE_TYPE)
|
|
28
|
+
print("[stt-local] model ready")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.get("/health")
|
|
32
|
+
def health():
|
|
33
|
+
return {"status": "ok", "model": MODEL_SIZE}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@app.post("/transcribe")
|
|
37
|
+
async def transcribe(file: UploadFile, language: str = Form("en")):
|
|
38
|
+
audio_bytes = await file.read()
|
|
39
|
+
# "auto"/"" -> None makes faster-whisper detect the language itself.
|
|
40
|
+
lang = None if language in ("auto", "", None) else language
|
|
41
|
+
segments, info = model.transcribe(
|
|
42
|
+
io.BytesIO(audio_bytes), language=lang, vad_filter=True
|
|
43
|
+
)
|
|
44
|
+
text = " ".join(seg.text.strip() for seg in segments).strip()
|
|
45
|
+
return {"text": text, "language": info.language}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Local text-to-speech sidecar for claude-phone.
|
|
3
|
+
Wraps Piper behind a tiny HTTP API so voice-app (Node) can call it the same
|
|
4
|
+
way it used to call ElevenLabs — except fully offline.
|
|
5
|
+
|
|
6
|
+
POST /speak json: {"text": "...", "voice": "en_US-lessac-medium"}
|
|
7
|
+
-> raw WAV bytes
|
|
8
|
+
|
|
9
|
+
GET /voices -> {"voices": ["en_US-lessac-medium", ...]} (installed models)
|
|
10
|
+
GET /health -> {"status": "ok"}
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import glob
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import tempfile
|
|
17
|
+
|
|
18
|
+
from fastapi import FastAPI, Response
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
VOICES_DIR = os.environ.get("PIPER_VOICES_DIR", "/voices")
|
|
22
|
+
DEFAULT_VOICE = os.environ.get("PIPER_VOICE", "en_US-lessac-medium")
|
|
23
|
+
|
|
24
|
+
app = FastAPI(title="claude-phone local TTS")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class SpeakRequest(BaseModel):
|
|
28
|
+
text: str
|
|
29
|
+
voice: str | None = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def voice_paths(voice: str):
|
|
33
|
+
model = os.path.join(VOICES_DIR, f"{voice}.onnx")
|
|
34
|
+
config = os.path.join(VOICES_DIR, f"{voice}.onnx.json")
|
|
35
|
+
return model, config
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@app.get("/health")
|
|
39
|
+
def health():
|
|
40
|
+
return {"status": "ok"}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@app.get("/voices")
|
|
44
|
+
def voices():
|
|
45
|
+
names = sorted(
|
|
46
|
+
os.path.splitext(os.path.basename(p))[0]
|
|
47
|
+
for p in glob.glob(os.path.join(VOICES_DIR, "*.onnx"))
|
|
48
|
+
)
|
|
49
|
+
return {"voices": names}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@app.post("/speak")
|
|
53
|
+
def speak(req: SpeakRequest):
|
|
54
|
+
voice = req.voice or DEFAULT_VOICE
|
|
55
|
+
model, config = voice_paths(voice)
|
|
56
|
+
|
|
57
|
+
if not os.path.exists(model):
|
|
58
|
+
return Response(
|
|
59
|
+
content=f"Voice model not found: {model}. Download it into {VOICES_DIR}/ "
|
|
60
|
+
f"(see README) and restart tts-local.".encode(),
|
|
61
|
+
status_code=404,
|
|
62
|
+
media_type="text/plain",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as tf:
|
|
66
|
+
out_path = tf.name
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
subprocess.run(
|
|
70
|
+
["piper", "--model", model, "--config", config, "--output_file", out_path],
|
|
71
|
+
input=req.text.encode("utf-8"),
|
|
72
|
+
check=True,
|
|
73
|
+
stdout=subprocess.DEVNULL,
|
|
74
|
+
stderr=subprocess.PIPE,
|
|
75
|
+
)
|
|
76
|
+
with open(out_path, "rb") as f:
|
|
77
|
+
wav_bytes = f.read()
|
|
78
|
+
return Response(content=wav_bytes, media_type="audio/wav")
|
|
79
|
+
finally:
|
|
80
|
+
try:
|
|
81
|
+
os.unlink(out_path)
|
|
82
|
+
except OSError:
|
|
83
|
+
pass
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# API Contract: Device Personality Query Endpoint
|
|
2
|
+
**Version:** 1.0.0
|
|
3
|
+
**Status:** DRAFT
|
|
4
|
+
**Author:** Glenn (The Researcher)
|
|
5
|
+
**Date:** 2025-12-22
|
|
6
|
+
|
|
7
|
+
## 1. Overview
|
|
8
|
+
This specification defines the contract for the `POST /api/query` endpoint in the `voice-app` service. This endpoint allows external systems (specifically n8n automation workflows) to synchronously query specific device personalities (e.g., "Cephanie", "Morpheus") without initiating a voice call.
|
|
9
|
+
|
|
10
|
+
The primary goal is to retrieve **Structured JSON** responses from the LLM to drive conditional logic in automation flows.
|
|
11
|
+
|
|
12
|
+
## 2. Base Configuration
|
|
13
|
+
- **Host:** `http://10.70.7.81:3000` (Sippycup)
|
|
14
|
+
- **Content-Type:** `application/json`
|
|
15
|
+
- **Auth:** Internal Network Only (No token required for MVP)
|
|
16
|
+
|
|
17
|
+
## 3. Configuration Dependencies
|
|
18
|
+
The system relies on a `devices.json` configuration file to map identifiers to personalities.
|
|
19
|
+
|
|
20
|
+
**Definition (`devices.json`):**
|
|
21
|
+
```json
|
|
22
|
+
{
|
|
23
|
+
"9002": {
|
|
24
|
+
"name": "Cephanie",
|
|
25
|
+
"extension": "9002",
|
|
26
|
+
"voiceId": "gjV6HdJf5tTxacPrkBXH",
|
|
27
|
+
"systemPrompt": "You are Cephanie, Chuck's NAS. You are sassy, cynical, and technical. You hate rebooting."
|
|
28
|
+
},
|
|
29
|
+
"9003": {
|
|
30
|
+
"name": "Morpheus",
|
|
31
|
+
"extension": "9003",
|
|
32
|
+
"voiceId": "...",
|
|
33
|
+
"systemPrompt": "You are Morpheus, the wise AI overseer..."
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 4. Endpoint Specification
|
|
39
|
+
|
|
40
|
+
### `POST /api/query`
|
|
41
|
+
|
|
42
|
+
Directly queries a device personality with a text prompt and receives a text or JSON response.
|
|
43
|
+
|
|
44
|
+
#### 4.1 Request Schema
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"target": "string (required)",
|
|
49
|
+
"query": "string (required)",
|
|
50
|
+
"format": "string (optional, default: 'text')",
|
|
51
|
+
"timeout": "number (optional, default: 60)"
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**Parameters:**
|
|
56
|
+
|
|
57
|
+
| Field | Type | Required | Description | Validation |
|
|
58
|
+
|-------|------|----------|-------------|------------|
|
|
59
|
+
| `target` | string | Yes | The identifier of the device to query. Can be an **Extension** (e.g., "9002") or a **Name** (case-insensitive, e.g., "cephanie"). | Must match an entry in `devices.json`. |
|
|
60
|
+
| `query` | string | Yes | The input prompt or question for the device. | Min 1 char, Max 2000 chars. |
|
|
61
|
+
| `format` | string | No | The desired output format. Options: `text` (conversational), `json` (structured). | Enum: `['text', 'json']` |
|
|
62
|
+
| `timeout` | number | No | Request timeout in seconds. LLMs can be slow. | Max 120. |
|
|
63
|
+
|
|
64
|
+
#### 4.2 Response Schema
|
|
65
|
+
|
|
66
|
+
**Success Response (200 OK)**
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"success": true,
|
|
71
|
+
"timestamp": "2025-12-22T10:00:00.000Z",
|
|
72
|
+
"device": {
|
|
73
|
+
"name": "Cephanie",
|
|
74
|
+
"extension": "9002"
|
|
75
|
+
},
|
|
76
|
+
"response": {
|
|
77
|
+
"raw": "string",
|
|
78
|
+
"data": "object | null",
|
|
79
|
+
"format": "string"
|
|
80
|
+
},
|
|
81
|
+
"meta": {
|
|
82
|
+
"duration_ms": 1450,
|
|
83
|
+
"model": "claude-3-opus"
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Field Details:**
|
|
89
|
+
|
|
90
|
+
- `response.raw`: The exact string returned by the LLM.
|
|
91
|
+
- `response.data`:
|
|
92
|
+
- If `format="json"`, this contains the parsed JSON object.
|
|
93
|
+
- If `format="text"`, this is `null`.
|
|
94
|
+
- If JSON parsing fails, this is `null` (check `error` in logs, or `success: false` if strict).
|
|
95
|
+
|
|
96
|
+
**Error Response (4xx/5xx)**
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"success": false,
|
|
101
|
+
"error": "string (code)",
|
|
102
|
+
"message": "string (human readable)",
|
|
103
|
+
"details": "object (optional)"
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
**Common Error Codes:**
|
|
108
|
+
|
|
109
|
+
| Code | Status | Description |
|
|
110
|
+
|------|--------|-------------|
|
|
111
|
+
| `device_not_found` | 404 | The `target` did not match any configured device. |
|
|
112
|
+
| `validation_error` | 400 | Missing fields or invalid format. |
|
|
113
|
+
| `llm_timeout` | 504 | The LLM did not respond within the timeout window. |
|
|
114
|
+
| `llm_error` | 502 | The upstream Claude API server failed. |
|
|
115
|
+
| `parse_error` | 422 | `format="json"` was requested, but the LLM returned invalid JSON. |
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
## 5. Structured Output Strategy (JSON)
|
|
121
|
+
|
|
122
|
+
When `format: "json"` is requested, the `voice-app` MUST inject specific instructions into the System Prompt sent to Claude.
|
|
123
|
+
|
|
124
|
+
**System Prompt Injection:**
|
|
125
|
+
|
|
126
|
+
```text
|
|
127
|
+
[Original Device Persona]
|
|
128
|
+
|
|
129
|
+
IMPORTANT: You are currently acting as an API endpoint.
|
|
130
|
+
1. You must respond ONLY with valid JSON.
|
|
131
|
+
2. Do not include markdown formatting (like ```json).
|
|
132
|
+
3. Do not include conversational filler before or after the JSON.
|
|
133
|
+
4. Your response will be parsed by a machine.
|
|
134
|
+
|
|
135
|
+
Required JSON Structure:
|
|
136
|
+
{
|
|
137
|
+
"approved": boolean,
|
|
138
|
+
"reason": "string (short explanation)",
|
|
139
|
+
"metadata": { ...any relevant data... }
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
**Retry Logic (Internal):**
|
|
144
|
+
If the LLM returns text with markdown ticks (e.g., \`\`\`json ... \`\`\`), the `voice-app` parser should aggressively strip them before `JSON.parse()`.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
## 6. n8n Integration Guide
|
|
150
|
+
|
|
151
|
+
### 6.1 HTTP Request Node Configuration
|
|
152
|
+
|
|
153
|
+
- **Method:** POST
|
|
154
|
+
- **URL:** `http://10.70.7.81:3000/api/query`
|
|
155
|
+
- **Body Parameters:**
|
|
156
|
+
```json
|
|
157
|
+
{
|
|
158
|
+
"target": "Cephanie",
|
|
159
|
+
"query": "Is the server room temperature okay? It's currently 72F.",
|
|
160
|
+
"format": "json"
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 6.2 The IF Node (Conditional Logic)
|
|
165
|
+
|
|
166
|
+
Assuming the n8n HTTP Node outputs the API response, you can route based on the device's decision.
|
|
167
|
+
|
|
168
|
+
**Scenario:** "Ask Cephanie if I can reboot the server."
|
|
169
|
+
|
|
170
|
+
**Response Data (`body.response.data`):**
|
|
171
|
+
```json
|
|
172
|
+
{
|
|
173
|
+
"approved": false,
|
|
174
|
+
"reason": "I am currently scrubbing the filesystem. Do not touch me.",
|
|
175
|
+
"metadata": { "job_id": "scrub_291" }
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
**IF Node Expression:**
|
|
180
|
+
|
|
181
|
+
*Condition: Boolean*
|
|
182
|
+
|
|
183
|
+
```javascript
|
|
184
|
+
// Expression to check approval
|
|
185
|
+
{{ $json.body.response.data.approved }}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Fallback Expression (Safety):**
|
|
189
|
+
|
|
190
|
+
If parsing fails, default to `false`:
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
{{ $json.body.response.data ? $json.body.response.data.approved : false }}
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## 7. Example Scenarios
|
|
197
|
+
|
|
198
|
+
### Scenario A: Simple Status Check (Text)
|
|
199
|
+
**Request:**
|
|
200
|
+
```json
|
|
201
|
+
{ "target": "Morpheus", "query": "Who is currently connected?", "format": "text" }
|
|
202
|
+
```
|
|
203
|
+
**Response:**
|
|
204
|
+
```json
|
|
205
|
+
{
|
|
206
|
+
"success": true,
|
|
207
|
+
"response": {
|
|
208
|
+
"raw": "Currently, only Agent Smith is connected via the neural link.",
|
|
209
|
+
"data": null,
|
|
210
|
+
"format": "text"
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Scenario B: Automation Decision (JSON)
|
|
216
|
+
**Request:**
|
|
217
|
+
```json
|
|
218
|
+
{
|
|
219
|
+
"target": "Cephanie",
|
|
220
|
+
"query": "Alert: Disk usage at 90%. Should we expand volume?",
|
|
221
|
+
"format": "json"
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
**Response:**
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"success": true,
|
|
228
|
+
"response": {
|
|
229
|
+
"raw": "{\"approved\": true, \"reason\": \"90% is critical threshold. Expand immediately.\", \"metadata\": { \"urgent\": true }}",
|
|
230
|
+
"data": {
|
|
231
|
+
"approved": true,
|
|
232
|
+
"reason": "90% is critical threshold. Expand immediately.",
|
|
233
|
+
"metadata": { "urgent": true }
|
|
234
|
+
},
|
|
235
|
+
"format": "json"
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
```
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# Deployment
|
|
2
|
+
|
|
3
|
+
Running claude-phone-local beyond a first test. For the quickstart see
|
|
4
|
+
[../README.md](../README.md); for architecture see [../CLAUDE.md](../CLAUDE.md).
|
|
5
|
+
|
|
6
|
+
## Topology
|
|
7
|
+
|
|
8
|
+
Two moving parts:
|
|
9
|
+
|
|
10
|
+
| Where | What | Why there |
|
|
11
|
+
|---|---|---|
|
|
12
|
+
| Container | drachtio, FreeSWITCH, voice-app, STT, TTS | one image, supervisord, all loopback |
|
|
13
|
+
| Host | `claude-api-server` → `claude` CLI | the CLI is installed and authenticated on the host |
|
|
14
|
+
|
|
15
|
+
They meet at exactly one place: `host.docker.internal:3333`.
|
|
16
|
+
|
|
17
|
+
## Ports
|
|
18
|
+
|
|
19
|
+
| Port | Proto | Published | Purpose |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| 5070 | UDP/TCP | yes | drachtio SIP — the PBX talks to this |
|
|
22
|
+
| 30000–30100 | UDP | yes | RTP media |
|
|
23
|
+
| 3000 | TCP | yes | voice-app HTTP API |
|
|
24
|
+
| 5080 | UDP/TCP | **no** | FreeSWITCH SIP — internal only |
|
|
25
|
+
| 8021 | TCP | **no** | FreeSWITCH ESL — internal only |
|
|
26
|
+
| 9001 / 9002 | TCP | loopback | STT / TTS sidecars |
|
|
27
|
+
| 3333 | TCP | host | claude-api-server |
|
|
28
|
+
|
|
29
|
+
5080 and 8021 are deliberately unpublished: drachtio reaches FreeSWITCH inside
|
|
30
|
+
the container. Publishing them would expose your media server to the LAN.
|
|
31
|
+
|
|
32
|
+
RTP is **30000–30100** because the 3CX SBC uses 20000–20099; overlapping the two
|
|
33
|
+
breaks audio.
|
|
34
|
+
|
|
35
|
+
### Firewall
|
|
36
|
+
|
|
37
|
+
```powershell
|
|
38
|
+
New-NetFirewallRule -DisplayName "claude-phone SIP" -Direction Inbound `
|
|
39
|
+
-Protocol UDP -LocalPort 5070 -Action Allow
|
|
40
|
+
New-NetFirewallRule -DisplayName "claude-phone RTP" -Direction Inbound `
|
|
41
|
+
-Protocol UDP -LocalPort 30000-30100 -Action Allow
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Do **not** open 3333. See [Security](#security).
|
|
45
|
+
|
|
46
|
+
## Networking and NAT
|
|
47
|
+
|
|
48
|
+
Docker Desktop cannot do real host networking on Windows, so the compose file
|
|
49
|
+
uses a bridge with published ports. FreeSWITCH therefore cannot discover your
|
|
50
|
+
LAN address and must be told it.
|
|
51
|
+
|
|
52
|
+
The critical detail — SIP and RTP advertise **different** addresses:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
ext_rtp_ip = EXTERNAL_IP e.g. 172.16.14.225 (so the SBC reaches the media)
|
|
56
|
+
ext_sip_ip = container IP e.g. 172.18.0.2 (so drachtio reaches FreeSWITCH)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
`docker/entrypoint.sh` computes the container IP at boot. Verify after a deploy:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
docker logs claude-phone | grep "will advertise"
|
|
63
|
+
# [bootstrap] FreeSWITCH SIP will advertise 172.18.0.2; RTP will advertise 172.16.14.225
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
FreeSWITCH's `nat.auto` ACL also has to be defused — it treats `172.16.0.0/12`
|
|
67
|
+
as local, which swallows both a `172.16.x` LAN and the `172.18.x` Docker bridge.
|
|
68
|
+
The Dockerfile patches `mrf.xml` with `local-network-acl = none` at build time.
|
|
69
|
+
|
|
70
|
+
## Persistent state
|
|
71
|
+
|
|
72
|
+
Everything that must survive a restart is on the `./data` bind mount:
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
data/voices/ Piper models (~195 MB)
|
|
76
|
+
data/models/ Whisper cache (~1.9 GB)
|
|
77
|
+
data/config/ devices.json (your SIP credentials)
|
|
78
|
+
data/audio/ generated speech (scratch, safe to clear)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Back up `data/config/`. The rest re-downloads on its own.
|
|
82
|
+
|
|
83
|
+
A healthy restart is under a minute and re-downloads nothing:
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
[bootstrap] voice en_US-lessac-medium already present
|
|
87
|
+
[bootstrap] whisper model cached
|
|
88
|
+
[bootstrap] devices.json already present (left untouched)
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Running the host wrapper as a service
|
|
92
|
+
|
|
93
|
+
`claude-api-server` must stay up or calls connect and then fail to answer.
|
|
94
|
+
|
|
95
|
+
**Windows (NSSM):**
|
|
96
|
+
|
|
97
|
+
```powershell
|
|
98
|
+
nssm install claude-api "C:\Program Files\nodejs\node.exe" `
|
|
99
|
+
"C:\path\to\claude-api-server\server.js"
|
|
100
|
+
nssm set claude-api AppEnvironmentExtra CLAUDE_MODEL=claude-sonnet-5
|
|
101
|
+
nssm start claude-api
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
**Linux (systemd):** `/etc/systemd/system/claude-api.service`
|
|
105
|
+
|
|
106
|
+
```ini
|
|
107
|
+
[Unit]
|
|
108
|
+
Description=claude-phone API server
|
|
109
|
+
After=network.target
|
|
110
|
+
|
|
111
|
+
[Service]
|
|
112
|
+
Type=simple
|
|
113
|
+
User=youruser
|
|
114
|
+
WorkingDirectory=/path/to/claude-api-server
|
|
115
|
+
Environment=CLAUDE_MODEL=claude-sonnet-5
|
|
116
|
+
ExecStart=/usr/bin/node server.js
|
|
117
|
+
Restart=always
|
|
118
|
+
|
|
119
|
+
[Install]
|
|
120
|
+
WantedBy=multi-user.target
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
It must run **as the user whose Claude Code is authenticated** — it inherits
|
|
124
|
+
that login and config.
|
|
125
|
+
|
|
126
|
+
## Resource sizing
|
|
127
|
+
|
|
128
|
+
| | Idle | Per call |
|
|
129
|
+
|---|---|---|
|
|
130
|
+
| RAM | ~2.5 GB (Whisper `medium` resident) | +200 MB |
|
|
131
|
+
| CPU | negligible | 1–2 cores during transcription |
|
|
132
|
+
| Disk | ~2.2 GB in `./data` | a few MB of audio |
|
|
133
|
+
|
|
134
|
+
`WHISPER_MODEL` dominates memory: `small` ~1 GB, `medium` ~2.5 GB,
|
|
135
|
+
`large-v3` ~5 GB. Transcription is CPU-bound — it is the largest fixed part of
|
|
136
|
+
response latency.
|
|
137
|
+
|
|
138
|
+
## Monitoring
|
|
139
|
+
|
|
140
|
+
```bash
|
|
141
|
+
docker compose ps
|
|
142
|
+
docker logs claude-phone 2>&1 | grep -E "MULTI-REGISTRAR|CALL |error"
|
|
143
|
+
curl http://127.0.0.1:3000/api/devices # voice-app alive
|
|
144
|
+
curl http://127.0.0.1:3333/health # host wrapper alive
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Supervisord restarts any of the five processes that dies. Repeated restarts show
|
|
148
|
+
as `spawned:` lines without a matching `entered RUNNING state`.
|
|
149
|
+
|
|
150
|
+
SIP registration refreshes every ~27 minutes; losing it silently is the usual
|
|
151
|
+
cause of "calls stopped arriving".
|
|
152
|
+
|
|
153
|
+
## Security
|
|
154
|
+
|
|
155
|
+
`claude-api-server` runs the CLI with `--dangerously-skip-permissions`: no
|
|
156
|
+
prompts, every tool, full access to the host as your user, with your normal
|
|
157
|
+
`~/.claude` config including MCP servers and plugins.
|
|
158
|
+
|
|
159
|
+
- **Bind 3333 to localhost.** Docker Desktop still reaches it through
|
|
160
|
+
`host.docker.internal`. On a LAN-exposed `0.0.0.0` bind, anyone who can route
|
|
161
|
+
to the port can execute commands as you.
|
|
162
|
+
- **Never commit** `data/config/devices.json` or `.env`.
|
|
163
|
+
- Consider a dedicated 3CX extension with restricted inbound routing so only
|
|
164
|
+
your own extensions can dial it.
|
|
165
|
+
|
|
166
|
+
## Upgrading
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
git pull
|
|
170
|
+
docker compose build
|
|
171
|
+
docker compose up -d
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`./data` is untouched, so no models are re-downloaded and `devices.json` is
|
|
175
|
+
preserved. Roll back with `docker compose -f docker-compose.multi.yml up -d`
|
|
176
|
+
(the previous five-container layout) if a build regresses.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
FROM node:20-slim
|
|
2
|
+
|
|
3
|
+
WORKDIR /app
|
|
4
|
+
|
|
5
|
+
# Install dependencies first (better caching)
|
|
6
|
+
COPY package*.json ./
|
|
7
|
+
RUN npm install --production
|
|
8
|
+
|
|
9
|
+
# Copy application code
|
|
10
|
+
COPY . .
|
|
11
|
+
|
|
12
|
+
# Create audio directory
|
|
13
|
+
RUN mkdir -p /app/audio
|
|
14
|
+
|
|
15
|
+
# HTTP API port
|
|
16
|
+
EXPOSE 3000
|
|
17
|
+
# WebSocket for audio streaming
|
|
18
|
+
EXPOSE 3001
|
|
19
|
+
|
|
20
|
+
CMD ["node", "index.js"]
|