add tts tool

This commit is contained in:
Huaaudio 2026-03-23 14:15:51 +01:00
parent 9fe634895e
commit 4b6fd66e04
5 changed files with 141 additions and 0 deletions

View file

@ -522,6 +522,9 @@
"read_file": {
"enabled": true
},
"send_tts": {
"enabled": false
},
"spawn": {
"enabled": true
},

View file

@ -31,6 +31,7 @@ import (
"github.com/sipeed/picoclaw/pkg/skills"
"github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools"
"github.com/sipeed/picoclaw/pkg/tts"
"github.com/sipeed/picoclaw/pkg/utils"
)
@ -155,6 +156,13 @@ func registerSharedTools(
provider providers.LLMProvider,
) {
allowReadPaths := buildAllowReadPatterns(cfg)
var ttsProvider tts.TTSProvider
if cfg.Tools.IsToolEnabled("send_tts") {
ttsProvider = tts.DetectTTS(cfg)
if ttsProvider == nil {
logger.WarnCF("voice-tts", "send_tts enabled but no TTS provider configured", nil)
}
}
for _, agentID := range registry.ListAgentIDs() {
agent, ok := registry.GetAgent(agentID)
@ -251,6 +259,10 @@ func registerSharedTools(
agent.Tools.Register(sendFileTool)
}
if ttsProvider != nil {
agent.Tools.Register(tools.NewSendTTSTool(ttsProvider, nil))
}
// Skill discovery and installation tools
skills_enabled := cfg.Tools.IsToolEnabled("skills")
find_skills_enable := cfg.Tools.IsToolEnabled("find_skills")
@ -1038,6 +1050,11 @@ func (al *AgentLoop) SetMediaStore(s media.MediaStore) {
sf.SetMediaStore(s)
}
})
registry.ForEachTool("send_tts", func(t tools.Tool) {
if st, ok := t.(*tools.SendTTSTool); ok {
st.SetMediaStore(s)
}
})
}
// SetTranscriber injects a voice transcriber for agent-level audio transcription.

View file

@ -1218,6 +1218,7 @@ type ToolsConfig struct {
Message ToolConfig `json:"message" envPrefix:"PICOCLAW_TOOLS_MESSAGE_"`
ReadFile ReadFileToolConfig `json:"read_file" envPrefix:"PICOCLAW_TOOLS_READ_FILE_"`
SendFile ToolConfig `json:"send_file" envPrefix:"PICOCLAW_TOOLS_SEND_FILE_"`
SendTTS ToolConfig `json:"send_tts" envPrefix:"PICOCLAW_TOOLS_SEND_TTS_"`
Spawn ToolConfig `json:"spawn" envPrefix:"PICOCLAW_TOOLS_SPAWN_"`
SpawnStatus ToolConfig `json:"spawn_status" envPrefix:"PICOCLAW_TOOLS_SPAWN_STATUS_"`
SPI ToolConfig `json:"spi" envPrefix:"PICOCLAW_TOOLS_SPI_"`
@ -2156,6 +2157,8 @@ func (t *ToolsConfig) IsToolEnabled(name string) bool {
return t.WebFetch.Enabled
case "send_file":
return t.SendFile.Enabled
case "send_tts":
return t.SendTTS.Enabled
case "write_file":
return t.WriteFile.Enabled
case "mcp":

View file

@ -460,6 +460,9 @@ func DefaultConfig() *Config {
SendFile: ToolConfig{
Enabled: true,
},
SendTTS: ToolConfig{
Enabled: false,
},
MCP: MCPConfig{
ToolConfig: ToolConfig{
Enabled: false,

115
pkg/tools/tts_send.go Normal file
View file

@ -0,0 +1,115 @@
package tools
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/sipeed/picoclaw/pkg/media"
"github.com/sipeed/picoclaw/pkg/tts"
)
type SendTTSTool struct {
provider tts.TTSProvider
mediaStore media.MediaStore
}
func NewSendTTSTool(provider tts.TTSProvider, store media.MediaStore) *SendTTSTool {
return &SendTTSTool{
provider: provider,
mediaStore: store,
}
}
func (t *SendTTSTool) Name() string { return "send_tts" }
func (t *SendTTSTool) Description() string {
return "Synthesize speech from text and send it as an audio file to the user."
}
func (t *SendTTSTool) Parameters() map[string]any {
return map[string]any{
"type": "object",
"properties": map[string]any{
"text": map[string]any{
"type": "string",
"description": "The text to synthesize into speech.",
},
"filename": map[string]any{
"type": "string",
"description": "Optional filename for the audio file (e.g., response.ogg).",
},
},
"required": []string{"text"},
}
}
func (t *SendTTSTool) SetMediaStore(store media.MediaStore) {
t.mediaStore = store
}
func (t *SendTTSTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
text, _ := args["text"].(string)
text = strings.TrimSpace(text)
if text == "" {
return ErrorResult("text is required")
}
if t.provider == nil {
return ErrorResult("tts provider is not configured")
}
if t.mediaStore == nil {
return ErrorResult("media store not configured")
}
channel := ToolChannel(ctx)
chatID := ToolChatID(ctx)
if channel == "" || chatID == "" {
return ErrorResult("no target channel/chat available")
}
stream, err := t.provider.Synthesize(ctx, text)
if err != nil {
return ErrorResult(fmt.Sprintf("tts synthesize failed: %v", err)).WithError(err)
}
defer stream.Close()
if err := os.MkdirAll(media.TempDir(), 0o755); err != nil {
return ErrorResult(fmt.Sprintf("failed to create media temp dir: %v", err)).WithError(err)
}
file, err := os.CreateTemp(media.TempDir(), "tts-*.ogg")
if err != nil {
return ErrorResult(fmt.Sprintf("failed to create temp file: %v", err)).WithError(err)
}
defer file.Close()
if _, err := io.Copy(file, stream); err != nil {
return ErrorResult(fmt.Sprintf("failed to write tts audio: %v", err)).WithError(err)
}
filename, _ := args["filename"].(string)
filename = strings.TrimSpace(filename)
if filename == "" {
filename = fmt.Sprintf("tts-%d.ogg", time.Now().Unix())
}
if filepath.Ext(filename) == "" {
filename += ".ogg"
}
scope := fmt.Sprintf("tool:send_tts:%s:%s:%d", channel, chatID, time.Now().UnixNano())
ref, err := t.mediaStore.Store(file.Name(), media.MediaMeta{
Filename: filename,
ContentType: "audio/ogg",
Source: "tool:send_tts",
}, scope)
if err != nil {
return ErrorResult(fmt.Sprintf("failed to register audio: %v", err)).WithError(err)
}
return MediaResult("TTS audio sent", []string{ref})
}