A more neutral and elegant voice.Transcriber interface
This commit is contained in:
parent
b1386ad71f
commit
b74f92ed28
4 changed files with 99 additions and 31 deletions
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
"github.com/sipeed/picoclaw/cmd/picoclaw/internal"
|
||||||
|
|
@ -136,20 +135,10 @@ func gatewayCmd(debug bool) error {
|
||||||
agentLoop.SetChannelManager(channelManager)
|
agentLoop.SetChannelManager(channelManager)
|
||||||
agentLoop.SetMediaStore(mediaStore)
|
agentLoop.SetMediaStore(mediaStore)
|
||||||
|
|
||||||
// Wire up voice transcription if Groq API key is available
|
// Wire up voice transcription if a supported provider is configured.
|
||||||
groqAPIKey := cfg.Providers.Groq.APIKey
|
if transcriber := voice.DetectTranscriber(cfg); transcriber != nil {
|
||||||
if groqAPIKey == "" {
|
|
||||||
for _, mc := range cfg.ModelList {
|
|
||||||
if strings.HasPrefix(mc.Model, "groq/") && mc.APIKey != "" {
|
|
||||||
groqAPIKey = mc.APIKey
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if groqAPIKey != "" {
|
|
||||||
transcriber := voice.NewGroqTranscriber(groqAPIKey)
|
|
||||||
agentLoop.SetTranscriber(transcriber)
|
agentLoop.SetTranscriber(transcriber)
|
||||||
logger.InfoC("voice", "Groq voice transcription enabled (agent-level)")
|
logger.InfoCF("voice", "Transcription enabled (agent-level)", map[string]any{"provider": transcriber.Name()})
|
||||||
}
|
}
|
||||||
|
|
||||||
enabledChannels := channelManager.GetEnabledChannels()
|
enabledChannels := channelManager.GetEnabledChannels()
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ var audioAnnotationRe = regexp.MustCompile(`\[(voice|audio)(?::[^\]]*)?\]`)
|
||||||
// transcribeAudioInMessage resolves audio media refs, transcribes them, and
|
// transcribeAudioInMessage resolves audio media refs, transcribes them, and
|
||||||
// replaces audio annotations in msg.Content with the transcribed text.
|
// replaces audio annotations in msg.Content with the transcribed text.
|
||||||
func (al *AgentLoop) transcribeAudioInMessage(ctx context.Context, msg bus.InboundMessage) bus.InboundMessage {
|
func (al *AgentLoop) transcribeAudioInMessage(ctx context.Context, msg bus.InboundMessage) bus.InboundMessage {
|
||||||
if al.transcriber == nil || !al.transcriber.IsAvailable() || al.mediaStore == nil || len(msg.Media) == 0 {
|
if al.transcriber == nil || al.mediaStore == nil || len(msg.Media) == 0 {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,17 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
"github.com/sipeed/picoclaw/pkg/logger"
|
"github.com/sipeed/picoclaw/pkg/logger"
|
||||||
"github.com/sipeed/picoclaw/pkg/utils"
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Transcriber interface {
|
type Transcriber interface {
|
||||||
|
Name() string
|
||||||
Transcribe(ctx context.Context, audioFilePath string) (*TranscriptionResponse, error)
|
Transcribe(ctx context.Context, audioFilePath string) (*TranscriptionResponse, error)
|
||||||
IsAvailable() bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GroqTranscriber struct {
|
type GroqTranscriber struct {
|
||||||
|
|
@ -157,8 +159,22 @@ func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string)
|
||||||
return &result, nil
|
return &result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *GroqTranscriber) IsAvailable() bool {
|
func (t *GroqTranscriber) Name() string {
|
||||||
available := t.apiKey != ""
|
return "groq"
|
||||||
logger.DebugCF("voice", "Checking transcriber availability", map[string]any{"available": available})
|
}
|
||||||
return available
|
|
||||||
|
// DetectTranscriber inspects cfg and returns the appropriate Transcriber, or
|
||||||
|
// nil if no supported transcription provider is configured.
|
||||||
|
func DetectTranscriber(cfg *config.Config) Transcriber {
|
||||||
|
// Direct Groq provider config takes priority.
|
||||||
|
if key := cfg.Providers.Groq.APIKey; key != "" {
|
||||||
|
return NewGroqTranscriber(key)
|
||||||
|
}
|
||||||
|
// Fall back to any model-list entry that uses the groq/ protocol.
|
||||||
|
for _, mc := range cfg.ModelList {
|
||||||
|
if strings.HasPrefix(mc.Model, "groq/") && mc.APIKey != "" {
|
||||||
|
return NewGroqTranscriber(mc.APIKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,25 +8,88 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Ensure GroqTranscriber satisfies the Transcriber interface at compile time.
|
// Ensure GroqTranscriber satisfies the Transcriber interface at compile time.
|
||||||
var _ Transcriber = (*GroqTranscriber)(nil)
|
var _ Transcriber = (*GroqTranscriber)(nil)
|
||||||
|
|
||||||
func TestIsAvailable(t *testing.T) {
|
func TestGroqTranscriberName(t *testing.T) {
|
||||||
tests := []struct {
|
tr := NewGroqTranscriber("sk-test")
|
||||||
name string
|
if got := tr.Name(); got != "groq" {
|
||||||
apiKey string
|
t.Errorf("Name() = %q, want %q", got, "groq")
|
||||||
want bool
|
|
||||||
}{
|
|
||||||
{"with key", "sk-test-key", true},
|
|
||||||
{"empty key", "", false},
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDetectTranscriber(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
cfg *config.Config
|
||||||
|
wantNil bool
|
||||||
|
wantName string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no config",
|
||||||
|
cfg: &config.Config{},
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "groq provider key",
|
||||||
|
cfg: &config.Config{
|
||||||
|
Providers: config.ProvidersConfig{
|
||||||
|
Groq: config.ProviderConfig{APIKey: "sk-groq-direct"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantName: "groq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "groq via model list",
|
||||||
|
cfg: &config.Config{
|
||||||
|
ModelList: []config.ModelConfig{
|
||||||
|
{Model: "openai/gpt-4o", APIKey: "sk-openai"},
|
||||||
|
{Model: "groq/llama-3.3-70b", APIKey: "sk-groq-model"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantName: "groq",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "groq model list entry without key is skipped",
|
||||||
|
cfg: &config.Config{
|
||||||
|
ModelList: []config.ModelConfig{
|
||||||
|
{Model: "groq/llama-3.3-70b", APIKey: ""},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantNil: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "provider key takes priority over model list",
|
||||||
|
cfg: &config.Config{
|
||||||
|
Providers: config.ProvidersConfig{
|
||||||
|
Groq: config.ProviderConfig{APIKey: "sk-groq-direct"},
|
||||||
|
},
|
||||||
|
ModelList: []config.ModelConfig{
|
||||||
|
{Model: "groq/llama-3.3-70b", APIKey: "sk-groq-model"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantName: "groq",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
tr := NewGroqTranscriber(tc.apiKey)
|
tr := DetectTranscriber(tc.cfg)
|
||||||
if got := tr.IsAvailable(); got != tc.want {
|
if tc.wantNil {
|
||||||
t.Errorf("IsAvailable() = %v, want %v", got, tc.want)
|
if tr != nil {
|
||||||
|
t.Errorf("DetectTranscriber() = %v, want nil", tr)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if tr == nil {
|
||||||
|
t.Fatal("DetectTranscriber() = nil, want non-nil")
|
||||||
|
}
|
||||||
|
if got := tr.Name(); got != tc.wantName {
|
||||||
|
t.Errorf("Name() = %q, want %q", got, tc.wantName)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue