<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>

<!-- Based on work by Nick Gammon based on work by Tyler Spivey -->
<!-- See: http://www.gammon.com.au/forum/bbshowpost.php?bbsubject_id=7767 -->
<muclient>
<plugin
   name="Text_To_Speech"
   author="Avi Kelman (Fiendish)"
   id="463242566069ebfd1b379ec1"
   language="Lua"
   purpose="Speaks incoming text using SAPI."
   date_written="2010-09-01"
   requires="4.60"
   version="2.0"
   save_state="y"
   >
   
<description trim="y">
Text To Speech Plug-in Commands:

 sapi on                   : turn on automatic speaking of MUD output.
 sapi off                  : turn off automatic speaking of MUD output.
 sapi skip                 : skips one sentence in output stream.
 sapi clear                : clear the speech output queue.
 sapi faster               : speeds speech up.
 sapi slower               : slows speech down.
 sapi rate [number]        : set speech rate to [number].
 sapi list voices          : list the available TTS voices.
 sapi voice [number]       : switch to the given TTS voice number.
 sapi test                 : speak a test phrase.
 sapi punctuation [number] : set punctuation filtering level to [number].
 sapi say [text]           : speak the given text.

Scripts can also speak stuff by doing:

 CallPlugin ("463242566069ebfd1b379ec1", "say", "What to say")
</description>
</plugin>

<aliases>
  <alias
   match="sapi on"
   enabled="n"
   omit_from_command_history="y"
   omit_from_log="y"
   omit_from_output="y"
   script="speak_on"
   sequence="100"
   group="speech"
  >
  </alias>

  <alias
   match="sapi off"
   enabled="n"
   omit_from_command_history="y"
   omit_from_log="y"
   omit_from_output="y"
   script="speak_off"
   sequence="100"
   group="speech"
  >
  </alias>
  
  <alias
   match="sapi faster"
   enabled="n"
   omit_from_command_history="y"
   omit_from_log="y"
   omit_from_output="y"
   script="speak_faster"
   sequence="100"
   group="speech"
  >
  </alias>
  
  <alias
   match="sapi slower"
   enabled="n"
   omit_from_command_history="y"
   omit_from_log="y"
   omit_from_output="y"
   script="speak_slower"
   sequence="100"
   group="speech"
  >
  </alias>
    
  <alias
   match="^sapi voice( .+)?$"
   regexp="y"
   enabled="n"
   omit_from_command_history="y"
   omit_from_log="y"
   omit_from_output="y"
   script="speak_choose_voice"
   sequence="100"
   group="speech"
  >
  </alias>
  
  <alias
   match="sapi list voices"
   enabled="n"
   omit_from_command_history="y"
   omit_from_log="y"
   omit_from_output="y"
   script="list_voices"
   sequence="100"
   group="speech"
  >
  </alias>
    
  <alias
   match="sapi test"
   enabled="n"
   omit_from_command_history="y"
   omit_from_log="y"
   omit_from_output="y"
   script="speak_test"
   sequence="100"
   group="speech"
  >
  </alias>
  
  <alias
   match="^sapi rate( .+)?$"
   enabled="n"
   regexp="y"
   omit_from_command_history="y"
   omit_from_log="y"
   omit_from_output="y"
   script="speak_rate"
   sequence="100"
   group="speech"
  >
  </alias>
  
  <alias
   match="sapi skip"
   enabled="n"
   omit_from_log="y"
   omit_from_output="y"
   script="speak_skip"
   sequence="100"
   group="speech"
  >
  </alias>

  <alias
   match="sapi clear"
   enabled="n"
   omit_from_log="y"
   omit_from_output="y"
   script="speak_skip_all"
   sequence="100"
   group="speech"
  >
  </alias>
  
  <alias
   match="^sapi say (.*)$"
   regexp="y"
   enabled="n"
   omit_from_log="y"
   omit_from_output="y"
   sequence="100"
   group="speech"
   send_to="12"
  >
  <send>
    say("%1")
  </send>
  </alias>
    
  <alias
   match="^sapi help( printed)?$"
   regexp="y"
   enabled="n"
   omit_from_log="y"
   omit_from_output="y"
   sequence="100"
   group="speech"
   send_to="12"
  ><send>
     if "%1" == "" then
       say("This help is being spoken but not printed to the screen. To print it to the screen, use: sapi help printed.")
       say(GetPluginInfo(GetPluginID(), 3), true)
     else
       NoteSilent(GetPluginInfo(GetPluginID(), 3))
     end
   </send>
  </alias>

  <alias
   match="sapi debug"
   enabled="n"
   omit_from_log="y"
   omit_from_output="y"
   sequence="100"
   group="speech"
   send_to="12"
  ><send>
     show_spoken_lines = not show_spoken_lines
   </send>
  </alias>
  
  <alias
   match="sapi punctuation( .+)?"
   regexp="y"
   enabled="n"
   omit_from_log="y"
   omit_from_output="y"
   sequence="100"
   group="speech"
   script="speak_punctuation_level"
  ></alias>
</aliases>

<triggers>
</triggers>

<!--  Script  -->

<script>
<![CDATA[

local punc_descs = {
  "Say all punctuation.",
  "Say only non-standard punctuation.",
  "Extra filtering to mask symbols and other garbage."
}

function current_punctuation_level()
  return "SAPI punctuation level is currently "..tostring(punctuation_level)..". "..punc_descs[punctuation_level]
end

function speak_punctuation_level(name, line, wildcards)
  local function list_options()
    say("Punctuation level options are:")
    for i,v in ipairs(punc_descs) do say("Level "..tostring(i)..": "..v) end
    say("Level "..tostring(#punc_descs).." is recommended.")
  end
  
  local arg = Trim(wildcards[1])
  if arg == "" then
    say(current_punctuation_level())
    list_options()
    return
  end
  arg = tonumber(arg)
  if (arg == nil) or (punc_descs[arg] == nil) then
    say("SAPI punctuation level must be an index number between 1 and 3.")
    list_options()
    return
  end
  punctuation_level = arg
  SetVariable("punctuation_level", tostring(punctuation_level))
  say("SAPI punctuation level set to "..tostring(punctuation_level)..". "..punc_descs[punctuation_level])
end

-- turn on speaking
function speak_on (name, line, wildcards)
  speak = true
  SetVariable("speak", speak and "1" or "0")
  say("SAPI auto speak now activated.")
end -- function speak_on

-- turn off speaking
function speak_off (name, line, wildcards)
  speak = false
  SetVariable("speak", speak and "1" or "0")
  speak_skip_all()
  say("SAPI auto speak now deactivated.")
end -- function speak_off

-- skip a sentence
function speak_skip (name, line, wildcards)
  talk:Skip("Sentence", 1)
end -- function speak_skip

function speak_skip_all (name, line, wildcards)
  talk:Speak(' ', SAPI_ENUMS.SpeechVoiceSpeakFlags.SVSFPurgeBeforeSpeak)
end

function speak_faster (name, line, wildcards)
  talk.Rate = talk.Rate + 1
  SetVariable("talk_Rate", talk.Rate)
  speak_test()
end

function speak_slower (name, line, wildcards)
  talk.Rate = talk.Rate - 1
  SetVariable("talk_Rate", talk.Rate)
  speak_test()
end

function speak_rate (name, line, wildcards)
  local input = Trim(wildcards[1])
  if input == "" then
    say("SAPI rate currently set to "..talk.Rate)
    return
  end
  if tonumber(input) then
    talk.Rate = tonumber(wildcards[1])
    SetVariable("talk_Rate", talk.Rate)
    speak_test()
  else
    say("Invalid SAPI rate \""..wildcards[1].."\"")
  end
end

western_happy_faces = "[8:]%-?[D%)%]]" -- arbitrary heuristics
western_sad_faces = "[8:;]%-?[%(%[]" -- arbitrary heuristics
another_sad_face = "%-_%-" -- arbitrary heuristics
another_happy_face = "%^_%^" -- arbitrary heuristics
western_wink_faces = ";%-?[D%)%]]" -- arbitrary heuristics
western_tongue_faces = "[8:;]%-?[pP9b]" -- arbitrary heuristics
remove_two_or_more_of_these = "[%-%+%^#$~><*`_]" -- arbitrary heuristics
remove_three_or_more_of_these = "[%p\\/|_%-%(%)%[]{}%%%+%^#%$~><*`_]" -- arbitrary heuristics
silent_if_only_these = "[%p%s]+" -- arbitrary heuristics
two_or_more_pattern = remove_two_or_more_of_these:rep(2).."+" -- arbitrary heuristics
three_or_more_pattern = remove_three_or_more_of_these:rep(3).."+" -- arbitrary heuristics
pad_left_braces = "([%[%(%{])" -- arbitrary heuristics
pad_right_braces = "([%]%}%)])" -- arbitrary heuristics
function filter_more_punctuation(msg) -- uses arbitrary heuristics
  msg = msg:gsub(western_happy_faces, ", smiley. "):gsub(western_sad_faces, ", sad-face. "):gsub(western_wink_faces, ", winks. "):gsub(western_tongue_faces, ", sticks-tongue-out. "):gsub(another_sad_face, ", sad-face. "):gsub(another_happy_face, ", smiley. "):gsub(three_or_more_pattern, " "):gsub(two_or_more_pattern, " "):gsub("|", " "):gsub(pad_left_braces, " %1"):gsub(pad_right_braces, "%1 ")
  if msg:gsub(silent_if_only_these, "") == "" then
    return ""
  else
    return msg
  end
end

function speak_choose_voice (name, line, wildcards)
  local new_voice = nil
  local param = Trim(wildcards[1])
  local voice_number = tonumber(param)
  if voice_number~=nil and voice_number >= 1 and voice_number <= NUM_SAPI_VOICES and talk:GetVoices():Item(voice_number-1).ID ~= "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\SampleTTSVoice" then
    current_voice_index = voice_number-1
    new_voice = talk:GetVoices():Item(current_voice_index)
  else
    if param ~= "" then
      say(param.." is not a valid SAPI voice number.")
    else
      say("The SAPI voice command must be given a number corresponding to the desired voice.")
    end
    list_voices()
    return
  end
  
  talk:setVoice(new_voice) -- luacom can't do `talk.Voice =`
  SetVariable("talk_Voice_ID", talk.Voice.ID)
  speak_test()
end

function speak_test (name, line, wildcards)
  say("SAPI speech is set to voice "..tostring(current_voice_index+1)..", "..talk.Voice:GetDescription()..", speaking at rate " .. tostring(talk.Rate))
end

function list_voices (name, line, wildcards)
  local enumerate_voices = luacom.GetEnumerator(talk:GetVoices())
  local voice = enumerate_voices:Next()
  local i = 0
  while voice do
    if voice.ID ~= "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft\\Speech\\Voices\\Tokens\\SampleTTSVoice" then
      talk:setVoice(voice)
      say("Voice number "..tostring(i+1).." is "..voice:GetDescription())
    end
    i = i + 1
    voice = enumerate_voices:Next()
  end
  talk:setVoice(talk:GetVoices():Item(current_voice_index))
  say("Type sapi voice [number] to set the desired SAPI voice.")
end

-- installation
function OnPluginInstall ()

  -- load Lua COM
  require "luacom"

  -- Instantiate a SAPI voice obejct
  talk = assert(luacom.CreateObject("SAPI.SpVoice"), [[
  Cannot open SAPI.
  Note for Linux and Mac OS users.
  Text to speech functionality depends on the Microsoft Speech API.
  This is not included by default in Wine, and SpeechSDK51.exe must be separately installed.
  You can download it from http://download.microsoft.com/download/B/4/3/B4314928-7B71-4336-9DE7-6FA4CF00B7B3/SpeechSDK51.exe
  ]])

  SAPI_ENUMS = luacom.GetTypeInfo(talk):GetTypeLib():ExportEnumerations()
  NUM_SAPI_VOICES = talk:GetVoices().Count
  assert(NUM_SAPI_VOICES > 0, "No SAPI voices found.")

  current_voice = GetVariable("talk_Voice_ID") or talk.Voice.ID

  -- find index of saved voice
  current_voice_index = 0
  local enumerate_voices = luacom.GetEnumerator(talk:GetVoices())
  local voice = enumerate_voices:Next()
  local i = 0
  while voice do
    if voice.ID == current_voice then
      current_voice_index = i
      break
    end
    i = i + 1
    voice = enumerate_voices:Next()
  end

  talk:setVoice(voice) -- luacom can't do `talk.Voice =`
  talk.Rate = GetVariable("talk_Rate") or talk.Rate
  speak = ((GetVariable("speak") or "1") == "1")
  punctuation_level = tonumber(GetVariable("punctuation_level"))
  if punc_descs[punctuation_level] == nil then
    punctuation_level = 3
  end
  show_spoken_lines = false
  EnableGroup("speech", true)

  -- Method call
  say("SAPI Plug-in installed and is ready")
  say(current_punctuation_level())
end -- function OnPluginInstall

-- here to draw the screen
function OnPluginScreendraw (t, log, line)

  if (t == 0 or t == 1) and speak then
    say(line, true)
  end -- if

end -- function

function NoteSilent(what)
   local prev_speak = speak
   speak = false
   Note(what)
   speak = prev_speak
end

-- Speak from other scripts
-- eg. CallPlugin ("463242566069ebfd1b379ec1", "say", "What to say")
function say (what, force_debug_off)

  -- check loaded
  if not talk then
    return false -- failure
  end -- if not installed

  if show_spoken_lines and not force_debug_off then
    local escaped_what = what:gsub("\\","\\\\"):gsub("\"","\\\"")
    -- this uses DoAfterSpecial instead of Note directly, because of http://www.gammon.com.au/forum/?id=12915&reply=38#reply38
    DoAfterSpecial(0.1, "CallPlugin (\"463242566069ebfd1b379ec1\", \"NoteSilent\", \""..escaped_what.."\")", 12)
  end
  if punctuation_level == 1 then
     talk:Speak(what, SAPI_ENUMS.SpeechVoiceSpeakFlags.SVSFlagsAsync + SAPI_ENUMS.SpeechVoiceSpeakFlags.SVSFNLPSpeakPunc)
  elseif punctuation_level == 2 then
     talk:Speak(what, SAPI_ENUMS.SpeechVoiceSpeakFlags.SVSFlagsAsync)
  else
     local cleaned_speech = filter_more_punctuation(what)
     if cleaned_speech ~= "" then
        talk:Speak(cleaned_speech, SAPI_ENUMS.SpeechVoiceSpeakFlags.SVSFlagsAsync)
     end
  end

  return true -- OK
end -- function say

]]>
</script>

</muclient>
