Files
2026-06-01 00:56:01 -04:00

162 lines
3.3 KiB
Go

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
}