2014年02月15日

F#で漢数字とアラビア数字の変換を考える

前回(F#で数の読みを考える)、アラビア数字からなる数値の読みを出力するプログラムをF#で考えてみました。今度は、アラビア数字の数値文字列と漢数字文字列の変換を考えてみます。

まず、アラビア数字から漢数字への変換は、前回の位取りのある数値の読みを出力するプログラムを修正することで簡単にできます。音便などを考える必要もありません。

※垓と穣の間の「ジョ」はこのブログでは表示できないので、二つの漢字をつなげて「禾予」と表示しています。適切に表示でき、実行できる環境ならば、これを本来の文字で置き換えてください。

let san2kan num_str =
    let rec reverse = function
        | "" -> ""
        | x  -> x.[1..] |> reverse |> (+) <| string x.[0]
    let rec loop rank str =
        let jusshin_kurai = function
            |  0 -> ""
            |  1 -> "十"
            |  2 -> "百"
            |  3 -> "千"
            |  _ -> ""
        let manshin_kurai = function
            |  1 -> "万"
            |  2 -> "億"
            |  3 -> "兆"
            |  4 -> "京"
            |  5 -> "垓"
            |  6 -> "禾予"
            |  7 -> "穣"
            |  8 -> "溝"
            |  9 -> "澗"
            | 10 -> "正"
            | 11 -> "載"
            | 12 -> "極"
            | 13 -> "恒河沙"
            | 14 -> "阿僧祇"
            | 15 -> "那由他"
            | 16 -> "不可思議"
            | 17 -> "無量大数"
            |  _ -> ""
        let kazu_yomi = function
            | '2' -> "二"
            | '3' -> "三"
            | '4' -> "四"
            | '5' -> "五"
            | '6' -> "六"
            | '7' -> "七"
            | '8' -> "八"
            | '9' -> "九"
            |  _  -> ""
        match String.length str > 3 with
        | false -> ""
        | true  ->
            match manshin_kurai rank with
            | "" when rank > 1 ->
                failwith  <| "エラー: 桁が多すぎます: '" + num_str + "'"
            | kurai ->
                let s = [|
                    for i in 0..3 ->
                        if (i = 0 || (i = 3 && String.length num_str<>4))
                            && str.[i] = '1'
                        then "一" + jusshin_kurai i
                        elif str.[i] = '0' then ""
                        else kazu_yomi str.[i] + jusshin_kurai i |]
                loop <| rank + 1 <| str.[4..]
                |> (+) <|
                match s.[3]+s.[2]+s.[1]+s.[0] with
                | "" -> ""
                | x -> x + kurai
    match num_str with
    | ""  -> ""
    | "0" -> "零"
    |  x  -> x |> reverse |> (+) <| "000" |> loop 0

[<EntryPoint>]
let main args =
    let routine line =
        let number =
            String.collect( fun x -> if x = ',' then "" else string x ) line
        if not ( String.forall( fun x -> System.Char.IsDigit x ) number ) then
            failwith <| "エラー: 数字以外の文字があります: '" + line + "'"
        number
        |> san2kan
        |> printfn "%s"
    try
        match args.Length with
        | 0 ->
            let line = System.Console.ReadLine()
            routine line
        | 1 -> routine args.[0]
        | _ -> failwith "エラー: 引数はひとつだけ指定してください"
        0
    with
        Failure msg ->
            eprintfn "%s" msg
            eprintf "機 能: 算用数字を漢数字に変換する"
            -1

以上のようになります。今回は各桁の値は配列に保持することにし、for文を使ったシーケンス式で値を計算しています。前回は説明していませんでしたが、百と十の位は値が一のときはその表記を省略しています。千は、万や億などに属する場合は省略せずに一千と書き、単純な千のときは省略するようにしています。

 

今度は、アラビア数字を漢数字の数値に変換する方法を考えてみます。

これは漢数字の階層構造に目を付けて考えていきます。漢数字に現れる単語は、重複するものもありますが、一つずつ増えていくものと、十倍ずつ増えていくものと、万倍ずつ増えていくものの三つのグループに分類できます。この三者の階層構造によって漢数字は組み立てられています。このことを踏まえて、再帰を使って記述すると、次のプログラムができます。

open System.Text.RegularExpressions

let convert_kan2san input =
    let separate input pattern =
        let m = Regex.Match(input, "(.*?)" + pattern + "(.*)")
        if m.Success then Some (m.Groups.[1].Value, m.Groups.[2].Value)
        else None
    let place_digit = function
        | ""   -> "1"
        | "一" -> "1"
        | "二" -> "2"
        | "三" -> "3"
        | "四" -> "4"
        | "五" -> "5"
        | "六" -> "6"
        | "七" -> "7"
        | "八" -> "8"
        | "九" -> "9"
        |  _   -> failwith "エラー:正規の漢数字ではありません:" + input
    let place_4digits skip data =
        let rec place_digits skip str rank =
            match [|"千";"百";"十";""|].[rank] with
            | "" ->
                if skip || str <> "" then place_digit str else "0"
            | kurai ->
                match separate str kurai with
                | Some (left, right) ->
                    (place_digit left) + place_digits false right (rank+1)
                | None ->
                    (if skip then "" else "0") + place_digits skip str (rank+1)
        place_digits skip data 0
    let rec place_digits skip str rank =
        match
            [|
            "無量大数";"不可思議";"那由他";"阿僧祇";"恒河沙";
            "極";"載";"正";"澗";"溝";"穣";"禾予";"垓";"京";"兆";"億";"万";""|].[rank] with
        | "" ->
            if skip || str <> "" then place_4digits skip str else "0000"
        | kurai ->
            match separate str kurai with
            | Some (left, right) ->
                (place_4digits skip left) + place_digits false right (rank+1)
            | None ->
                (if skip then "" else "0000") + place_digits skip str (rank+1)
    if input = "" || input = "零" then "0" else place_digits true input 0

[<EntryPoint>]
let main args =
    let routine line =
        line
        |> convert_kan2san
        |> printfn "%s"
    try
        match args.Length with
        | 0 ->
            let line = System.Console.ReadLine()
            routine line
        | 1 -> routine args.[0]
        | _ -> failwith "エラー: 引数はひとつだけ指定してください"
        0
    with
        Failure msg ->
            eprintfn "%s" msg
            eprintf "機 能: 漢数字を算用数字に変換します"
            -1

位を表す漢字を見つけるための関数として、separateを定義しています。戻り値の値をオプション型にし、見つかったときは位の文字の左と右の文字列からなるタプルを返します。見つからなかったときはNoneを返します。

ここでは再帰関数place_digitsを使って文字列を連結させながらの繰り返しを記述しています。この関数は十進の位と万進の位のそれぞれの処理用に同じ名前で別々に定義しています。最初のパターンマッチで位の配列が終わりかどうか判断させて、終わっていなければ次のパターンマッチで、先の関数separateを使い文字列中に現在対象としている位があるかどうか調べています。

この一連の処理で問題となるのが、位の文字が存在しないときの処理です。解析中の漢数字中の最高位の位が見つかるまでは、文字列中に現れない万進の位も十進の位も空文字列に変換されます。しかし最高位の位がどの万進と十進の位の組み合わせかが確定すると、今度は文字列中に現れない万進の位は”0000”、現れない十進の位は”0”に変換されます。つまり最高位が見つかったかどうかを示すものがどうしても必要になります。この状態を示す変数として、再帰で渡され続ける引数skipを使っています。

 

上記の十進の位と万進の位についての再帰を使った繰り返しは、ほとんど同じ処理をしています。これはクラスを使って一つにまとめることができます。まだ使ったことのないクラスを使う練習として参考までに書いてみます。

open System.Text.RegularExpressions

type Converter( array, func, str ) =
    let ranks : string [] = array
    let subconvert = func
    let padding = str
    member this.place_digits(skip, str, rank) =
        match ranks.[rank] with
        | ""    ->
            if skip || str <> "" then subconvert skip str else padding
        | kurai ->
            let separate input pattern =
                let m = Regex.Match(input, "(.*?)" + pattern + "(.*)")
                if m.Success then Some (m.Groups.[1].Value, m.Groups.[2].Value)
                else None
            match separate str kurai with
            | Some (left, right) ->
                (subconvert skip left) + this.place_digits(false, right, rank+1)
            | None ->
                (if skip then "" else padding) + this.place_digits(skip, str, rank+1)

let convert_kan2san input =
    let rank_ju  = [|"千";"百";"十";""|]
    let rank_man = [|
        "無量大数";"不可思議";"那由他";"阿僧祇";"恒河沙";
        "極";"載";"正";"澗";"溝";"穣";"禾予";"垓";"京";"兆";"億";"万";""|]
    let place_digit skip = function
        | ""   -> "1"
        | "一" -> "1"
        | "二" -> "2"
        | "三" -> "3"
        | "四" -> "4"
        | "五" -> "5"
        | "六" -> "6"
        | "七" -> "7"
        | "八" -> "8"
        | "九" -> "9"
        |  _   -> failwith "エラー:正規の漢数字ではありません:" + input
    let place_4digits skip data =
        let c = Converter( rank_ju, place_digit, "0" )
        c.place_digits( skip, data, 0 )
    let c = Converter( rank_man, place_4digits, "0000" )
    if input = "" || input = "零" then "0" else c.place_digits( true, input, 0 )

処理を共通にするために関数place_digitに意味のない引数skipを持たせます。思いのほか、クラスを作るのも使うのも難しくはありませんでした。ただ今回はクラスを作ることで、かえって処理の流れが見えにくくなってしまったようです。



posted by takayan at 14:07 | Comment(0) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする

2014年02月09日

F#で数の読みを考える

今年はF#をやっています。関数型のプログラミングは何年も前から興味はあって、本も買ってはみたものの、なかなか先に進めませんでした。でもつい最近、自分でも気づかないうちに、目から鱗が落ちたみたいです。PythonやJavascriptも好きですが、これもなかなか楽しく作れます。

入門用のプログラムで動作を確認するのも飽きたので、去年Pythonで作った日本語の読み文字列を出力するプログラムをF#で作ってみようと考えました。全体はまだ無理ですが、整数の読みを出力するぐらいなら何とかできました。コンソールアプリケーションとして使えるようにしてみます。

まず、Pythonでのコード。以前のものから無駄なところを修正したものです。標準入力の数字列を読み込んで、位取りをしないときと、したときの両方の読みの文字列を出力します。

import sys

def yomi_raretsu_kazu(num):
    yomi = {
        u'0':u'レイ',
        u'1':u'イチ',
        u'2':u'ニー',
        u'3':u'サン',
        u'4':u'ヨン',
        u'5':u'ゴー',
        u'6':u'ロク',
        u'7':u'ナナ',
        u'8':u'ハチ',
        u'9':u'キュー',
    }
    if not num:
        return u''
    for n, y in yomi.items():
        num = num.replace( n, y )
    return num

def yomi_integer(num):

    if num == None or num == u'':
        return u''

    if num == u'0':
        return u'ゼロ'

    manshin_kurai = {
        0:u'',
        1:u'マン',
        2:u'オク',
        3:u'チョー',
        4:u'ケー',
        5:u'ガイ'
    }

    kazu_yomi = {
        u'0':u'',
        u'1':u'',
        u'2':u'ニ',
        u'3':u'サン',
        u'4':u'ヨン',
        u'5':u'ゴ',
        u'6':u'ロク',
        u'7':u'ナナ',
        u'8':u'ハチ',
        u'9':u'キュー',
    }

    list = []
    rank = 0
    rev  = num[::-1] + u'000'
    while len(rev) > 3:

        if rank >= len(manshin_kurai):
            return yomi_raretsu_kazu(num)

        if rev[0] == u'1':
           s0 = u'イチ'
        else:
           s0 = kazu_yomi[rev[0]]

        if rev[1] == u'0':
           s1 = u''
        else:
           s1 = kazu_yomi[rev[1]] + u'ジュー'

        if rev[2] == u'0':
            s2 = u''
        elif rev[2] == u'3':
            s2 = u'サンビャク'
        elif rev[2] == u'6':
            s2 = u'ロッピャク'
        elif rev[2] == u'8':
            s2 = u'ハッピャク'
        else:
            s2 = kazu_yomi[rev[2]] + u'ヒャク'

        if rev[3] == u'0':
            s3 = u''
        elif rev[3] == u'1' and len(num) != 4:
            s3 = u'イッセン'
        elif rev[3] == u'3':
            s3 = u'サンゼン'
        elif rev[3] == u'8':
            s3 = u'ハッセン'
        else:
            s3 = kazu_yomi[rev[3]] + u'セン'

        if s3+s2+s1+s0 != "":
            list.insert( 0, s3+s2+s1+s0 + manshin_kurai[rank] )
        else:
            list.insert(0,"")
        rev = rev[4:]
        rank += 1
    return ''.join(list)

if __name__ == '__main__':
    item = sys.stdin.readline().rstrip('\n')
    print( yomi_raretsu_kazu( item ))
    print( yomi_integer( item ))

これを参考にF#でもやってみます。F#では2つのプログラムにしてみます。

まず、位取りしない方です。

let yomi_raretsu_kazu str =
    String.collect ( fun ch ->
        match ch with
        | '0' -> "レイ"
        | '1' -> "イチ"
        | '2' -> "ニー"
        | '3' -> "サン"
        | '4' -> "ヨン"
        | '5' -> "ゴー"
        | '6' -> "ロク"
        | '7' -> "ナナ"
        | '8' -> "ハチ"
        | '9' -> "キュー"
        |  _  -> "" ) str

とてもシンプルです。Pythonでは辞書型を使って実現しましたが、F#では、文字をパターンマッチするラムダ式を作って、その結果を高階関数のcollectで連結しました。F#の特徴的なものを使って簡潔に書けました。

この関数をコンソールアプリケーションにしてみます。引数が1つの時はそれを変換し、引数が何もないときは標準入力から文字列を取り込みます。引数が2つ以上になるとエラーです。

let yomi_raretsu_kazu str =
    String.collect ( fun ch ->
        match ch with
        | '0' -> "レイ"
        | '1' -> "イチ"
        | '2' -> "ニー"
        | '3' -> "サン"
        | '4' -> "ヨン"
        | '5' -> "ゴー"
        | '6' -> "ロク"
        | '7' -> "ナナ"
        | '8' -> "ハチ"
        | '9' -> "キュー"
        |  _  -> "" ) str

[<EntryPoint>]
let main args =
    let routine line =
        if not (String.forall(fun x -> System.Char.IsDigit x) line) then
            failwith <| "エラー: 数字以外の文字があります: '" + line + "'"
        line
        |> yomi_raretsu_kazu
        |> printfn "%s"
    try
        match args.Length with
        | 0 ->
            let line = System.Console.ReadLine()
            routine line
        | 1 -> routine args.[0]
        | _ -> failwith "エラー: 引数はひとつだけ指定してください"
        0
    with
        Failure msg ->
            eprintfn "%s" msg
            eprintfn "機 能: 位取りのない数字列の読みを返します"
            -1

エントリーポイントとして関数mainを定義しています。これは先ほど作った関数の呼び出しと、例外処理からなっています。この呼出のあたりは、わざわざパイプライン演算子「|>」を使って書いてます。理屈さえ分かれば、括弧を使って記述するより、処理の流れが明快に表現できるところが、面白いです。

次は、位取りをする方です。それなりに複雑になりますが、パターンマッチで書くと、見た目にも明快です。

let yomi_integer num_str =
    let rec reverse = function
        | "" -> ""
        | x  -> x.[1..] |> reverse |> (+) <| string x.[0]
    let rec loop rank str =
        let manshin_kurai = function
            |  1 -> "マン"
            |  2 -> "オク"
            |  3 -> "チョー"
            |  4 -> "ケー"
            |  5 -> "ガイ"
            |  6 -> "ジョ"
            |  7 -> "ジョー"
            |  8 -> "コー"
            |  9 -> "カン"
            | 10 -> "セイ"
            | 11 -> "サイ"
            | 12 -> "ゴク"
            | 13 -> "ゴーガシャ"
            | 14 -> "アソーギ"
            | 15 -> "ナユタ"
            | 16 -> "フカシギ"
            | 17 -> "ムリョータイスー"
            | _ -> ""
        let kazu_yomi = function
            | '2' -> "ニ"
            | '3' -> "サン"
            | '4' -> "ヨン"
            | '5' -> "ゴ"
            | '6' -> "ロク"
            | '7' -> "ナナ"
            | '8' -> "ハチ"
            | '9' -> "キュー"
            |  _  -> ""
        match (String.length str > 3, manshin_kurai rank) with
        | (false, _    ) -> ""
        | (true,  ""   ) when rank > 1 ->
            failwith  <| "エラー: 桁が多すぎます: '" + num_str + "'"
        | (true,  kurai) ->
            let s0 =
                match str.[0] with
                | '1' when kurai <> "" ->
                    match kurai.[0] with
                    |'カ'|'ケ'|'コ'|'サ'|'セ'|'チ' -> "イッ"
                    | _ -> "イチ"
                | '6' when kurai <> "" ->
                    match kurai.[0] with
                    |'カ'|'ケ'|'コ' ->  "ロッ"
                    | _ -> "ロク"
                | '8' when kurai <> "" ->
                    match kurai.[0] with
                    |'カ'|'ケ'|'コ'|'サ'|'セ'|'チ' -> "ハッ"
                    | _ -> "ハチ"
                |  x  -> kazu_yomi x
            let s1 =
                match (str.[1], str.[0]) with
                | ('0',  _ ) -> ""
                | ( x , '0') when kurai <> "" ->
                    kazu_yomi x + match kurai.[0] with
                                  |'カ'|'ケ'|'コ'|'サ'|'セ'|'チ' -> "ジッ"
                                  | _ -> "ジュー"
                | ( x ,  _ ) -> kazu_yomi x + "ジュー"
            let s2 =
                match (str.[2],str.[1], str.[0]) with
                | ('0', _,  _ ) -> ""
                | ('3', _,  _ ) -> "サンビャク"
                | ('6', _,  _ ) -> "ロッピャク"
                | ('8', _,  _ ) -> "ハッピャク"
                | ( x ,'0','0') when kurai <> "" ->
                    kazu_yomi x + match kurai.[0] with
                                  |'カ'|'ケ'|'コ' -> "ヒャッ"
                                  | _ -> "ヒャク"
                | ( x , _,  _ ) ->  kazu_yomi x + "ヒャク"
            let s3 =
                match str.[3] with
                | '0' -> ""
                | '1' when String.length num_str <> 4 -> "イッセン"
                | '3' -> "サンゼン"
                | '8' -> "ハッセン"
                |  x  ->  kazu_yomi x + "セン"
            loop <| rank + 1 <| str.[4..]
            |> (+) <| match s3+s2+s1+s0 with | "" -> "" | x -> x + kurai
    match num_str with
    | ""  -> ""
    | "0" -> "ゼロ"
    |  x  -> x |> reverse |> (+) <| "000" |> loop 0

[<EntryPoint>]
let main args =
    let routine line =
        let number =
            String.collect(fun x -> if x=',' then "" else string x) line
        if not(String.forall(fun x -> System.Char.IsDigit x) number) then
            failwith <| "エラー: 数字以外の文字があります: '" + line + "'"
        number
        |> yomi_integer
        |> printfn "%s"
    try
        match args.Length with
        | 0 ->
            let line = System.Console.ReadLine()
            routine line
        | 1 -> routine args.[0]
        | _ -> failwith "エラー: 引数はひとつだけ指定してください"
        0
    with
        Failure msg ->
            eprintfn "%s" msg
            eprintf "機 能: 整数文字列の読みを返します"
            -1

(高位における音便を考慮に入れていなかったので処理を追加、ついでにパターンマッチにタプルが使えることを覚えたので整理。2014/02/11修正)

これは、再帰関数の習作になりました。まず数値文字列を逆順にしたほうが添え字を桁の番号にできるので、反転用の関数reverseを作ります。これを再帰で定義します。Pythonほどではありませんが、文字列でスライスが使えるのは楽でいいです。ついでにパイプライン演算子を使って、括弧を使わない表記にしています。

次の関数loopも再帰です。日本語での数字の読みは万進法なので4桁ずつ区切って処理していますが、これを再帰を使って連結していきます。なお、繰り返しの最後で4桁に満たない可能性もあるので、この再帰関数を最初に呼び出す段階であらかじめ”000”をくっつけています。

中心になる関数yomi_integerそのものは数字だけしか受け付けませんが、入力文字列では桁区切りのコンマはあった方が見やすいので、それを呼び出す関数mainでその桁区切りのコンマを削除しています。

作り始めたばかりですが、この言語は面白いです。元々.NETの言語ですが、Monoのおかげで、Windows以外でも使えます。今回Visual Studioだけでなく、オープンソースのgithubにあるfsharpでも動作を確認しました。今までちょっとしたことはPythonで書いていましたが、しばらくはF#です。

参考リンク:



posted by takayan at 02:08 | Comment(0) | TrackBack(0) | プログラミング | このブログの読者になる | 更新情報をチェックする

2013年12月31日

Open JTalk 1.07 をいろいろビルドしてみました。

毎年恒例のOpen JTalkのクリスマスのバージョンアップが今年も行われました。今まで通りにできるかどうか確認してみました。

■ Ubuntu 13.10

まずubuntu 13.10 64ビット版と32ビット版でやってみました。以前のバージョンのhts_engine_API-1.07には64ビット環境ではhtsvoiceファイルのデータの読み込みに問題があって、ここで公開していたパッチをあてたりしないとうまくいきませんでしたが、今回は確かに修正なしでビルドできました。

実際の手順ですが、Open JTalk からhts_engine_API-1.08.tar.gz、open_jtalk-1.07.tar.gzそして、hts_voice_nitech_jp_atr503_m001-1.05.tar.gzの3つのファイルをダウンロードします。

hts_engine_API-1.08、次にopen_jtalk-1.07に対して、それぞれ展開し、フォルダ内で、お決まりの次のコマンドを実行します。

./configure 
make
sudo make install

インストール先やopen_jtalkの辞書の文字コードなどを変えるときは、./configure のオプションを使います。

標準では、実行ファイルは/usr/local/binに、辞書は/usr/local/dicに入ります。hts_voice_nitech_jp_atr503_m001-1.05.tar.gzファイル内のhtsvoiceファイルはこれに合わせて、分かりやすく/usr/local/voice/に置きます。

簡単な動作確認は次のようにします。test.txtに1行書いて、辞書の文字コードと同じエンコードでドキュメントフォルダーなど適当なところに保存します。標準では辞書のエンコードははEUC-JPですね。そしてtext.txtを保存した場所で次のコマンドを実行すれば、同じ場所に音声ファイルが作成されるはずです。

open_jtalk -x /usr/local/dic -m /usr/local/voice/nitech_jp_atr503_m001.htsvoice -z 6000 -ow out.wav test.txt 

ただ、当初からの問題ですが、標準のhtsvoice音響モデルファイルm001で長文を読ませると、やはり不安定になります。以下に示すどのような環境でのビルドでもこの問題は出てきます。mei_normalなど他の音響モデルでは別に問題にはなりません。

このm001が不安定になる問題の回避策としてOpen JTalk [ja.nishimotz.com]でjpcommon_label.cの修正が示されていますが、今回のバージョンで対応する変更箇所は以下のとおりになります。

diff -ru open_jtalk-1.07_original/jpcommon/jpcommon_label.c open_jtalk-1.07/jpcommon/jpcommon_label.c
--- open_jtalk-1.07_original/jpcommon/jpcommon_label.c	2013-12-24 23:25:41 +0900
+++ open_jtalk-1.07/jpcommon/jpcommon_label.c	2013-12-26 00:29:04 +0900
@@ -296,6 +296,7 @@
       if (index == a)
          break;
    }
+   if (i > 3) i = 3;
    return i;
 }
 
@@ -395,6 +396,7 @@
 
    for (i = 0, index = m->next; index != NULL; index = index->next)
       i++;
+   if (i > 10) i = 10;
    return index_mora_in_utterance(m) + i;
 }

 

Windows 8.1 (64ビット)でもビルドできるか試してみました。

■ VC++

まず、マイクロソフトのVC++でやってみました。VC++は最新の「Visual Studio Express 2013 for Windows Desktop」の無料で使えるExpressエディションのもので試しました。結果は、通常のビルドは問題ありませんでした。

Express版は次のリンク先からダウンロードして、インストールします。手順はリンク先に書いてあるので、ここでは割愛します。

Microsoft Visual Studio Express 2013 for Windows Desktop

VC++インストール後、64ビットOSではhts_engine_API-1.08.tar.gzとopen_jtalk-1.07.tar.gzを展開したそれぞれのフォルダ内で、次のコマンドを実行すれば、ビルドできます。インストール先は標準設定でc:\open_jtalkになります。

call "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat"
nmake -f Makefile.mak
nmake -f Makefile.mak install

※32ビットWindowsの場合はProgram Files (x86)をProgram Filesにする。

hts_voice_nitech_jp_atr503_m001-1.05.tar.gzの中にあるhtsvoiceファイルがc:\open_jtalk\voiceフォルダに入れてあるとすると、次のようなコマンドで確認できます。Ubuntuでやったときと違い、これだけで音声を発します。

echo こんにちは >test.txt
c:\open_jtalk\bin\open_jtalk -x c:\open_jtalk\dic -m c:\open_jtalk\voice\nitech_jp_atr503_m001.htsvoice -z 6000 test.txt

このExpress版には、64ビットアプリケーションをビルドするためにクロスコンパイラも付属しています。しかしこの機能を使って64ビットアプリケーションを作るには修正が必要でした。

まず、hts_engine_API-1.08についてですが、これは前も指摘したことがあるwaveOutOpenに関連する修正です。

diff -ru hts_engine_API-1.08_original/lib/HTS_audio.c hts_engine_API-1.08/lib/HTS_audio.c
--- hts_engine_API-1.08_original/lib/HTS_audio.c	2013-12-24 23:22:44 +0900
+++ hts_engine_API-1.08/lib/HTS_audio.c	2013-12-28 16:11:17 +0900
@@ -85,7 +85,7 @@
 } HTS_AudioInterface;
 
 /* HTS_AudioInterface_callback_function: callback function from audio device */
-static void CALLBACK HTS_AudioInterface_callback_function(HWAVEOUT hwaveout, UINT msg, DWORD user_data, DWORD param1, DWORD param2)
+static void CALLBACK HTS_AudioInterface_callback_function(HWAVEOUT hwaveout, UINT msg, DWORD_PTR user_data, DWORD_PTR param1, DWORD_PTR param2)
 {
    WAVEHDR *wavehdr = (WAVEHDR *) param1;
    HTS_AudioInterface *audio_interface = (HTS_AudioInterface *) user_data;
@@ -177,7 +177,7 @@
    audio_interface->waveformatex.nBlockAlign = AUDIO_CHANNEL * audio_interface->waveformatex.wBitsPerSample / 8;
    audio_interface->waveformatex.nAvgBytesPerSec = sampling_frequency * audio_interface->waveformatex.nBlockAlign;
    /* open */
-   result = waveOutOpen(&audio_interface->hwaveout, WAVE_MAPPER, &audio_interface->waveformatex, (DWORD) HTS_AudioInterface_callback_function, (DWORD) audio_interface, CALLBACK_FUNCTION);
+   result = waveOutOpen(&audio_interface->hwaveout, WAVE_MAPPER, &audio_interface->waveformatex, (DWORD_PTR) HTS_AudioInterface_callback_function, (DWORD_PTR) audio_interface, CALLBACK_FUNCTION);
    if (result != MMSYSERR_NOERROR) {
       HTS_error(0, "hts_engine: Failed to open your output audio_interface device to play waveform.\n");
       HTS_free(audio_interface);

一方、open_jtalk-1.07ではMeCabの以下の部分です。これは以前から知られているMeCabの修正です。「MeCab を MinGW-w64 でビルド。ついでに、Java バインディングもビルド」 を参考にC++らしい型キャストにしてみました。

diff -ru open_jtalk-1.07_original/mecab/src/feature_index.cpp open_jtalk-1.07/mecab/src/feature_index.cpp
--- open_jtalk-1.07_original/mecab/src/feature_index.cpp	2013-12-11 14:56:19 +0900
+++ open_jtalk-1.07/mecab/src/feature_index.cpp	2013-12-30 23:18:15 +0900
@@ -353,7 +353,7 @@
               if (!r) goto NEXT;
               os_ << r;
             } break;
-            case 't':  os_ << (size_t)path->rnode->char_type;     break;
+            case 't':  os_ << static_cast<unsigned int>(path->rnode->char_type);     break;
             case 'u':  os_ << ufeature; break;
             default:
               CHECK_DIE(false) << "unknown meta char: " <<  *p;
diff -ru open_jtalk-1.07_original/mecab/src/writer.cpp open_jtalk-1.07/mecab/src/writer.cpp
--- open_jtalk-1.07_original/mecab/src/writer.cpp	2013-12-11 14:56:22 +0900
+++ open_jtalk-1.07/mecab/src/writer.cpp	2013-12-30 23:16:25 +0900
@@ -257,7 +257,7 @@
             // input sentence
           case 'S': os->write(lattice->sentence(), lattice->size()); break;
             // sentence length
-          case 'L': *os << lattice->size(); break;
+          case 'L': *os << static_cast<unsigned int>(lattice->size()); break;
             // morph
           case 'm': os->write(node->surface, node->length); break;
           case 'M': os->write(reinterpret_cast
これらの修正を行った後、hts_engine_APIから順にそれぞれの展開したフォルダ内で次のコマンドを実行するとビルドできました。
call "C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\vcvarsall.bat" x86_amd64 
nmake -f Makefile.mak 
nmake -f Makefile.mak install

必要性はほとんどないでしょうが、Program Files (x86) をProgram Filesに書き換えれば、32ビットパソコンでとりあえず64ビットの実行ファイルを作ることができます(もちろん実行はできませんが)。ただし、辞書のコンパイルのとき、作成したばかりの64ビット向けのコマンドを実行しようとしてエラーを出すので、辞書は32ビットのコマンドで作るか公式サイトからダウンロードしてくる必要があります。

 

■ Windows 8.1上の Cygwin, Mingw

cygwin、mingwでもビルドしてみました。Mingwは以前mingw-get-instで作った環境ではうまくできたのですが、再現性を確保するために、現在の mingw-get-setupを使って環境を作ろうとしましたが、どうしても作れませんでした。仕方がないので、cygwinでmingwのコンパイラを使って作りました。

前提となる、Cygwinの環境作りですが、Cygwinのサイトから、setup-x86.exeかsetup-x86_64.exeをダウンロードして、インストールします。詳しいインストールの仕方はここでは割愛します。ダウンロード先にある説明を読むか、割と新しいマイナビニュースの記事などを参考にしてください。今回のビルドを行うには、最低、makeとgcc-coreとgcc-g++の3つのパッケージが必要です。またmingwでコンパイルを行う場合は、さらに対応するgccとg++を入れます。具体的には、32ビットアプリケーションに必要なのがmingw64-i686-gcc-coreとmingw64-i686-gcc-g++で、64ビットアプリケーション向けはmingw64-x86_64-gcc-coreとmingw64-x86_64-gcc-g++です。

まず、Cygwinのコンパイラを使った場合ですが、32ビットアプリケーションを作る場合はソースの修正は必要ありませんでした。しかし64ビットアプリケーションを作るときは、コンパイルは何事もなく完了するのですが、実行すると異常終了しました。原因は上記VC++と同様で、hts_engine_API-1.08のファイルに上記の修正をします。一方のopen_jtalk-1.07のファイルに関しては修正は必要ありませんでした。

hts_engine_API-1.08とopen_jtalk-1.07とそれぞれの展開した先で次のコマンドを実行します。

./configure
make
make install

なお、文字コードは標準ではEUCになってしまいますが、WindowsではシフトJISの方が今でもなにかと都合がいいので、open_jtalk-1.07をビルドするときは、./configure –with-charset=SHIFT_JISとした方がいいかもしれません。

次に、Mingw向けのビルドです。これは少し修正が必要でした。

64ビットアプリケーションを作る場合、hts_engine_API-1.08のファイルに上記の修正が必要でした。そしてopen_jtalk-1.07のファイルはCygwinと違って、Windowsのときと同様の修正が必要でした。

※下の打ち消し線の部分の代わりの記事は、下方の「■追記(2014/02/01)」のところ書いています。

さらに、これは32ビット向けの場合も含めて、open_jtalk-1.07のファイルのMeCabの部分にあるプリプロセッサ命令の定数__CYGWIN__を__MINGW32__を置き換えます。これは完全な解決法ではありません。本来は一つ一つ確認して是非を確認すべきですし、置き換えではなく、条件文に__MINGW32__の項目を追加したほうがいいのですが、とりあえずうまくうまくいきました。

実際には、open_jtalk-1.07/mecab/src に入って次のコマンドで一気に置き換えました

find . -type f | xargs sed -i 's/__CYGWIN__/__MINGW32__/g'

それから、ビルド中の辞書のコンパイルにおいて、生成したばかりの実行ファイルを使いますが、このときdllが見つからずにコンパイルが止まってしまいます。それを防ぐために、予めパスに追加しておきます。

32ピットアプリケーションを作るときは、こんな感じ、

export PATH=$PATH:/usr/i686-w64-mingw32/sys-root/mingw/bin

64ピットアプリケーションを作るときは、こんな感じです。

export PATH=$PATH:/usr/x86_64-w64-mingw32/sys-root/mingw/bin

準備が整ったら、今までと同じように、hts_engine_API-1.08から先に展開したそれぞれのフォルダ内で以下のコマンドを実行します。以下に示すのは64ビットアプリケーションを作る例です。32ビットの場合はそれぞれの=の後はi686-w64-mingw32にします。

./configure --host=x86_64-w64-mingw32 --build=x86_64-w64-mingw32
make
make install

ここで作られた実行ファイルは残念ながらlibstdc++-6.dllやlibgcc_s_seh-1.dllもしくはlibgcc_s_sjlj-1.dllが必要になっています。これを静的リンクにする方法はあるみたいですが、よくわからなかったので、今回そこまでできませんでした。

(2014.01.11追記) configure を実行する前に、C コンパイラを次のように定義するとスタティックリンクになります。

export CC="x86_64-w64-mingw32-gcc -static"

なおこれは64bitアプリケーションを作る場合です。32ビットアプリケーションはi686-w64-mingw32-gccに置き換えます。できた実行ファイルは今回13126kBととても大きくなりましたが、stripコマンドでスリムにすると1159kBになりました。(追記終わり)

 

以前Windowsでのビルドをやってくれるバッチファイルを作りました(「open jtalk 1.06 を VS2012で ビルドするバッチ」)。特に設定を変えなければ、今回のものもビルドできました。そのままでもよかったのですが、上記で紹介したパッチを含めて、いくつかの細かな修正をしたものを今回新しく作りました。使い方は以前と同じなので、こちらで確認ください。上に示したようにがシフトJIS向けのビルドは上記のたった三行でビルド可能なので、あくまでも参考までにご使用ください。

ダウンロード:openjtalk_buildbatch-005.zip
(MD5: 7789eb6a3270eefbadaf827df6028417)

最後に、Open JTalkとともに、MMDAgentも更新がありました。その関連アーカイブのMMDAgent_Example-1.4.zipにmeiのhtsvoiceファイルが入りました。これを手に入れるために、今年の初めに旧形式からの変換器htsvconvを作ったわけですが、確認したらバイナリ的に同一のものなので、htscnvの変換結果は正解ということで、いいですね。

■追記(2014/02/01)

mingwへの対応があまりにも大雑把なので修正しておきます。今回のopen_jtalkで使われているMeCabのバージョンは最新のものの一つ前の0.994です。0.99系列でWindows版のMeCabはユニコード正式対応になったのですが、このときの大幅な変更により、mingwでまともなコンパイルができなくなってしまいました。この問題はまだ解決はしていません。一番有益な情報は、mecab-develメーリングリストの97番の投稿です。

MeCabのmingwに関する現状はそうなのですが、open_jtalkに使われているMeCabはちょっと事情が違います。open_jtalkの辞書はまだユニコード対応ではなく、UTF-8を含むマルチバイト文字コードのままなので、ユニコード対応部分を丸ごとすっ飛ばしても関係ありません。それを簡潔に、cygwinのコンパイラ向けの回避策をそのまま使って実現したのが、前回の__CYGWIN__を__MINGW32__による置換というわけです。

自分がmingwでのコンパイルに興味がなかったので、深く掘り下げませんでした。でも、やはりこれは手抜き過ぎる解決策です。最小限の変更箇所のパッチを作ってみました。open_jtalk-1.07_mingw.patch

diff -ru open_jtalk-1.07.orig/mecab/src/common.h open_jtalk-1.07/mecab/src/common.h
--- open_jtalk-1.07.orig/mecab/src/common.h	2013-12-11 14:56:17 +0900
+++ open_jtalk-1.07/mecab/src/common.h	2014-02-01 17:20:15 +0900
@@ -144,7 +144,7 @@
 #define EXIT_SUCCESS 0
 #endif
 
-#if defined(_WIN32) && !defined(__CYGWIN__)
+#if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__MINGW32__)
 #define WPATH(path) (MeCab::Utf8ToWide(path).c_str())
 #else
 #define WPATH(path) (path)
diff -ru open_jtalk-1.07.orig/mecab/src/thread.h open_jtalk-1.07/mecab/src/thread.h
--- open_jtalk-1.07.orig/mecab/src/thread.h	2013-12-11 14:56:21 +0900
+++ open_jtalk-1.07/mecab/src/thread.h	2014-02-01 17:20:15 +0900
@@ -85,13 +85,20 @@
 
 namespace MeCab {
 
+#ifdef __MINGW32__
+#if defined(__i386__) && !defined(__x86_64) && !defined(__SSE2__)
+#undef YieldProcessor
+#define YieldProcessor() __asm__ __volatile__("rep; nop")
+#endif
+#endif
+
 #if (defined(_WIN32) && !defined(__CYGWIN__))
 #undef atomic_add
 #undef compare_and_swap
 #undef yield_processor
 #define atomic_add(a, b) ::InterlockedExchangeAdd(a, b)
 #define compare_and_swap(a, b, c)  ::InterlockedCompareExchange(a, c, b)
-#define yield_processor() ::YieldProcessor()
+#define yield_processor() YieldProcessor()
 #define HAVE_ATOMIC_OPS 1
 #endif
 
diff -ru open_jtalk-1.07.orig/mecab/src/winmain.h open_jtalk-1.07/mecab/src/winmain.h
--- open_jtalk-1.07.orig/mecab/src/winmain.h	2013-12-11 14:56:23 +0900
+++ open_jtalk-1.07/mecab/src/winmain.h	2014-02-01 17:20:16 +0900
@@ -46,7 +46,7 @@
 /* for Open JTalk
 #if defined(_WIN32) || defined(__CYGWIN__)
 */
-#if defined(_WIN32) && !defined(__CYGWIN__) /* for Open JTalk */
+#if defined(_WIN32) && !defined(__CYGWIN__) && !defined(__MINGW32__) /* for Open JTalk */
 
 #include 
 #include 
diff -ru open_jtalk-1.07.orig/mecab-naist-jdic/Makefile.in open_jtalk-1.07/mecab-naist-jdic/Makefile.in
--- open_jtalk-1.07.orig/mecab-naist-jdic/Makefile.in	2013-12-24 23:26:07 +0900
+++ open_jtalk-1.07/mecab-naist-jdic/Makefile.in	2014-02-01 17:20:16 +0900
@@ -349,7 +349,7 @@
 
 
 char.bin matrix.bin sys.dic unk.dic: naist-jdic.csv matrix.def left-id.def rewrite.def pos-id.def right-id.def char.def unk.def feature.def
-	../mecab/src/mecab-dict-index -d . -o . -f EUC-JP -t @MECAB_CHARSET@
+#	../mecab/src/mecab-dict-index -d . -o . -f EUC-JP -t @MECAB_CHARSET@
 
 clean:
 	rm -f char.bin matrix.bin sys.dic unk.dic

このパッチには、4つのファイルの修正が入っています。1つ目と3つ目は、ユニコード関連のコードを回避するための部分です。2番目はmecab-develの投稿で指摘されていた部分ですが、YieldProcessor()に関する修正です。二カ所に分かれていますが、最初のものはヘッダファイルでのマクロの定義がおかしかったので再定義している部分です。次の部分は、YieldProcessorマクロの内容がコンパイラに定義されている関数のときは問題ないのですが、マクロの内容がインラインアセンブラのときに起きる不具合への対策です。4つ目のファイルの修正は、辞書のコンパイルを行わないようにするだけのものです。これがあるとmakeが止まってしまうのでその回避です。辞書はここで生成されないので、open_jtalkのサイトから文字コードにあったものをダウンロードしてくる必要があります。

このパッチを当ててビルドするスクリプトは次のようになります。面倒なので変数を使って書いていますが、charsetに文字コード名、archにmingwのシステム名です。下の例だと32ビット向けに文字コードutf-8のコードが生成されます。インストールされる場所はprefix変数が示しているbinフォルダなどです。下記スクリプトでは上記のパッチを次の名前とし、open_jtalk-1.07_mingw.patchとして記述しています。上記の64ビット向けのopen_jtalk-1.07_x64.patchを持ってきて、2カ所のコメントを外せば64ビット向けのビルドもできます。

#!/usr/bin/env bash
charset=UTF_8
arch=i686-w64-mingw32
#arch=x86_64-w64-mingw32
export CC="$arch-gcc -static"
host="--host=$arch"
prefix="--prefix=/usr/$arch"
hpath="--with-hts-engine-header-path=/usr/$arch/include"
lpath="--with-hts-engine-library-path=/usr/$arch/lib"
rm -r hts_engine_API-1.08
tar zxvf hts_engine_API-1.08.tar.gz
cd hts_engine_API-1.08
./configure $host $prefix
make
make install
cd ..
rm -r open_jtalk-1.07
tar zxvf open_jtalk-1.07.tar.gz
patch -p0<open_jtalk-1.07_mingw.patch
#patch -p0<open_jtalk-1.07_x64.patch
cd open_jtalk-1.07
./configure $host $prefix $hpath $lpath --with-charset=$charset
make
make install

2つのmake installの先頭に「sudo 」を付ければ、Mingw-w64をインストールしたUbuntuでも使えます。



posted by takayan at 23:07 | Comment(0) | TrackBack(0) | 音声合成 | このブログの読者になる | 更新情報をチェックする

2013年12月09日

gnome orcaでespeakに日本語をしゃべらせる方法2

前回の続きです。数字を日本語で読むようにしてみました。

ファイルのダウンロード先はこの記事の最後に書いてあります。利用法は前回書いたとおりです。現時点でUbuntu13.10で使われているGnome orcaのバージョンは3.10.1なので、今回のパッチはこのバージョン向けのものです。他のバージョンで動くかどうか分かりません。Ubuntu以外で動くことはFedora 19で確認しましたが、speechdispatcherfactory.pyやen_dictの場所はシステム毎に確認する必要があります。またPython3用のMeCabモジュールもインストールしておく必要があります。

日本語の中の数字の読みの規則を明文化していくと、ほんとに複雑なことを頭の中で処理しながら日本人が生きていることを思い知らされます。漢数字に算用数字、漢数字でも位取り表記で使ったり、さらに万進数の位の漢字に算用数字をくっつけた折衷表記をしたり、その算用数字は整数だけでなく小数を使ったりもします。

それだけでなく読み方も大変です。本や匹など助数詞が後ろに付くと、数字は促音化したりしなかったり、助数詞の方は濁音になったり、半濁音になったり、ならなかったり、規則的な傾向はあるのだけれど、覚えなくてはならない例外がいくつもあります。四はヨンなのか、ヨなのか、シなのか、助数詞によって使い分けています。七もそうです。九もそうです。

そういう日本語の複雑な数字の読みをGnome Orca のespeakで発音できるようにプログラムを作ってみました。日本語のスクリーンリーダーや、日本語の音声合成ソフトでは当たり前の機能ですが、いざ作ってみるといろいろ面倒でした。

作っている間ずっと考えていたのは、こういうのは一人のプログラマが個人的に読み方を決めてはいけないなということです。日本語を普段使っている人なら違和感のある表現もいろいろくみ取って理解してもらえますが、それに期待して手抜きしたものが、日本語学習者にそれが本来の形だと思い込まれても困ります。また音声ガイドという性質上、一般的な日本人の日本語よりも、アナウンサーの日本語を手本にすべきだと考えました。

面白かったので、実験的にいろいろなことをしています。具体的には次の読みをできるようにしてみました。
・数字と助数詞によって起きる発音の変化は『NHKことばのハンドブック』の第5章の表を手本とする。
・算用数字の直後の英字の列は単位でないかを確認し、そうならばカタカナ表記に置き換えて読む。
・コロンで区切った2桁:2桁:2桁の数値は時刻とする。
・スラッシュで区切った4桁/2桁/2桁の数値は日付とする。ハイフンで区切られた同様の日付は直後に時刻があるときだけ日付とする。これに関連して、括弧で囲まれた曜日を表す1文字の漢字を常に曜日として読み上げる。
・ハイフンや2つ以上のピリオドでつながれた算用数字は一字ずつ読む。
・電話番号、郵便番号だとはっきり分かるものはハイフンを「ノ」と読む。
・コンマによる桁区切りは3桁だけでなく4桁区切りにも対応する。
・位を表す漢字と位取りの数字の組み合わせの読みは、万の位以上や小数の算用数字にも対応する。算用数字と組み合わせる位の漢字は万進数に限らず、百万や千などの位に対応する。このとき本来の値で読み替える。
・整数の左側、小数部分の右側にある桁埋めの「0」は読み上げない。
・16進数と2進数の接頭語として0xと0bを認識し、ついでに読み終わった後に十進数での値も読み上げる。
・URLに現れるパーセントエンコードは16進数数字として読み上げず、デコードして文字として読み上げる。

eSpeakの方ですが、jaの作り方は分かりましたが、他にもファイルをインストールしないといけなくなるので、今回もen_dictの置き換えだけにします。前回は、音声表記をローマ字に変えた方が細かな発音指定ができるんじゃないかなと考えていまいしたが、結局カタカナの方が都合がいいことに気がつきました。漢字仮名交じり文からカタカナにするだけでも情報をたくさんそぎ落としてしまっているのに、これをアルファベットにしたら、文中の英単語と日本語との区別ができなくなってしまいます。意外にこの区別は便利です。そういうわけで、今回もカタカナで作っています。ほんの少し、「ンム」を[m]、「ング」を[ng]を表すように変えてみました。なお日本語の中の無声音化は、eSpeak側で記述するルールで対応できるかもしれませんが、まだ理解できていないので今回はやっていません。


アーカイブに入れてあるkatakanize.pyは動作の確認ができるように、標準入力の1行をespeakで解釈できる形式で出力するようにしています。次のコマンドでキーボードからの入力を読み上げてくれます。なお以前はちょっと書き換えれば、python2で動くようにしていましたが、python3のみで動きます。

python3 katakanize.py|espeak

また、test.txtにutf-8の文字コードで日本語を1行書いておくと、次のコマンドでwavファイルを作ります。

cat test.txt|python3 katakanize.py|espeak -wtest.wav

ダウンロード:orca-3.10.1-espeak-jp_003.zip

更新履歴
コンマの区切りが思ったようになっていなかったので修正しました。2013/12/13
テスト用のコードを入れたままにしていたので修正しました。2013/12/16



posted by takayan at 08:15 | Comment(2) | TrackBack(0) | GNOME Orca | このブログの読者になる | 更新情報をチェックする

2013年11月10日

gnome orcaでespeakに日本語をしゃべらせる方法

現在書いているものが終わるまで他のことを書かないでおこうと思ってましたが、面白いアイデアが浮かんだのでここでプログラミングの話題を書くことにします。

集中力が切れたので、気分転換にUbuntu13.10を入れていろいろ試していました。久しぶりに起動したスクリーンリーダーgnome orcaのeSpeakの音声が、仮名文字や漢字のところで、ジャパニーズレターとかチャイニーズレターと言うようになっていました。以前は日本語文字列のところで意味不明な記号の羅列を延々と読み上げていたので、親切な修正と言えます。うるさいことには変わりないですが。

ちなみに、gnome orcaをオン/オフする標準のキーボードショートカットは、altキー+superキー(win)+sキーです。マウスでの操作だと、画面右上の[システム]ボタンを押して、出てきたメニューの中から[システム設定]を選び、開いた「システム設定」ウィンドウの中の[ユニバーサルアクセス]ボタンを押して、[視覚]タブにあるスクリーンリーダーのトグルボタンで切り替えます。

検索してみると、NVDA日本語版のてくてくラボのespeakのページにespeakのその指摘がありました。半年以上前の記事です。そこにはen_extraファイルを使って言語enで日本語を読ませる短いですが興味深い実験が書いてありました。

これはorcaに使えます。orcaの標準音声はespeakのenなので、これで日本の文字を発音できるようになれば素晴らしいです。さっそく勢いで作ってみました。

英語の発音記号を使ってカナ文字の発音をen_extraに定義していきます。このファイルを追加して、発音辞書であるen_dictにその内容を反映させると、eSpeakでカナ文字を発音してくれるようになります。Windowsでこの作業を行うには、eSpeakのホームページからespeak-1.47.11-win.zip や espeakedit-1.47.11-win.zipをとってきてインストールしておく必要があります。

発音辞書の設定は、espeakがインストールされた場所のdictsourceフォルダにあるrules、list、extraファイルで行います。その実行ファイルはcommand_lineフォルダにあるespeak.exeです。英語enの場合は次のコマンドになります。

espeak --compile=en

dictファイルはespeak-dataフォルダに生成されます。なお最近のWindowsでは管理者権限で実行しないと、VirtualStore内に作られてしまいます。

本当は言語jaを創設すべきところなんでしょうが、理解するのに時間がかかりそうだったので、このまま英語の追加の発音として作ってみました。今後、日本語独自のrules、list、extraファイルを作る場合は、eSpeakのサイトにあるPronunciation Dictionaryのページを参考にすればいいはずです。

せっかくなので、今後のことを考えて、仮名文字単体ではなく、長音、促音、拗音なども合わせて、ある程度の日本語の音節を表現するものを作ってみました。「ん」の発音の区別はしていません。これはカナ文字ではなくローマ字に変換するとやりやすくなるでしょう。他にも漏れてるものもあるかもしれません。発音も間違っているかもしれません。

このen_dictをNVDAにあるen_dictファイルと置き換えると、たどたどしくですが、かなを読み上げてくれるようになりました。同じファイルをubuntuに持っていってgnome orcaのen_dictと置き換えても、うまくいきました。漢字などまだ読めない文字がたくさんありますが、いかにも電子音電子音しているeSpeakの読み上げの中から日本語が聞こえてくると、それだけでも楽しいものです。

具体的なubuntuでのen_dictの置き換えですが、次の通りです。

まず、下記のアーカイブをダウンロードして、展開します。

ダウンロード:orca-3.10.1-espeak-jp_003.zip

(2013/12/13追記 後日作った数値読みの機能を追加したものにアーカイブを差し替えます。またubuntu13.10のorcaが3.10.1になったので対応バージョンも変えます。関連記事「gnome orcaでespeakに日本語をしゃべらせる方法2」 )

このアーカイブの内容は次の通りです。
・en_extra
・en_dict
・katakanize.py
・speechdispatcherfactory.patch

この中のen_dictを使います。新しいこのファイルがあるところで、端末から次のコマンドを実行します。

32ビット版ubuntuの場合、

sudo cp en_dict /usr/lib/i386-linux-gnu/espeak-data/en_dict

64ビット版ubuntuの場合、

sudo cp en_dict /usr/lib/x86_64-linux-gnu/espeak-data/en_dict

ただし別個にespeak関連パッケージをインストールした場合は別の場所のespeak-dataに変わるようです。

 

カナ文字だけならば発音できるようになりました。今度は、漢字の読み方を教えて漢字も読めるようにしてみます。要は漢字をカナ文字列に変換できればいいわけです。この問題の解決には、やはりMeCabを使います。gnome orcaなのでPython3で考えます。

ところが、ubuntuではpython3用のMeCabモジュールはapt-getでまだ入手できません。そのため自分でビルドしインストールする必要があります。修正も必要です。具体的な方法は、「Ubuntu - MeCabをPython3上から使えるようにする - Qiita [キータ]」に書かれています。注意点として、ビルド環境を整えるために、g++やpython3-devなどをapt-getでインストールしておかなければなりません。それから手順には書いてありませんでしたが、setup.pyも入手して実行しました。

ubuntuのpython3でmecabが使えるようになったので、次のような漢字の文字列をカタカナに変換する関数を作りました。(35行目にループ終了後に残ったwordを追加する処理を追加。2013/12/13)

# /usr/bin/env python3
# coding: utf-8

import MeCab
import re

def katakanize(text):
    regexp = re.compile(r'^[\x20-\x7E]+$')
    if regexp.match(text):
    	return text

    tagger = MeCab.Tagger('--node-format=%pS%f[8] --unk-format=%M --eos-format=\n')
    text = tagger.parse(text)
    #text = unicode(tagger.parse(text.encode('utf-8')),'utf-8')

    list = []
    word = ""
    prev = False
    for ch in text:
        if regexp.match(ch):
            if prev:
                word = word + ch
            elif word:
                list.append(word)
                word = ch
            else:
                word = ch
            prev = True
        else:
            if word:
                list.append(word)
                word = ""
            list.append(ch)
            prev = False
    else:
        if word:
            list.append(word)

    result = []
    word = ""
    for ch in list:
        if regexp.match(ch):
            if word:
                result.append(word)
                word = ""
            result.append(ch)
        elif ch == u"ッ":
            if word:
                result.append(word)
            word = ch
        elif ch == u"ー":
            word = word + ch
            result.append(word)
            word = ""
        elif ch == u"ャ" or ch == u"ュ" or ch == u"ョ":
            word = word + ch
        elif ch == u"ァ" or ch == u"ィ" or ch == u"ゥ" or ch == u"ェ" or ch == u"ォ":
            word = word + ch
        elif len(word) >= 1 and ( word[-1] == u"ク" or word[-1] == u"グ" )\
            and ( ch == u"ヮ" or ch == u"ヰ" or ch == u"ヱ" ):
            word = word + ch
        elif len(word) >= 1 and word[-1] == u"ッ":
            word = word + ch
        else:
            if word:
                result.append(word)
            word = ch
    if word:
        result.append(word)

    if result:
        return " ".join(result)
    else:
        return ""

if __name__ == "__main__":
    text = "これは文章変換の実験です。"
    text = katakanize(text)
    #text = katakanize(unicode(text,'utf-8')).encode('utf-8')
    print(text)

最後の数行はテスト用のコードです。python2で試してみるときは、辞書のエンコードを考慮して二か所のコメントアウトした部分を直前の行と入れ替えます。

カタカナ化という名前にしていますが、やっているのはもう少し複雑です。まず、MeCab.Taggerの引数内のf[8]で示している通り、表記ではなく発音の文字列を出力しています。例えば助詞の「は」は「ワ」となります。それから日本語文字とそれ以外を区別して、さらに日本語の場合は今回のen_extraにあるような塊になるように分かち書きになります。 この関数の部分を適当な箇所に貼付けて、その呼び出しをeSpeakに文字列を渡す直前に配置します。例えば変数textに文字列が入っていれば、その直前にtext = katakanize(text)という行を入れます。なおpython2では文字列や辞書のエンコードを考える必要があるので、上記のコメントアウト部分のような記述になります。

それでは具体的な修正方法です。 ここに示す方法はあくまでも実験です。ここに書いてあることを試したいときは、実務機以外、仮想マシーン(VMware PlayerVirtualBox)などにとどめてください。もちろん自己責任でお願いします。orcaのバージョンが変わったときは修正箇所もその影響も変わる可能性もあるので、そのときはあきらめてください。

やることはとてもシンプルです。先ほどやったen_dictをコピーする操作に加えて、一つのファイルにパッチをあてるだけです。その修正対象となるファイルは  /usr/lib/python3/dist-packages/orca/speechdispatcherfactory.pyです。このファイルには、linuxで標準的な音声合成のインターフェイスであるSpeech DispatcherへOrcaからコマンドを送るコードが書かれています。この中の、特に文字列を送って音声を鳴らすspeakコマンドのあるところが修正すべき箇所となります。Speech DispatcherはeSpeak以外の音声合成システムも管理しているので、それぞれの音声合成でカナ文字日本語の処理を用意しておけば、使えるようになるかもしれません。

端末を開いてorcaのバージョンを確認します。次の行を実行します。

orca -v

返ってきた文字列が3.10.1が3.9.92であることを確認してください。このバージョン以外ならば、うまくいくかどうかわかりませんので、あきらめてください。このバージョンだったならば、アーカイブの中のpatchファイルのあるところで、次のコマンドを実行します。問題があったときは、ソフトウェアセンターでorcaをインストールしなおすか、バックアップファイルをもとの場所に戻してください。

cp /usr/lib/python3/dist-packages/orca/speechdispatcherfactory.py speechdispatcherfactory.py
sudo patch /usr/lib/python3/dist-packages/orca/speechdispatcherfactory.py<speechdispatcherfactory.patch

これで、漢字交じりの日本語をカタカナ音節に変換する機構が組み込まれました。Alt+Win+sでOrcaを起動してみてください。

とりあえず、eSpeakの声で日本語をしゃべっています。問題はまだたくさんあります。数字の部分も英文と同じように本来のenで読んでもらうようにしています。そのため数値が英語読みになって、ちょっとおかしな表現になってしまいます。数字を日本語読みにする処理は以前ここでも書いたことがありますが、面倒そうだったので今回は入れていません。それくらいになると別ファイルにするか、モジュールとして独立させた方がよさそうです。他にも実用的にするにはもっと多くの処理が必要になってくるでしょう。

言語jaを作って、日本語用の音素を作って、別のfactory.pyを作って、さらにアクセント情報のあるunidicかOpen JTalkの辞書を使ったり、いろいろできるはずです。日本語の音声が必要な人たちに標準で用意できるようになると素晴らしいでしょう。

また新たな中途半端なものを作ってしまったわけですが、これを足掛かりに先に進めればいいと思います。



posted by takayan at 23:44 | Comment(0) | TrackBack(0) | GNOME Orca | このブログの読者になる | 更新情報をチェックする
×

この広告は90日以上新しい記事の投稿がないブログに表示されております。