音声合成


[tag:]

Raspberry PiやArduinoみたいなシングルボードコンピュータを 使って何かつくろうかと思っているが、たいていのことは スマートフォンでもできるのであまりいい用途が思いつかない。

とりあえず、合成音声の出力はしたいなーと思い、 いろいろと探してみると、フリーでもよさそうなものが出てくる。

rospeex  NICTが開発しているらしい。
 杉浦孔明さんという方が関わっているようで、  http://rospeex.ucri.jgn-x.jp/nauth_json/jsServices/VoiceTraSS  にPOSTすると合成されたデータが返ってくるようだ(デモ)。
 Python + クラウド音声合成 で高品質なアニメ声読み上げを参考に  スクリプトを起こしてみるとうまくいった。
 pythonではpyaudioを使うとwavの再生もでき、再生部分も含めるとこんな感じになる。
 アニメ声なのが、場面によってはやや使いづらい。

# -*- coding: utf-8 -*-
import sys
import base64
import urllib2
import json
import wave
import time
import pyaudio

tts_url ='http://rospeex.ucri.jgn-x.jp/nauth_json/jsServices/VoiceTraSS'

if __name__=='__main__':
    message = sys.argv[1]

    # command
    tts_command = { 'method':'speak',
    'params':['1.1',
    {'language':'ja','text':message,'voiceType':"*",'audioType':"audio/x-wav"}]}

    obj_command = json.dumps(tts_command)
    req = urllib2.Request(tts_url, obj_command)
    received = urllib2.urlopen(req).read()

    # extract wav file
    obj_received = json.loads(received)
    tmp = obj_received['result']['audio'] # extract result->audio
    speech = base64.decodestring(tmp.encode('utf-8'))

    f = open ("tmp.wav",'wb')
    f.write(speech)
    f.close

    w = wave.open("tmp.wav",'rb')
    p = pyaudio.PyAudio()
    def callback(in_data, frame_count, time_info, status):
        data = w.readframes(frame_count)
        return (data, pyaudio.paContinue)
    stream = p.open(format=p.get_format_from_width(w.getsampwidth()),
                channels=w.getnchannels(),
                rate=w.getframerate(),
                output=True,
                stream_callback=callback)
    stream.start_stream()
    while stream.is_active():
        time.sleep(0.1)
    stream.stop_stream()
    stream.close()

VoiceTextAPI  こちらはHOYAが開発していて、無料登録するとAPIが使える。
 いろいろな言語のライブラリが用意されていたので、  golangのものを使ってみた。github/yosssi/go-voicetext  VoiceTextAPIのバックエンドにもgolangが使われているらしい(スライド)。
 ファイルに書かれた内容を句読点等で区切りながら読み上げるサンプル。
 splitの実装は句読点を残すために場当たり的実装になっているので要注意。
 合成音声の出来はかなりいい感じである。
 最近はARM用のパッケージもgolang公式で配布されるようになったので  Raspberry Piなんかでも動かせるのだろう。

package main

import (
    "bufio"
    "fmt"
    "os"
    "os/exec"
    "strings"
    "time"
    "unicode/utf8"

    "github.com/yosssi/go-voicetext"
)

const (
    APIKEY = "0000000000000" // 無料登録して取得したAPIキー
)

func main() {
    c := voicetext.NewClient(APIKEY, nil)

    var message string
    var cmd *exec.Cmd
    var period bool

    var scanner *bufio.Scanner
    if len(os.Args) >= 2 {
        f, err := os.Open(os.Args[1])
        if err != nil {
            os.Exit(1)
        }
        scanner = bufio.NewScanner(f)
    } else {
        scanner = bufio.NewScanner(os.Stdin)
    }
    isBreak := func(r rune) bool {
        switch r {
        case '、', '。', ',', '.', '「':
            return true
        default:
            return false
        }
    }
    split := func(data []byte, atEOF bool) (advance int, token []byte, err error) {
        start := 0
        for width := 0; start < len(data); start += width {
            var r rune
            r, width = utf8.DecodeRune(data[start:])
            if !isBreak(r) {
                break
            }
        }
        for width, i := 0, start; i < len(data); i+= width {
            var r rune
            r, width = utf8.DecodeRune(data[i:])
            if isBreak(r) {
                return i + width, data[start:i+3], nil
            }
        }
        if atEOF && len(data) > start {
            return len(data), data[start:], nil
        }
        return start, nil, nil
    }
    scanner.Split(split)
    for scanner.Scan() {
        message = scanner.Text()
        result, err := c.TTS(message, &voicetext.TTSOptions{
            Speaker: voicetext.SpeakerHikari,
            Pitch: 80,
            Speed: 115,
            Volume: 150,
        })
        if err != nil {
            os.Exit(1)
        }
        if result.ErrMsg != nil {
            fmt.Println(result.ErrMsg)
            os.Exit(1)
        }
        if cmd != nil {
            cmd.Wait()
            os.Remove("tmp.wav")
            if period {
                time.Sleep(1000*time.Millisecond)
            } else {
                time.Sleep(400*time.Millisecond)
            }
        }
        if strings.HasSuffix(message, "。") || strings.HasSuffix(message, ".") {
            period = true
        } else {
            period = false
        }
        f, err := os.Create("tmp.wav")
        if err != nil {
            os.Exit(1)
        }
        defer f.Close()
        if _, err := f.Write(result.Sound); err != nil {
            os.Exit(1)
        }
        cmd = exec.Command("aplay", "tmp.wav")
        err = cmd.Start()
        if err != nil {
            os.Exit(1)
        }
    }
    if err := scanner.Err(); err != nil {
        os.Exit(1)
    }
    if cmd != nil {
        cmd.Wait()
        os.Remove("tmp.wav")
    }
}