From f57a8ba2615d95bc109ec0791c0990cc2017fa7e Mon Sep 17 00:00:00 2001 From: Hua Audio Date: Mon, 23 Mar 2026 15:44:14 +0100 Subject: [PATCH] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- pkg/audio/sentence.go | 2 +- pkg/channels/discord/discord.go | 25 ++++++++++++++----- pkg/channels/discord/voice.go | 43 +++++++++++++++++++++++++++------ pkg/tts/tts.go | 2 ++ pkg/voice/agent.go | 4 +-- 5 files changed, 60 insertions(+), 16 deletions(-) diff --git a/pkg/audio/sentence.go b/pkg/audio/sentence.go index c7a9b2f26..6a0c2c0b0 100644 --- a/pkg/audio/sentence.go +++ b/pkg/audio/sentence.go @@ -7,7 +7,7 @@ import ( // SplitSentences splits text into sentence-sized chunks suitable for TTS synthesis. // It splits on sentence-ending punctuation (.!?\n) while avoiding false splits -// on abbreviations and decimal numbers. Very short fragments are merged with +// on decimal numbers. Very short fragments are merged with // the next sentence to prevent choppy playback. func SplitSentences(text string) []string { if text == "" { diff --git a/pkg/channels/discord/discord.go b/pkg/channels/discord/discord.go index 1b0355503..2dea2373e 100644 --- a/pkg/channels/discord/discord.go +++ b/pkg/channels/discord/discord.go @@ -678,10 +678,18 @@ func (c *DiscordChannel) listenVoiceControl(ctx context.Context) { } func (c *DiscordChannel) playTTS(ctx context.Context, vc *discordgo.VoiceConnection, text string) { - // Clear cancelTTS when playback finishes (normal or interrupted) + // Capture the cancel func associated with this playback (if any). + c.ttsMu.Lock() + playbackCancel := c.cancelTTS + c.ttsMu.Unlock() + + // Clear cancelTTS when playback finishes (normal or interrupted), + // but only if it still refers to this playback's cancel func. defer func() { c.ttsMu.Lock() - c.cancelTTS = nil + if c.cancelTTS == playbackCancel { + c.cancelTTS = nil + } c.ttsMu.Unlock() }() @@ -700,12 +708,17 @@ func (c *DiscordChannel) playTTS(ctx context.Context, vc *discordgo.VoiceConnect var prefetch chan ttResult - // Ensure any in-flight prefetch is drained on exit to prevent stream leaks + // Ensure any in-flight prefetch is drained on exit to prevent stream leaks, + // but avoid blocking indefinitely if the prefetch goroutine is stuck or never sends. defer func() { if prefetch != nil { - result := <-prefetch - if result.stream != nil { - result.stream.Close() + select { + case result := <-prefetch: + if result.stream != nil { + result.stream.Close() + } + default: + // No prefetched result available to drain; avoid blocking on exit. } } }() diff --git a/pkg/channels/discord/voice.go b/pkg/channels/discord/voice.go index 6f9e10dfa..fdcc1adda 100644 --- a/pkg/channels/discord/voice.go +++ b/pkg/channels/discord/voice.go @@ -17,7 +17,12 @@ func (c *DiscordChannel) handleVoiceCommand(s *discordgo.Session, m *discordgo.M if m.Content == "!vc join" { vs, err := s.State.VoiceState(m.GuildID, m.Author.ID) if err != nil || vs == nil { - s.ChannelMessageSend(m.ChannelID, "You need to be in a voice channel first!") + if _, sendErr := s.ChannelMessageSend(m.ChannelID, "You need to be in a voice channel first!"); sendErr != nil { + logger.InfoCF("discord", "Failed to send voice channel requirement message", map[string]any{ + "channel": m.ChannelID, + "error": sendErr, + }) + } return true } @@ -25,21 +30,45 @@ func (c *DiscordChannel) handleVoiceCommand(s *discordgo.Session, m *discordgo.M vc, err := s.ChannelVoiceJoin(c.ctx, m.GuildID, vs.ChannelID, false, false) vc, err := s.ChannelVoiceJoin(c.ctx, m.GuildID, vs.ChannelID, false, false) if err != nil { - s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Failed to join voice channel: %v", err)) + if _, sendErr := s.ChannelMessageSend(m.ChannelID, fmt.Sprintf("Failed to join voice channel: %v", err)); sendErr != nil { + logger.InfoCF("discord", "Failed to send voice join error message", map[string]any{ + "channel": m.ChannelID, + "error": sendErr, + }) + } return true } go c.receiveVoice(vc, m.GuildID, m.ChannelID) - s.ChannelMessageSend(m.ChannelID, "Joined Voice Channel! Listening for audio...") + if _, sendErr := s.ChannelMessageSend(m.ChannelID, "Joined Voice Channel! Listening for audio..."); sendErr != nil { + logger.InfoCF("discord", "Failed to send voice join success message", map[string]any{ + "channel": m.ChannelID, + "error": sendErr, + }) + } return true } else if m.Content == "!vc leave" { vc, exists := s.VoiceConnections[m.GuildID] if exists && vc != nil { - vc.Disconnect(c.ctx) - vc.Disconnect(c.ctx) - s.ChannelMessageSend(m.ChannelID, "Left Voice Channel.") + if err := vc.Disconnect(c.ctx); err != nil { + logger.InfoCF("discord", "Failed to disconnect from voice channel", map[string]any{ + "guild": m.GuildID, + "error": err, + }) + } + if _, sendErr := s.ChannelMessageSend(m.ChannelID, "Left Voice Channel."); sendErr != nil { + logger.InfoCF("discord", "Failed to send voice leave success message", map[string]any{ + "channel": m.ChannelID, + "error": sendErr, + }) + } } else { - s.ChannelMessageSend(m.ChannelID, "Not in a voice channel.") + if _, sendErr := s.ChannelMessageSend(m.ChannelID, "Not in a voice channel."); sendErr != nil { + logger.InfoCF("discord", "Failed to send voice not-in-channel message", map[string]any{ + "channel": m.ChannelID, + "error": sendErr, + }) + } } return true } diff --git a/pkg/tts/tts.go b/pkg/tts/tts.go index b43d0bbb9..07da18bb8 100644 --- a/pkg/tts/tts.go +++ b/pkg/tts/tts.go @@ -81,6 +81,8 @@ func NewOpenAITTSProvider(apiKey string, apiBase string, proxyURL string) *OpenA client.Transport = &http.Transport{ Proxy: http.ProxyURL(pURL), } + } else { + logger.Warnf("NewOpenAITTSProvider: invalid proxy URL %q: %v; proceeding without proxy", proxyURL, err) } } diff --git a/pkg/voice/agent.go b/pkg/voice/agent.go index f3bfbea9c..a9b8cb9ec 100644 --- a/pkg/voice/agent.go +++ b/pkg/voice/agent.go @@ -244,10 +244,10 @@ func (a *Agent) processUtterance(ctx context.Context, acc *speechAccumulator) { SenderID: acc.speakerID, ChatID: acc.chatID, Content: res.Text + oralPrompt, - Content: res.Text + oralPrompt, Peer: bus.Peer{Kind: "channel", ID: acc.chatID}, Metadata: map[string]string{ - "is_voice": "true", + "is_voice": "true", + "oral_prompt": oralPrompt, }, }); err != nil { logger.ErrorCF("voice-agent", "Failed to publish inbound message", map[string]any{"error": err})