summaryrefslogtreecommitdiff
path: root/crystal/irc.cr
diff options
context:
space:
mode:
authorJoe Rayhawk <jrayhawk+git@omgwallhack.org>2022-07-07 19:58:42 -0700
committerJoe Rayhawk <jrayhawk+git@omgwallhack.org>2022-07-07 19:58:42 -0700
commit8031a23e31c65cdc75d68b5e3f28faf0071720c6 (patch)
treefb3df1f0a591fc5169e06adb6d40fbf0a2d69d94 /crystal/irc.cr
parent11667811c444f4721f06efed4407069f24e79df0 (diff)
downloadtwitchtools-8031a23e31c65cdc75d68b5e3f28faf0071720c6.tar.gz
twitchtools-8031a23e31c65cdc75d68b5e3f28faf0071720c6.zip
crystal/irc.cr: finish prototyping windows tts support
Diffstat (limited to 'crystal/irc.cr')
-rwxr-xr-x[-rw-r--r--]crystal/irc.cr215
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