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) } }