All checks were successful
CI / changes (push) Successful in 1s
CI / test (push) Successful in 6s
CI / build-ai-gateway (push) Has been skipped
CI / build-ha-gateway (push) Has been skipped
CI / build-discord-bot (push) Successful in 1m33s
CI / build-tts-gateway (push) Successful in 37s
CI / build-tts-sidecar (push) Has been skipped
- Implemented the /speak command in Discord bot to synthesize speech using the TTS gateway. - Added voice handling logic to join voice channels and play synthesized audio. - Created tests for the new command and voice functionalities. - Introduced TTSGateway interface for TTS service communication. - Updated configuration to include TTS gateway address. - Documented the TTS gateway integration and model artifact distribution process.
125 lines
3.7 KiB
Go
125 lines
3.7 KiB
Go
package ttsgateway
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"log/slog"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"gitea.nik4nao.com/nik/home-services/discord-bot/internal/logger"
|
|
ttsv1 "gitea.nik4nao.com/nik/home-services/gen/tts/v1"
|
|
"go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc"
|
|
"google.golang.org/grpc"
|
|
"google.golang.org/grpc/credentials"
|
|
"google.golang.org/grpc/credentials/insecure"
|
|
)
|
|
|
|
// Client implements the app's TTS driven port over gRPC.
|
|
type Client struct {
|
|
conn *grpc.ClientConn
|
|
client ttsv1.TTSServiceClient
|
|
log *slog.Logger
|
|
}
|
|
|
|
// New constructs a gRPC client for the internal tts-gateway service.
|
|
func New(ctx context.Context, addr, tlsDir string, log *slog.Logger) (*Client, error) {
|
|
transportCreds := insecure.NewCredentials()
|
|
if tlsDir != "" {
|
|
creds, err := loadTransportCredentials(tlsDir)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load mTLS credentials: %w", err)
|
|
}
|
|
transportCreds = creds
|
|
}
|
|
|
|
conn, err := grpc.NewClient(
|
|
addr,
|
|
grpc.WithTransportCredentials(transportCreds),
|
|
grpc.WithStatsHandler(otelgrpc.NewClientHandler()),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dial tts-gateway: %w", err)
|
|
}
|
|
|
|
return &Client{
|
|
conn: conn,
|
|
client: ttsv1.NewTTSServiceClient(conn),
|
|
log: log,
|
|
}, nil
|
|
}
|
|
|
|
// Close closes the underlying gRPC connection.
|
|
func (c *Client) Close() error {
|
|
if err := c.conn.Close(); err != nil {
|
|
return fmt.Errorf("close tts-gateway client: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Synthesize forwards a speech synthesis request to tts-gateway. noise_scale/
|
|
// noise_scale_w/length_scale are left unset so tts-gateway applies its own defaults.
|
|
func (c *Client) Synthesize(ctx context.Context, speakerName, text string) ([]byte, string, error) {
|
|
start := time.Now()
|
|
log := logger.FromContext(ctx).With("grpc.method", "TTSService/Synthesize")
|
|
resp, err := c.client.Synthesize(ctx, &ttsv1.SynthesizeRequest{
|
|
SpeakerName: speakerName,
|
|
Text: text,
|
|
})
|
|
if err != nil {
|
|
log.Error("grpc call failed",
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
"error", err.Error(),
|
|
)
|
|
return nil, "", fmt.Errorf("synthesize speech: %w", err)
|
|
}
|
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
|
return resp.GetAudio(), resp.GetMimeType(), nil
|
|
}
|
|
|
|
// ListSpeakers returns the speaker roster from tts-gateway, optionally filtered by search.
|
|
func (c *Client) ListSpeakers(ctx context.Context, search string) ([]string, error) {
|
|
start := time.Now()
|
|
log := logger.FromContext(ctx).With("grpc.method", "TTSService/ListSpeakers")
|
|
resp, err := c.client.ListSpeakers(ctx, &ttsv1.ListSpeakersRequest{Search: search})
|
|
if err != nil {
|
|
log.Error("grpc call failed",
|
|
"duration_ms", time.Since(start).Milliseconds(),
|
|
"error", err.Error(),
|
|
)
|
|
return nil, fmt.Errorf("list tts-gateway speakers: %w", err)
|
|
}
|
|
log.Debug("grpc call completed", "duration_ms", time.Since(start).Milliseconds())
|
|
return append([]string(nil), resp.GetSpeakerNames()...), nil
|
|
}
|
|
|
|
func loadTransportCredentials(tlsDir string) (credentials.TransportCredentials, error) {
|
|
cert, err := tls.LoadX509KeyPair(
|
|
filepath.Join(tlsDir, "tls.crt"),
|
|
filepath.Join(tlsDir, "tls.key"),
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load client key pair: %w", err)
|
|
}
|
|
|
|
caPEM, err := os.ReadFile(filepath.Join(tlsDir, "ca.crt"))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read server CA: %w", err)
|
|
}
|
|
|
|
rootCAs := x509.NewCertPool()
|
|
if !rootCAs.AppendCertsFromPEM(caPEM) {
|
|
return nil, fmt.Errorf("append server CA: invalid PEM")
|
|
}
|
|
|
|
return credentials.NewTLS(&tls.Config{
|
|
Certificates: []tls.Certificate{cert},
|
|
RootCAs: rootCAs,
|
|
ServerName: "tts-gateway.home-services.svc.cluster.local",
|
|
MinVersion: tls.VersionTLS13,
|
|
}), nil
|
|
}
|