| 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 |
| } |
|
|
| |
| evt := realtime.ServerEvent{Type: realtime.EventAssistantReply, Payload: result.Reply} |
| b, _ := json.Marshal(evt) |
| _ = conn.WriteMessage(websocket.TextMessage, b) |
|
|
| |
| 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) |
| } |
|
|