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).
|
// Attach TTS synthesis callbacks to the message tool (enables voice=true).
|
||||||
if cfg.Tools.TTS.Enabled {
|
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() {
|
if synthesizer.IsAvailable() {
|
||||||
logger.InfoCF("voice", "TTS enabled — voice=true supported in message tool", map[string]interface{}{
|
logger.InfoCF("voice", "TTS enabled — voice=true supported in message tool", map[string]interface{}{
|
||||||
"api_base": cfg.Tools.TTS.APIBase,
|
"api_base": cfg.Tools.TTS.APIBase,
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,9 @@
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
"token": "YOUR_TELEGRAM_BOT_TOKEN",
|
||||||
"proxy": "",
|
"proxy": "",
|
||||||
"allow_from": ["YOUR_USER_ID"]
|
"allow_from": [
|
||||||
|
"YOUR_USER_ID"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"discord": {
|
"discord": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
|
@ -119,6 +121,14 @@
|
||||||
"api_key": "YOUR_BRAVE_API_KEY",
|
"api_key": "YOUR_BRAVE_API_KEY",
|
||||||
"max_results": 5
|
"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": {
|
"heartbeat": {
|
||||||
|
|
|
||||||
|
|
@ -217,9 +217,12 @@ type WhisperConfig struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type TTSConfig struct {
|
type TTSConfig struct {
|
||||||
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_TTS_ENABLED"`
|
Enabled bool `json:"enabled" env:"PICOCLAW_TOOLS_TTS_ENABLED"`
|
||||||
APIBase string `json:"api_base" env:"PICOCLAW_TOOLS_TTS_API_BASE"`
|
APIBase string `json:"api_base" env:"PICOCLAW_TOOLS_TTS_API_BASE"`
|
||||||
Voice string `json:"voice" env:"PICOCLAW_TOOLS_TTS_VOICE"`
|
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 {
|
type ToolsConfig struct {
|
||||||
|
|
@ -343,6 +346,9 @@ func DefaultConfig() *Config {
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
APIBase: "http://localhost:8100",
|
APIBase: "http://localhost:8100",
|
||||||
Voice: "en_us-lessac-medium",
|
Voice: "en_us-lessac-medium",
|
||||||
|
Model: "tts-1",
|
||||||
|
Format: "mp3",
|
||||||
|
Speed: 1.0,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Heartbeat: HeartbeatConfig{
|
Heartbeat: HeartbeatConfig{
|
||||||
|
|
|
||||||
|
|
@ -13,41 +13,75 @@ import (
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"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 {
|
type KokoroSynthesizer struct {
|
||||||
apiBase string
|
apiBase string
|
||||||
voice string
|
voice string
|
||||||
model string
|
model string
|
||||||
|
format string
|
||||||
|
speed float64
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
type kokoroRequest struct {
|
type kokoroRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Input string `json:"input"`
|
Input string `json:"input"`
|
||||||
Voice string `json:"voice"`
|
Voice string `json:"voice"`
|
||||||
Format string `json:"response_format,omitempty"`
|
Format string `json:"response_format,omitempty"`
|
||||||
|
Speed float64 `json:"speed,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewKokoroSynthesizer creates a Kokoro TTS client.
|
// TTSProfile holds the full voice profile for the synthesizer.
|
||||||
// apiBase defaults to "http://localhost:8102".
|
type TTSProfile struct {
|
||||||
// voice defaults to "af_nova".
|
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 {
|
func NewKokoroSynthesizer(apiBase, voice string) *KokoroSynthesizer {
|
||||||
if apiBase == "" {
|
return NewKokoroSynthesizerFromProfile(TTSProfile{
|
||||||
apiBase = "http://localhost:8102"
|
APIBase: 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 voice == "" {
|
if p.Voice == "" {
|
||||||
voice = "af_nova"
|
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 Kokoro TTS synthesizer", map[string]interface{}{
|
logger.InfoCF("voice", "Creating TTS synthesizer", map[string]interface{}{
|
||||||
"api_base": apiBase,
|
"api_base": p.APIBase,
|
||||||
"voice": voice,
|
"voice": p.Voice,
|
||||||
|
"model": p.Model,
|
||||||
|
"format": p.Format,
|
||||||
|
"speed": p.Speed,
|
||||||
})
|
})
|
||||||
|
|
||||||
return &KokoroSynthesizer{
|
return &KokoroSynthesizer{
|
||||||
apiBase: apiBase,
|
apiBase: p.APIBase,
|
||||||
voice: voice,
|
voice: p.Voice,
|
||||||
model: "kokoro",
|
model: p.Model,
|
||||||
|
format: p.Format,
|
||||||
|
speed: p.Speed,
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 60 * time.Second,
|
Timeout: 60 * time.Second,
|
||||||
},
|
},
|
||||||
|
|
@ -66,7 +100,8 @@ func (s *KokoroSynthesizer) Synthesize(ctx context.Context, text string) (string
|
||||||
Model: s.model,
|
Model: s.model,
|
||||||
Input: text,
|
Input: text,
|
||||||
Voice: s.voice,
|
Voice: s.voice,
|
||||||
Format: "mp3",
|
Format: s.format,
|
||||||
|
Speed: s.speed,
|
||||||
}
|
}
|
||||||
|
|
||||||
bodyBytes, err := json.Marshal(reqBody)
|
bodyBytes, err := json.Marshal(reqBody)
|
||||||
|
|
@ -93,7 +128,7 @@ func (s *KokoroSynthesizer) Synthesize(ctx context.Context, text string) (string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write audio to temp file
|
// Write audio to temp file
|
||||||
tmpFile, err := os.CreateTemp("", "picoclaw-tts-*.mp3")
|
tmpFile, err := os.CreateTemp("", "picoclaw-tts-*."+s.format)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to create temp audio file: %w", err)
|
return "", fmt.Errorf("failed to create temp audio file: %w", err)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue