From 411df971ecd4476278e863745c262d0b46eec2b0 Mon Sep 17 00:00:00 2001 From: KoheiYamashita Date: Tue, 24 Feb 2026 15:13:27 +0900 Subject: [PATCH] refactor: remove Groq voice transcription dependency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Groq Whisper APIによる音声文字起こし機能を削除。 各チャンネル(Discord/Telegram/Slack)の音声添付処理を 汎用メディア処理に簡素化し、pkg/voiceパッケージを削除。 Co-Authored-By: Claude Opus 4.6 --- pkg/channels/discord.go | 65 +++------------- pkg/channels/slack.go | 26 +------ pkg/channels/telegram.go | 31 +------- pkg/utils/media.go | 20 ----- pkg/voice/transcriber.go | 159 --------------------------------------- 5 files changed, 16 insertions(+), 285 deletions(-) delete mode 100644 pkg/voice/transcriber.go diff --git a/pkg/channels/discord.go b/pkg/channels/discord.go index 348fb7275..3aa33e1f4 100644 --- a/pkg/channels/discord.go +++ b/pkg/channels/discord.go @@ -12,20 +12,17 @@ import ( "github.com/KarakuriAgent/clawdroid/pkg/config" "github.com/KarakuriAgent/clawdroid/pkg/logger" "github.com/KarakuriAgent/clawdroid/pkg/utils" - "github.com/KarakuriAgent/clawdroid/pkg/voice" ) const ( - transcriptionTimeout = 30 * time.Second - sendTimeout = 10 * time.Second + sendTimeout = 10 * time.Second ) type DiscordChannel struct { *BaseChannel session *discordgo.Session - config config.DiscordConfig - transcriber *voice.GroqTranscriber - ctx context.Context + config config.DiscordConfig + ctx context.Context } func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) { @@ -39,16 +36,11 @@ func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordC return &DiscordChannel{ BaseChannel: base, session: session, - config: cfg, - transcriber: nil, - ctx: context.Background(), + config: cfg, + ctx: context.Background(), }, nil } -func (c *DiscordChannel) SetTranscriber(transcriber *voice.GroqTranscriber) { - c.transcriber = transcriber -} - func (c *DiscordChannel) getContext() context.Context { if c.ctx == nil { return context.Background() @@ -319,48 +311,13 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag }() for _, attachment := range m.Attachments { - isAudio := utils.IsAudioFile(attachment.Filename, attachment.ContentType) - - if isAudio { - localPath := c.downloadAttachment(attachment.URL, attachment.Filename) - if localPath != "" { - localFiles = append(localFiles, localPath) - - transcribedText := "" - if c.transcriber != nil && c.transcriber.IsAvailable() { - ctx, cancel := context.WithTimeout(c.getContext(), transcriptionTimeout) - result, err := c.transcriber.Transcribe(ctx, localPath) - cancel() // 立即释放context资源,避免在for循环中泄漏 - - if err != nil { - logger.ErrorCF("discord", "Voice transcription failed", map[string]any{ - "error": err.Error(), - }) - transcribedText = fmt.Sprintf("[audio: %s (transcription failed)]", attachment.Filename) - } else { - transcribedText = fmt.Sprintf("[audio transcription: %s]", result.Text) - logger.DebugCF("discord", "Audio transcribed successfully", map[string]any{ - "text": result.Text, - }) - } - } else { - transcribedText = fmt.Sprintf("[audio: %s]", attachment.Filename) - } - - content = appendContent(content, transcribedText) + localPath := c.downloadAttachment(attachment.URL, attachment.Filename) + if localPath != "" { + localFiles = append(localFiles, localPath) + if dataURL := utils.EncodeFileToDataURL(localPath); dataURL != "" { + mediaPaths = append(mediaPaths, dataURL) } else { - logger.WarnCF("discord", "Failed to download audio attachment", map[string]any{ - "url": attachment.URL, - "filename": attachment.Filename, - }) - } - } else { - localPath := c.downloadAttachment(attachment.URL, attachment.Filename) - if localPath != "" { - localFiles = append(localFiles, localPath) - if dataURL := utils.EncodeFileToDataURL(localPath); dataURL != "" { - mediaPaths = append(mediaPaths, dataURL) - } + content = appendContent(content, fmt.Sprintf("[audio: %s]", attachment.Filename)) } } } diff --git a/pkg/channels/slack.go b/pkg/channels/slack.go index 6f45bf2e8..c357f5caf 100644 --- a/pkg/channels/slack.go +++ b/pkg/channels/slack.go @@ -6,7 +6,6 @@ import ( "os" "strings" "sync" - "time" "github.com/slack-go/slack" "github.com/slack-go/slack/slackevents" @@ -16,7 +15,6 @@ import ( "github.com/KarakuriAgent/clawdroid/pkg/config" "github.com/KarakuriAgent/clawdroid/pkg/logger" "github.com/KarakuriAgent/clawdroid/pkg/utils" - "github.com/KarakuriAgent/clawdroid/pkg/voice" ) type SlackChannel struct { @@ -24,9 +22,8 @@ type SlackChannel struct { config config.SlackConfig api *slack.Client socketClient *socketmode.Client - botUserID string - transcriber *voice.GroqTranscriber - ctx context.Context + botUserID string + ctx context.Context cancel context.CancelFunc pendingAcks sync.Map } @@ -58,10 +55,6 @@ func NewSlackChannel(cfg config.SlackConfig, messageBus *bus.MessageBus) (*Slack }, nil } -func (c *SlackChannel) SetTranscriber(transcriber *voice.GroqTranscriber) { - c.transcriber = transcriber -} - func (c *SlackChannel) Start(ctx context.Context) error { logger.InfoC("slack", "Starting Slack channel (Socket Mode)") @@ -252,21 +245,10 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) { } localFiles = append(localFiles, localPath) - if utils.IsAudioFile(file.Name, file.Mimetype) && c.transcriber != nil && c.transcriber.IsAvailable() { - ctx, cancel := context.WithTimeout(c.ctx, 30*time.Second) - defer cancel() - result, err := c.transcriber.Transcribe(ctx, localPath) - - if err != nil { - logger.ErrorCF("slack", "Voice transcription failed", map[string]interface{}{"error": err.Error()}) - content += fmt.Sprintf("\n[audio: %s (transcription failed)]", file.Name) - } else { - content += fmt.Sprintf("\n[voice transcription: %s]", result.Text) - } - } else if dataURL := utils.EncodeFileToDataURL(localPath); dataURL != "" { + if dataURL := utils.EncodeFileToDataURL(localPath); dataURL != "" { mediaPaths = append(mediaPaths, dataURL) } else { - content += fmt.Sprintf("\n[file: %s]", file.Name) + content += fmt.Sprintf("\n[audio: %s]", file.Name) } } } diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 24ec2e0b7..91249058c 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -21,7 +21,6 @@ import ( "github.com/KarakuriAgent/clawdroid/pkg/config" "github.com/KarakuriAgent/clawdroid/pkg/logger" "github.com/KarakuriAgent/clawdroid/pkg/utils" - "github.com/KarakuriAgent/clawdroid/pkg/voice" ) type TelegramChannel struct { @@ -30,7 +29,6 @@ type TelegramChannel struct { commands TelegramCommander config *config.Config chatIDs map[string]int64 - transcriber *voice.GroqTranscriber placeholders sync.Map // chatID -> messageID stopThinking sync.Map // chatID -> thinkingCancel } @@ -74,16 +72,11 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann bot: bot, config: cfg, chatIDs: make(map[string]int64), - transcriber: nil, placeholders: sync.Map{}, stopThinking: sync.Map{}, }, nil } -func (c *TelegramChannel) SetTranscriber(transcriber *voice.GroqTranscriber) { - c.transcriber = transcriber -} - func (c *TelegramChannel) Start(ctx context.Context) error { logger.InfoC("telegram", "Starting Telegram bot (polling mode)...") @@ -256,32 +249,10 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes localFiles = append(localFiles, voicePath) mediaPaths = append(mediaPaths, voicePath) - transcribedText := "" - if c.transcriber != nil && c.transcriber.IsAvailable() { - ctx, cancel := context.WithTimeout(ctx, 30*time.Second) - defer cancel() - - result, err := c.transcriber.Transcribe(ctx, voicePath) - if err != nil { - logger.ErrorCF("telegram", "Voice transcription failed", map[string]interface{}{ - "error": err.Error(), - "path": voicePath, - }) - transcribedText = "[voice (transcription failed)]" - } else { - transcribedText = fmt.Sprintf("[voice transcription: %s]", result.Text) - logger.InfoCF("telegram", "Voice transcribed successfully", map[string]interface{}{ - "text": result.Text, - }) - } - } else { - transcribedText = "[voice]" - } - if content != "" { content += "\n" } - content += transcribedText + content += "[voice]" } } diff --git a/pkg/utils/media.go b/pkg/utils/media.go index 194ded38d..71b42e646 100644 --- a/pkg/utils/media.go +++ b/pkg/utils/media.go @@ -68,26 +68,6 @@ func EncodeFileToDataURL(path string) string { return "data:" + mime + ";base64," + encoded } -// IsAudioFile checks if a file is an audio file based on its filename extension and content type. -func IsAudioFile(filename, contentType string) bool { - audioExtensions := []string{".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac", ".wma"} - audioTypes := []string{"audio/", "application/ogg", "application/x-ogg"} - - for _, ext := range audioExtensions { - if strings.HasSuffix(strings.ToLower(filename), ext) { - return true - } - } - - for _, audioType := range audioTypes { - if strings.HasPrefix(strings.ToLower(contentType), audioType) { - return true - } - } - - return false -} - // SanitizeFilename removes potentially dangerous characters from a filename // and returns a safe version for local filesystem storage. func SanitizeFilename(filename string) string { diff --git a/pkg/voice/transcriber.go b/pkg/voice/transcriber.go deleted file mode 100644 index f69a0000f..000000000 --- a/pkg/voice/transcriber.go +++ /dev/null @@ -1,159 +0,0 @@ -package voice - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "mime/multipart" - "net/http" - "os" - "path/filepath" - "time" - - "github.com/KarakuriAgent/clawdroid/pkg/logger" - "github.com/KarakuriAgent/clawdroid/pkg/utils" -) - -type GroqTranscriber struct { - apiKey string - apiBase string - httpClient *http.Client -} - -type TranscriptionResponse struct { - Text string `json:"text"` - Language string `json:"language,omitempty"` - Duration float64 `json:"duration,omitempty"` -} - -func NewGroqTranscriber(apiKey string) *GroqTranscriber { - logger.DebugCF("voice", "Creating Groq transcriber", map[string]interface{}{"has_api_key": apiKey != ""}) - - apiBase := "https://api.groq.com/openai/v1" - return &GroqTranscriber{ - apiKey: apiKey, - apiBase: apiBase, - httpClient: &http.Client{ - Timeout: 60 * time.Second, - }, - } -} - -func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string) (*TranscriptionResponse, error) { - logger.InfoCF("voice", "Starting transcription", map[string]interface{}{"audio_file": audioFilePath}) - - audioFile, err := os.Open(audioFilePath) - if err != nil { - logger.ErrorCF("voice", "Failed to open audio file", map[string]interface{}{"path": audioFilePath, "error": err}) - return nil, fmt.Errorf("failed to open audio file: %w", err) - } - defer audioFile.Close() - - fileInfo, err := audioFile.Stat() - if err != nil { - logger.ErrorCF("voice", "Failed to get file info", map[string]interface{}{"path": audioFilePath, "error": err}) - return nil, fmt.Errorf("failed to get file info: %w", err) - } - - logger.DebugCF("voice", "Audio file details", map[string]interface{}{ - "size_bytes": fileInfo.Size(), - "file_name": filepath.Base(audioFilePath), - }) - - var requestBody bytes.Buffer - writer := multipart.NewWriter(&requestBody) - - part, err := writer.CreateFormFile("file", filepath.Base(audioFilePath)) - if err != nil { - logger.ErrorCF("voice", "Failed to create form file", map[string]interface{}{"error": err}) - return nil, fmt.Errorf("failed to create form file: %w", err) - } - - copied, err := io.Copy(part, audioFile) - if err != nil { - logger.ErrorCF("voice", "Failed to copy file content", map[string]interface{}{"error": err}) - return nil, fmt.Errorf("failed to copy file content: %w", err) - } - - logger.DebugCF("voice", "File copied to request", map[string]interface{}{"bytes_copied": copied}) - - if err := writer.WriteField("model", "whisper-large-v3"); err != nil { - logger.ErrorCF("voice", "Failed to write model field", map[string]interface{}{"error": err}) - return nil, fmt.Errorf("failed to write model field: %w", err) - } - - if err := writer.WriteField("response_format", "json"); err != nil { - logger.ErrorCF("voice", "Failed to write response_format field", map[string]interface{}{"error": err}) - return nil, fmt.Errorf("failed to write response_format field: %w", err) - } - - if err := writer.Close(); err != nil { - logger.ErrorCF("voice", "Failed to close multipart writer", map[string]interface{}{"error": err}) - return nil, fmt.Errorf("failed to close multipart writer: %w", err) - } - - url := t.apiBase + "/audio/transcriptions" - req, err := http.NewRequestWithContext(ctx, "POST", url, &requestBody) - if err != nil { - logger.ErrorCF("voice", "Failed to create request", map[string]interface{}{"error": err}) - return nil, fmt.Errorf("failed to create request: %w", err) - } - - req.Header.Set("Content-Type", writer.FormDataContentType()) - req.Header.Set("Authorization", "Bearer "+t.apiKey) - - logger.DebugCF("voice", "Sending transcription request to Groq API", map[string]interface{}{ - "url": url, - "request_size_bytes": requestBody.Len(), - "file_size_bytes": fileInfo.Size(), - }) - - resp, err := t.httpClient.Do(req) - if err != nil { - logger.ErrorCF("voice", "Failed to send request", map[string]interface{}{"error": err}) - return nil, fmt.Errorf("failed to send request: %w", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - logger.ErrorCF("voice", "Failed to read response", map[string]interface{}{"error": err}) - return nil, fmt.Errorf("failed to read response: %w", err) - } - - if resp.StatusCode != http.StatusOK { - logger.ErrorCF("voice", "API error", map[string]interface{}{ - "status_code": resp.StatusCode, - "response": string(body), - }) - return nil, fmt.Errorf("API error (status %d): %s", resp.StatusCode, string(body)) - } - - logger.DebugCF("voice", "Received response from Groq API", map[string]interface{}{ - "status_code": resp.StatusCode, - "response_size_bytes": len(body), - }) - - var result TranscriptionResponse - if err := json.Unmarshal(body, &result); err != nil { - logger.ErrorCF("voice", "Failed to unmarshal response", map[string]interface{}{"error": err}) - return nil, fmt.Errorf("failed to unmarshal response: %w", err) - } - - logger.InfoCF("voice", "Transcription completed successfully", map[string]interface{}{ - "text_length": len(result.Text), - "language": result.Language, - "duration_seconds": result.Duration, - "transcription_preview": utils.Truncate(result.Text, 50), - }) - - return &result, nil -} - -func (t *GroqTranscriber) IsAvailable() bool { - available := t.apiKey != "" - logger.DebugCF("voice", "Checking transcriber availability", map[string]interface{}{"available": available}) - return available -}