Edit from coplilot review

This commit is contained in:
Huaaudio 2026-03-21 09:31:16 +01:00
parent a3a1fdae86
commit 33109e862c
4 changed files with 18 additions and 52 deletions

View file

@ -57,6 +57,7 @@ type AudioChunk struct {
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
SpeakerID string `json:"speaker_id"` // User ID or SSRC SpeakerID string `json:"speaker_id"` // User ID or SSRC
ChatID string `json:"chat_id"` // Where to respond ChatID string `json:"chat_id"` // Where to respond
Channel string `json:"channel"` // Source channel type (e.g. "discord")
Sequence uint64 `json:"sequence"` Sequence uint64 `json:"sequence"`
Timestamp uint32 `json:"timestamp"` Timestamp uint32 `json:"timestamp"`
SampleRate int `json:"sample_rate"` SampleRate int `json:"sample_rate"`

View file

@ -162,7 +162,7 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
if c.cancelTTS != nil { if c.cancelTTS != nil {
c.cancelTTS() c.cancelTTS()
} }
ttsCtx, ttsCancel := context.WithCancel(context.Background()) ttsCtx, ttsCancel := context.WithCancel(c.ctx)
c.cancelTTS = ttsCancel c.cancelTTS = ttsCancel
c.ttsMu.Unlock() c.ttsMu.Unlock()
@ -652,7 +652,10 @@ func (c *DiscordChannel) listenVoiceControl(ctx context.Context) {
select { select {
case <-ctx.Done(): case <-ctx.Done():
return return
case ctrl := <-c.bus.VoiceControlsChan(): case ctrl, ok := <-c.bus.VoiceControlsChan():
if !ok {
return
}
if ctrl.Type == "command" && ctrl.Action == "leave" { if ctrl.Type == "command" && ctrl.Action == "leave" {
if strings.HasPrefix(ctrl.SessionID, "discord_vc_") { if strings.HasPrefix(ctrl.SessionID, "discord_vc_") {
guildID := strings.TrimPrefix(ctrl.SessionID, "discord_vc_") guildID := strings.TrimPrefix(ctrl.SessionID, "discord_vc_")

View file

@ -1,7 +1,6 @@
package discord package discord
import ( import (
"bytes"
"context" "context"
"fmt" "fmt"
"io" "io"
@ -9,6 +8,7 @@ import (
"github.com/bwmarrin/discordgo" "github.com/bwmarrin/discordgo"
"github.com/sipeed/picoclaw/pkg/audio"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
) )
@ -60,58 +60,14 @@ func streamOggOpusToDiscord(ctx context.Context, vc *discordgo.VoiceConnection,
vc.Speaking(true) vc.Speaking(true)
defer vc.Speaking(false) defer vc.Speaking(false)
var packet []byte return audio.DecodeOggOpus(r, func(frame []byte) error {
header := make([]byte, 27)
for {
// Check for interruption
select { select {
case <-ctx.Done(): case <-ctx.Done():
return ctx.Err() return ctx.Err()
default: case vc.OpusSend <- frame:
}
if _, err := io.ReadFull(r, header); err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
return nil return nil
} }
return fmt.Errorf("failed to read ogg header: %w", err) })
}
if string(header[:4]) != "OggS" {
return fmt.Errorf("invalid ogg magic string")
}
pageSegments := int(header[26])
segmentTable := make([]byte, pageSegments)
if _, err := io.ReadFull(r, segmentTable); err != nil {
return fmt.Errorf("failed to read segment table: %w", err)
}
for _, lacing := range segmentTable {
segment := make([]byte, lacing)
if _, err := io.ReadFull(r, segment); err != nil {
return fmt.Errorf("failed to read segment data: %w", err)
}
packet = append(packet, segment...)
// If lacing is less than 255, the packet is complete
if lacing < 255 {
if len(packet) > 0 {
// Ignore Ogg Opus headers
if !bytes.HasPrefix(packet, []byte("OpusHead")) && !bytes.HasPrefix(packet, []byte("OpusTags")) {
select {
case <-ctx.Done():
return ctx.Err()
case vc.OpusSend <- packet:
}
}
// Start new packet
packet = nil
}
}
}
}
} }
func (c *DiscordChannel) receiveVoice(vc *discordgo.VoiceConnection, guildID string, chatID string) { func (c *DiscordChannel) receiveVoice(vc *discordgo.VoiceConnection, guildID string, chatID string) {
@ -193,6 +149,7 @@ func (c *DiscordChannel) receiveVoice(vc *discordgo.VoiceConnection, guildID str
SessionID: sessionID, SessionID: sessionID,
SpeakerID: fmt.Sprintf("%d", p.SSRC), SpeakerID: fmt.Sprintf("%d", p.SSRC),
ChatID: chatID, ChatID: chatID,
Channel: "discord",
Sequence: sequence, Sequence: sequence,
Timestamp: p.Timestamp, Timestamp: p.Timestamp,
SampleRate: 48000, SampleRate: 48000,

View file

@ -26,6 +26,7 @@ type speechAccumulator struct {
chatID string chatID string
speakerID string speakerID string
sessionID string sessionID string
channel string
} }
func (a *speechAccumulator) Push(chunk bus.AudioChunk) { func (a *speechAccumulator) Push(chunk bus.AudioChunk) {
@ -117,6 +118,7 @@ func (a *Agent) handleChunk(chunk bus.AudioChunk) {
chatID: chunk.ChatID, chatID: chunk.ChatID,
speakerID: chunk.SpeakerID, speakerID: chunk.SpeakerID,
sessionID: chunk.SessionID, sessionID: chunk.SessionID,
channel: chunk.Channel,
} }
a.sessions[key] = acc a.sessions[key] = acc
logger.DebugCF("voice-agent", "Started accumulating voice", map[string]any{"key": key, "file": filename}) logger.DebugCF("voice-agent", "Started accumulating voice", map[string]any{"key": key, "file": filename})
@ -185,7 +187,10 @@ func (a *Agent) processUtterance(ctx context.Context, acc *speechAccumulator) {
logger.InfoCF("voice-agent", "Transcription result", map[string]any{"text": res.Text, "duration": res.Duration}) logger.InfoCF("voice-agent", "Transcription result", map[string]any{"text": res.Text, "duration": res.Duration})
channelType := "discord" channelType := acc.channel
if channelType == "" {
channelType = "discord" // fallback for legacy chunks
}
text := strings.ToLower(strings.TrimSpace(res.Text)) text := strings.ToLower(strings.TrimSpace(res.Text))
if strings.Contains(text, "leave the voice channel") || strings.Contains(text, "leave voice") || if strings.Contains(text, "leave the voice channel") || strings.Contains(text, "leave voice") ||