Code review
This commit is contained in:
parent
031a967b25
commit
5baff6019c
4 changed files with 71 additions and 23 deletions
|
|
@ -92,25 +92,38 @@ func (t *GroqTranscriber) Transcribe(ctx context.Context, audioFilePath string)
|
||||||
|
|
||||||
audioFile, err := os.Open(audioFilePath)
|
audioFile, err := os.Open(audioFilePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open audio file: %w", err)
|
return nil, fmt.Errorf("failed to open audio file %s: %w", audioFilePath, err)
|
||||||
}
|
}
|
||||||
defer audioFile.Close()
|
defer audioFile.Close()
|
||||||
|
|
||||||
fileInfo, err := audioFile.Stat()
|
fileInfo, err := audioFile.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to stat audio file %s: %w", audioFilePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var requestBody bytes.Buffer
|
var requestBody bytes.Buffer
|
||||||
writer := multipart.NewWriter(&requestBody)
|
writer := multipart.NewWriter(&requestBody)
|
||||||
|
|
||||||
part, err := writer.CreateFormFile("file", filepath.Base(audioFilePath))
|
part, err := writer.CreateFormFile("file", filepath.Base(audioFilePath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, fmt.Errorf("failed to create form file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, copyErr := io.Copy(part, audioFile); copyErr != nil {
|
||||||
|
return nil, fmt.Errorf("failed to copy audio data: %w", copyErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writer.WriteField("model", "whisper-large-v3-turbo"); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write model field: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writer.WriteField("response_format", "json"); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to write response_format field: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = writer.Close(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to close multipart writer: %w", err)
|
||||||
}
|
}
|
||||||
io.Copy(part, audioFile)
|
|
||||||
writer.WriteField("model", "whisper-large-v3")
|
|
||||||
writer.WriteField("response_format", "json")
|
|
||||||
writer.Close()
|
|
||||||
|
|
||||||
return t.doRequest(ctx, &requestBody, writer.FormDataContentType(), fileInfo.Size())
|
return t.doRequest(ctx, &requestBody, writer.FormDataContentType(), fileInfo.Size())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -670,6 +670,13 @@ func (c *DiscordChannel) listenVoiceControl(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *DiscordChannel) playTTS(ctx context.Context, vc *discordgo.VoiceConnection, text string) {
|
func (c *DiscordChannel) playTTS(ctx context.Context, vc *discordgo.VoiceConnection, text string) {
|
||||||
|
// Clear cancelTTS when playback finishes (normal or interrupted)
|
||||||
|
defer func() {
|
||||||
|
c.ttsMu.Lock()
|
||||||
|
c.cancelTTS = nil
|
||||||
|
c.ttsMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
sentences := audio.SplitSentences(text)
|
sentences := audio.SplitSentences(text)
|
||||||
if len(sentences) == 0 {
|
if len(sentences) == 0 {
|
||||||
return
|
return
|
||||||
|
|
@ -685,6 +692,16 @@ func (c *DiscordChannel) playTTS(ctx context.Context, vc *discordgo.VoiceConnect
|
||||||
|
|
||||||
var prefetch chan ttResult
|
var prefetch chan ttResult
|
||||||
|
|
||||||
|
// Ensure any in-flight prefetch is drained on exit to prevent stream leaks
|
||||||
|
defer func() {
|
||||||
|
if prefetch != nil {
|
||||||
|
result := <-prefetch
|
||||||
|
if result.stream != nil {
|
||||||
|
result.stream.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
for i, sentence := range sentences {
|
for i, sentence := range sentences {
|
||||||
// Check for cancellation (interruption)
|
// Check for cancellation (interruption)
|
||||||
select {
|
select {
|
||||||
|
|
@ -731,12 +748,4 @@ func (c *DiscordChannel) playTTS(ctx context.Context, vc *discordgo.VoiceConnect
|
||||||
|
|
||||||
prefetch = nextPrefetch
|
prefetch = nextPrefetch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain any leftover prefetch
|
|
||||||
if prefetch != nil {
|
|
||||||
result := <-prefetch
|
|
||||||
if result.stream != nil {
|
|
||||||
result.stream.Close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ func (c *DiscordChannel) receiveVoice(vc *discordgo.VoiceConnection, guildID str
|
||||||
|
|
||||||
time.Sleep(250 * time.Millisecond) // Wait a bit for connection to settle
|
time.Sleep(250 * time.Millisecond) // Wait a bit for connection to settle
|
||||||
|
|
||||||
// Abort if the context has already been cancelled.
|
// Abort if the context has already been canceled.
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,19 @@ func (a *Agent) Start(ctx context.Context) {
|
||||||
logger.InfoCF("voice-agent", "Started Voice Agent orchestrator", nil)
|
logger.InfoCF("voice-agent", "Started Voice Agent orchestrator", nil)
|
||||||
go a.listenChunks(ctx)
|
go a.listenChunks(ctx)
|
||||||
go a.vadTick(ctx)
|
go a.vadTick(ctx)
|
||||||
|
|
||||||
|
// Cleanup sessions on shutdown
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
a.mu.Lock()
|
||||||
|
for key, acc := range a.sessions {
|
||||||
|
acc.Close()
|
||||||
|
os.Remove(acc.file)
|
||||||
|
delete(a.sessions, key)
|
||||||
|
}
|
||||||
|
a.mu.Unlock()
|
||||||
|
logger.InfoCF("voice-agent", "Cleaned up voice sessions on shutdown", nil)
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) listenChunks(ctx context.Context) {
|
func (a *Agent) listenChunks(ctx context.Context) {
|
||||||
|
|
@ -100,6 +113,12 @@ func (a *Agent) listenChunks(ctx context.Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Agent) handleChunk(chunk bus.AudioChunk) {
|
func (a *Agent) handleChunk(chunk bus.AudioChunk) {
|
||||||
|
// Only accept Opus-encoded audio
|
||||||
|
if chunk.Format != "opus" {
|
||||||
|
logger.DebugCF("voice-agent", "Ignoring unsupported audio format", map[string]any{"format": chunk.Format})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
a.mu.Lock()
|
a.mu.Lock()
|
||||||
defer a.mu.Unlock()
|
defer a.mu.Unlock()
|
||||||
|
|
||||||
|
|
@ -197,24 +216,29 @@ func (a *Agent) processUtterance(ctx context.Context, acc *speechAccumulator) {
|
||||||
|
|
||||||
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") ||
|
||||||
strings.Contains(text, "disconnect voice") {
|
strings.Contains(text, "disconnect voice") || strings.Contains(text, "leave the channel") ||
|
||||||
|
strings.Contains(text, "leave channel") {
|
||||||
logger.InfoCF("voice-agent", "Voice command triggered: leave", nil)
|
logger.InfoCF("voice-agent", "Voice command triggered: leave", nil)
|
||||||
a.bus.PublishVoiceControl(ctx, bus.VoiceControl{
|
if err := a.bus.PublishVoiceControl(ctx, bus.VoiceControl{
|
||||||
SessionID: acc.sessionID,
|
SessionID: acc.sessionID,
|
||||||
Type: "command",
|
Type: "command",
|
||||||
Action: "leave",
|
Action: "leave",
|
||||||
})
|
}); err != nil {
|
||||||
a.bus.PublishOutbound(ctx, bus.OutboundMessage{
|
logger.ErrorCF("voice-agent", "Failed to publish leave control", map[string]any{"error": err})
|
||||||
|
}
|
||||||
|
if err := a.bus.PublishOutbound(ctx, bus.OutboundMessage{
|
||||||
Channel: channelType,
|
Channel: channelType,
|
||||||
ChatID: acc.chatID,
|
ChatID: acc.chatID,
|
||||||
Content: "Goodbye! Leaving the voice channel.",
|
Content: "Goodbye! Leaving the voice channel.",
|
||||||
})
|
}); err != nil {
|
||||||
|
logger.ErrorCF("voice-agent", "Failed to publish goodbye message", map[string]any{"error": err})
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
oralPrompt := "\n\n[SYSTEM]: The user just spoke this to you over voice chat. Please reply in a highly concise, conversational, oral style suitable for text-to-speech. Do not use markdown, emojis, asterisks, or code blocks. Speak naturally."
|
oralPrompt := "\n\n[SYSTEM]: The user just spoke this to you over voice chat. Please reply in a highly concise, conversational, oral style suitable for text-to-speech. Do not use markdown, emojis, asterisks, or code blocks. Speak naturally."
|
||||||
|
|
||||||
a.bus.PublishInbound(ctx, bus.InboundMessage{
|
if err := a.bus.PublishInbound(ctx, bus.InboundMessage{
|
||||||
Channel: channelType,
|
Channel: channelType,
|
||||||
SenderID: acc.speakerID,
|
SenderID: acc.speakerID,
|
||||||
ChatID: acc.chatID,
|
ChatID: acc.chatID,
|
||||||
|
|
@ -223,5 +247,7 @@ func (a *Agent) processUtterance(ctx context.Context, acc *speechAccumulator) {
|
||||||
Metadata: map[string]string{
|
Metadata: map[string]string{
|
||||||
"is_voice": "true",
|
"is_voice": "true",
|
||||||
},
|
},
|
||||||
})
|
}); err != nil {
|
||||||
|
logger.ErrorCF("voice-agent", "Failed to publish inbound message", map[string]any{"error": err})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue