Goで読み上げチャットボットを作ってみた。。


mattnさんのgo-xmpp使ったらあっさり作れた。 スピーチエンジンはOSX内蔵のものかGoogleのものを選べるようにしてみた。 ユーザー名やパスワードを指定して起動するとチャットメッセージを受け取るとそれをしゃべります。

動作に必要なもの

  • go get github.com/mattn/go-xmpp
  • soxのインストールまたはOS-Xのスピーチボイスにkyokoを追加。

ソースコード

package main

import (
   "bufio"
   "flag"
   "fmt"
   "github.com/mattn/go-xmpp"
   "io"
   "log"
   "net/http"
   "net/url"
   "os"
   "os/exec"
   "strings"
)

type Speaker interface {
   Speech(string)
}

type OSXSpeech struct {
}

func NewOSXSpeech() Speaker {
   return &OSXSpeech{}
}

func (o *OSXSpeech) Speech(msg string) {
   fmt.Println("Say:", msg)
   exec.Command("say", msg).Run()
}

const (
   TTSPEECHURL = "http://translate.google.com/translate_tts?"
   USERAGENT   = "Mozilla/5.0 AppleWebKit Safari"
)

type GoogleSpeech struct {
}

func NewGoogleSpeech() Speaker {
   return &GoogleSpeech{}
}

func (o *GoogleSpeech) Speech(msg string) {
   values := &url.Values{}
   values.Set("tl", "ja")
   values.Set("q", msg)
   req, err := http.NewRequest("GET", TTSPEECHURL+values.Encode(), nil)
   if err != nil {
      log.Fatalln(err)
   }
   req.Header.Set("User-Agent", USERAGENT)
   client := &http.Client{}
   resp, err := client.Do(req)
   if err != nil {
      log.Fatalln(err)
   }
   defer resp.Body.Close()
   fmt.Println("Say:", msg)
   // soxのplayコマンドでmp3型式の再生をします。
   cmd := exec.Command("play", "-t", "mp3", "-")
   wr, err := cmd.StdinPipe()
   if err != nil {
      log.Fatalln(err)
   }
   er := cmd.Start()
   if er != nil {
      log.Fatalln(er)
   }
   // GoogleTTSからのレスポンスを標準入力に流し込む。
   go func() {
      io.Copy(wr, resp.Body)
      wr.Close()
   }()
   cmd.Wait()
}

var (
   server   = flag.String("server", "talk.google.com:443", "server")
   username = flag.String("username", "", "username")
   password = flag.String("password", "", "password")
   notls    = flag.Bool("notls", false, "No TLS")
   debug    = flag.Bool("debug", false, "debug output")
   google   = flag.Bool("google", false, "use google TTS engine")
)

func main() {
   var speech Speaker
   if *google {
      speech = NewGoogleSpeech()
   } else {
      speech = NewOSXSpeech()
   }
   flag.Usage = func() {
      fmt.Fprintf(os.Stderr, "usage: example [options]\n")
      flag.PrintDefaults()
      os.Exit(2)
   }
   flag.Parse()
   if *username == "" || *password == "" {
      flag.Usage()
   }

   var talk *xmpp.Client
   var err error
   if *notls {
      talk, err = xmpp.NewClientNoTLS(*server, *username, *password, *debug)
   } else {
      talk, err = xmpp.NewClient(*server, *username, *password, *debug)
   }
   if err != nil {
      log.Fatal(err)
   }

   go func() {
      for {
         chat, err := talk.Recv()
         if err != nil {
            log.Fatal(err)
         }
         switch v := chat.(type) {
         case xmpp.Chat:
            if len(v.Text) > 0 {
               fmt.Println(v.Remote, v.Text)
               speech.Speech(v.Text)
            }
         case xmpp.Presence:
            fmt.Println(v.From, v.Show)
         }
      }
   }()
   for {
      in := bufio.NewReader(os.Stdin)
      line, err := in.ReadString('\n')
      if err != nil {
         continue
      }
      line = strings.TrimRight(line, "\n")

      tokens := strings.SplitN(line, " ", 2)
      if len(tokens) == 2 {
         talk.Send(xmpp.Chat{Remote: tokens[0], Type: "chat", Text: tokens[1]})
      }
   }
}

まとめ

以下のような要素で実現されていますー。

  • 基本はmattnさんのexampleにしゃべる機能をつけました。
  • HTTPリクエストでユーザーエージェントの書き換え。
  • 子プロセスにパイプでデータを流し込む。

パイプの受け渡しをgoroutineでこんなに簡単に書けるんですねー。