Skip to content
Open
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
33 changes: 33 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,39 @@ CALLME_USER_PHONE_NUMBER=+1234567890
# OpenAI API key (required for speech-to-text and text-to-speech)
CALLME_OPENAI_API_KEY=sk-xxx

# ===================
# Telnyx STT/TTS (optional)
# ===================
# When CALLME_PHONE_PROVIDER=telnyx, you can use Telnyx for speech-to-text
# and text-to-speech too, reusing the same API key. This avoids the need
# for a separate OpenAI account for speech services.
#
# STT: streaming transcription via wss://api.telnyx.com/v2/speech-to-text/transcription
# TTS: streaming synthesis via wss://api.telnyx.com/v2/text-to-speech/speech
# (audio format: linear16 PCM at 24kHz mono; phone bridge resamples to 8kHz mu-law)
#
# Set CALLME_STT_PROVIDER=telnyx and/or CALLME_TTS_PROVIDER=telnyx to enable.
# If CALLME_PHONE_PROVIDER=telnyx, STT defaults to telnyx automatically.

# CALLME_STT_PROVIDER=telnyx
# CALLME_TTS_PROVIDER=telnyx

# Telnyx API key for STT/TTS. Defaults to CALLME_PHONE_AUTH_TOKEN when omitted.
# CALLME_TELNYX_API_KEY=your_telnyx_api_key

# STT: transcription engine (Telnyx, Deepgram, Google, Azure). Default: Telnyx
# CALLME_TELNYX_STT_ENGINE=Telnyx

# STT: audio input format (mulaw, linear16, alaw). Default: mulaw (matches phone bridge)
# CALLME_TELNYX_STT_INPUT_FORMAT=mulaw

# STT: language code (BCP-47, e.g. en-US). Default: en-US
# CALLME_TELNYX_STT_LANGUAGE=en-US

# TTS: voice ID. See https://api.telnyx.com/v2/text-to-speech/voices for the full catalog.
# Default: AWS.Polly.Matthew-Neural (en-US male)
# CALLME_TELNYX_TTS_VOICE=AWS.Polly.Matthew-Neural

# ===================
# ngrok
# ===================
Expand Down
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ CALLME_PHONE_AUTH_TOKEN=<Auth Token>

### 3. Set Environment Variables


Add these to `~/.claude/settings.json` (recommended) or export them in your shell:

```json
Expand All @@ -91,21 +92,27 @@ Add these to `~/.claude/settings.json` (recommended) or export them in your shel
| `CALLME_PHONE_AUTH_TOKEN` | Telnyx API Key or Twilio Auth Token |
| `CALLME_PHONE_NUMBER` | Phone number Claude calls from (E.164 format) |
| `CALLME_USER_PHONE_NUMBER` | Your phone number to receive calls |
| `CALLME_OPENAI_API_KEY` | OpenAI API key (required for STT; also for TTS unless using Kokoro) |
| `CALLME_OPENAI_API_KEY` | OpenAI API key (required for OpenAI STT/TTS; not required when using Telnyx for both STT and TTS) |
| `CALLME_NGROK_AUTHTOKEN` | ngrok auth token for webhook tunneling |

#### Optional Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `CALLME_TTS_PROVIDER` | `openai` | TTS engine: `openai` or `kokoro` (free, local — see [Kokoro TTS](#kokoro-tts-free-local)) |
| `CALLME_TTS_PROVIDER` | `openai` | TTS engine: `openai`, `kokoro` (free, local — see [Kokoro TTS](#kokoro-tts-free-local)), or `telnyx` (reuses Telnyx API key) |
| `CALLME_STT_PROVIDER` | `telnyx`* | STT engine: `openai-realtime` or `telnyx` (reuses Telnyx API key). *Defaults to `telnyx` when `CALLME_PHONE_PROVIDER=telnyx` |
| `CALLME_TTS_VOICE` | `onyx` / `af_bella` | Voice name (default depends on TTS provider) |
| `CALLME_KOKORO_URL` | - | URL of existing Kokoro instance (e.g. `http://localhost:8880/v1`). If unset, auto-starts Docker container |
| `CALLME_PORT` | `0` (auto) | Local HTTP server port (0 = OS picks a free port) |
| `CALLME_NGROK_DOMAIN` | - | Custom ngrok domain (paid feature) |
| `CALLME_TRANSCRIPT_TIMEOUT_MS` | `180000` | Timeout for user speech (3 minutes) |
| `CALLME_STT_SILENCE_DURATION_MS` | `800` | Silence duration to detect end of speech |
| `CALLME_TELNYX_PUBLIC_KEY` | - | Telnyx public key for webhook signature verification (recommended) |
| `CALLME_TELNYX_STT_ENGINE` | `Telnyx` | Telnyx STT engine: `Telnyx`, `Deepgram`, `Google`, or `Azure` |
| `CALLME_TELNYX_STT_INPUT_FORMAT` | `mulaw` | Telnyx STT audio format: `mulaw`, `linear16`, or `alaw` |
| `CALLME_TELNYX_STT_LANGUAGE` | `en-US` | Telnyx STT language (BCP-47 code) |
| `CALLME_TELNYX_TTS_VOICE` | `AWS.Polly.Matthew-Neural` | Telnyx TTS voice ID (see `/v2/text-to-speech/voices` for catalog) |
| `CALLME_TELNYX_API_KEY` | - | Telnyx API key for STT/TTS (defaults to `CALLME_PHONE_AUTH_TOKEN`) |

### 4. Install Plugin

Expand Down
54 changes: 50 additions & 4 deletions server/src/providers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,28 @@
* Supports Telnyx or Twilio for phone, OpenAI for TTS and Realtime STT.
*/

import type { PhoneProvider, TTSProvider, RealtimeSTTProvider, ProviderRegistry } from './types.js';
import type { PhoneProvider, TTSProvider, RealtimeSTTProvider, ProviderRegistry, STTConfig } from './types.js';
import { TelnyxPhoneProvider } from './phone-telnyx.js';
import { TwilioPhoneProvider } from './phone-twilio.js';
import { OpenAITTSProvider } from './tts-openai.js';
import { KokoroTTSProvider } from './tts-kokoro.js';
import { OpenAIRealtimeSTTProvider } from './stt-openai-realtime.js';
import { TelnyxSTTProvider } from './stt-telnyx.js';
import { TelnyxTTSProvider } from './tts-telnyx.js';

export * from './types.js';

export type PhoneProviderType = 'telnyx' | 'twilio';
export type TTSProviderType = 'openai' | 'kokoro';
export type STTProviderType = 'openai-realtime' | 'telnyx';
export type TTSProviderType = 'openai' | 'kokoro' | 'telnyx';

export interface ProviderConfig {
// Phone provider selection
phoneProvider: PhoneProviderType;

// STT provider selection
sttProvider: STTProviderType;

// TTS provider selection
ttsProvider: TTSProviderType;

Expand All @@ -43,6 +49,13 @@ export interface ProviderConfig {

// Kokoro TTS (when ttsProvider is 'kokoro')
kokoroUrl?: string;

// Telnyx STT/TTS (uses the same API key as the Telnyx phone provider)
telnyxApiKey?: string;
telnyxSttEngine?: string; // 'Telnyx' | 'Deepgram' | 'Google' | 'Azure'
telnyxSttInputFormat?: string; // 'mulaw' | 'linear16' | 'alaw'
telnyxSttLanguage?: string;
telnyxTtsVoice?: string;
}

export function loadProviderConfig(): ProviderConfig {
Expand All @@ -52,10 +65,12 @@ export function loadProviderConfig(): ProviderConfig {

// Default to telnyx if not specified
const phoneProvider = (process.env.CALLME_PHONE_PROVIDER || 'telnyx') as PhoneProviderType;
const sttProvider = (process.env.CALLME_STT_PROVIDER || (process.env.CALLME_PHONE_PROVIDER === 'telnyx' ? 'telnyx' : 'openai-realtime')) as STTProviderType;
const ttsProvider = (process.env.CALLME_TTS_PROVIDER || 'openai') as TTSProviderType;

return {
phoneProvider,
sttProvider,
ttsProvider,
phoneAccountSid: process.env.CALLME_PHONE_ACCOUNT_SID || '',
phoneAuthToken: process.env.CALLME_PHONE_AUTH_TOKEN || '',
Expand All @@ -66,6 +81,11 @@ export function loadProviderConfig(): ProviderConfig {
sttModel: process.env.CALLME_STT_MODEL || 'gpt-4o-transcribe',
sttSilenceDurationMs,
kokoroUrl: process.env.CALLME_KOKORO_URL,
telnyxApiKey: process.env.CALLME_TELNYX_API_KEY || process.env.CALLME_PHONE_AUTH_TOKEN,
telnyxSttEngine: process.env.CALLME_TELNYX_STT_ENGINE,
telnyxSttInputFormat: process.env.CALLME_TELNYX_STT_INPUT_FORMAT,
telnyxSttLanguage: process.env.CALLME_TELNYX_STT_LANGUAGE,
telnyxTtsVoice: process.env.CALLME_TELNYX_TTS_VOICE,
};
}

Expand All @@ -88,6 +108,15 @@ export function createPhoneProvider(config: ProviderConfig): PhoneProvider {
}

export function createTTSProvider(config: ProviderConfig): TTSProvider {
if (config.ttsProvider === 'telnyx') {
const provider = new TelnyxTTSProvider();
provider.initialize({
apiKey: config.telnyxApiKey || config.phoneAuthToken,
voice: config.telnyxTtsVoice,
});
return provider;
}

if (config.ttsProvider === 'kokoro') {
const provider = new KokoroTTSProvider();
provider.initialize({
Expand All @@ -106,6 +135,18 @@ export function createTTSProvider(config: ProviderConfig): TTSProvider {
}

export function createSTTProvider(config: ProviderConfig): RealtimeSTTProvider {
if (config.sttProvider === 'telnyx') {
const provider = new TelnyxSTTProvider();
provider.initialize({
apiKey: config.telnyxApiKey || config.phoneAuthToken,
model: config.telnyxSttLanguage,
silenceDurationMs: config.sttSilenceDurationMs,
transcriptionEngine: config.telnyxSttEngine,
inputFormat: config.telnyxSttInputFormat,
} as STTConfig);
return provider;
}

const provider = new OpenAIRealtimeSTTProvider();
provider.initialize({
apiKey: config.openaiApiKey,
Expand Down Expand Up @@ -143,8 +184,13 @@ export function validateProviderConfig(config: ProviderConfig): string[] {
if (!config.phoneNumber) {
errors.push('Missing CALLME_PHONE_NUMBER');
}
if (!config.openaiApiKey) {
errors.push('Missing CALLME_OPENAI_API_KEY (required for speech-to-text, even when using Kokoro TTS)');
// OpenAI key is required whenever any non-Telnyx STT or TTS provider is
// selected. The previous logic only errored when BOTH were non-Telnyx,
// which let mixed configs (e.g. openai-realtime STT + telnyx TTS) start
// up clean and then fail at runtime.
const needsOpenAI = config.sttProvider === 'openai-realtime' || config.ttsProvider === 'openai';
if (!config.openaiApiKey && needsOpenAI) {
errors.push('Missing CALLME_OPENAI_API_KEY (required when using OpenAI for STT or TTS)');
}

return errors;
Expand Down
Loading