259 lines
6.6 KiB
Go
259 lines
6.6 KiB
Go
|
|
package main
|
||
|
|
|
||
|
|
import (
|
||
|
|
"flag"
|
||
|
|
"log"
|
||
|
|
"os"
|
||
|
|
"os/signal"
|
||
|
|
"sync"
|
||
|
|
"syscall"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"go-ts/pkg/ts3client"
|
||
|
|
"go-ts/pkg/xai"
|
||
|
|
)
|
||
|
|
|
||
|
|
// VoiceSession represents an active xAI voice session for a user
|
||
|
|
type VoiceSession struct {
|
||
|
|
ClientID uint16
|
||
|
|
Nickname string
|
||
|
|
XAI *xai.Client
|
||
|
|
AudioBuffer []int16 // Buffer to accumulate audio samples
|
||
|
|
AudioQueue chan []int16 // Queue for sending audio with proper timing
|
||
|
|
done chan struct{} // Signal to stop audio sender
|
||
|
|
}
|
||
|
|
|
||
|
|
// Bot manages the TeamSpeak connection and xAI sessions
|
||
|
|
type Bot struct {
|
||
|
|
ts3 *ts3client.Client
|
||
|
|
apiKey string
|
||
|
|
voice string
|
||
|
|
prompt string
|
||
|
|
|
||
|
|
selfID uint16 // Our own ClientID
|
||
|
|
sessions map[uint16]*VoiceSession
|
||
|
|
sessionsMu sync.RWMutex
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
serverAddr := flag.String("server", "127.0.0.1:9987", "TeamSpeak 3 Server Address")
|
||
|
|
nickname := flag.String("nickname", "GrokBot", "Bot nickname")
|
||
|
|
voice := flag.String("voice", xai.VoiceAra, "xAI voice (Ara, Rex, Sal, Eve, Leo)")
|
||
|
|
flag.Parse()
|
||
|
|
|
||
|
|
apiKey := os.Getenv("XAI_API_KEY")
|
||
|
|
if apiKey == "" {
|
||
|
|
log.Fatal("XAI_API_KEY environment variable not set")
|
||
|
|
}
|
||
|
|
|
||
|
|
log.Println("=== xAI Voice Bot for TeamSpeak ===")
|
||
|
|
log.Printf("Server: %s", *serverAddr)
|
||
|
|
log.Printf("Nickname: %s", *nickname)
|
||
|
|
log.Printf("Voice: %s", *voice)
|
||
|
|
|
||
|
|
bot := &Bot{
|
||
|
|
apiKey: apiKey,
|
||
|
|
voice: *voice,
|
||
|
|
prompt: "Eres Grok, un asistente de voz amigable y útil. Responde de forma concisa y natural.",
|
||
|
|
sessions: make(map[uint16]*VoiceSession),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create TeamSpeak client
|
||
|
|
bot.ts3 = ts3client.New(*serverAddr, ts3client.Config{
|
||
|
|
Nickname: *nickname,
|
||
|
|
})
|
||
|
|
|
||
|
|
// Register event handlers
|
||
|
|
bot.ts3.On(ts3client.EventConnected, func(e *ts3client.ConnectedEvent) {
|
||
|
|
bot.selfID = e.ClientID // Store our own ID
|
||
|
|
log.Printf("✓ Conectado a TeamSpeak! ClientID=%d, Server=%s", e.ClientID, e.ServerName)
|
||
|
|
})
|
||
|
|
|
||
|
|
bot.ts3.On(ts3client.EventChannelList, func(e *ts3client.ChannelListEvent) {
|
||
|
|
log.Printf("✓ %d canales disponibles", len(e.Channels))
|
||
|
|
})
|
||
|
|
|
||
|
|
bot.ts3.On(ts3client.EventClientEnter, func(e *ts3client.ClientEnterEvent) {
|
||
|
|
log.Printf("→ Usuario entró: %s (ID=%d)", e.Nickname, e.ClientID)
|
||
|
|
|
||
|
|
// Don't create session for ourselves (compare by ID, not nickname)
|
||
|
|
if e.ClientID == bot.selfID {
|
||
|
|
log.Printf(" (Soy yo, ignorando)")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Create xAI session for this user
|
||
|
|
go bot.createSession(e.ClientID, e.Nickname)
|
||
|
|
})
|
||
|
|
|
||
|
|
bot.ts3.On(ts3client.EventClientLeft, func(e *ts3client.ClientLeftEvent) {
|
||
|
|
log.Printf("← Usuario salió: ID=%d (%s)", e.ClientID, e.Reason)
|
||
|
|
|
||
|
|
// Close xAI session for this user
|
||
|
|
bot.closeSession(e.ClientID)
|
||
|
|
})
|
||
|
|
|
||
|
|
bot.ts3.On(ts3client.EventAudio, func(e *ts3client.AudioEvent) {
|
||
|
|
// Forward audio from TeamSpeak to all xAI sessions
|
||
|
|
// In a real implementation, you'd want to track which user
|
||
|
|
// is speaking and only send to their session
|
||
|
|
bot.sessionsMu.RLock()
|
||
|
|
for _, session := range bot.sessions {
|
||
|
|
if session.XAI != nil && session.XAI.IsConnected() {
|
||
|
|
session.XAI.SendAudio(e.PCM)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
bot.sessionsMu.RUnlock()
|
||
|
|
})
|
||
|
|
|
||
|
|
bot.ts3.On(ts3client.EventError, func(e *ts3client.ErrorEvent) {
|
||
|
|
if e.ID != "0" {
|
||
|
|
log.Printf("! Error del servidor: [%s] %s", e.ID, e.Message)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
|
||
|
|
// Handle shutdown
|
||
|
|
go func() {
|
||
|
|
sigChan := make(chan os.Signal, 1)
|
||
|
|
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||
|
|
<-sigChan
|
||
|
|
log.Println("Cerrando...")
|
||
|
|
|
||
|
|
// Close all xAI sessions
|
||
|
|
bot.sessionsMu.Lock()
|
||
|
|
for _, session := range bot.sessions {
|
||
|
|
if session.XAI != nil {
|
||
|
|
session.XAI.Close()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
bot.sessionsMu.Unlock()
|
||
|
|
|
||
|
|
bot.ts3.Disconnect()
|
||
|
|
// os.Exit(0)
|
||
|
|
}()
|
||
|
|
|
||
|
|
// Connect to TeamSpeak
|
||
|
|
if err := bot.ts3.Connect(); err != nil {
|
||
|
|
log.Fatalf("Error de conexión: %v", err)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// createSession creates a new xAI voice session for a user
|
||
|
|
func (b *Bot) createSession(clientID uint16, nickname string) {
|
||
|
|
log.Printf("[Session] Creando sesión xAI para %s...", nickname)
|
||
|
|
|
||
|
|
// Create session with audio queue
|
||
|
|
session := &VoiceSession{
|
||
|
|
ClientID: clientID,
|
||
|
|
Nickname: nickname,
|
||
|
|
AudioBuffer: make([]int16, 0, 960*10),
|
||
|
|
AudioQueue: make(chan []int16, 500), // Buffer up to 500 frames (~10 sec)
|
||
|
|
done: make(chan struct{}),
|
||
|
|
}
|
||
|
|
|
||
|
|
// Start audio sender goroutine with proper 20ms timing
|
||
|
|
go b.audioSender(session)
|
||
|
|
|
||
|
|
xaiClient := xai.New(b.apiKey)
|
||
|
|
|
||
|
|
// Set up audio callback - buffer and queue in 960-sample chunks
|
||
|
|
xaiClient.OnAudio(func(pcm []int16) {
|
||
|
|
b.sessionsMu.Lock()
|
||
|
|
session.AudioBuffer = append(session.AudioBuffer, pcm...)
|
||
|
|
|
||
|
|
// Queue complete 960-sample frames
|
||
|
|
for len(session.AudioBuffer) >= 960 {
|
||
|
|
frame := make([]int16, 960)
|
||
|
|
copy(frame, session.AudioBuffer[:960])
|
||
|
|
session.AudioBuffer = session.AudioBuffer[960:]
|
||
|
|
|
||
|
|
// Non-blocking send to queue
|
||
|
|
select {
|
||
|
|
case session.AudioQueue <- frame:
|
||
|
|
default:
|
||
|
|
// Queue full, drop frame
|
||
|
|
}
|
||
|
|
}
|
||
|
|
b.sessionsMu.Unlock()
|
||
|
|
})
|
||
|
|
|
||
|
|
// Set up transcript callback for logging
|
||
|
|
xaiClient.OnTranscript(func(text string) {
|
||
|
|
log.Printf("[Grok] %s", text)
|
||
|
|
})
|
||
|
|
|
||
|
|
// Clear audio queue when user starts speaking (interruption)
|
||
|
|
xaiClient.OnSpeechStarted(func() {
|
||
|
|
b.sessionsMu.Lock()
|
||
|
|
// Clear the buffer
|
||
|
|
session.AudioBuffer = session.AudioBuffer[:0]
|
||
|
|
// Drain the queue
|
||
|
|
for len(session.AudioQueue) > 0 {
|
||
|
|
<-session.AudioQueue
|
||
|
|
}
|
||
|
|
b.sessionsMu.Unlock()
|
||
|
|
log.Printf("[Session] Audio queue cleared (user interruption)")
|
||
|
|
})
|
||
|
|
|
||
|
|
// Connect to xAI
|
||
|
|
if err := xaiClient.Connect(); err != nil {
|
||
|
|
log.Printf("[Session] Error conectando a xAI: %v", err)
|
||
|
|
close(session.done)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Configure the session
|
||
|
|
if err := xaiClient.ConfigureSession(b.voice, b.prompt); err != nil {
|
||
|
|
log.Printf("[Session] Error configurando sesión: %v", err)
|
||
|
|
xaiClient.Close()
|
||
|
|
close(session.done)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Store the xAI client in session
|
||
|
|
session.XAI = xaiClient
|
||
|
|
|
||
|
|
b.sessionsMu.Lock()
|
||
|
|
b.sessions[clientID] = session
|
||
|
|
b.sessionsMu.Unlock()
|
||
|
|
|
||
|
|
log.Printf("[Session] ✓ Sesión xAI activa para %s", nickname)
|
||
|
|
}
|
||
|
|
|
||
|
|
// audioSender sends audio frames to TeamSpeak with proper 20ms timing
|
||
|
|
func (b *Bot) audioSender(session *VoiceSession) {
|
||
|
|
ticker := time.NewTicker(20 * time.Millisecond)
|
||
|
|
defer ticker.Stop()
|
||
|
|
|
||
|
|
for {
|
||
|
|
select {
|
||
|
|
case <-session.done:
|
||
|
|
return
|
||
|
|
case <-ticker.C:
|
||
|
|
// Try to get a frame from the queue
|
||
|
|
select {
|
||
|
|
case frame := <-session.AudioQueue:
|
||
|
|
if err := b.ts3.SendAudio(frame); err != nil {
|
||
|
|
log.Printf("[Session] Error enviando audio: %v", err)
|
||
|
|
}
|
||
|
|
default:
|
||
|
|
// No frame available, that's ok
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// closeSession closes an xAI session for a user
|
||
|
|
func (b *Bot) closeSession(clientID uint16) {
|
||
|
|
b.sessionsMu.Lock()
|
||
|
|
defer b.sessionsMu.Unlock()
|
||
|
|
|
||
|
|
if session, ok := b.sessions[clientID]; ok {
|
||
|
|
log.Printf("[Session] Cerrando sesión xAI para %s", session.Nickname)
|
||
|
|
if session.XAI != nil {
|
||
|
|
session.XAI.Close()
|
||
|
|
}
|
||
|
|
delete(b.sessions, clientID)
|
||
|
|
}
|
||
|
|
}
|