2014年04月15日

点字タイプソフト作ってみました。

Windows 8向けに点字タイプで文字が打てるプログラムを作ってみました。これを起動しておくと、様々なソフトの文字入力ができる部分において、キーボードのホームポジションで点字打ちをすることで、そこに文字を入力できるようになります。機能はあまり多くありませんが、せっかく作ったので、ここに公開します。ダウンロードはこの記事の下の方の brlinput.zip リンクからできます。

とにかくシンプルに作ってみました。本体は一つの実行ファイルのみです。キーの割り当ても変えられません。できるのは、点字入力で対応する平仮名の出力、そして点字の出力です。途中で両手入力、片手入力を切り替えることはできませんが、起動時に選べます。漢字入力は、IMEを起動してから、ひらがなを入力することで行います。英字への切り替えは外国語引用符で行います。「⠦(点字。環境によっては表示不可、2,3,6の点)」で英字に切り替わり、「⠴(3,5,6の点)」でかなに戻ります。数字は数字符「⠼(3,4,5,6の点)」で始まり、第一つなぎ符「⠤(3,6の点)」で終わります。なお数字の桁数に制限はありません。記号の多くは実装していません。

今後、漢点字や六点漢字、その他の記号やコマンドなどいろいろ機能を追加していくこともできそうですが、これ自体は低機能を目指します。多機能の要望があれば別に有料版として開発していくことにします。

実行すると、画面の右下にIMEの言語バーのようなものが表示されます。これには現在の入力の状態が表示されています。またタスクトレイには白抜きの「め」が表示されます。この状態で点字タイプ入力が可能になります。FDSJKLがそれぞれ点字の1から6の点に対応しています。いわゆるパーキンス式です。試しにFキーを押せば「あ」が表示されるはずです。全部押すと「め」です。なお、日本語変換は自動では切り替わらないので、自分で切り替える必要があります。

デスクトップパソコンで6キー同時押しができないときは、違うキーボードに変えればうまくいく可能性があります。ノートパソコンで打てないときは、不便ですが外付けキーボードで何とかしないといけないでしょう。

このプログラムには4つのモードがあります。平仮名墨字(すみじ)出力、無効、六点点字出力、八点点字出力です。これらの切り替えはALT+CTRL+変換キーで行います。ありがちなショートカットなのでかぶって使えないかもしれません。スクリーンリーダー(PC-Talker、NVDA)利用中は動作内容をSAPI5音声で簡単に報告します。

当初は予定していなかったのですが、プログラムを書くときに文字パレットから点字をとってくるのが大変だったので、点字の活字を直接出力するモードも付け加えました。六点(⠿)だけでなく、八点(⣿)も打てるようにしています。もちろん、八点入力モードはキーボードで8キー同時認識ができることが前提です(ダメなときは、同時押し対応のゲーム用外付けキーボードを使えば、USB接続でも8キー同時認識が可能かもしれませんが、試していません)。入力方法ですが漢点字の一般的な方法を踏襲しています。上の二つの点を0の点、7の点として無変換キーとスペースキーで入力します。残りの点は六点点字と同じです。なお、NVDA利用中に無変換キーをNVDAキーとして使っているときは入力がうまくできないので、そのときは「日本語設定」で「無変換をNVDAキーとして使用」のチェックを外してください。

動作確認はWindowsVISTA/7/8.1で行いました。実行には.NET Framework3.5が必要です。パソコンに.NET FrameWork3.5が入っていないのに実行しようとすると、インストールの案内が出るはずですので、指示に従って入れてください。標準フォントにユニコード点字が入っていない場合はバーに点字は表示されません。そのときは、こちらのBraille6.ttf をインストールするとバーに六点点字を表示できます。もちろん文書への点字出力にはユニコードの点字のあるフォントに対応しているエディタやワープロソフトが必要です。

終了方法ですが、マウスの操作ができる場合は、点字入力パネルを右クリックすれば、終了項目だけのメニューが出るので、それをクリックしてください。確認が出るのでまたエンターしてください。タスクトレイにあるアイコンでも同様の方法で終了できます。キーボード操作で終了させるには、Windows+Bでタスクトレイの操作に切り替え、「点字タイプ」のところに右矢印キーで移動して、アプリケーションキー(もしくはシフト+F10)でメニューを出します。これは先ほどのマウスで右クリックしたときのメニューと同じで、終了を選んでエンターをすると同様に終了します。

片手入力にも対応しています。切り替え方ですが、付属のファイルなどを作りたくなかったので、実行ファイル名で区別しました。実行ファイルの最後の文字が「r(アール)」のときは右手用、「l(エル)」のときは左手用になるようにしています。どちらでもないときは両手用です。つまり、エクスプローラの設定により拡張子が見えていないときは「brlinputr」と実行ファイル名を書き換えると右手用に、拡張子が見えているときは「brlinputl.exe」とすれば、左手用になります。一時的な切り替えならば、タスクトレーのアイコンを右クリックして「手の切り替え」からできます。

そしてその入力方法ですが、これも既存のソフトの方法を踏襲しています。左側、右側それぞれに何らかの点があるときは、そのまま左側を打った後、右側を打てば、一つの点字と認識されます。問題は片側に点が何もない場合です。そのときは空白を示すキーを打ちます。右手入力のときはセミコロンキー、左手入力のときはAキーがそれです。例えば、右手入力で「い」を入力したいときは、同時に「Jk」を打った後、「;」を打ちます。

当然ですが、スタートアップに登録すれば起動時に常駐させることもできます。ですが今回は自動化していませんので、必要ならば、手作業でスタートアップに入れてください。点字入力を一時的に無効にするときは、先ほど説明したモードを切り替えるALT+CTRL+変換キーで「無効」を選びます。

最後に、このソフトは無保証です。自己責任でご利用ください。通常、ここで公開するプログラムはソースも公開していますが、今回は非公開とします。

⇒ダウンロードbrlinput.zip ver.1.0.0.4(20140715)
(リンク先のDownloadボタンを押せばダウンロードできます。)
ハッシュ値(MD5)を確認したいときはこちら

(更新履歴)

2014/07/15
修正版(ver.1.0.0.4)に差し替えました。 
・英字のときsdfjklが正しく打てなくなっていたのを修正。

2014/07/13
修正版(ver.1.0.0.3)に差し替えました。 
・読点が打てなくなっていたので、修正(5、6の点、その後、スペース)。
・モード切替のショートカットをCtrl+Alt+変換キーに変更。
・修飾キー(Shift、Ctrl、Alt、Win)が押されているときは、点字入力とは見なさない。

2014/07/12
修正版(ver.1.0.0.2)に差し替えました。
・Shift+CapsLockでキャプスロックをオンにしたとき、本来は英字の大文字化の前置符号である6の点を、その間だけ小文字化の前置符号とみなすことにした。それに伴いその間の音声ガイドも「小文字」となるようにした。
・該当しない点字パターンを入力したとき、PC-TalkerもしくはNVDAで「エラー」と発音するようにした。
・入力する手の切り替えメニューにチェックが付くようにした。

2014/07/11
修正版(ver.1.0.0.1)に差し替えました。
・数値と外字引用の処理がおかしくなってしまったので、修正
・モードの切り替えをCtrl+Alt+Bに変更
・英大文字の入力ができるようにしました。英字入力が可能な時に、6の点だけを打つと、後続の一文字だけを大文字にします。連続して英大文字を使うときは、Shift+CapsLockです。

2014/07/07
修正版(ver.1.0.0.0)に差し替えました。
・問題だった「や」を修正しました。
・入力する手の選択をタスクトレイのメニューが選べるようにしました。
・点字キーとスペースでいくつかのコマンドを入力できるようにしました(スペースキーのタイミングに注意)。
入力できるのは一般的なソフトに近い以下の通りです。以下の点字を入力して手を離す前に「スペース」を追加して、手を離せば実行できます。ただし右手入力のときは右側入力のときにスペースキーです。左手入力のときは右側の入力のときにスペースの代わりに無変換キーです。なお01の列は1の点から順に押すときを1、押さないときを0で表しています。括弧内は一般的な機能です。
"100000" ... Enter
"010000" ... →
"110000" ... Ins
"000100" ... ←
"111100" ... PgDn
"000010" ... Del
"100010" ... Ctrl+End(文書末へ移動)
"010010" ... ↑
"101010" ... Tab
"011110" ... Ctrl+HOME(文書先頭へ移動)
"000001" ... Esc
"001001" ... ↓
"101001" ... PgUp
"111001" ... Home(行頭へ)
"111011" ... End(行末へ)
"111111" ... Alt+F4(ウィンドウを閉じる)



拍手する
posted by takayan at 02:40 | Comment(22) | TrackBack(0) | 点字入力 | このブログの読者になる | 更新情報をチェックする

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 | このブログの読者になる | 更新情報をチェックする
×

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