Skip to content

Instantly share code, notes, and snippets.

@raccy
Last active December 2, 2015 02:51
Show Gist options
  • Save raccy/ab6ed1cf0b2d5560bd8f to your computer and use it in GitHub Desktop.
Save raccy/ab6ed1cf0b2d5560bd8f to your computer and use it in GitHub Desktop.
Viqo経由でめいちゃんにコメをしゃべって貰おうとしたら、なぜかこうなった。
#!/usr/bin/env ruby
# coding: utf-8
=begin
kometalk.rb
Copyright (c) 2015 IGARASHI Makoto (raccy)
This software is released under the MIT License.
http://opensource.org/licenses/mit-license.php
コマンドラインからの入力文字をOpen JTalkでしゃべってくれます。
実行はすぐに終わって、裏でサーバが立ち上がって、しゃべります。
サーバはしばらくすると、落ちます。
Mac以外で動くかどうかは知らん。Windowsは無理だと思う。
使い方:
ruby kometalk.rb ほげほげ
でよみます。
下の__END__以下でopne_jtalkのパスとかは指定して下さい。
open_jtalkの細かいオプションは・・・直接ソースを触って下さい。
=end
require "pathname"
require "timeout"
require "yaml"
require "logger"
class Kometalk
DEFAULT_CONFIG = {
open_jtalk: "open_jtalk/bin/open_jtalk",
dic_dir: "open_jtalk/dic",
htvoice: "open_jtalk/voice/mei_normal.htsvoice",
text_file: "kometalk.txt",
wave_file: "kometalk.wav",
info_file: "kometalk.inf",
lock_file: "kometalk.lock",
fifo_file: "kometalk.fifo",
pid_file: "kometalk.pid",
log_file: "kometalk.log",
server_timeout: 60,
client_timeout: 10,
play_wave: "/usr/bin/afplay",
learn_yaml: "learn.yml",
learn_say: "%sって読む。めぃ、覚えた。",
learn_non: "%sって読まない。めぃ、忘れた。",
learn_unk: "%sのこと、めぃ、知らない。",
learn_reg: "その正規表現は、%sって読むのね。。",
learn_nor: "正規表現が間違っているよ。",
}
def initialize(work_dir, config)
@work_path = Pathname(work_dir)
@config = DEFAULT_CONFIG.merge(config)
@learn = []
@logger = Logger.new(path(:log_file))
@logger.level = Logger::INFO
end
def path(sym)
return @work_path + @config[sym]
end
def say(*str_list)
path(:lock_file).open("w") do |lock_file|
lock_file.flock(File::LOCK_EX)
run_server(lock_file)
begin
timeout(@config[:client_timeout]) do |i|
path(:fifo_file).open("a") do |fifo_file|
str_list.each do |str|
fifo_file.puts str
end
end
end
rescue Timeout::Error
@logger.error("client timeout")
end
end
end
def run_server(lock_file)
if path(:pid_file).file?
path(:pid_file).open("r+") do |pid_file|
unless pid_file.flock(File::LOCK_EX | File::LOCK_NB)
@logger.debug("server running")
return
end
end
end
unless path(:fifo_file).exist?
system("mkfifo #{path(:fifo_file)}")
end
@logger.info("fork child")
fork do
@logger.debug("close lock file")
lock_file.close
@logger.debug("setsid")
Process.setsid
@logger.debug("setproctitle")
begin
Process.setproctitle("(#{File.basename(__FILE__)})")
rescue => e
@logger.debug(e.message)
$0 = "(#{File.basename(__FILE__)})"
end
@logger.info("fork server")
fork do
begin
@logger.debug("daemonize")
Process.daemon
path(:pid_file).open("w") do |pid_file|
pid_file.flock(File::LOCK_EX)
pid_file.puts(Process.pid)
pid_file.flush
if path(:learn_yaml).file?
@logger.info("load learn yaml")
@learn = YAML.load(path(:learn_yaml).read)
unless @learn.is_a?(Array)
@logger.info("initilaize learn")
@learn = []
end
end
path(:fifo_file).open("r") do |fifo_file|
begin
while true
line = nil
timeout(@config[:server_timeout]) do |i|
until line = fifo_file.gets
@logger.debug("sleep 1")
sleep 1
end
end
talk(line)
end
rescue Timeout::Error
@logger.info("server timeout")
end
end
@logger.info("save learn yaml")
path(:learn_yaml).write(YAML.dump(@learn))
end
rescue => e
@logger.error(e.message)
end
exit!(0)
end
exit!(0)
end
Process.wait
end
def talk(str)
@logger.debug("talk #{str}")
str.encode!('UTF-8', 'UTF-8-MAC')
case str
when /^s\/((?:[^\/]|\\\/)+)\/((?:[^\/]|\\\/)+)\/g$/
reg_str = $1
replace = $2
begin
regexp = Regexp.compile(reg_str)
@logger.info("learn s/#{reg_str}/#{replace}/g")
@learn[0] ||= {}
@learn[0][regexp] = replace
str = @config[:learn_reg] % replace
rescue RegexpError
@logger.warn("invalid regexp #{reg_str}")
str = @config[:learn_nor]
end
when /^\s*教育\s*[((]\s*(\S+)\s*[==]\s*(\S+)\s*[)|)]\s*$/
word = $1
yomi = $2
@logger.info("learn #{word} = #{yomi}")
@learn[word.size] ||= {}
@learn[word.size][word] = yomi
str = @config[:learn_say] % $2
when /^\s*忘却\s*[((]\s*(\S+)\s*[))]\s*$/
word = $1
@learn[word.size] ||= {}
yomi = @learn[word.size].delete(word)
if yomi
@logger.info("forget #{word}")
str = @config[:learn_non] % yomi
else
@logger.warn("unknown #{word}")
str = @config[:learn_unk] % word
end
else
@learn.reverse_each do |learn_child|
if learn_child.is_a?(Hash)
learn_child.each_pair do |key, value|
str.gsub!(key, value)
end
end
end
end
@logger.info("jtalk #{str}")
path(:text_file).open("w") do |io|
io.puts str
end
cmd = ""
cmd << path(:open_jtalk).to_s
cmd << " -x " << path(:dic_dir).to_s
cmd << " -m " << path(:htvoice).to_s
cmd << " -ow " << path(:wave_file).to_s
cmd << " -ot " << path(:info_file).to_s
# customize
# cmd << " -s i " # sampling frequency [ auto][ 1-- ]
# cmd << " -p i " # frame period (point) [ auto][ 1-- ]
# cmd << " -a f " # all-pass constant [ auto][ 0.0-- 1.0]
# cmd << " -b 0.0 " # postfiltering coefficient [ 0.0][ 0.0-- 1.0]
cmd << " -r 1.2 " # speech speed rate [ 1.0][ 0.0-- ]
# cmd << " -fm 0.0 " # additional half-tone [ 0.0][ -- ]
# cmd << " -u 0.5 " # voiced/unvoiced threshold [ 0.5][ 0.0-- 1.0]
# cmd << " -jm 1.0 " # weight of GV for spectrum [ 1.0][ 0.0-- ]
# cmd << " -jf 1.0 " # weight of GV for log F0 [ 1.0][ 0.0-- ]
cmd << " " << path(:text_file).to_s
@logger.debug("exec #{cmd}")
system(cmd)
system("#{path(:play_wave).to_s} #{path(:wave_file).to_s}")
end
end
if __FILE__ == $0
config = YAML.load(DATA).each_with_object({}){|(k,v),m|m[k.to_s.intern]=v}
kometalk = Kometalk.new(File.expand_path(File.dirname(__FILE__)), config)
kometalk.say(*ARGV)
end
__END__
open_jtalk: open_jtalk/bin/open_jtalk
dic_dir: open_jtalk/dic
htvoice: open_jtalk/voice/mei_normal.htsvoice
play_wave: /usr/bin/afplay
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment