Convert-TextToSpeech.ps1
function Convert-TextToSpeech { <# .SYNOPSIS Converts text to speech. Requires TTS engine (Windows) .DESCRIPTION Converts text to speech using any of the installed TTS voices. Speech is outputted to the default audio device unless a path to a wav file is specified. In this case, the voice is recorded to file. .EXAMPLE Convert-TextToSpeech -Culture en-us -Text 'this is english' Outputs English text .EXAMPLE Convert-TextToSpeech -Culture de-de -Text 'dies ist deutsch' Outputs German text (make sure the German TTS voice is installed. You may have to install the German voice pack in Windows. .EXAMPLE 'This is a test' | Convert-TextToSpeech -Culture en-us -OutputPath $env:temp\recording.wav -PassThru | Invoke-Item Record a file "recording.wav" in the temp folder, then play the file back in your default media player .EXAMPLE 1..10 | Convert-TextToSpeech -Culture en-us -Volume 100 Counts from 1 to 10 with an English voice .EXAMPLE 1..10 | Convert-TextToSpeech -Culture de-de -Volume 100 Counts from 1 to 10 with a German voice .EXAMPLE 1..10 | Convert-TextToSpeech -Volume 100 -Voice 'Microsoft Zira Desktop' Counts from 1 to 10 using the e-us female adult Zira voice (make sure the voice is installed, or choose a different voice) .EXAMPLE 1..10 | Convert-TextToSpeech -Culture en-us -Volume 100 -PassThru -OutputPath c:\counterFiles Create 10 files, named 1.wav to 10.wav, and store in c:\counterfiles. Output the generated files to the console. .EXAMPLE 1..4 | Convert-TextToSpeech -Culture en-us -Volume 100 -PassThru -OutputPath c:\counterFiles | Get-AudioFileInfo Create four voice recordings as wav files, and output the audio codec and bitrate details .EXAMPLE 1..3 | Convert-TextToSpeech -Culture en-us -PassThru -OutputPath { "c:\testFiles\{0:d3}.wav" -f $_ } -Text { "File $_ WAV Format" } | Foreach-Object { $_ | Invoke-Item; Start-Sleep -Seconds 2 } Creates three files named "001.wav" to "003.wav", which contain the english narration of "File xx WAV Format", where xx is a number between 1 and 10, and play the first 2 seconds of each file in your media player. .EXAMPLE 1..10 | Convert-TextToSpeech -Culture en-us -PassThru -OutputPath { "c:\testFiles\{0:d3}.wav" -f $_ } -Text { "File $_ WAV" } | Convert-AudioWavFile -PassThru | Get-AudioFileInfo Creates ten files named "001.wav" to "010.wav", which contain the english narration of "File xx WAV", where xx is a number between 1 and 10. The created audio files are then converted to "ADPCM IMA WAV" format which uses 1:4 compression and can be played back by simple MP3 players such as DFPlayer Mini. .EXAMPLE 1..10 | Convert-TextToSpeech -Culture en-us -PassThru -OutputPath { "c:\testFiles\{0:d3}.wav" -f $_ } -Text { "File $_ MP3" } | Convert-AudioWavFile -CreateMp3 -Force -PassThru | Get-AudioFileInfo Creates ten files named "001.wav" to "010.wav", which contain the english narration of "File xx MP3", where xx is a number between 1 and 10. The resulting files "001.wav" - "010.wav" are then converted to MP3 and renamed to "001.mp3" - "010.mp3" .EXAMPLE 1..10 | Convert-TextToSpeech -Culture en-us -PassThru -OutputPath { "c:\testFiles\{0:d3}.wav" -f $_ } -Text { "File $_ WAV" } | Convert-AudioWavFile -PassThru | Get-AudioFileInfo Creates ten files named "001.wav" to "010.wav" using the male en-us voice "David", which contain the english narration of "File xx WAV", where xx is a number between 1 and 10. The created audio files are then converted to "ADPCM IMA WAV" format which uses 1:4 compression and can be played back by simple MP3 players such as DFPlayer Mini. #> [CmdletBinding(DefaultParameterSetName='Audio')] param ( # Output Text [Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName,Position=0)] [string] $Text, # Output Volume (0 to 100, default: 100) [ValidateRange(0,100)] [int] $Volume = 100, # Output Speed (-10 to 10, default: 0) [ValidateRange(-10,10)] [int] $Speed = 0, # Output Culture (default: en-us). Available cultures depend on installed voices. [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $exists = Test-Path -Path 'variable:_availableCultures' if (!$exists) { Add-Type -AssemblyName System.Speech $speak = [System.Speech.Synthesis.SpeechSynthesizer]::new() # get installed voice cultures $script:_availableCultures = ($speak.GetInstalledVoices() | Where-Object Enabled | Select-Object -ExpandProperty VoiceInfo).Culture | Sort-Object -Property Name -Unique | Foreach-Object { # create completionresult items: $displayname = $_.DisplayName $id = $_.lcid $name = $_.name [System.Management.Automation.CompletionResult]::new($name, $name, "ParameterValue", "$displayName`r`nLCID: $id") } $speak.Dispose() } # return available cultures $script:_availableCultures | Where-Object { $_.ListItemText -like ($wordToComplete.Replace('"','').Replace("'",'') + '*') } })] [System.Globalization.CultureInfo] $Culture = 'en-us', # Voice to use for text synthesis. Make sure the voice you specify is installed. [ArgumentCompleter({ param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters) $exists = Test-Path -Path 'variable:_availableVoices' if (!$exists) { Add-Type -AssemblyName System.Speech $speak = [System.Speech.Synthesis.SpeechSynthesizer]::new() # get installed voices $script:_availableVoices = $speak.GetInstalledVoices() | Where-Object Enabled | Select-Object -ExpandProperty VoiceInfo $speak.Dispose() } # filter by Culture (if specified) $voices = if ($fakeBoundParameters.ContainsKey('Culture')) { $Script:_availableVoices | Where-Object Culture -eq $fakeBoundParameters['Culture'] } else { $Script:_availableVoices } $voices | Where-Object { $_.Name -like ($wordToComplete.Replace('"','').Replace("'",'') + '*') } | Foreach-Object { # create completionresult items: $gender = $_.Gender $age = $_.age $name = $_.name $hasSpecialChar = $name -match '[\s()\[\]"]' $nameQuoted = if ($hasSpecialChar) { "'{0}'" -f $Name } else { $name.Replace("'", "''") } $culture = $_.culture.DisplayName $description = $_.description $id = $_.id $tooltip = "$description`r`n$culture`r`n$age - $gender`r`n$id" [System.Management.Automation.CompletionResult]::new($nameQuoted, $name, "ParameterValue", $tooltip) } })] [string] $Voice, # optional: Path to a wav file to record the spoken text [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='File',Mandatory)] [string] $OutputPath, # emit the created audio files [Parameter(ValueFromPipelineByPropertyName,ParameterSetName='File')] [switch] $PassThru ) begin { Add-Type -AssemblyName System.Speech $recordingFormat = [System.Speech.AudioFormat.SpeechAudioFormatInfo]::new(16000, [System.Speech.AudioFormat.AudioBitsPerSample]::Sixteen, [System.Speech.AudioFormat.AudioChannel]::Mono) } process { $speak = [System.Speech.Synthesis.SpeechSynthesizer]::new() $speak.Rate = $Speed $speak.Volume = $Volume # set the selected voice if ($PSBoundParameters.ContainsKey('Voice')) { # a specific voice was selected. Make sure it exists: $voiceObj = $speak.GetInstalledVoices() | Where-Object { $_.Enabled -and $_.VoiceInfo.Name -eq $Voice } | Select-Object -First 1 if (!$voiceObj) { throw "voice '$Voice' not found. It may not be installed on your system. Select a different voice, or use the default voice." } # does voice match selected culture? if ($PSBoundParameters.ContainsKey('Culture')) { if ($Culture -ne $voiceObj.VoiceInfo.Culture) { Write-Warning ("Selected voice '{0}' is culture {1} but you specified culture {2}. Select a different voice if you want to output in culture {2}." -f $Voice, $Voice.culture.name, $Culture.Name) } } $speak.SelectVoice($Voice) } elseif ($PSBoundParameters.ContainsKey('Culture')) { # just a culture was specified, pick first voice that matches: $voiceObj = $speak.GetInstalledVoices($Culture) | Select-Object -First 1 if (!$voiceObj) { throw "No voice installed that matches culture '$Culture'. Select a different culture, or install the voice pack for the requested culture." } $speak.SelectVoice($voiceObj.VoiceInfo.Name) } $recordingPath = '' if ($PSBoundParameters.ContainsKey('OutputPath')) { # is the file a wav file? $extension = [System.IO.Path]::GetExtension($OutputPath).ToLower().Trim() $recordingPath = if ([string]::IsNullOrWhiteSpace($extension)) { # a folder path was specified # make sure the folder exists $outputFolder = $OutputPath.Trim() if ($outputFolder -eq '') { throw "An empty output path was specified." } $exists = Test-Path -Path $outputFolder if (!$exists) { $null = New-Item -Path $outputFolder -ItemType Directory } # the output file is the first 20 characters of the spoken text $maxLen = [Math]::Min($Text.Length, 20) $filename = $Text.Substring(0, $maxLen) + '.wav' Join-Path -Path $OutputPath -ChildPath $filename } elseif ($extension -eq '.wav') { # ensure parent path exists: $parentFolder = $OutputPath | Split-Path $exists = Test-Path -Path $parentFolder if (!$exists) { $null = New-Item -Path $parentFolder -ItemType Directory } $OutputPath } else { throw "Output path must be a *.wav file or an output folder. No other file extensions are supported." } # output to file $speak.SetOutputToWaveFile($recordingPath, $recordingFormat) } else { # output to standard audio $speak.SetOutputToDefaultAudioDevice() $recordingPath = '' } # perform the output $speak.Speak($Text) # release all resources, or else recorded files are locked and cannot be manipulated by following pipeline cmdlets $speak.Dispose() if ($PassThru) { Get-Item -Path $recordingPath } } end { $exists = Test-Path -Path 'variable:_availablevoices' if ($exists) { Remove-Variable -Name '_availablevoices' -scope script } $exists = Test-Path -Path 'variable:_availableCultures' if ($exists) { Remove-Variable -Name '_availableCultures' -scope script } } } |