(暫定)メインプログラムの説明

Last-modified: 2018-07-07 (土) 22:20:06
 
gui_3.jpg

できること

入力欄に入力した言葉に対して、無脳ちゃんが受け応えしてくれます。ただし、大抵は短い罵り言葉ですが、ときどき長文も返してくれます。ただし、たいてい意味不明ですが、ときどき気の利いたことを言うこともあります(単なる偶然です。)。

 

起動方法

(あらかじめ、必要なライブラリ等をインストールしておく必要があります。(READMEをごらんください。))
ファイルの置いてあるディレクトリに移動して、

 $ ruby gui.rb

と実行します。
音声を出したい場合は、

 $ ruby gui.rb -v
 

終了したいときは、入力欄に「バイバイ」または「ばいばい」と入力します。

 

会話の続け方と機械学習(?)のコツ

  • 完全にゼロから入力した文章を蓄積して学習(?)していくので、はじめのうちはかなりおバカです。辛抱強く付き合ってください。
  • 入力した文章から名詞を見つけて返答するようになっているので、名詞のない文章には反応しません(名詞があっても反応しないこともあります。)。
  • 名詞だけだと、文章(「てにをは」など)を学習できません。
  • 以上から、特にはじめのうちは完結した文章をたくさん入力することをお勧めします。
    • よい例
       「あなたが今言ったことはとてもいい内容ですね。」
       「人生で大事なのは友情と努力と勝利の3つだと思うわけです。」
    • 悪い例
       「いいね」、「あ、そう」、「だよね」、「友情、努力、勝利」
  • 変な言葉を覚え込ませてしまい、忘れさせたい場合は、MRKOV_DIC.yaml内のその言葉を別の言葉に置換しましょう。単純にその単語だけ削除するとMRKOV_DIC.yamlが壊れる恐れがあります。

内部でやっていること

基本的には、こちらの入力した言葉から、ランダムに名詞をひとつ抜き出して、マルコフ辞書からその名詞につながる言葉をランダムにつなげているだけ。

  • こちらの入力
     おやつにアイス食べたい。
  • 名詞抽出
     おやつ、アイス
  • 名詞をひとつ取り出す。
     アイス
  • 「アイス」で始まる言葉の塊をマルコフ辞書から探す。
     |アイス|は|やっぱり|
     |アイス|の|味|
     ......
  • 言葉の塊をひとつ取り出す。
     |アイス|の|味|
  • 今度は、「味」で始まる言葉の塊をマルコフ辞書から探す。
     |味|なら|食べ|
     |味|の|焼きそば|
  • 言葉の塊をひとつ取り出す。
     |味|の|焼きそば|
  • 今度は「焼きそば」で始まる言葉をマルコフ辞書から探す。という作業を繰り返していく。
  • 最後に、取り出した言葉の塊をつなげて、文章らしきものにして、出力する。
     アイスの味の焼きそば....
     

プログラム全文

ここでは、GUI部分と会話部分がひとつのファイルになっているが、本来やらせたいこと別にファイルで作成すべき。将来のこととか考えないで作っていったため、こんなことになってしまった。
これを「老舗温泉旅館プログラミング」あるいは「田舎の観光ホテルプログラミング」という。

 require 'gtk3'
 require 'yaml'
 require 'natto'
 require 'pp'
 require 'open_jtalk'
 #### Ui クラスを定義
 class Ui
   def initialize
     @markov_dic = YAML.load_file 'MARKOV_DIC.yaml'
   rescue
     @markov_dic = []
   end
   def make_window
     win = Gtk::Window.new
     #### 次のコードで「閉じる」を押すと、プログラムは終了。
     win.signal_connect('destroy') { Gtk.main_quit }
     win.set_border_width(10)
     #### 縦の箱を用意
     vbox = Gtk::Box.new(:vertical, 10)
     #### 上から画像、無脳ちゃん(仮)の言葉、こちらの入力欄を置く
     ibox = Gtk::Image.new(file: 'yome.png')
     tp = Gdk::RGBA.new(1.0, 0.5, 1.0, 0.5)
     ibox.override_background_color(:normal, tp)
     yome_area = Gtk::TextView.new
     yome_area.set_size_request(240, -1)
     yome_area.set_cursor_visible(false)
     yome_area.set_editable(false)
     @output = '何か用事なの?私だって忙しんだからね。そうそう『バイバイ」でさよならよ。'
     yome_area.buffer.text = "ヨメ> #{@output}"
     yome_talk
     ore_area = Gtk::Entry.new
     ore_area.set_size_request(240, -1)
     ore_area.signal_connect('activate') do |e|
       @entry = e.text
       if @entry == 'バイバイ' || @entry == 'ばいばい'
         puts @entry
         YAML.dump(@markov_dic, File.open('MARKOV_DIC.yaml', 'w'))
         @output = 'じゃ、またね。バイバイ'
         yome_area.buffer.text = "ヨメ> #{@output}"
         yome_talk
         exit
       end
       ore_strings
       yome_strings
       yome_area.buffer.text = "ヨメ> #{@output}"
       e.text = ''
       yome_talk
     end
     vbox.pack_start(ibox, expand: true, fill: true, padding: 0)
     vbox.pack_start(yome_area, expand: true, fill: true, padding: 0)
     vbox.pack_start(ore_area, expand: true, fill: true, padding: 0)
     ore_area.grab_focus
     win.add(vbox)
     win.show_all
     Gtk.main
   end
   private
   def ore_strings
     ## 単語分割し、配列に代入
     parse_ore = Natto::MeCab.new
     parsed_str = []
     parse_ore.parse(@entry) do |line|
       parsed_str.push line.surface
     end
     ## マルコフ連鎖用辞書に収録
     cycle = parsed_str.size - 3
     cycle.times do |key1, key2, val|
       key1 = parsed_str[0]
       key2 = parsed_str[1]
       val = parsed_str[2]
       keys = [key1, key2]
       cell_arr = [keys, val]
       @markov_dic.push cell_arr
       parsed_str.shift
     end
     @markov_dic.uniq!
     @markov_dic.shuffle!
     ## 嫁の返事のキーワードとなる名詞をランダムに抜き出す。
     ore_words = []
     parse_ore.parse(@entry) do |line|
       if line.feature =~ /固有名詞/ || line.feature =~ /名詞,サ変接続/ || line.feature =~ /名詞,一般/
         ore_words.push line.surface
       end
     end
     @keyword = ore_words[rand(ore_words.size)]
   end
   #### ときどきあるはずのない引数を受け取って突然死する。
   def yome_strings (*)
     @yome_strings = ['え!? 嘘でしょ? もー、信じらんない! ']
     @markov_dic.each do |line|
       next unless line[0][0] == @keyword
       if @keyword == '。' || @keyword == '?'
         @yome_strings.push @keyword
         break
       end
       @yome_strings.push line[0][0]
       @yome_strings.push line[0][1]
       @keyword = line[1]
       next if @keyword != line[0][0]
       yome_strings @keyword
     end
     @yome_strings.push ' バカ、死ね、カス!'
     @output = @yome_strings.join
     @yome_strings = ['ヨメ(仮)> ']
   end
   def yome_talk
     if ARGV[0] == '-v'
       OpenJtalk.load(OpenJtalk::Config::Mei::FAST) do |openjtalk|
         header, data = openjtalk.synthesis(openjtalk.normalize_text(@output))
         OpenJtalk::WaveFileWriter.save('yome.wav', header, data)
         `aplay yome.wav`
       end
       #  File.delete 'yome.wav'
     end
   end
 end
 Ui.new.make_window
 

gui.rb詳細

追加のライブラリー呼び出し

 require 'gtk3'
 require 'yaml'
 require 'natto'
 require 'pp'
 require 'open_jtalk'

Uiクラスの初期化。マルコフ辞書を配列@markov_dicに読み込ませる。マルコフ辞書がないときには、空の@markov_dicを作る。

   def initialize
     @markov_dic = YAML.load_file 'MARKOV_DIC.yaml'
   rescue
     @markov_dic = []
   end

GUIインターフェースの作成とプログラムの実行をするメソッド(本当は、それぞれの機能を分けた方がいい。)

   def make_window
     win = Gtk::Window.new
     #### 次のコードで「×ボタン」を押すと、プログラムは終了。
     win.signal_connect('destroy') { Gtk.main_quit }
     win.set_border_width(10)
     #### 縦の箱を用意
     vbox = Gtk::Box.new(:vertical, 10
     #### 無脳ちゃん画像を表示
     ibox = Gtk::Image.new(file: 'yome.png')
     tp = Gdk::RGBA.new(1.0, 0.5, 1.0, 0.5)
     ibox.override_background_color(:normal, tp)
     #### 無脳ちゃんの言葉を表示
     yome_area = Gtk::TextView.new
     yome_area.set_size_request(240, -1)
     yome_area.set_cursor_visible(false)
     yome_area.set_editable(false)
     @output = '何か用事なの?私だって忙しんだからね。そうそう『バイバイ」でさよならよ。'
     yome_area.buffer.text = "ヨメ> #{@output}"
     yome_talk
     #### こちらの入力欄
     ore_area = Gtk::Entry.new
     ore_area.set_size_request(240, -1)
     ore_area.signal_connect('activate') do |e|
       @entry = e.text
       #### ばいばい、またはバイバイでプログラム終了
       if @entry == 'バイバイ' || @entry == 'ばいばい'
         puts @entry
         YAML.dump(@markov_dic, File.open('MARKOV_DIC.yaml', 'w'))
         @output = 'じゃ、またね。バイバイ'
         yome_area.buffer.text = "ヨメ> #{@output}"
         yome_talk
         exit
       end

以下こちらの入力に応じて、無脳ちゃんの言葉を表示するとともに、音声を出す(あれ?ここにあるのは不自然では?)。

       ore_strings
       yome_strings
       yome_area.buffer.text = "ヨメ> #{@output}"
       e.text = ''
       yome_talk
     end

以下、GUI描画

     vbox.pack_start(ibox, expand: true, fill: true, padding: 0)
     vbox.pack_start(yome_area, expand: true, fill: true, padding: 0)
     vbox.pack_start(ore_area, expand: true, fill: true, padding: 0)
     ore_area.grab_focus
     win.add(vbox)
     win.show_all
     Gtk.main
   end

以下、プライベートメソッド
ore_stringsでは、1こちらの入力内容をマルコフ辞書に追加する。2ランダムに名詞をひとつ抽出して、yome_stringsにわたす。作業を行う。

   def ore_strings
     ## 単語分割し、配列に代入
     parse_ore = Natto::MeCab.new
     parsed_str = []
     parse_ore.parse(@entry) do |line|
       parsed_str.push line.surface
     end
     ## マルコフ連鎖用辞書に収録
     cycle = parsed_str.size - 3
     cycle.times do |key1, key2, val|
       key1 = parsed_str[0]
       key2 = parsed_str[1]
       val = parsed_str[2]
       keys = [key1, key2]
       cell_arr = [keys, val]
       @markov_dic.push cell_arr
       parsed_str.shift
     end
     @markov_dic.uniq!
     @markov_dic.shuffle!
     ## 嫁の返事のキーワードとなる名詞をランダムに抜き出す。
     ore_words = []
     parse_ore.parse(@entry) do |line|
       if line.feature =~ /固有名詞/ || line.feature =~ /名詞,サ変接続/ || line.feature =~ /名詞,一般/
         ore_words.push line.surface
       end
     end
     @keyword = ore_words[rand(ore_words.size)]
   end

yome_stringsでは、ore_stringsから受けた言葉を元に文章らしきものを作成する。
(*)がついているのは、ときどきあるはずのない引数を受け取って突然死するのを防止するため、実際には意味のない引数を受け取れるようにしてるため。

   def yome_strings (*)
     @yome_strings = ['え!? 嘘でしょ? もー、信じらんない! ']
     @markov_dic.each do |line|
       next unless line[0][0] == @keyword
       if @keyword == '。' || @keyword == '?'
         @yome_strings.push @keyword
         break
       end
       @yome_strings.push line[0][0]
       @yome_strings.push line[0][1]
       @keyword = line[1]
       next if @keyword != line[0][0]
       ## yome_stringsメソッドを再帰的に実行
       yome_strings @keyword
     end
     @yome_strings.push ' バカ、死ね、カス!'
     @output = @yome_strings.join
     @yome_strings = ['ヨメ(仮)> ']
   end

open_Jtaikを使って、音声を出すためのメソッド

   def yome_talk
     if ARGV[0] == '-v'
       OpenJtalk.load(OpenJtalk::Config::Mei::FAST) do |openjtalk|
         header, data = openjtalk.synthesis(openjtalk.normalize_text(@output))
         OpenJtalk::WaveFileWriter.save('yome.wav', header, data)
         `aplay yome.wav`
       end
       #  File.delete 'yome.wav'
     end
   end

実行

 Ui.new.make_window