├── README.md ├── go.mod └── main.go /README.md: -------------------------------------------------------------------------------- 1 | # say 2 | 3 | say - Convert text to audible speech 4 | 5 | ## Usage 6 | 7 | ``` 8 | $ say べんぞうさんの中あったかいなりー 9 | ``` 10 | 11 | ## Install 12 | 13 | ``` 14 | $ go get github.com/mattn/say 15 | ``` 16 | 17 | ## Setup 18 | 19 | 1. Open https://cloud.voicetext.jp/ in your browser. 20 | 2. Get API key. 21 | 3. Paste API key into `~/.voicetext-apikey` 22 | 23 | ## License 24 | 25 | MIT 26 | 27 | ## Author 28 | 29 | Yasuhiro Matsumoto (a.k.a mattn) 30 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mattn/say 2 | 3 | go 1.13 4 | 5 | require github.com/mattn/go-soundplayer v0.0.1 6 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "net/http" 10 | "net/url" 11 | "os" 12 | "path/filepath" 13 | "runtime" 14 | "strings" 15 | "time" 16 | 17 | "github.com/mattn/go-soundplayer" 18 | ) 19 | 20 | var ( 21 | speaker = flag.String("speaker", "hikari", "show/haruka/hikari/takeru/santa/bear") 22 | emotion = flag.String("emotion", "", "happiness/anger/sadness") 23 | emotion_level = flag.Int("emotion_level", 1, "1/2") 24 | pitch = flag.Int("pitch", 100, "50% - 200%") 25 | speed = flag.Int("speed", 100, "50% - 400%") 26 | volume = flag.Int("volume", 100, "50% - 200%") 27 | keep = flag.Bool("keep", false, "remain wav file") 28 | ) 29 | 30 | type response struct { 31 | Error struct { 32 | Message string `json:"message"` 33 | } `json:"error"` 34 | } 35 | 36 | func say() int { 37 | flag.Parse() 38 | if flag.NArg() == 0 { 39 | flag.Usage() 40 | return 1 41 | } 42 | 43 | home := os.Getenv("HOME") 44 | if runtime.GOOS == "windows" { 45 | home = os.Getenv("USERPROFILE") 46 | } 47 | 48 | b, err := ioutil.ReadFile(filepath.Join(home, ".voicetext-apikey")) 49 | if err != nil { 50 | fmt.Fprintln(os.Stderr, "say", err) 51 | return 1 52 | } 53 | apikey := strings.TrimSpace(string(b)) 54 | 55 | params := url.Values{} 56 | params.Set("text", strings.Join(flag.Args(), " ")) 57 | params.Set("speaker", *speaker) 58 | if *emotion != "" { 59 | params.Set("emotion", *emotion) 60 | params.Set("emotion_level", fmt.Sprint(*emotion_level)) 61 | } 62 | params.Set("pitch", fmt.Sprint(*pitch)) 63 | params.Set("speed", fmt.Sprint(*speed)) 64 | params.Set("volume", fmt.Sprint(*volume)) 65 | req, err := http.NewRequest("POST", "https://api.voicetext.jp/v1/tts", strings.NewReader(params.Encode())) 66 | if err != nil { 67 | fmt.Fprintln(os.Stderr, "say", err) 68 | return 1 69 | } 70 | req.SetBasicAuth(apikey, "") 71 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 72 | res, err := http.DefaultClient.Do(req) 73 | if err != nil { 74 | fmt.Fprintln(os.Stderr, "say", err) 75 | return 1 76 | } 77 | defer res.Body.Close() 78 | if res.StatusCode != 200 { 79 | var resp response 80 | if err = json.NewDecoder(res.Body).Decode(&resp); err == nil { 81 | fmt.Fprintln(os.Stderr, "say", resp.Error.Message) 82 | } else { 83 | fmt.Fprintln(os.Stderr, "say", "something wrong") 84 | } 85 | return 1 86 | } 87 | 88 | dir, err := ioutil.TempDir(os.TempDir(), "say") 89 | if err != nil { 90 | fmt.Fprintln(os.Stderr, "say", err) 91 | return 1 92 | } 93 | defer os.RemoveAll(dir) 94 | 95 | f, err := os.Create(filepath.Join(dir, "say.wav")) 96 | if err != nil { 97 | fmt.Fprintln(os.Stderr, "say", err) 98 | return 1 99 | } 100 | 101 | _, err = io.Copy(f, res.Body) 102 | if err != nil { 103 | fmt.Fprintln(os.Stderr, "say", err) 104 | return 1 105 | } 106 | f.Close() 107 | 108 | err = soundplayer.Play(f.Name()) 109 | if err != nil { 110 | fmt.Fprintln(os.Stderr, "say", err) 111 | return 1 112 | } 113 | 114 | if *keep { 115 | now := time.Now().Format("say20060102030405.wav") 116 | copyFile(filepath.Base(now), f.Name()) 117 | } 118 | 119 | return 0 120 | } 121 | 122 | func copyFile(dst, src string) error { 123 | in, err := os.Open(src) 124 | if err != nil { 125 | return err 126 | } 127 | defer in.Close() 128 | out, err := os.Create(dst) 129 | if err != nil { 130 | return err 131 | } 132 | defer out.Close() 133 | _, err = io.Copy(out, in) 134 | err = out.Close() 135 | if err != nil { 136 | return err 137 | } 138 | return err 139 | } 140 | 141 | func main() { 142 | os.Exit(say()) 143 | } 144 | --------------------------------------------------------------------------------