Skip to content

Instantly share code, notes, and snippets.

@NigelThorne
Last active March 4, 2016 06:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save NigelThorne/8446065 to your computer and use it in GitHub Desktop.
Save NigelThorne/8446065 to your computer and use it in GitHub Desktop.
AutoHotKey_L script to TTS the currently selected text.
require 'sinatra'
require 'win32ole'
Sinatra::Application.reset!
class MySinatraApp < Sinatra::Base
set :port, 7732 #SPEAK
@@is_paused = false
module SpeechVoiceSpeakFlags
#SpVoice Flags
SVSFDefault = 0
SVSFlagsAsync = 1
SVSFPurgeBeforeSpeak = 2
SVSFIsFilename = 4
SVSFIsXML = 8
SVSFIsNotXML = 16
SVSFPersistXML = 32
#Normalizer Flags
SVSFNLPSpeakPunc = 64
#Masks
SVSFNLPMask = 64
SVSFVoiceMask = 127
SVSFUnusedFlags = -128
end
@@queue ||= []
get '/' do
"""<html><body>
<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/2.2.0/jquery.min.js\"></script>
<script>
function post(link) {
var xhttp = new XMLHttpRequest();
xhttp.open(\"POST\", link , true);
xhttp.send();
}
</script>
<form id='sayform' action='./say' method='post'>What should I say?: <input name='text' type='text'></input></form></br>
<button type='button' onclick=\"post('./pause')\">Pause</button>
<button type='button' onclick=\"post('./resume')\">Resume</button>
<button type='button' onclick=\"post('./stop')\">Stop</button>
<button type='button' onclick=\"post('./toggle')\">Pause/Resume</button>
<script>
$('#sayform').submit(function(e){
e.preventDefault();
$.ajax({
url:'./say',
type:'post',
data:$('#sayform').serialize(),
success:function(){
$('#sayform input').val(\"\");
}
});
});
</script>
</body></html>"""
end
post '/say' do
@@is_paused = false
voice.Speak(params["text"], SpeechVoiceSpeakFlags::SVSFlagsAsync)
return "ok"
end
post '/say_to_file' do
@@is_paused = false
out= voice.AudioOutputStream
temp_file = "c:\\temp\\output.wav"
rm_file(temp_file)
fs = WIN32OLE.new('SAPI.SpFileStream')
fs.Open(temp_file, 3, true)
puts "file open"
format_ex = WIN32OLE.new('SAPI.SpWaveFormatEx')
voice.AudioOutputStream = fs
voice.AudioOutputStream.Format.Type = 39 # SAFT48kHz16BitStereo = 39
puts "stream set"
voice.Speak(params["text"], SpeechVoiceSpeakFlags::SVSFPurgeBeforeSpeak)
puts "text spoken: #{params["text"]}"
fs.Close()
voice.AudioOutputStream = out
puts "stream reset"
return "ok #{temp_file}"
end
post'/pause' do
voice.Pause
@@is_paused = true
return "ok"
end
post'/stop' do
voice.Speak("", SpeechVoiceSpeakFlags::SVSFPurgeBeforeSpeak)
@@is_paused = false
return "ok"
end
post '/resume' do
voice.Resume
@@is_paused = false
return "ok"
end
post '/toggle' do
if (@@is_paused)
@@is_paused = false
voice.Resume
else
@@is_paused = true
voice.Pause
end
return "ok"
end
get '/status' do
voice.Speak("status")
return "COM Object: #{@@voice.nil?}"
end
def voice
begin
@@voice.IsUISupported(nil) if(@@voice != nil)
rescue
@@voice = nil
end
@@voice ||= WIN32OLE.new('SAPI.SpVoice')
end
def rm_file(file)
File.delete(file) if File.exist?(file)
end
end
# config.ru
require 'rubygems'
require 'sinatra'
require 'rack/reloader'
require './app'
set :environment, :development
use Rack::Reloader, 0 if development?
run Sinatra::Application
source "http://rubygems.org"
gem "eventmachine", "1.0.8"
gem "thin"
gem "win32-service"
gem "sinatra"
GEM
remote: http://rubygems.org/
specs:
daemons (1.2.3)
eventmachine (1.0.8)
ffi (1.9.10-x86-mingw32)
rack (1.6.4)
rack-protection (1.5.3)
rack
sinatra (1.4.6)
rack (~> 1.4)
rack-protection (~> 1.4)
tilt (>= 1.3, < 3)
thin (1.6.4)
daemons (~> 1.0, >= 1.0.9)
eventmachine (~> 1.0, >= 1.0.4)
rack (~> 1.0)
tilt (2.0.1)
win32-service (0.8.7)
ffi
PLATFORMS
x86-mingw32
DEPENDENCIES
eventmachine (= 1.0.8)
sinatra
thin
win32-service
require "./say"
pause
require 'rubygems'
require 'win32/service'
include Win32
SERVICE_NAME = 'ruby_tts_service'
# delete the service
# NOTE: if the services applet is up during this operation, the service won't be removed from that ui
# unitil you close and reopen it (it gets marked for deletion)
#Service.delete(SERVICE_NAME)
#path = File.expand_path(File.dirname(__FILE__)).gsub('/','\\')
# Create a new service
Service.create({
:service_name => SERVICE_NAME,
:host => nil,
:service_type => Service::WIN32_OWN_PROCESS,
:description => 'A tts web service',
:start_type => Service::AUTO_START,
:error_control => Service::ERROR_NORMAL,
:binary_path_name => "\"#{`where ruby`.chomp}\" -C \"#{`echo %cd%`.chomp}\" windows_service.rb",
:load_order_group => 'Network',
:dependencies => nil,
:display_name => SERVICE_NAME
})
Service.start(SERVICE_NAME)
# encoding: utf-8
#!/usr/bin/env ruby
begin
SUBSTITUTIONS = {
# TODO: Read urls correctly... http://aumel-constash.vsl.com.au:7990/projects/BOND/repos/bddmanagement as http aumel-constash dot vsl dot com dot au, port 7990, projects, BOND, repos, bdd Management
/UCASE([0-9]+)/ => "yous case \\1",
/\b\.Net\b/ => " dot net ",
"/" => "-n-",
/\bgit\b/i => " gitt ",
/\bLBS\b/ => " Leica Biosystems ",
'no.' => "no .",
'XP' => 'ex-pea',
'APIs' => 'A P eyes',
'GOTO' => 'Go Too',
' AND ' => ' , and ',
' WHEN ' => ' , when ',
' THEN ' => ' , then ',
/plugin/ => "plug in",
# 'WIP' => '"Work In Progress"',
'IMO' => '"In My Opinion"',
/[A-Z][A-Z][A-Z][A-Z]+((?=[^A-Za-z])|(?!.))/ => lambda{|x|x.downcase}, #All caps becomes word
"\u0092" => "'",
/n[^a-z0-9\s]t/ => 'n\'t',
/[®«]/ => "", # don't read trademark sign
/ A / => "'a'",
/PMs/ => "pee-emms",
/RESTful/ => "restful",
/Actionee/i => "Action-e",
/Leica/i => "Liker",
/Axeda/i => "Exceedar",
/\.exe(?=[^a-z])/i => " executable ",
/\.txt(?=[^a-z])/i => " text file ",
/rebranded/ => "re-branded",
/App(?=[\s\.])/ => " application ",
'GUI' => " gooee ",
/localhost/ => "local host",
/tear/ => "tair",
/(?<word>[A-Z][a-z]*)(?=[A-Z ,\.;:\t\/])/ => "'\\k<word>' ", # CamelCaseWords should be split by spaces
/((?<!.)|(?<=\n))(?<line>[\?\-]\t[^\n]*)\n/m => "\n\\k<line>.\n", # dot points becomes sentences.
/\.dll/ => " Deelle el",
'\\' => ' slash ',
'default' => 'deefault',
'×' => 'times',
'disconnect' => 'dis-connect',
'btw' => 'by the way',
'vagaries' => 'vaigeries',
'verbalised' => 'verbaleyesed',
'(s)' => 's',
'BOND-III' => 'BOND3',
' is.' => ' is .',
'Telerik' => 'Tellerrick',
'reading and writing' => ' banana and <phoneme alphabet="x-cmu" ph="T AH0 M EY1 T OW0">writing</phoneme>',
}
# gitt
#Hi, I'm Fred. The time is currently <say-as format="dmy" interpret-as="date">01/02/1960</say-as>
require 'cgi'
#def to_speakxml(text)
# text.gsub!(/\r?\n/, " ")
# expression = Regexp.union(SUBSTITUTIONS.keys)
# begin
# <<-eos
# <?xml version="1.0" encoding="UTF-8"?>
# <speak xmlns="http://www.w3.org/2001/10/synthesis" version="1.0" xml:lang="en-UK">
# <voice xml:lang="en-UK">
# #{SUBSTITUTIONS.reduce(text){ |o, (r,s)|
# s.is_a?(Proc) ? o.gsub(r, &s) : o.gsub(r, s)
# }
# }
# </voice>
# </speak>
# eos
# rescue => e
# e.to_s
# end
#end
def to_speakxml(text)
text.gsub!(/\r?\n/, " ")
expression = Regexp.union(SUBSTITUTIONS.keys)
begin
<<-eos
<?xml version="1.0" encoding="UTF-8"?>
<vxml version = "2.1">
<form id="F_1">
<block>
#{SUBSTITUTIONS.reduce(text){ |o, (r,s)|
s.is_a?(Proc) ? o.gsub(r, &s) : o.gsub(r, s)
}
}
</block>
</form>
</vxml>
eos
rescue => e
e.to_s
end
end
if(__FILE__ == $0)
ARGF.set_encoding(Encoding::UTF_8)
puts to_speakxml(ARGF.read)
end
rescue => e
puts e.to_s
end
# encoding: utf-8
require 'win32ole'
require "./say"
begin
text = File.read('c:\\temp\\tmp_ahk_tts_clip.txt')
# ARGF.set_encoding(Encoding::UTF_8)
# text = ARGF.read();
data = text.bytes.to_a.pack("U*")
require_relative 'rephrase'
if data == ""
toggle
else
text = to_speakxml(data)
File.write('C:\\temp\\tmp_ahk_clip_out.txt',text)
say text
end
rescue => e
text = e.message
text = "empty message" if text.empty?
WIN32OLE.new('SAPI.SpVoice').Speak(text)
end
require "./say"
resume
require './app.rb'
MySinatraApp.run! :host => 'localhost', :port => 7732, :server => 'thin'
require 'uri'
require 'net/http'
def say(text)
post("say", 'text' => text)
end
def toggle()
post("toggle", 'text' => '')
end
def pause()
post("pause", 'text' => '')
end
def resume()
post("resume", 'text' => '')
end
def stop()
post("stop", 'text' => '')
end
def post(act, body)
uri = URI.parse("http://localhost:7732/#{act}")
response = Net::HTTP.post_form(uri, body)
end
require "./say"
stop
---
address: localhost
port: 7732
servers: 1
max_conns: 10
max_persistent_conns: 2
timeout: 30
environment: production
pid: tmp/pids/thin-production.pid
log: log/thin-production.log
daemonize: true
;;;;;;;;;;;;;;;;;;;;TTS;;;;;;;;;;;;;;;;;;;;;;
#^D:: ; Win + Ctrl + D
ClipSaved := ClipboardAll
Clipboard = ; Start off empty to allow ClipWait to detect when the text has arrived.
Send ^c
ClipWait, 0.1 ; Wait for the clipboard to contain text.
FileDelete , c:\temp\tmp_ahk_tts_clip.txt
FileAppend , %Clipboard% , c:\temp\tmp_ahk_tts_clip.txt
Gui, Add, Button, gPause, &Pause
Gui, Add, Button, gResume ys, &Resume
Gui, Add, Button, gStop ys, &Stop
Gui, Show,, TTS
Run, %comspec% /c "".\rephrase_runner.rb" c:\temp\tmp_ahk_tts_clip.txt" ,,;Hide
Clipboard := ClipSaved
ClipSaved = ; Free the memory
Return
Stop:
Run, %comspec% /c "".\stop.rb"" ,,;Hide
Gui, Cancel
Return
Pause:
Run, %comspec% /c "".\pause.rb"" ,,;Hide
Return
Resume:
Run, %comspec% /c "".\resume.rb"" ,,;Hide
Return
# This runs a simple sinatra app as a service
LOG_FILE = 'C:\\sinatra_daemon.log'
require 'rubygems'
require 'sinatra/base'
require './app'
#begin
require 'win32/daemon'
include Win32
$stdout.reopen("thin-server.log", "w")
$stdout.sync = true
$stderr.reopen($stdout)
class TestDaemon < Daemon
def service_main
log 'started'
MySinatraApp.run! :host => 'localhost', :port => 7732, :server => 'thin'
while running?
log 'running'
sleep 10
end
end
def service_stop
log 'ended'
exit!
end
def log(text)
File.open('log.txt', 'a') { |f| f.puts "#{Time.now}: #{text}" }
end
end
TestDaemon.mainloop
#rescue Exception => err
#File.open(LOG_FILE,'a+'){ |f| f.puts " ***Daemon failure #{Time.now} err=#{err} " }
#raise
#end
@NigelThorne
Copy link
Author

This script replaced the clipboard .. but currently has a side effect that while reading you can't cut/paste any more! :(
I'm now piping the text through a rephrase script that rewrites things to read them how I like them.

TODO: I may move the call to COM into the ruby.. then I can start it on another thread and free up the clipboard earlier.
I may also introduce a sinatra app or a service, running locally, that you send text to.. this would let me start/stop/pause/replace what's being read. Currently you have to wait for it to finish reading.

@NigelThorne
Copy link
Author

You could have fun with this... http://xkcd.com/1288/

@NigelThorne
Copy link
Author

It how frees up the clipboard before it's stop reading. this makes it much nicer to use.
TODO: make it stop reading when you trigger it with no text, or start reading something else with new text.

@NigelThorne
Copy link
Author

Added index web page so you can Pause/Say/Stop/Resume from there if you like.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment