refactor for #1648

This commit is contained in:
Huaaudio 2026-03-21 08:05:54 +01:00
parent a0ee78cff4
commit 4845fcb97f
10 changed files with 73 additions and 16 deletions

View file

@ -18,6 +18,7 @@ import (
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/sipeed/picoclaw/pkg/asr"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/commands" "github.com/sipeed/picoclaw/pkg/commands"
@ -31,7 +32,6 @@ import (
"github.com/sipeed/picoclaw/pkg/state" "github.com/sipeed/picoclaw/pkg/state"
"github.com/sipeed/picoclaw/pkg/tools" "github.com/sipeed/picoclaw/pkg/tools"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
"github.com/sipeed/picoclaw/pkg/voice"
) )
type AgentLoop struct { type AgentLoop struct {
@ -51,7 +51,7 @@ type AgentLoop struct {
fallback *providers.FallbackChain fallback *providers.FallbackChain
channelManager *channels.Manager channelManager *channels.Manager
mediaStore media.MediaStore mediaStore media.MediaStore
transcriber voice.Transcriber transcriber asr.Transcriber
cmdRegistry *commands.Registry cmdRegistry *commands.Registry
mcp mcpRuntime mcp mcpRuntime
hookRuntime hookRuntime hookRuntime hookRuntime
@ -1040,7 +1040,7 @@ func (al *AgentLoop) SetMediaStore(s media.MediaStore) {
} }
// SetTranscriber injects a voice transcriber for agent-level audio transcription. // SetTranscriber injects a voice transcriber for agent-level audio transcription.
func (al *AgentLoop) SetTranscriber(t voice.Transcriber) { func (al *AgentLoop) SetTranscriber(t asr.Transcriber) {
al.transcriber = t al.transcriber = t
} }

View file

@ -1,4 +1,4 @@
package voice package asr
import ( import (
"bytes" "bytes"

View file

@ -1,4 +1,4 @@
package voice package asr
import ( import (
"testing" "testing"

55
pkg/audio/ogg.go Normal file
View file

@ -0,0 +1,55 @@
package audio
import (
"bytes"
"fmt"
"io"
)
// DecodeOggOpus reads an Ogg format stream and extracts individual Opus payloads.
// It calls onFrame for every complete Opus frame found in the stream.
func DecodeOggOpus(r io.Reader, onFrame func([]byte) error) error {
var packet []byte
header := make([]byte, 27)
for {
if _, err := io.ReadFull(r, header); err != nil {
if err == io.EOF || err == io.ErrUnexpectedEOF {
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")) {
if err := onFrame(packet); err != nil {
return err
}
}
// Start new packet
packet = nil
}
}
}
}
}

View file

@ -68,6 +68,6 @@ type AudioChunk struct {
// VoiceControl represents state or commands for voice sessions. // VoiceControl represents state or commands for voice sessions.
type VoiceControl struct { type VoiceControl struct {
SessionID string `json:"session_id"` SessionID string `json:"session_id"`
Type string `json:"type"` // "state", "command" Type string `json:"type"` // "state", "command"
Action string `json:"action"` // "idle", "listening", "start", "stop" Action string `json:"action"` // "idle", "listening", "start", "stop"
} }

View file

@ -20,8 +20,8 @@ import (
"github.com/sipeed/picoclaw/pkg/identity" "github.com/sipeed/picoclaw/pkg/identity"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
"github.com/sipeed/picoclaw/pkg/media" "github.com/sipeed/picoclaw/pkg/media"
"github.com/sipeed/picoclaw/pkg/tts"
"github.com/sipeed/picoclaw/pkg/utils" "github.com/sipeed/picoclaw/pkg/utils"
"github.com/sipeed/picoclaw/pkg/voice"
) )
const ( const (
@ -44,7 +44,7 @@ type DiscordChannel struct {
typingStop map[string]chan struct{} // chatID → stop signal typingStop map[string]chan struct{} // chatID → stop signal
botUserID string // stored for mention checking botUserID string // stored for mention checking
bus *bus.MessageBus bus *bus.MessageBus
tts voice.TTSProvider tts tts.TTSProvider
} }
func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) { func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) {
@ -93,7 +93,7 @@ func (c *DiscordChannel) Start(ctx context.Context) error {
c.botUserID = botUser.ID c.botUserID = botUser.ID
c.session.AddHandler(c.handleMessage) c.session.AddHandler(c.handleMessage)
go c.listenVoiceControl(c.ctx) go c.listenVoiceControl(c.ctx)
if err := c.session.Open(); err != nil { if err := c.session.Open(); err != nil {

View file

@ -4,14 +4,14 @@ import (
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/channels"
"github.com/sipeed/picoclaw/pkg/config" "github.com/sipeed/picoclaw/pkg/config"
"github.com/sipeed/picoclaw/pkg/voice" "github.com/sipeed/picoclaw/pkg/tts"
) )
func init() { func init() {
channels.RegisterFactory("discord", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) { channels.RegisterFactory("discord", func(cfg *config.Config, b *bus.MessageBus) (channels.Channel, error) {
ch, err := NewDiscordChannel(cfg.Channels.Discord, b) ch, err := NewDiscordChannel(cfg.Channels.Discord, b)
if err == nil { if err == nil {
ch.tts = voice.DetectTTS(cfg) ch.tts = tts.DetectTTS(cfg)
} }
return ch, err return ch, err
}) })

View file

@ -12,6 +12,7 @@ import (
"time" "time"
"github.com/sipeed/picoclaw/pkg/agent" "github.com/sipeed/picoclaw/pkg/agent"
"github.com/sipeed/picoclaw/pkg/asr"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/channels" "github.com/sipeed/picoclaw/pkg/channels"
_ "github.com/sipeed/picoclaw/pkg/channels/dingtalk" _ "github.com/sipeed/picoclaw/pkg/channels/dingtalk"
@ -303,7 +304,7 @@ func setupAndStartServices(
agentLoop.SetChannelManager(runningServices.ChannelManager) agentLoop.SetChannelManager(runningServices.ChannelManager)
agentLoop.SetMediaStore(runningServices.MediaStore) agentLoop.SetMediaStore(runningServices.MediaStore)
if transcriber := voice.DetectTranscriber(cfg); transcriber != nil { if transcriber := asr.DetectTranscriber(cfg); transcriber != nil {
agentLoop.SetTranscriber(transcriber) agentLoop.SetTranscriber(transcriber)
logger.InfoCF("voice", "Transcription enabled (agent-level)", map[string]any{"provider": transcriber.Name()}) logger.InfoCF("voice", "Transcription enabled (agent-level)", map[string]any{"provider": transcriber.Name()})
@ -538,7 +539,7 @@ func restartServices(
fmt.Println(" ✓ Device event service restarted") fmt.Println(" ✓ Device event service restarted")
} }
transcriber := voice.DetectTranscriber(cfg) transcriber := asr.DetectTranscriber(cfg)
al.SetTranscriber(transcriber) al.SetTranscriber(transcriber)
if transcriber != nil { if transcriber != nil {
logger.InfoCF("voice", "Transcription re-enabled (agent-level)", map[string]any{"provider": transcriber.Name()}) logger.InfoCF("voice", "Transcription re-enabled (agent-level)", map[string]any{"provider": transcriber.Name()})

View file

@ -1,4 +1,4 @@
package voice package tts
import ( import (
"bytes" "bytes"

View file

@ -11,6 +11,7 @@ import (
"github.com/pion/rtp" "github.com/pion/rtp"
"github.com/pion/webrtc/v3/pkg/media/oggwriter" "github.com/pion/webrtc/v3/pkg/media/oggwriter"
"github.com/sipeed/picoclaw/pkg/asr"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
) )
@ -61,13 +62,13 @@ func (a *speechAccumulator) Close() {
type Agent struct { type Agent struct {
bus *bus.MessageBus bus *bus.MessageBus
transcriber Transcriber transcriber asr.Transcriber
mu sync.Mutex mu sync.Mutex
sessions map[string]*speechAccumulator // keyed by sessionID_speakerID sessions map[string]*speechAccumulator // keyed by sessionID_speakerID
} }
func NewAgent(mb *bus.MessageBus, t Transcriber) *Agent { func NewAgent(mb *bus.MessageBus, t asr.Transcriber) *Agent {
return &Agent{ return &Agent{
bus: mb, bus: mb,
transcriber: t, transcriber: t,