ishaq101's picture
[KM-462] [DED][AI] Agnostic Interviewer Agent (Voice Capability) - Add
add6016
Raw
History Blame Contribute Delete
2.51 kB
package api
import (
"context"
"encoding/json"
"log/slog"
"net/http"
"github.com/gorilla/websocket"
"project/internal/app"
"project/internal/realtime"
)
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
type TTSProvider interface {
Synthesize(ctx context.Context, text string) ([]byte, error)
}
type WSHandler struct {
AppService *app.Service
Hub *realtime.SessionHub
TTS TTSProvider
}
func NewWSHandler(appService *app.Service, hub *realtime.SessionHub, tts TTSProvider) *WSHandler {
return &WSHandler{AppService: appService, Hub: hub, TTS: tts}
}
func (h *WSHandler) HandleAudio(w http.ResponseWriter, r *http.Request) {
sessionID := r.URL.Query().Get("session_id")
if sessionID == "" {
http.Error(w, "session_id required", http.StatusBadRequest)
return
}
conn, err := upgrader.Upgrade(w, r, nil)
if err != nil {
slog.Error("ws upgrade failed", "err", err)
return
}
defer conn.Close()
h.Hub.Register(sessionID, conn)
defer h.Hub.Unregister(sessionID)
buf := realtime.NewAudioBuffer()
for {
msgType, data, err := conn.ReadMessage()
if err != nil {
break
}
switch msgType {
case websocket.BinaryMessage:
buf.Append(data)
case websocket.TextMessage:
var evt realtime.ClientEvent
if err := json.Unmarshal(data, &evt); err != nil {
continue
}
if evt.Type == realtime.EventEndUtterance {
audio := buf.Flush()
go h.processAudio(context.Background(), conn, sessionID, audio)
}
}
}
}
func (h *WSHandler) processAudio(ctx context.Context, conn *websocket.Conn, sessionID string, audio []byte) {
result, err := h.AppService.StreamAudioMessage(ctx, sessionID, audio, func(chunk string) {
evt := realtime.ServerEvent{Type: realtime.EventTokenChunk, Payload: chunk}
b, _ := json.Marshal(evt)
_ = conn.WriteMessage(websocket.TextMessage, b)
})
if err != nil {
slog.Error("audio processing failed", "session_id", sessionID, "err", err)
return
}
// kirim full reply untuk backward compat (transcript lengkap)
evt := realtime.ServerEvent{Type: realtime.EventAssistantReply, Payload: result.Reply}
b, _ := json.Marshal(evt)
_ = conn.WriteMessage(websocket.TextMessage, b)
// synthesize ke audio lalu kirim sebagai binary frame
audioBytes, err := h.TTS.Synthesize(ctx, result.Reply)
if err != nil {
slog.Error("tts synthesis failed", "session_id", sessionID, "err", err)
return
}
_ = conn.WriteMessage(websocket.BinaryMessage, audioBytes)
}