Documentation
¶
Overview ¶
Package phone is the voice connector: the user talks to Factor on a real phone call, and Factor can call or text back. The voice shell is Patter (Python), supervised exactly like the smrti memory sidecar; Factor itself is the brain, plugged in through Patter's CustomLLM seam as an OpenAI-compatible endpoint on loopback. SMS goes straight to the carrier's REST API from Go. Config section:
"channels": {"phone": {"user_number": "+15550001234", ...}}
Everything is optional: no channels.phone section, nothing runs.
Index ¶
- Constants
- func EnsurePatter(ctx context.Context, home string, autoInstall bool, progress Progress) (path string, installed bool, err error)
- func FindSpeechPython(home string) (string, bool)
- func FindVoiceShellPython(home string) (string, bool)
- func Install(ctx context.Context, home string, progress Progress) (string, error)
- func ProbeSpeechServer(ctx context.Context, baseURL string) error
- func SpeechBaseURL(cfg SpeechConfig) string
- func SpeechVenvDir(home string) string
- func VenvDir(home string) string
- func WriteScript(path string) error
- func WriteSpeechScript(path string) error
- type AudioEndpoint
- type Config
- type Phone
- func (p *Phone) BindTurnRunner(run channel.TurnFunc)
- func (p *Phone) Down() string
- func (p *Phone) Healthy() bool
- func (p *Phone) MaxMessageLength() int
- func (p *Phone) Name() string
- func (p *Phone) Send(ctx context.Context, msg bus.OutboundMessage) error
- func (p *Phone) Start(ctx context.Context) error
- func (p *Phone) Stop() error
- func (p *Phone) Tier() string
- func (p *Phone) Toolset() []tools.Tool
- type Progress
- type SpeechChoices
- type SpeechConfig
- type SpeechServer
- type Status
- type TurnFunc
Constants ¶
const ( // PackageSpec pins the Patter release the embedded shell script is written // against. Bump it together with the script. PackageSpec = "getpatter==0.6.2" // PackageName is the distribution the venv is probed for. PackageName = "getpatter" // MinPythonMinor is the oldest Python 3.x minor Patter supports. MinPythonMinor = 11 // InstallHint is what a user is told to run when the automatic install is // off or has failed. InstallHint = "python3 -m venv ~/.factor/voice-venv && ~/.factor/voice-venv/bin/pip install " + PackageSpec // InstallTimeout bounds one install attempt (the wheels are chunky). InstallTimeout = 15 * time.Minute )
const ( // SpeechInstallTimeout bounds one install. The wheels run to a few hundred // megabytes and the model weights follow them, on whatever connection the // user has. SpeechInstallTimeout = 30 * time.Minute )
Variables ¶
This section is empty.
Functions ¶
func EnsurePatter ¶
func EnsurePatter(ctx context.Context, home string, autoInstall bool, progress Progress) (path string, installed bool, err error)
EnsurePatter returns the voice shell interpreter, installing Patter when it is missing and allowed. installed reports whether this call did the work.
func FindSpeechPython ¶
FindSpeechPython returns the interpreter that can run the speech server — the private venv, once both engines are actually installed in it. A venv that exists but cannot import them is not usable: reporting it as ready would make the supervisor restart-loop against an ImportError.
func FindVoiceShellPython ¶
FindVoiceShellPython returns the interpreter that can run the voice shell: the private venv, once Patter is actually installed in it. A venv that exists but has no Patter is not usable — reporting it as ready would make the supervisor restart-loop against an ImportError.
func Install ¶
Install builds the private venv and installs the pinned Patter release into it, returning the interpreter to run the voice shell with.
func ProbeSpeechServer ¶ added in v0.16.0
ProbeSpeechServer reports whether an OpenAI-compatible speech server is answering at baseURL. Any HTTP response counts.
func SpeechBaseURL ¶ added in v0.16.0
func SpeechBaseURL(cfg SpeechConfig) string
SpeechBaseURL is the endpoint the managed server answers on for a given configuration — what an empty base_url on a local tier resolves to.
func SpeechVenvDir ¶
SpeechVenvDir is the private virtualenv the speech server runs in. It is deliberately not the voice shell's: the speech engines drag in a large, version-sensitive native stack, and a resolver conflict there must not be able to take the phone down with it.
func WriteScript ¶
WriteScript materializes the embedded voice shell next to the config, and refreshes it whenever Factor is upgraded. The script is the only Patter-facing surface in the whole channel, so keeping it on disk also makes it inspectable when a call misbehaves.
func WriteSpeechScript ¶
WriteSpeechScript materializes the embedded speech server, refreshing it whenever Factor is upgraded.
Types ¶
type AudioEndpoint ¶
type AudioEndpoint struct {
Provider string `json:"provider"`
BaseURL string `json:"base_url,omitempty"`
Model string `json:"model,omitempty"`
Voice string `json:"voice,omitempty"`
}
AudioEndpoint selects one speech provider. BaseURL is only read for local-openai, where it points at the user's own server; Model and Voice name the artifacts that server serves (a faster-whisper size, a Piper/Kokoro voice).
type Config ¶
type Config struct {
Enabled *bool `json:"enabled,omitempty"`
UserNumber string `json:"user_number"`
AllowFrom []string `json:"allow_from,omitempty"`
AllowCallTo []string `json:"allow_call_to,omitempty"`
Carrier string `json:"carrier"`
PhoneNumber string `json:"phone_number"`
TwilioAccountSID string `json:"twilio_account_sid"`
TwilioAuthToken string `json:"twilio_auth_token"`
// Telnyx credentials. TelnyxConnectionID is the Call Control Application
// the number belongs to — Factor points its webhook at wherever the voice
// shell is reachable on every boot. TelnyxPublicKey verifies the carrier's
// webhooks; the shell refuses unsigned ones, so a call without it never
// connects.
TelnyxAPIKey string `json:"telnyx_api_key"`
TelnyxConnectionID string `json:"telnyx_connection_id"`
TelnyxPublicKey string `json:"telnyx_public_key"`
ElevenLabsAPIKey string `json:"elevenlabs_api_key"`
VoiceID string `json:"voice_id,omitempty"`
Language string `json:"language"`
STT AudioEndpoint `json:"stt"`
STTAPIKey string `json:"stt_api_key"`
TTS AudioEndpoint `json:"tts"`
// SpeechServer tunes the local speech server Factor runs itself. It is
// what an empty base_url on a local tier resolves to: choosing a local
// tier is a request for local speech, not for a server to go and install.
SpeechServer SpeechConfig `json:"speech_server,omitempty"`
// LocalAudioFallback falls back to the cloud tier when a configured local
// speech server is unreachable at startup, instead of failing every call.
// nil means enabled.
LocalAudioFallback *bool `json:"local_audio_fallback,omitempty"`
Proactive string `json:"proactive"`
MaxCallMinutes int `json:"max_call_minutes"`
Tunnel string `json:"tunnel"`
WebhookURL string `json:"webhook_url,omitempty"`
SidecarPort int `json:"sidecar_port"`
BridgePort int `json:"bridge_port"`
// Command overrides the Python interpreter that runs the voice shell.
// AutoInstall (nil means on) creates a private venv and installs Patter.
Command string `json:"command,omitempty"`
AutoInstall *bool `json:"auto_install,omitempty"`
// Test overrides, following the Telegram precedent: the carrier's REST base
// and the voice shell's control API. A control_api_base skips spawning the
// sidecar entirely and talks to whatever is already listening there.
APIBase string `json:"api_base,omitempty"`
ControlAPIBase string `json:"control_api_base,omitempty"`
}
Config is the channels.phone section. Every secret is a top-level string so the config layer's (non-recursive) secret scrubber redacts it from tool output for free — do not nest them.
type Phone ¶
type Phone struct {
// contains filtered or unexported fields
}
Phone is the voice connector. Unlike the other channels it does not publish inbound turns onto the bus: a phone call is synchronous, so the bridge runs each turn directly and hands the reply straight back to the voice shell. The bus is still used for the one thing that is genuinely asynchronous — a finished outbound call reporting back to whoever asked for it.
func New ¶
func New(cfg Config, b *bus.MessageBus) (*Phone, error)
New builds the connector from an already-decoded config section.
func (*Phone) BindTurnRunner ¶
BindTurnRunner attaches the agent loop. The gateway calls this after building channels; without it the bridge answers 503 and calls are refused rather than silently dropped.
func (*Phone) Healthy ¶
Healthy reports whether the voice shell is answering; Down explains why not.
func (*Phone) MaxMessageLength ¶
func (*Phone) Send ¶
Send delivers a bus message — a cron result, a finished job, anything the agent wants to say when the user is not on the line. How it arrives is the user's choice: a text (default), a phone call, or nothing at all.
type SpeechChoices ¶
type SpeechChoices struct {
SttEngine string `json:"stt_engine"`
SttModel string `json:"stt_model"`
WhisperModel string `json:"whisper_model"`
WhisperDevice string `json:"whisper_device"`
WhisperCompute string `json:"whisper_compute"`
PiperVoice string `json:"piper_voice"`
// Warning is what the installer wants the user to know about the stack it
// just built — a machine with no GPU gets a slower, less accurate
// transcriber, and finding that out on a call is worse than being told.
Warning string `json:"warning,omitempty"`
}
SpeechChoices is what the installer settled on, which Factor writes back into the config so a call never has to rediscover it.
func InstallSpeech ¶
func InstallSpeech(ctx context.Context, home, language string, cfg SpeechConfig, needSTT, needTTS bool, progress Progress) (SpeechChoices, error)
InstallSpeech builds the virtualenv, installs the engines, and downloads the weights for a language — everything a local tier needs before it can take a call. needSTT and needTTS follow the tier, so a user who only wanted local transcription does not wait on a voice download.
func PrepareSpeech ¶
func PrepareSpeech(ctx context.Context, home, language string, cfg SpeechConfig, needSTT, needTTS bool, progress Progress) (SpeechChoices, error)
PrepareSpeech downloads the weights and reports what the installer chose for this machine and language. It is separate from the virtualenv build so a language change re-downloads a voice without reinstalling everything.
func (SpeechChoices) Summary ¶
func (c SpeechChoices) Summary() string
Summary describes the installed stack in one line, for the wizard and status.
type SpeechConfig ¶
type SpeechConfig struct {
Port int `json:"port,omitempty"`
// SttEngine picks the transcription engine: "parakeet" (the TDT
// transducer, best accuracy a CPU can afford, 25 languages) or "whisper"
// (every language). Blank lets the installer choose for this machine and
// language. Setting whisper_model alone also forces Whisper, so a config
// written before engines existed keeps meaning what it meant.
SttEngine string `json:"stt_engine,omitempty"`
// SttModel names the Parakeet model. Blank means the installer's default
// (nemo-parakeet-tdt-0.6b-v3).
SttModel string `json:"stt_model,omitempty"`
// WhisperModel is a faster-whisper size ("tiny", "base", "small", …) or a
// Hugging Face repo. Blank lets the installer choose for this machine.
WhisperModel string `json:"whisper_model,omitempty"`
WhisperDevice string `json:"whisper_device,omitempty"`
WhisperCompute string `json:"whisper_compute,omitempty"`
// PiperVoice names the voice, e.g. "es_MX-ald-medium". Blank resolves one
// from the language against Piper's catalogue.
PiperVoice string `json:"piper_voice,omitempty"`
// DataDir holds the downloaded weights. Blank means ~/.factor/speech.
DataDir string `json:"data_dir,omitempty"`
Command string `json:"command,omitempty"`
AutoInstall *bool `json:"auto_install,omitempty"`
}
SpeechConfig is channels.phone.speech_server: the knobs on the speech server Factor manages. Every one is optional — the installer picks defaults that suit the machine and the language, and writes them back here.
type SpeechServer ¶ added in v0.16.0
type SpeechServer struct {
// contains filtered or unexported fields
}
SpeechServer supervises one managed local speech server for a channel other than the phone: spawn, health-poll, restart with backoff, install on demand.
func NewSpeechServer ¶ added in v0.16.0
func NewSpeechServer(cfg SpeechConfig, home, language, token string, needSTT, needTTS bool) *SpeechServer
NewSpeechServer builds a supervisor for the server described by cfg. The token is the caller's boot secret; needSTT and needTTS decide which weights the server loads.
func (*SpeechServer) Down ¶ added in v0.16.0
func (s *SpeechServer) Down() string
func (*SpeechServer) Healthy ¶ added in v0.16.0
func (s *SpeechServer) Healthy() bool
Healthy reports whether the server is answering; Down explains why not.
func (*SpeechServer) SetProbeInterval ¶ added in v0.16.0
func (s *SpeechServer) SetProbeInterval(d time.Duration)
SetProbeInterval shortens the health-poll cadence. It exists for tests in other packages, which cannot reach the supervisor's unexported field.
func (*SpeechServer) Start ¶ added in v0.16.0
func (s *SpeechServer) Start(ctx context.Context)
Start begins supervising; it returns immediately.
func (*SpeechServer) Stop ¶ added in v0.16.0
func (s *SpeechServer) Stop()
Stop shuts the server down and waits for the supervisor to exit.
func (*SpeechServer) WaitHealthy ¶ added in v0.16.0
WaitHealthy blocks until the server answers or the timeout passes — model loading takes tens of seconds, and probing earlier reads as an outage.
type Status ¶
type Status struct {
Configured bool
Enabled bool
Number string
Tier string
Python string // interpreter the voice shell runs in; "" when not installed
Healthy bool
Problem string
// Speech describes the local speech stack when Factor runs one: what is
// installed, and whether it is answering. A local tier whose server is
// down still takes calls — on the cloud tier — so this is the difference
// between "quieter than you asked for" and "broken".
Speech string
SpeechInstalled bool
SpeechHealthy bool
}
Status is what `factor status` reports about the voice channel: whether it is configured, which speech tier it runs, whether the voice shell is installed, and whether it is answering right now.