feat(voice/tts): add full voice profile to TTSConfig
Expand TTSConfig with model, format, and speed so the agent's voice is
fully configurable from config.json without touching code.
- TTSConfig gains: Model, Format, Speed fields (all env-overridable)
- KokoroSynthesizer: add TTSProfile struct + NewKokoroSynthesizerFromProfile()
NewKokoroSynthesizer() is kept as a convenience wrapper (backward-compat)
- kokoroRequest: pass format and speed through to the API
- Temp file extension follows configured format (mp3/wav/ogg/etc.)
- main.go: wire all profile fields from config
- config.example.json: updated with model/format/speed examples
Full profile example:
"tts": {
"enabled": true,
"api_base": "http://localhost:8100",
"voice": "af_nova",
"model": "kokoro",
"format": "mp3",
"speed": 1.0
}
This commit is contained in:
parent
c3a7629343
commit
1e68aa12b0
4 changed files with 83 additions and 26 deletions
|
|
@ -636,7 +636,13 @@ func gatewayCmd() {
|
|||
|
||||
// Attach TTS synthesis callbacks to the message tool (enables voice=true).
|
||||
if cfg.Tools.TTS.Enabled {
|
||||
synthesizer := voice.NewKokoroSynthesizer(cfg.Tools.TTS.APIBase, cfg.Tools.TTS.Voice)
|
||||
synthesizer := voice.NewKokoroSynthesizerFromProfile(voice.TTSProfile{
|
||||
APIBase: cfg.Tools.TTS.APIBase,
|
||||
Voice: cfg.Tools.TTS.Voice,
|
||||
Model: cfg.Tools.TTS.Model,
|
||||
Format: cfg.Tools.TTS.Format,
|
||||
Speed: cfg.Tools.TTS.Speed,
|
||||
})
|
||||
if synthesizer.IsAvailable() {
|
||||
logger.InfoCF("voice", "TTS enabled — voice=true supported in message tool", map[string]interface{}{
|
||||
"api_base": cfg.Tools.TTS.APIBase,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,9 @@
|
|||
"enabled": false,
|
||||
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||
"proxy": "",
|
||||
"allow_from": ["YOUR_USER_ID"]
|
||||
"allow_from": [
|
||||
"YOUR_USER_ID"
|
||||
]
|
||||
},
|
||||
"discord": {
|
||||
"enabled": false,
|
||||
|
|
@ -119,6 +121,14 @@
|
|||
"api_key": "YOUR_BRAVE_API_KEY",
|
||||
"max_results": 5
|
||||
}
|
||||
},
|
||||
"tts": {
|
||||
"enabled": false,
|
||||
"api_base": "http://localhost:8100",
|
||||
"voice": "en_us-lessac-medium",
|
||||
"model": "tts-1",
|
||||
"format": "mp3",
|
||||
"speed": 1.0
|
||||
}
|
||||
},
|
||||
"heartbeat": {
|
||||
|
|
|
|||
|
|
@ -220,6 +220,9 @@ type TTSConfig struct {
|
|||
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_TTS_ENABLED"`
|
||||
APIBase string `json:"api_base" env:"PICOCLAW_TOOLS_TTS_API_BASE"`
|
||||
Voice string `json:"voice" env:"PICOCLAW_TOOLS_TTS_VOICE"`
|
||||
Model string `json:"model" env:"PICOCLAW_TOOLS_TTS_MODEL"`
|
||||
Format string `json:"format" env:"PICOCLAW_TOOLS_TTS_FORMAT"`
|
||||
Speed float64 `json:"speed" env:"PICOCLAW_TOOLS_TTS_SPEED"`
|
||||
}
|
||||
|
||||
type ToolsConfig struct {
|
||||
|
|
@ -343,6 +346,9 @@ func DefaultConfig() *Config {
|
|||
Enabled: false,
|
||||
APIBase: "http://localhost:8100",
|
||||
Voice: "en_us-lessac-medium",
|
||||
Model: "tts-1",
|
||||
Format: "mp3",
|
||||
Speed: 1.0,
|
||||
},
|
||||
},
|
||||
Heartbeat: HeartbeatConfig{
|
||||
|
|
|
|||
|
|
@ -13,11 +13,14 @@ import (
|
|||
"github.com/sipeed/picoclaw/pkg/logger"
|
||||
)
|
||||
|
||||
// KokoroSynthesizer uses a Kokoro TTS server (OpenAI-compatible /v1/audio/speech API).
|
||||
// KokoroSynthesizer uses any OpenAI-compatible /v1/audio/speech endpoint
|
||||
// (Kokoro, Piper, Chatterbox, OpenAI, etc.).
|
||||
type KokoroSynthesizer struct {
|
||||
apiBase string
|
||||
voice string
|
||||
model string
|
||||
format string
|
||||
speed float64
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
|
|
@ -26,28 +29,59 @@ type kokoroRequest struct {
|
|||
Input string `json:"input"`
|
||||
Voice string `json:"voice"`
|
||||
Format string `json:"response_format,omitempty"`
|
||||
Speed float64 `json:"speed,omitempty"`
|
||||
}
|
||||
|
||||
// NewKokoroSynthesizer creates a Kokoro TTS client.
|
||||
// apiBase defaults to "http://localhost:8102".
|
||||
// voice defaults to "af_nova".
|
||||
// TTSProfile holds the full voice profile for the synthesizer.
|
||||
type TTSProfile struct {
|
||||
APIBase string
|
||||
Voice string
|
||||
Model string
|
||||
Format string
|
||||
Speed float64
|
||||
}
|
||||
|
||||
// NewKokoroSynthesizer creates a TTS client from a voice profile.
|
||||
// Sensible defaults are applied for any zero-value field.
|
||||
func NewKokoroSynthesizer(apiBase, voice string) *KokoroSynthesizer {
|
||||
if apiBase == "" {
|
||||
apiBase = "http://localhost:8102"
|
||||
}
|
||||
if voice == "" {
|
||||
voice = "af_nova"
|
||||
return NewKokoroSynthesizerFromProfile(TTSProfile{
|
||||
APIBase: apiBase,
|
||||
Voice: voice,
|
||||
})
|
||||
}
|
||||
|
||||
logger.InfoCF("voice", "Creating Kokoro TTS synthesizer", map[string]interface{}{
|
||||
"api_base": apiBase,
|
||||
"voice": voice,
|
||||
// NewKokoroSynthesizerFromProfile creates a TTS client with full profile control.
|
||||
func NewKokoroSynthesizerFromProfile(p TTSProfile) *KokoroSynthesizer {
|
||||
if p.APIBase == "" {
|
||||
p.APIBase = "http://localhost:8100"
|
||||
}
|
||||
if p.Voice == "" {
|
||||
p.Voice = "en_us-lessac-medium"
|
||||
}
|
||||
if p.Model == "" {
|
||||
p.Model = "tts-1"
|
||||
}
|
||||
if p.Format == "" {
|
||||
p.Format = "mp3"
|
||||
}
|
||||
if p.Speed == 0 {
|
||||
p.Speed = 1.0
|
||||
}
|
||||
|
||||
logger.InfoCF("voice", "Creating TTS synthesizer", map[string]interface{}{
|
||||
"api_base": p.APIBase,
|
||||
"voice": p.Voice,
|
||||
"model": p.Model,
|
||||
"format": p.Format,
|
||||
"speed": p.Speed,
|
||||
})
|
||||
|
||||
return &KokoroSynthesizer{
|
||||
apiBase: apiBase,
|
||||
voice: voice,
|
||||
model: "kokoro",
|
||||
apiBase: p.APIBase,
|
||||
voice: p.Voice,
|
||||
model: p.Model,
|
||||
format: p.Format,
|
||||
speed: p.Speed,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 60 * time.Second,
|
||||
},
|
||||
|
|
@ -66,7 +100,8 @@ func (s *KokoroSynthesizer) Synthesize(ctx context.Context, text string) (string
|
|||
Model: s.model,
|
||||
Input: text,
|
||||
Voice: s.voice,
|
||||
Format: "mp3",
|
||||
Format: s.format,
|
||||
Speed: s.speed,
|
||||
}
|
||||
|
||||
bodyBytes, err := json.Marshal(reqBody)
|
||||
|
|
@ -93,7 +128,7 @@ func (s *KokoroSynthesizer) Synthesize(ctx context.Context, text string) (string
|
|||
}
|
||||
|
||||
// Write audio to temp file
|
||||
tmpFile, err := os.CreateTemp("", "picoclaw-tts-*.mp3")
|
||||
tmpFile, err := os.CreateTemp("", "picoclaw-tts-*."+s.format)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create temp audio file: %w", err)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue