2026-01-15 22:38:39 +01:00
package main
import (
"flag"
2026-01-16 14:19:02 +01:00
"fmt"
2026-01-15 22:38:39 +01:00
"log"
"os"
"os/signal"
"sync"
"syscall"
"time"
"go-ts/pkg/ts3client"
"go-ts/pkg/xai"
)
2026-01-16 14:19:02 +01:00
// UserInfo tracks connected users (no individual sessions)
type UserInfo struct {
ClientID uint16
Nickname string
2026-01-15 22:38:39 +01:00
}
2026-01-16 14:19:02 +01:00
// Bot manages the TeamSpeak connection and single global xAI session
2026-01-15 22:38:39 +01:00
type Bot struct {
2026-01-16 10:39:27 +01:00
ts3 * ts3client . Client
apiKey string
voice string
prompt string
2026-01-16 14:19:02 +01:00
greeting string
2026-01-15 22:38:39 +01:00
2026-01-16 14:19:02 +01:00
selfID uint16
users map [ uint16 ] * UserInfo
usersMu sync . RWMutex
startTime time . Time
// Global xAI Session (one for all users)
globalXAI * xai . Client
globalMu sync . Mutex
// Input audio mixing (multiple users → one stream)
inputBuffer [ ] int16
inputMu sync . Mutex
lastInputTime time . Time
// Output audio (xAI response → TeamSpeak)
outputQueue chan [ ] int16
outputBuffer [ ] int16
outputMu sync . Mutex
done chan struct { }
2026-01-15 22:38:39 +01:00
}
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)" )
2026-01-16 14:19:02 +01:00
greeting := flag . String ( "greeting" , "" , "Greeting message when users join (empty to disable)" )
room := flag . String ( "room" , "" , "Channel name to join after connecting (empty = stay in default)" )
2026-01-15 22:38:39 +01:00
flag . Parse ( )
apiKey := os . Getenv ( "XAI_API_KEY" )
if apiKey == "" {
log . Fatal ( "XAI_API_KEY environment variable not set" )
}
2026-01-16 14:19:02 +01:00
log . Println ( "=== xAI Voice Bot for TeamSpeak (Unified Session) ===" )
2026-01-15 22:38:39 +01:00
log . Printf ( "Server: %s" , * serverAddr )
log . Printf ( "Nickname: %s" , * nickname )
log . Printf ( "Voice: %s" , * voice )
bot := & Bot {
2026-01-16 14:19:02 +01:00
apiKey : apiKey ,
voice : * voice ,
prompt : "Eres Grok, un asistente de voz amigable y útil en un canal de TeamSpeak. Puedes escuchar a múltiples personas hablando. Responde de forma concisa y natural. Si varias personas hablan, trata de entender el contexto de la conversación grupal." ,
greeting : * greeting ,
users : make ( map [ uint16 ] * UserInfo ) ,
startTime : time . Now ( ) ,
inputBuffer : make ( [ ] int16 , 0 , 960 * 50 ) , // ~1 second buffer
outputQueue : make ( chan [ ] int16 , 500 ) , // ~10 seconds of audio
done : make ( chan struct { } ) ,
2026-01-15 22:38:39 +01:00
}
// Create TeamSpeak client
bot . ts3 = ts3client . New ( * serverAddr , ts3client . Config {
Nickname : * nickname ,
} )
// Register event handlers
bot . ts3 . On ( ts3client . EventConnected , func ( e * ts3client . ConnectedEvent ) {
2026-01-16 14:19:02 +01:00
bot . selfID = e . ClientID
2026-01-15 22:38:39 +01:00
log . Printf ( "✓ Conectado a TeamSpeak! ClientID=%d, Server=%s" , e . ClientID , e . ServerName )
2026-01-16 14:19:02 +01:00
// Initialize global xAI session after connecting
go func ( ) {
if err := bot . initGlobalSession ( ) ; err != nil {
log . Printf ( "[Global] Error iniciando sesión xAI: %v" , err )
}
} ( )
2026-01-15 22:38:39 +01:00
} )
bot . ts3 . On ( ts3client . EventChannelList , func ( e * ts3client . ChannelListEvent ) {
log . Printf ( "✓ %d canales disponibles" , len ( e . Channels ) )
2026-01-16 14:19:02 +01:00
// Join specified room if provided
if * room != "" {
go func ( ) {
// Small delay to ensure connection is fully established
time . Sleep ( 500 * time . Millisecond )
ch := bot . ts3 . GetChannelByName ( * room )
if ch != nil {
log . Printf ( "[Room] Uniéndose al canal: %s (ID=%d)" , ch . Name , ch . ID )
if err := bot . ts3 . JoinChannel ( ch . ID ) ; err != nil {
log . Printf ( "[Room] Error al unirse al canal: %v" , err )
} else {
log . Printf ( "[Room] ✓ Unido al canal: %s" , ch . Name )
}
} else {
log . Printf ( "[Room] ⚠ Canal no encontrado: %s" , * room )
}
} ( )
}
2026-01-15 22:38:39 +01:00
} )
bot . ts3 . On ( ts3client . EventClientEnter , func ( e * ts3client . ClientEnterEvent ) {
2026-01-16 14:19:02 +01:00
// Don't track ourselves
2026-01-15 22:38:39 +01:00
if e . ClientID == bot . selfID {
log . Printf ( " (Soy yo, ignorando)" )
return
}
2026-01-16 14:19:02 +01:00
log . Printf ( "→ Usuario entró: %s (ID=%d)" , e . Nickname , e . ClientID )
bot . usersMu . Lock ( )
bot . users [ e . ClientID ] = & UserInfo {
ClientID : e . ClientID ,
Nickname : e . Nickname ,
}
bot . usersMu . Unlock ( )
// Notify xAI about new user (if past startup grace period)
if bot . greeting != "" && time . Since ( bot . startTime ) > 3 * time . Second {
bot . globalMu . Lock ( )
if bot . globalXAI != nil && bot . globalXAI . IsConnected ( ) {
msg := fmt . Sprintf ( "%s. El usuario %s acaba de unirse al canal." , bot . greeting , e . Nickname )
if err := bot . globalXAI . SendText ( msg ) ; err != nil {
log . Printf ( "[Global] Error enviando notificación: %v" , err )
}
}
bot . globalMu . Unlock ( )
}
2026-01-15 22:38:39 +01:00
} )
bot . ts3 . On ( ts3client . EventClientLeft , func ( e * ts3client . ClientLeftEvent ) {
2026-01-16 14:19:02 +01:00
bot . usersMu . Lock ( )
if user , ok := bot . users [ e . ClientID ] ; ok {
log . Printf ( "← Usuario salió: %s (ID=%d, %s)" , user . Nickname , e . ClientID , e . Reason )
delete ( bot . users , e . ClientID )
}
bot . usersMu . Unlock ( )
2026-01-15 22:38:39 +01:00
} )
2026-01-16 14:19:02 +01:00
// Audio handler: Mix ALL incoming audio into unified buffer
2026-01-15 22:38:39 +01:00
bot . ts3 . On ( ts3client . EventAudio , func ( e * ts3client . AudioEvent ) {
2026-01-16 14:19:02 +01:00
bot . handleInputAudio ( e . SenderID , e . PCM )
2026-01-15 22:38:39 +01:00
} )
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
2026-01-16 10:39:27 +01:00
shutdownDone := make ( chan struct { } )
2026-01-15 22:38:39 +01:00
go func ( ) {
sigChan := make ( chan os . Signal , 1 )
signal . Notify ( sigChan , syscall . SIGINT , syscall . SIGTERM )
<- sigChan
log . Println ( "Cerrando..." )
2026-01-16 14:19:02 +01:00
// Signal all goroutines to stop
close ( bot . done )
// Close global xAI session
bot . globalMu . Lock ( )
if bot . globalXAI != nil {
bot . globalXAI . Close ( )
2026-01-15 22:38:39 +01:00
}
2026-01-16 14:19:02 +01:00
bot . globalMu . Unlock ( )
2026-01-15 22:38:39 +01:00
2026-01-16 14:19:02 +01:00
// Wait for goroutines
2026-01-16 10:39:27 +01:00
time . Sleep ( 200 * time . Millisecond )
2026-01-16 14:19:02 +01:00
// Disconnect from TeamSpeak
2026-01-15 22:38:39 +01:00
bot . ts3 . Disconnect ( )
2026-01-16 10:39:27 +01:00
close ( shutdownDone )
2026-01-15 22:38:39 +01:00
} ( )
2026-01-16 14:19:02 +01:00
// Start input sender (sends mixed audio to xAI)
go bot . runInputSender ( )
// Start output mixer (sends xAI audio to TeamSpeak)
go bot . runOutputSender ( )
2026-01-16 10:39:27 +01:00
2026-01-15 22:38:39 +01:00
// Connect to TeamSpeak
if err := bot . ts3 . Connect ( ) ; err != nil {
2026-01-16 10:39:27 +01:00
select {
case <- shutdownDone :
log . Println ( "Conexión cerrada por shutdown" )
default :
log . Fatalf ( "Error de conexión: %v" , err )
}
2026-01-15 22:38:39 +01:00
}
2026-01-16 10:39:27 +01:00
log . Println ( "Esperando confirmación final de shutdown..." )
<- shutdownDone
log . Println ( "Shutdown completado. Saliendo." )
os . Exit ( 0 )
2026-01-15 22:38:39 +01:00
}
2026-01-16 14:19:02 +01:00
// initGlobalSession creates the single xAI session for all users
func ( b * Bot ) initGlobalSession ( ) error {
log . Println ( "[Global] Iniciando sesión xAI global..." )
2026-01-15 22:38:39 +01:00
xaiClient := xai . New ( b . apiKey )
2026-01-16 14:19:02 +01:00
// Handle output audio from xAI → buffer for TeamSpeak
2026-01-15 22:38:39 +01:00
xaiClient . OnAudio ( func ( pcm [ ] int16 ) {
2026-01-16 14:19:02 +01:00
b . outputMu . Lock ( )
b . outputBuffer = append ( b . outputBuffer , pcm ... )
2026-01-15 22:38:39 +01:00
// Queue complete 960-sample frames
2026-01-16 14:19:02 +01:00
for len ( b . outputBuffer ) >= 960 {
2026-01-15 22:38:39 +01:00
frame := make ( [ ] int16 , 960 )
2026-01-16 14:19:02 +01:00
copy ( frame , b . outputBuffer [ : 960 ] )
b . outputBuffer = b . outputBuffer [ 960 : ]
2026-01-15 22:38:39 +01:00
select {
2026-01-16 14:19:02 +01:00
case b . outputQueue <- frame :
2026-01-15 22:38:39 +01:00
default :
2026-01-16 14:19:02 +01:00
// Queue full, drop oldest
2026-01-15 22:38:39 +01:00
}
}
2026-01-16 14:19:02 +01:00
b . outputMu . Unlock ( )
2026-01-15 22:38:39 +01:00
} )
2026-01-16 14:19:02 +01:00
// Log transcripts
2026-01-15 22:38:39 +01:00
xaiClient . OnTranscript ( func ( text string ) {
log . Printf ( "[Grok] %s" , text )
} )
// Connect to xAI
if err := xaiClient . Connect ( ) ; err != nil {
2026-01-16 14:19:02 +01:00
return fmt . Errorf ( "connect: %w" , err )
2026-01-15 22:38:39 +01:00
}
// Configure the session
if err := xaiClient . ConfigureSession ( b . voice , b . prompt ) ; err != nil {
xaiClient . Close ( )
2026-01-16 14:19:02 +01:00
return fmt . Errorf ( "configure: %w" , err )
2026-01-15 22:38:39 +01:00
}
2026-01-16 14:19:02 +01:00
b . globalMu . Lock ( )
b . globalXAI = xaiClient
b . globalMu . Unlock ( )
2026-01-15 22:38:39 +01:00
2026-01-16 14:19:02 +01:00
log . Println ( "[Global] ✓ Sesión xAI global activa" )
return nil
}
2026-01-15 22:38:39 +01:00
2026-01-16 14:19:02 +01:00
// handleInputAudio mixes incoming audio from any user into the unified buffer
func ( b * Bot ) handleInputAudio ( senderID uint16 , pcm [ ] int16 ) {
b . inputMu . Lock ( )
defer b . inputMu . Unlock ( )
2026-01-16 10:39:27 +01:00
2026-01-16 14:19:02 +01:00
// Extend buffer if needed
neededLen := len ( pcm )
currentLen := len ( b . inputBuffer )
if currentLen < neededLen {
// Extend with zeros
b . inputBuffer = append ( b . inputBuffer , make ( [ ] int16 , neededLen - currentLen ) ... )
}
// Mix (add with clipping protection)
for i , sample := range pcm {
val := int32 ( b . inputBuffer [ i ] ) + int32 ( sample )
if val > 32767 {
val = 32767
2026-01-16 10:39:27 +01:00
}
2026-01-16 14:19:02 +01:00
if val < - 32768 {
val = - 32768
}
b . inputBuffer [ i ] = int16 ( val )
2026-01-16 10:39:27 +01:00
}
2026-01-16 14:19:02 +01:00
b . lastInputTime = time . Now ( )
2026-01-15 22:38:39 +01:00
}
2026-01-16 14:19:02 +01:00
// runInputSender sends buffered audio to xAI every 20ms
func ( b * Bot ) runInputSender ( ) {
2026-01-15 22:38:39 +01:00
ticker := time . NewTicker ( 20 * time . Millisecond )
defer ticker . Stop ( )
for {
select {
2026-01-16 14:19:02 +01:00
case <- b . done :
2026-01-15 22:38:39 +01:00
return
case <- ticker . C :
2026-01-16 14:19:02 +01:00
b . inputMu . Lock ( )
if len ( b . inputBuffer ) >= 960 {
// Extract one frame
frame := make ( [ ] int16 , 960 )
copy ( frame , b . inputBuffer [ : 960 ] )
// Shift buffer (remove consumed samples)
b . inputBuffer = b . inputBuffer [ 960 : ]
b . inputMu . Unlock ( )
// Send to global xAI session
b . globalMu . Lock ( )
if b . globalXAI != nil && b . globalXAI . IsConnected ( ) {
b . globalXAI . SendAudio ( frame )
2026-01-15 22:38:39 +01:00
}
2026-01-16 14:19:02 +01:00
b . globalMu . Unlock ( )
} else {
b . inputMu . Unlock ( )
2026-01-15 22:38:39 +01:00
}
}
}
}
2026-01-16 14:19:02 +01:00
// runOutputSender sends xAI audio responses to TeamSpeak with proper timing
func ( b * Bot ) runOutputSender ( ) {
ticker := time . NewTicker ( 20 * time . Millisecond )
defer ticker . Stop ( )
2026-01-15 22:38:39 +01:00
2026-01-16 14:19:02 +01:00
for {
select {
case <- b . done :
return
case <- ticker . C :
select {
case frame := <- b . outputQueue :
if err := b . ts3 . SendAudio ( frame ) ; err != nil {
log . Printf ( "[Output] Error sending audio: %v" , err )
}
default :
// No audio to send
}
2026-01-15 22:38:39 +01:00
}
}
}