mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
chrome tts
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
package roverd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const chromeTTSDaemonPath = "/usr/local/bin/chromegtts-daemon"
|
||||
|
||||
type chromeTTSDaemon struct {
|
||||
log *log.Logger
|
||||
mu sync.Mutex
|
||||
cmd *exec.Cmd
|
||||
stdin io.WriteCloser
|
||||
scanner *bufio.Scanner
|
||||
}
|
||||
|
||||
func NewChromeTTSDaemon(logger *log.Logger) *chromeTTSDaemon {
|
||||
return &chromeTTSDaemon{log: logger}
|
||||
}
|
||||
|
||||
func (d *chromeTTSDaemon) Start(ctx context.Context) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
return d.startLocked(ctx)
|
||||
}
|
||||
|
||||
func (d *chromeTTSDaemon) Speak(ctx context.Context, text, voice string, pitch, speed float64) error {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
if err := d.startLocked(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
"text": text,
|
||||
"voice": voice,
|
||||
"pitch": pitch,
|
||||
"speed": speed,
|
||||
}
|
||||
encoded, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := d.stdin.Write(append(encoded, '\n')); err != nil {
|
||||
d.stopLocked()
|
||||
return fmt.Errorf("write chromegtts request: %w", err)
|
||||
}
|
||||
|
||||
type result struct {
|
||||
response chromeTTSResponse
|
||||
err error
|
||||
}
|
||||
done := make(chan result, 1)
|
||||
go func(scanner *bufio.Scanner) {
|
||||
var response chromeTTSResponse
|
||||
if !scanner.Scan() {
|
||||
done <- result{err: fmt.Errorf("chromegtts daemon stopped")}
|
||||
return
|
||||
}
|
||||
if err := json.Unmarshal(scanner.Bytes(), &response); err != nil {
|
||||
done <- result{err: fmt.Errorf("decode chromegtts response: %w", err)}
|
||||
return
|
||||
}
|
||||
done <- result{response: response}
|
||||
}(d.scanner)
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
d.stopLocked()
|
||||
return ctx.Err()
|
||||
case res := <-done:
|
||||
if res.err != nil {
|
||||
d.stopLocked()
|
||||
return res.err
|
||||
}
|
||||
if !res.response.OK {
|
||||
return fmt.Errorf("chromegtts failed: %s", res.response.Error)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (d *chromeTTSDaemon) Shutdown() {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
d.stopLocked()
|
||||
}
|
||||
|
||||
type chromeTTSResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Ready bool `json:"ready,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
func (d *chromeTTSDaemon) startLocked(ctx context.Context) error {
|
||||
if d.cmd != nil && d.cmd.ProcessState == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := exec.Command(chromeTTSDaemonPath)
|
||||
stdin, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cmd.Stderr = os.Stderr
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("start chromegtts daemon: %w", err)
|
||||
}
|
||||
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
var ready chromeTTSResponse
|
||||
if !scanner.Scan() {
|
||||
_ = cmd.Process.Kill()
|
||||
_ = cmd.Wait()
|
||||
return fmt.Errorf("chromegtts daemon exited before ready")
|
||||
}
|
||||
if err := json.Unmarshal(scanner.Bytes(), &ready); err != nil {
|
||||
_ = cmd.Process.Kill()
|
||||
_ = cmd.Wait()
|
||||
return fmt.Errorf("decode chromegtts ready: %w", err)
|
||||
}
|
||||
if !ready.OK {
|
||||
_ = cmd.Process.Kill()
|
||||
_ = cmd.Wait()
|
||||
return fmt.Errorf("chromegtts daemon not ready: %s", ready.Error)
|
||||
}
|
||||
|
||||
d.cmd = cmd
|
||||
d.stdin = stdin
|
||||
d.scanner = scanner
|
||||
if d.log != nil {
|
||||
d.log.Printf("chromegtts daemon started")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *chromeTTSDaemon) stopLocked() {
|
||||
if d.stdin != nil {
|
||||
_ = d.stdin.Close()
|
||||
d.stdin = nil
|
||||
}
|
||||
if d.cmd != nil && d.cmd.ProcessState == nil && d.cmd.Process != nil {
|
||||
_ = d.cmd.Process.Kill()
|
||||
_ = d.cmd.Wait()
|
||||
}
|
||||
d.cmd = nil
|
||||
d.scanner = nil
|
||||
}
|
||||
@@ -64,11 +64,12 @@ type servoPayload struct {
|
||||
}
|
||||
|
||||
type ttsPayload struct {
|
||||
Text string `json:"text"`
|
||||
Engine string `json:"engine,omitempty"`
|
||||
Voice string `json:"voice,omitempty"`
|
||||
Pitch int `json:"pitch,omitempty"`
|
||||
Speak bool `json:"speak,omitempty"`
|
||||
Text string `json:"text"`
|
||||
Engine string `json:"engine,omitempty"`
|
||||
Voice string `json:"voice,omitempty"`
|
||||
Pitch float64 `json:"pitch,omitempty"`
|
||||
Speed float64 `json:"speed,omitempty"`
|
||||
Speak bool `json:"speak,omitempty"`
|
||||
}
|
||||
|
||||
type hornPayload struct {
|
||||
|
||||
+11
-6
@@ -38,11 +38,11 @@ func (c *WSClient) handleTTSPayload(ctx context.Context, payload *ttsPayload) er
|
||||
if voice == "" {
|
||||
voice = strings.TrimSpace(c.cfg.Audio.DefaultVoice)
|
||||
}
|
||||
pitch := payload.Pitch
|
||||
if pitch <= 0 {
|
||||
pitch = c.cfg.Audio.DefaultPitch
|
||||
espeakPitch := int(payload.Pitch)
|
||||
if espeakPitch <= 0 {
|
||||
espeakPitch = c.cfg.Audio.DefaultPitch
|
||||
}
|
||||
pitch = clampInt(pitch, 0, 99)
|
||||
espeakPitch = clampInt(espeakPitch, 0, 99)
|
||||
|
||||
runCtx, cancel := context.WithTimeout(ctx, 12*time.Second)
|
||||
defer cancel()
|
||||
@@ -51,8 +51,8 @@ func (c *WSClient) handleTTSPayload(ctx context.Context, payload *ttsPayload) er
|
||||
switch engine {
|
||||
case "espeak", "e":
|
||||
args := []string{}
|
||||
if pitch > 0 {
|
||||
args = append(args, "-p", fmt.Sprintf("%d", pitch))
|
||||
if espeakPitch > 0 {
|
||||
args = append(args, "-p", fmt.Sprintf("%d", espeakPitch))
|
||||
}
|
||||
args = append(args, text)
|
||||
cmd = exec.CommandContext(runCtx, "espeak", args...)
|
||||
@@ -63,6 +63,11 @@ func (c *WSClient) handleTTSPayload(ctx context.Context, payload *ttsPayload) er
|
||||
}
|
||||
args = append(args, "-t", text)
|
||||
cmd = exec.CommandContext(runCtx, "flite", args...)
|
||||
case "chromegtts", "googletts", "gtts", "google":
|
||||
if c.chromeTTS == nil {
|
||||
return fmt.Errorf("chromegtts unavailable")
|
||||
}
|
||||
return c.chromeTTS.Speak(runCtx, text, voice, payload.Pitch, payload.Speed)
|
||||
default:
|
||||
return fmt.Errorf("unsupported tts engine: %s", engine)
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ type WSClient struct {
|
||||
recoverMu sync.Mutex
|
||||
recovering bool
|
||||
ttsQueue chan *ttsPayload
|
||||
chromeTTS *chromeTTSDaemon
|
||||
lastAux motorPWMPayload
|
||||
autoSideOn bool
|
||||
connMu sync.Mutex
|
||||
@@ -43,6 +44,10 @@ func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, even
|
||||
if cfg.Audio.TTSEnabled {
|
||||
ttsQueue = make(chan *ttsPayload, 2)
|
||||
}
|
||||
var chromeTTS *chromeTTSDaemon
|
||||
if cfg.Audio.TTSEnabled {
|
||||
chromeTTS = NewChromeTTSDaemon(logger)
|
||||
}
|
||||
var horn *HornSynth
|
||||
if cfg.Horn.Enabled {
|
||||
horn = NewHornSynth(cfg.Horn, logger)
|
||||
@@ -58,6 +63,7 @@ func NewWSClient(cfg *Config, adapter *SerialAdapter, frames <-chan []byte, even
|
||||
nightVision: nightVision,
|
||||
log: logger,
|
||||
ttsQueue: ttsQueue,
|
||||
chromeTTS: chromeTTS,
|
||||
audioLevels: AudioLevels{
|
||||
HornGain: 1.0,
|
||||
TTSGain: 1.0,
|
||||
@@ -79,6 +85,9 @@ func (c *WSClient) Run(ctx context.Context) error {
|
||||
c.markConnected()
|
||||
defer conn.Close(websocket.StatusInternalError, "closed")
|
||||
defer c.markDisconnected()
|
||||
if c.chromeTTS != nil {
|
||||
defer c.chromeTTS.Shutdown()
|
||||
}
|
||||
|
||||
if err := c.sendHello(ctx, conn); err != nil {
|
||||
return err
|
||||
@@ -89,6 +98,7 @@ func (c *WSClient) Run(ctx context.Context) error {
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
c.startTTSWorker(ctx)
|
||||
c.warmChromeTTS(ctx)
|
||||
go func() {
|
||||
errCh <- c.readLoop(ctx, conn)
|
||||
}()
|
||||
@@ -356,6 +366,17 @@ func (c *WSClient) startTTSWorker(ctx context.Context) {
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *WSClient) warmChromeTTS(ctx context.Context) {
|
||||
if c.chromeTTS == nil {
|
||||
return
|
||||
}
|
||||
go func() {
|
||||
if err := c.chromeTTS.Start(ctx); err != nil {
|
||||
c.log.Printf("chromegtts warmup failed: %v", err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (c *WSClient) handleServoCommand(payload *servoPayload) error {
|
||||
switch {
|
||||
case payload.Angle != nil:
|
||||
|
||||
Reference in New Issue
Block a user