# encoding: ascii
# api: powershell
# title: Speech Recognition
# description: This is an update to my “Speech.psm1” script module for doing voice/speech recognition. With this version, speech macros will be executed asynchronously, so it doesn’t tie up the shell for the duration :) Of course, if the shell is actually BUSY, it will delay execution of macros.  See usage examples at the bottom of the script.
# version: 0.1
# type: module
# author: Joel Bennett
# license: CC0
# function: Update-SpeechCommands
# x-poshcode-id: 2671
# x-archived: 2017-03-18T12:51:32
# x-published: 2012-05-10T23:49:00
#
#
$null = [Reflection.Assembly]::LoadWithPartialName("System.Speech")

## Create the two main objects we need for speech recognition and synthesis

if(!$Global:SpeechModuleListener){ ## For XP's sake, don't create them twice...
   $Global:SpeechModuleSpeaker = new-object System.Speech.Synthesis.SpeechSynthesizer
   $Global:SpeechModuleListener = new-object System.Speech.Recognition.SpeechRecognizer
}

$Script:SpeechModuleMacros = @{}
## Add a way to turn it off
$Script:SpeechModuleMacros.Add("Stop Listening", { $script:listen = $false; Suspend-Listening })
$Script:SpeechModuleComputerName = ${Env:ComputerName}

function Update-SpeechCommands {
#.Synopsis 
#  Recreate the speech recognition grammar
#.Description
#  This parses out the speech module macros, 
#  and recreates the speech recognition grammar and semantic results, 
#  and then updates the SpeechRecognizer with the new grammar, 
#  and makes sure that the ObjectEvent is registered.
   $choices = new-object System.Speech.Recognition.Choices
   foreach($choice in $Script:SpeechModuleMacros.GetEnumerator()) {   
      $g = New-Object System.Speech.Recognition.GrammarBuilder
      $phrases = @($choice.Key -split "\s*\*\s*")
      for($i=0;$i -lt $phrases.Count;$i++) {
         if($phrases[$i].Length -gt 0) {
            $g.Append( $phrases[$i] )
            if($i+1 -lt $phrases.Count) {
               $g.AppendDictation()
            }
         } elseif($i -eq 0) {
            $g.AppendDictation()
         }
      }
      $choices.Add( (New-Object System.Speech.Recognition.SemanticResultValue $g, 
                     $choice.Value.ToString()).ToGrammarBuilder() )
   }

   if($VerbosePreference -ne "SilentlyContinue") { $Script:SpeechModuleMacros.Keys | ForEach-Object { Write-Host "$($Script:SpeechModuleComputerName), $_" -Fore Cyan } }

   $builder = New-Object System.Speech.Recognition.GrammarBuilder "$($Script:SpeechModuleComputerName), "
   $builder.Append((New-Object System.Speech.Recognition.SemanticResultKey "Commands", $choices.ToGrammarBuilder()))
   $grammar = new-object System.Speech.Recognition.Grammar $builder
   $grammar.Name = "Power VoiceMacros"

   ## Take note of the events, but only once (make sure to remove the old one)
   Unregister-Event "SpeechModuleCommandRecognized" -ErrorAction SilentlyContinue
   $null = Register-ObjectEvent $grammar SpeechRecognized `
               -SourceIdentifier "SpeechModuleCommandRecognized" `
               -Action { iex $event.SourceEventArgs.Result.Semantics.Item("Commands").Value }
   
   $Global:SpeechModuleListener.UnloadAllGrammars()
   $Global:SpeechModuleListener.LoadGrammarAsync( $grammar )
}

function Add-SpeechCommands {
#.Synopsis
#  Add one or more commands to the speech-recognition macros, and update the recognition
#.Parameter CommandText
#  The string key for the command to remove
   [CmdletBinding()]
   Param([hashtable]$VoiceMacros,[string]$Computer=$Script:SpeechModuleComputerName)
   
   ## Add the new macros
   $Script:SpeechModuleMacros += $VoiceMacros 
   ## Update the default if they change it, so they only have to do that once.
   $Script:SpeechModuleComputerName = $Computer 
   Update-SpeechCommands
}

function Remove-SpeechCommands {
#.Synopsis
#  Remove one or more command from the speech-recognition macros, and update the recognition
#.Parameter CommandText
#  The string key for the command to remove
   Param([string[]]$CommandText)
   foreach($command in $CommandText) { $Script:SpeechModuleMacros.Remove($Command) }
   Update-SpeechCommands
}

function Clear-SpeechCommands {
#.Synopsis
#  Removes all commands from the speech-recognition macros, and update the recognition
#.Parameter CommandText
#  The string key for the command to remove
   $Script:SpeechModuleMacros = @{}
   ## Default value: A way to turn it off
   $Script:SpeechModuleMacros.Add("Stop Listening", { Suspend-Listening })
   Update-SpeechCommands
}


function Start-Listening {
#.Synopsis
#  Sets the SpeechRecognizer to Enabled
   $Global:SpeechModuleListener.Enabled = $true
   Say "Speech Macros are $($Global:SpeechModuleListener.State)"
   Write-Host "Speech Macros are $($Global:SpeechModuleListener.State)"
}

function Suspend-Listening {
#.Synopsis
#  Sets the SpeechRecognizer to Disabled
   $Global:SpeechModuleListener.Enabled = $false
   Say "Speech Macros are disabled"
   Write-Host "Speech Macros are disabled"
}

function Suspend-Speech {
   $Global:SpeechModuleSpeaker.Pause()
}
function Resume-Speech {
   $Global:SpeechModuleSpeaker.Resume()
}

function Out-Speech {
#.Synopsis
#  Speaks the input object
#.Description
#  Uses the default SpeechSynthesizer settings to speak the string representation of the InputObject
#.Parameter InputObject
#  The object to speak 
#  NOTE: this should almost always be a pre-formatted string,
#        most objects don't render to very speakable text.
   Param( 
   [Parameter(Position=0)]
   [Object[]]
   $Property
,
   [Parameter()]
   [String]
   $Expand
,
   [Parameter()]
   [Object]
   $GroupBy
,
   [Parameter(ValueFromPipeline=$true)]
   [Alias("IO")]
   [PSObject]$InputObject
,
   [Parameter()]
   [String]
   $View
,
   [Parameter()]
   [Switch]
   $Async
   )
   begin {
      if($Async) {
         $PSBoundParameters.Remove("Async") | out-null
      }
      $InputCollection = new-object System.Collections.Generic.List[PSObject]
   }
   process {
      $InputCollection.Add($InputObject)
   }
   end {
      # $PSBoundParameters.Remove("InputObject") | out-null
      $PSBoundParameters["InputObject"] = $InputCollection
      
      if(!$Async) {
         # Write-Host $($PSBoundParameters | ft |out-string)
         # Write-Host $($InputCollection | ft |out-string) -fore Gray
         # Format-List @PSBoundParameters | Out-String -Stream | Write-Host -Fore Yellow
         
         Format-List @PSBoundParameters | ForEach-Object { 
            if($_.GetType().Name -eq "FormatEntryData") { $_; sleep -milli 500} else {$_}
         } | Out-String -stream | ForEach-Object {
            Write-Host $_
            $Global:SpeechModuleSpeaker.Speak($_)
         }
      } else {
         $speech = (Format-List @PSBoundParameters | Out-String -width 1e4) -replace "`r`n`r`n","`r`n`r`n`r`n"
         Write-Host $speech
         $Global:SpeechModuleSpeaker.SpeakAsync($speech) | Out-Null
      }
   }
}

function Remove-SpeechXP {
#.Synopis
#  Dispose of the SpeechModuleListener and SpeechModuleSpeaker
   $Global:SpeechModuleListener.Dispose(); $Global:SpeechModuleListener = $null
   $Global:SpeechModuleSpeaker.Dispose(); $Global:SpeechModuleSpeaker = $null
}

set-alias asc Add-SpeechCommands
set-alias rsc Remove-SpeechCommands
set-alias csc Clear-SpeechCommands
set-alias say Out-Speech
set-alias listen Start-Listening
Export-ModuleMember -Function * -Alias * -Variable SpeachModuleListener, SpeechModuleSpeaker

###################################################################################################
## USAGE EXAMPLES:
###################################################################################################
#  Add-SpeechCommands @{
#     "What time is it?" = { Say "It is $(Get-Date -f "h:mm tt")" }
#     "What day is it?"  = { Say $(Get-Date -f "dddd, MMMM dd") }
#     "What's running?"  = {
#        $proc = ps | sort ws -desc
#        Say $("$($proc.Count) processes, including $($proc[0].name), which is using " +
#              "$([int]($proc[0].ws/1mb)) megabytes of memory")
#     }
#  } -Computer "Laptop" -Verbose 
#
#  Add-SpeechCommands @{ "Run Notepad" = { & "C:\Programs\DevTools\Notepad++\notepad++.exe" } }
#  Add-SpeechCommands @{ "Check Gee Mail" = { Start-Process "https://mail.google.com" } }
#  Add-SpeechCommands @{ "Run Notepad" = { & "C:\Programs\DevTools\Notepad++\notepad++.exe" } }
