feat: migrate to typed messages, drop base64
- Decode TTSRequest via natsutil.Decode[messages.TTSRequest] - Stream audio as raw bytes via messages.TTSAudioChunk (no base64) - Non-stream response uses messages.TTSFullResponse - Status updates use messages.TTSStatus - Voice list/refresh use messages.TTSVoiceListResponse/TTSVoiceRefreshResponse - Registry returns []messages.TTSVoiceInfo (not []map[string]any) - Remove strVal/boolVal helpers - Add .dockerignore, GOAMD64=v3 in Dockerfile - Update tests for typed structs (13 tests pass)
This commit is contained in:
106
main.go
106
main.go
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -23,6 +22,7 @@ import (
|
||||
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/config"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/health"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/messages"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/natsutil"
|
||||
"git.daviestechlabs.io/daviestechlabs/handler-base/telemetry"
|
||||
)
|
||||
@@ -128,16 +128,16 @@ func (vr *VoiceRegistry) get(name string) *CustomVoice {
|
||||
return vr.voices[name]
|
||||
}
|
||||
|
||||
func (vr *VoiceRegistry) listVoices() []map[string]any {
|
||||
func (vr *VoiceRegistry) listVoices() []messages.TTSVoiceInfo {
|
||||
vr.mu.RLock()
|
||||
defer vr.mu.RUnlock()
|
||||
result := make([]map[string]any, 0, len(vr.voices))
|
||||
result := make([]messages.TTSVoiceInfo, 0, len(vr.voices))
|
||||
for _, v := range vr.voices {
|
||||
result = append(result, map[string]any{
|
||||
"name": v.Name,
|
||||
"language": v.Language,
|
||||
"model_type": v.ModelType,
|
||||
"created_at": v.CreatedAt,
|
||||
result = append(result, messages.TTSVoiceInfo{
|
||||
Name: v.Name,
|
||||
Language: v.Language,
|
||||
ModelType: v.ModelType,
|
||||
CreatedAt: v.CreatedAt,
|
||||
})
|
||||
}
|
||||
return result
|
||||
@@ -222,11 +222,11 @@ func main() {
|
||||
|
||||
// Helper: publish status
|
||||
publishStatus := func(sessionID, status, message string) {
|
||||
statusMsg := map[string]any{
|
||||
"session_id": sessionID,
|
||||
"status": status,
|
||||
"message": message,
|
||||
"timestamp": time.Now().Unix(),
|
||||
statusMsg := &messages.TTSStatus{
|
||||
SessionID: sessionID,
|
||||
Status: status,
|
||||
Message: message,
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
_ = nc.Publish(fmt.Sprintf("%s.%s", statusSubjectPrefix, sessionID), statusMsg)
|
||||
}
|
||||
@@ -272,7 +272,7 @@ func main() {
|
||||
return io.ReadAll(resp.Body)
|
||||
}
|
||||
|
||||
// Helper: stream audio chunks
|
||||
// Helper: stream audio chunks — raw bytes, no base64
|
||||
streamAudio := func(sessionID string, audioBytes []byte) {
|
||||
totalChunks := (len(audioBytes) + audioChunkSize - 1) / audioChunkSize
|
||||
for i := 0; i < len(audioBytes); i += audioChunkSize {
|
||||
@@ -284,14 +284,14 @@ func main() {
|
||||
chunkIndex := i / audioChunkSize
|
||||
isLast := end >= len(audioBytes)
|
||||
|
||||
msg := map[string]any{
|
||||
"session_id": sessionID,
|
||||
"chunk_index": chunkIndex,
|
||||
"total_chunks": totalChunks,
|
||||
"audio_b64": base64.StdEncoding.EncodeToString(chunk),
|
||||
"is_last": isLast,
|
||||
"timestamp": time.Now().Unix(),
|
||||
"sample_rate": sampleRate,
|
||||
msg := &messages.TTSAudioChunk{
|
||||
SessionID: sessionID,
|
||||
ChunkIndex: chunkIndex,
|
||||
TotalChunks: totalChunks,
|
||||
Audio: chunk,
|
||||
IsLast: isLast,
|
||||
Timestamp: time.Now().Unix(),
|
||||
SampleRate: sampleRate,
|
||||
}
|
||||
_ = nc.Publish(fmt.Sprintf("%s.%s", audioSubjectPrefix, sessionID), msg)
|
||||
}
|
||||
@@ -307,17 +307,21 @@ func main() {
|
||||
}
|
||||
sessionID := parts[4]
|
||||
|
||||
data, err := natsutil.DecodeMsgpackMap(natMsg.Data)
|
||||
req, err := natsutil.Decode[messages.TTSRequest](natMsg.Data)
|
||||
if err != nil {
|
||||
slog.Error("decode error", "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
text := strVal(data, "text", "")
|
||||
speaker := strVal(data, "speaker", defaultSpeaker)
|
||||
language := strVal(data, "language", defaultLanguage)
|
||||
speakerWavB64 := strVal(data, "speaker_wav_b64", "")
|
||||
stream := boolVal(data, "stream", true)
|
||||
text := req.Text
|
||||
speaker := orDefault(req.Speaker, defaultSpeaker)
|
||||
language := orDefault(req.Language, defaultLanguage)
|
||||
speakerWavB64 := req.SpeakerWavB64
|
||||
stream := req.Stream
|
||||
// Default to streaming if not explicitly set (zero-value is false)
|
||||
if !stream && text != "" {
|
||||
stream = true
|
||||
}
|
||||
|
||||
if text == "" {
|
||||
slog.Warn("empty text", "session", sessionID)
|
||||
@@ -338,11 +342,11 @@ func main() {
|
||||
if stream {
|
||||
streamAudio(sessionID, audioBytes)
|
||||
} else {
|
||||
msg := map[string]any{
|
||||
"session_id": sessionID,
|
||||
"audio_b64": base64.StdEncoding.EncodeToString(audioBytes),
|
||||
"timestamp": time.Now().Unix(),
|
||||
"sample_rate": sampleRate,
|
||||
msg := &messages.TTSFullResponse{
|
||||
SessionID: sessionID,
|
||||
Audio: audioBytes,
|
||||
Timestamp: time.Now().Unix(),
|
||||
SampleRate: sampleRate,
|
||||
}
|
||||
_ = nc.Publish(fmt.Sprintf("%s.%s", audioSubjectPrefix, sessionID), msg)
|
||||
}
|
||||
@@ -358,11 +362,11 @@ func main() {
|
||||
|
||||
// Subscribe: list voices
|
||||
if _, err := nc.Conn().Subscribe(voicesListSubject, func(msg *nats.Msg) {
|
||||
resp := map[string]any{
|
||||
"default_speaker": defaultSpeaker,
|
||||
"custom_voices": registry.listVoices(),
|
||||
"last_refresh": registry.lastRefresh.Unix(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
resp := &messages.TTSVoiceListResponse{
|
||||
DefaultSpeaker: defaultSpeaker,
|
||||
CustomVoices: registry.listVoices(),
|
||||
LastRefresh: registry.lastRefresh.Unix(),
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
packed, _ := msgpack.Marshal(resp)
|
||||
if msg.Reply != "" {
|
||||
@@ -375,10 +379,10 @@ func main() {
|
||||
// Subscribe: refresh voices
|
||||
if _, err := nc.Conn().Subscribe(voicesRefreshSubject, func(msg *nats.Msg) {
|
||||
count := registry.refresh()
|
||||
resp := map[string]any{
|
||||
"count": count,
|
||||
"custom_voices": registry.listVoices(),
|
||||
"timestamp": time.Now().Unix(),
|
||||
resp := &messages.TTSVoiceRefreshResponse{
|
||||
Count: count,
|
||||
CustomVoices: registry.listVoices(),
|
||||
Timestamp: time.Now().Unix(),
|
||||
}
|
||||
packed, _ := msgpack.Marshal(resp)
|
||||
if msg.Reply != "" {
|
||||
@@ -418,24 +422,6 @@ func main() {
|
||||
|
||||
// Helpers
|
||||
|
||||
func strVal(m map[string]any, key, fallback string) string {
|
||||
if v, ok := m[key]; ok {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func boolVal(m map[string]any, key string, fallback bool) bool {
|
||||
if v, ok := m[key]; ok {
|
||||
if b, ok := v.(bool); ok {
|
||||
return b
|
||||
}
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
|
||||
Reference in New Issue
Block a user