diff options
author | Joe Rayhawk <jrayhawk+git@omgwallhack.org> | 2022-07-07 19:58:42 -0700 |
---|---|---|
committer | Joe Rayhawk <jrayhawk+git@omgwallhack.org> | 2022-07-07 19:58:42 -0700 |
commit | 8031a23e31c65cdc75d68b5e3f28faf0071720c6 (patch) | |
tree | fb3df1f0a591fc5169e06adb6d40fbf0a2d69d94 | |
parent | 11667811c444f4721f06efed4407069f24e79df0 (diff) | |
download | twitchtools-8031a23e31c65cdc75d68b5e3f28faf0071720c6.tar.gz twitchtools-8031a23e31c65cdc75d68b5e3f28faf0071720c6.zip |
crystal/irc.cr: finish prototyping windows tts support
-rwxr-xr-x[-rw-r--r--] | crystal/irc.cr | 215 |
1 files changed, 170 insertions, 45 deletions
diff --git a/crystal/irc.cr b/crystal/irc.cr index 9ac7e8b..2df28fa 100644..100755 --- a/crystal/irc.cr +++ b/crystal/irc.cr @@ -23,36 +23,90 @@ if ENV["LOCALAPPDATA"]? settings["statedir"] = Path.windows( ENV["LOCALAPPDATA"] + "\\bungmobott\\state\\" ).to_s end +settings["tempdir"] = "/tmp/bungmobott/" +ENV["TEMP"]? && ( settings["tempdir"] = "#{ENV["TEMP"]}\\bungmobott\\" ) + ENV["XDG_CONFIG_HOME"]? && ( settings["configdir"] = ENV["XDG_CONFIG_HOME"] + "/bungmobott/" ) #ENV["XDG_DATA_HOME"]? && ( settings["datadir"] = ENV["XDG_DATA_HOME"] ) # unused? ENV["XDG_STATE_HOME"]? && ( settings["statedir"] = ENV["XDG_STATE_HOME"] + "/bungmobott/" ) Dir.mkdir_p( settings["configdir"] ) Dir.mkdir_p( settings["statedir"] ) +Dir.mkdir_p( settings["tempdir"] ) settings["home"] = Path.home.to_s -["access_token", "channel", "channel_id", "client_id", "client_id_twitch"].each do |key| +regextwitchuser = /^[0-9a-zA-Z_]+$/ + +error = false +[ "access_token", "client_id" ].each do |key| begin settings[key] = File.read( settings["configdir"] + key ).chomp rescue IO::Error - STDERR.puts "Warning: Missing " + settings["configdir"] + key + STDERR.puts "ERROR: Missing " + settings["configdir"] + key + error = true end end +# enable gcloud? +if File.exists?( settings["configdir"] + "gcloud_token" ) && ( settings["gcloud_token"] = File.read( settings["configdir"] + "gcloud_token" ).chomp ) + gcloud = true +else + gcloud = false + STDERR.puts "Warning: #{settings["configdir"]}gcloud_token is missing; GCS voices disabled." +end + +# enable aws? +if ! File.exists?( ENV["USERPROFILE"] + "\\.aws\\credentials" ) + STDERR.puts "Warning: #{ENV["USERPROFILE"]}\\.aws\\credentials is missing; AWS voices disabled." + aws = false +elsif ! Process.find_executable( "aws.exe" ) + STDERR.puts "Warning: aws.exe is missing; AWS voices disabled." + aws = false +else + aws = true +end + +client = Twitcr::Client.new( settings ) + +# derive channel_id from channel or vice versa +if ( + File.exists?( settings["configdir"] + "channel" ) && + ( settings["channel"] = File.read( settings["configdir"] + "channel" ).chomp ) + ) || ( + File.exists?( settings["configdir"] + "channel_id" ) && + ( settings["channel_id"] = File.read( settings["configdir"] + "channel_id" ).chomp ) + ) + if ! ( settings["channel"]? =~ regextwitchuser ) && settings["channel_id"]? =~ /^[0-9]+$/ + settings["channel"] = client.user( settings["channel_id"].to_u64 ).login + elsif ( settings["channel_id"]? =~ regextwitchuser ) && ! settings["channel_id"]? =~ /^[0-9]+$/ + settings["channel_id"] = client.user( settings["channel"] ).id.to_s + end +else + STDERR.puts "ERROR: Missing #{settings["configdir"]}channel and channel_id configuration keys." + error = true + # exit 2 +end +if error == true + {% if flag?(:windows) %} + puts "press enter to end program" + gets + {% end %} + exit 1 +end + if File.exists?( settings["configdir"] + "chatuser" ) File.read( settings["configdir"] + "chat_user" ).chomp else - STDERR.puts "Warning: Missing " + settings["configdir"] + "chat_user; using configured channel instead." + STDERR.puts "Missing " + settings["configdir"] + "chat_user; using configured channel instead." settings["chat_user"] = settings["channel"] end obsipc = Channel( String ).new ircipc = Channel( Tuple( String, String ) ).new +t2sipc = Channel( Tuple( String, String ) ).new snifflast = Int64.new(0) -client = Twitcr::Client.new( settings ) - def urbandef( term : String ) #http://api.urbandictionary.com/v0/define?term=waifu client = HTTP::Client.new( "api.urbandictionary.com" ) @@ -62,16 +116,7 @@ def urbandef( term : String ) return json["list"][0]["definition"].to_s.gsub( /[\[\]]/, "" ).gsub( /\n/, "" ) end -#def ircmsg( home : String, channel : String, msg : String) -# if msg !~ /(\r|\n)/ -# sock = Socket.unix -# sock.connect Socket::UNIXAddress.new( home + "/.irssi/twitch-socket".to_s, type: Socket::Type::DGRAM ) -# sock.connect Socket::UNIXAddress.new( home + "/.irssi/twitch-socket".to_s ) -# sock.puts( "#" + channel + " " + msg ) -# sock.close -# end -#end - +# Currently only used in flags?(:unix) def t2smsg( settings : Hash(String, String), msg : String) if File.exists?( settings["statedir"] + "/.t2s.sock" ) sock = Socket.unix @@ -145,7 +190,7 @@ def getvoice( settings : Hash(String, String), userdir : String, chatuser : Stri end # text2speech -def t2s( settings : Hash(String, String), userdir : String, chatuser : String, text : String ) +def t2s( t2sipc : Channel, settings : Hash(String, String), userdir : String, chatuser : String, text : String ) if ( text !~ /^ *(!|\|)/ ) namesub, voice_setting, voice = getvoice( settings, userdir, chatuser ) subs = Array( Tuple( Regex, String ) ){ @@ -186,27 +231,9 @@ def t2s( settings : Hash(String, String), userdir : String, chatuser : String, t }.each do | subtuple | text = text.gsub( subtuple[0], subtuple[1] ) end - {% if flag?(:windows) %} - if ( match = voice.match( /^Microsoft-([A-Za-z]+)/ ) ) - if ( match[1] =~ /Lili|Mary|Mike|Sam|Anna/ ) - msttsvoice="Microsoft #{match[1]}" - else - msttsvoice="Microsoft #{match[1]} Desktop" - end - pp msttsvoice - p = Process.new( "powershell.exe", [ "-Command", " - echo test; - Add-Type -AssemblyName System.Speech; - $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; - $speak.SelectVoice(\"#{msttsvoice}\"); - $speak.Speak($Input); - $speak.Finalize($Input); - "], - input: Process::Redirect::Pipe, output: STDOUT ) - p.input.puts "#{namesub} #{text}" - p.input.close - end - {% elsif flag?(:unix) %} + {% if flag?(:windows) %} # send to thread + t2sipc.send( { voice, "#{namesub} #{text}" } ) + {% elsif flag?(:unix) %} # send to socket t2smsg( settings, "#{voice} #{namesub} #{text}" ) {% end %} return( voice ) @@ -238,6 +265,25 @@ def reversesource( scenes : Hash( String, Hash( String, Hash( String, Int64 | Fl return sourcemap end +def playmp3file( filepath : String ) + p = Process.new( + "powershell.exe", + [ "-Command", "#Set-PSDebug -Trace 1; + Add-Type -AssemblyName presentationCore; + $player = New-Object system.windows.media.mediaplayer; + $player.open(\"#{filepath}\"); + $player.volume = .99; + $player.play(); + Start-Sleep -Milliseconds 1000; + $duration = $player.NaturalDuration.TimeSpan.TotalMilliseconds; + Start-Sleep -Milliseconds ($duration - 1000 ); + "], + output: STDOUT, error: STDERR + ) + # https://geekeefy.wordpress.com/2016/07/19/powershellmediaplayer/ has some ideas + p.wait +end + macro testrefuser2uid( path ) {% if flag?(:windows) %} File.exists?( {{path}} ) && ( File.read( {{ path }} ) =~ /^[0-9]+$/ ) @@ -281,7 +327,82 @@ end lastvoice = Array(String).new -# obs-websocket +# Put tts stuff into the same thread so each playback blocks the next +spawn do + loop do + begin + while t2stuple = t2sipc.receive + voice, text = [ *t2stuple ] + if ( match = voice.match( /^Microsoft-([A-Za-z]+)/ ) ) + if ( match[1] =~ /Lili|Mary|Mike|Sam|Anna/ ) + msttsvoice="Microsoft #{match[1]}" + else + msttsvoice="Microsoft #{match[1]} Desktop" + end + p = Process.new( + "powershell.exe", + [ "-Command", " + Add-Type -AssemblyName System.Speech; + $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; + $speak.SelectVoice(\"#{msttsvoice}\"); + $speak.Speak($Input); + $speak.Finalize; + "], + input: Process::Redirect::Pipe, output: STDOUT + ) + p.input.puts text + p.input.close + p.wait + elsif gcloud && ( match = voice.match( /^([a-zA-Z]{2,3}-[a-zA-Z]{2})/ ) ) # Google cloud voice + request = Hash( String, Hash( String, String ) ){ + "input" => { "text" => text }, + "audioConfig" => { "audioEncoding" => "MP3" }, + "voice" => { + "name" => voice, + "languageCode" => match[1], + }, + } + body = request.to_json + ssl_context = OpenSSL::SSL::Context::Client.new + ssl_context.verify_mode = OpenSSL::SSL::VerifyMode::NONE + + headers = HTTP::Headers.new + headers["Content-Type"] = "application/json; charset=utf-8" + + response = HTTP::Client.exec( "POST", "https://texttospeech.googleapis.com/v1/text:synthesize?key=#{settings["gcloud_token"]}", headers, body, tls: ssl_context ) + + response.body + + filepath="#{settings["tempdir"]}#{Time.utc.to_unix}.mp3" + json=JSON.parse(response.body) + File.write( filepath, Base64.decode_string( json["audioContent"].as_s ) ) + playmp3file( filepath ) + File.delete( filepath ) + elsif aws # AWS polly voices + filepath="#{settings["tempdir"]}#{Time.utc.to_unix}.mp3" + p = Process.new( + "aws.exe", [ + "polly", "synthesize-speech", + "--output-format", "mp3", + "--voice-id", voice, + "--text", text, + filepath + ], output: STDOUT, error: STDERR + ) + p.wait + playmp3file( filepath ) + File.delete( filepath ) + else # unknown + STDERR.puts "Voice not recognized or available." + end + end + rescue ex + puts ex + end + end +end + +# obs-websocket thread spawn do loop do begin @@ -408,7 +529,7 @@ spawn do obs_pubsub.run rescue ex : Socket::ConnectError # these are a bit noisy - pp ex + #pp ex obs_pubsub && obs_pubsub.close rescue ex : IO::Error pp ex @@ -424,14 +545,14 @@ spawn do end end -# IRC +# main thread: IRC loop do begin - bot = Twitch::IRC::Client.new( nick: settings["chat_user"], token: "oauth:" + settings["access_token"], log_mode: true ) + bot = Twitch::IRC::Client.new( nick: settings["chat_user"], token: "oauth:" + settings["access_token"], log_mode: true ) bot.tags = [ "membership", "tags", "commands" ] - # Outgoing + # Outgoing IRC message thread # "Most IRC servers limit messages to 512 bytes in length, including the trailing CR-LF characters." # PRIVMSG #channel message\r\n spawn do @@ -450,7 +571,7 @@ loop do snifflast = Time.utc.to_unix t2smsg( settings, chatuser + " sniff null" ) end - if ( t2sreturn = t2s( settings, userdir, chatuser, message.params[1] ) ) + if ( t2sreturn = t2s( t2sipc, settings, userdir, chatuser, message.params[1] ) ) lastvoice.insert( 0, t2sreturn ) lastvoice = lastvoice[0..4] end @@ -470,7 +591,7 @@ loop do end else if match[2]? - voicesub = match[2].downcase + voicesub = match[2].downcase if voicesub =~ /^(disabled|null|disable|none)$/ if File.exists?( userdir + "/voicesub" ) File.delete( userdir + "/voicesub" ) @@ -601,7 +722,7 @@ loop do #sleep 0.5 # I should probably set this up to add the source, and then delete the source at the end of playback. request["visible"] = true - ircipc.send( { "##{settings["channel"]}", "Playing media-#{cmd}" } ) + #ircipc.send( { "##{settings["channel"]}", "Playing media-#{cmd}" } ) obsipc.send( request.to_json ) elsif ( ( cmd =~ /^(status|title)$/ ) && ( mod || own ) ) begin @@ -793,6 +914,10 @@ loop do # loop to reconnect rescue ex pp ex + {% if flag?(:windows) %} + puts "press enter to end program" + gets + {% end %} exit 1 end end |