package speech
import (
"bufio"
"bytes"
"errors"
"os/exec"
"runtime"
"strings"
"sync"
)
var (
mu sync.Mutex
currentCmd *exec.Cmd
voiceOnce sync.Once
cachedVoice string
)
func parseMacVoice(data []byte) (string, bool) {
var firstVoice string
scanner := bufio.NewScanner(bytes.NewReader(data))
for scanner.Scan() {
line := scanner.Text()
idx := strings.Index(line, "ja_JP")
if idx == -1 {
continue
}
prefix := strings.TrimSpace(line[:idx])
voiceName := prefix
if paren := strings.Index(voiceName, "("); paren != -1 {
voiceName = strings.TrimSpace(voiceName[:paren])
}
if voiceName == "Kyoko" {
return "Kyoko", true
}
if firstVoice == "" && voiceName != "" {
firstVoice = voiceName
}
}
return firstVoice, firstVoice != ""
}
func macVoice() string {
voiceOnce.Do(func() {
out, err := exec.Command("say", "-v", "?").Output()
if err != nil {
return
}
v, ok := parseMacVoice(out)
if ok {
cachedVoice = v
}
})
return cachedVoice
}
func speechCmd(text string) (string, []string, error) {
if runtime.GOOS == "darwin" {
if _, err := exec.LookPath("say"); err != nil {
return "", nil, errors.New("speech: say command not found")
}
voice := macVoice()
if voice != "" {
return "say", []string{"-v", voice, text}, nil
}
return "say", []string{text}, nil
}
if runtime.GOOS == "linux" {
if path, err := exec.LookPath("spd-say"); err == nil {
return path, []string{"-l", "ja", "-w", text}, nil
}
if path, err := exec.LookPath("espeak-ng"); err == nil {
return path, []string{"-v", "ja", text}, nil
}
return "", nil, errors.New("speech: neither spd-say nor espeak-ng found")
}
return "", nil, errors.New("speech: unsupported platform")
}
// Speak speaks text non-blocking. If an utterance is already in progress,
// Speak kills the previous utterance process before starting the new one.
func Speak(text string) error {
prog, args, err := speechCmd(text)
if err != nil {
return err
}
mu.Lock()
if currentCmd != nil && currentCmd.Process != nil {
_ = currentCmd.Process.Kill()
}
cmd := exec.Command(prog, args...)
if err := cmd.Start(); err != nil {
mu.Unlock()
return err
}
currentCmd = cmd
mu.Unlock()
go func() {
_ = cmd.Wait()
mu.Lock()
if currentCmd == cmd {
currentCmd = nil
}
mu.Unlock()
}()
return nil
}