A few days ago, someone on Twitter humorously lamented the fact that I expected them to actually read a blog post. After the laughter subsided I thought, well why does he have to? Perhaps I can make it easier for him. Plus I needed something fun for today. So I put together a PowerShell function I call Invoke-BlogReader which I think you'll have fun playing with. It isn't 100% perfect and as with most Friday Fun posts, serves more as an educational device than anything.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
The function uses the .NET System.Speech class which seems to be a bit easier to use than legacy COM alternative. Here's a snippet you can test.
Add-Type -AssemblyName System.speech
$voice = New-Object System.Speech.Synthesis.SpeechSynthesizer
$voice.Speak("Hello. I am $($voice.voice.Name)")
So basically, all I have to do is get the contents of a blog article and pass the text to the voice object. That is a bit easier said than done which you'll see as you look through this code.
#Requires -version 3.0
Function Invoke-BlogReader {
[cmdletbinding()]
Param(
[Parameter(Position=0,Mandatory,HelpMessage="Enter the blog post URL")]
[ValidateNotNullorEmpty()]
[string]$Url,
[ValidateScript({$_ -ge 1})]
[int]$Count = 5,
[switch]$ShowText,
[validateSet("Male","Female")]
[string]$Gender="Male"
)
Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"
Try {
Write-Verbose "Requesting $url"
$web = Invoke-WebRequest -Uri $url -DisableKeepAlive -ErrorAction Stop
Write-Verbose "Selecting content"
$content = $web.ParsedHtml.getElementsByTagName("div") |
where {$_.classname -match'entry-content|post-content'} |
Select -first 1
}
Catch {
Write-Warning "Failed to retrieve content from $url"
#bail out
Return
}
if ($content) {
#define a regex to match sentences
[regex]$rx="(\S.+?[.!?])(?=\s+|$)"
Write-Verbose "Parsing sentences"
$sentences = $rx.matches($content.innertext)
Write-Verbose "Adding System.Speech assembly"
Add-Type -AssemblyName System.speech
Write-Verbose "Initializing a voice"
$voice = New-Object System.Speech.Synthesis.SpeechSynthesizer
$voice.SelectVoiceByHints($gender)
Write-Verbose "Speaking"
$voice.Speak($($web.ParsedHtml.title))
$voice.speak("Published on $($web.ParsedHtml.lastModified)")
Write-Verbose "Here are the first $count lines"
for ($i=0 ;$i -lt $count;$i++) {
$text = $sentences[$i].Value
if ($ShowText) {
write-host "$text " -NoNewline
}
$voice.speak($text)
}
write-host "`n"
}
else {
Write-Warning "No web content found"
}
#clean up
$voice.Dispose()
Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
} #end function
The function uses the Invoke-WebRequest cmdlet to retrieve the content from the specified web page and I then parse the HTML looking for the content.
$web = Invoke-WebRequest -Uri $url -DisableKeepAlive -ErrorAction Stop
Write-Verbose "Selecting content"
$content = $web.ParsedHtml.getElementsByTagName("div") |
where {$_.classname -match'entry-content|post-content'} |
Select -first 1
This is the trickiest part because different blogs and sites use different tags and classes. There is a getElementsbyClassName method, but that seems to be hit and miss for me, so I've opted for the slower but consistent process of using Where-Object.
Once I have the content, I could simply pass the text, but I realized I may not want to listen to the entire post. Especially for my own which often have script samples. Those aren't very pleasant to listen to. So I realized I needed to parse the content into sentences. Regular expressions to the rescue.
[regex]$rx="(\S.+?[.!?])(?=\s+|$)" Write-Verbose "Parsing sentences" $sentences = $rx.matches($content.innertext)
Don't ask me to explain the regex pattern. I "found" it and it works. That's all that matters. $Sentences is an array of regex match objects. All I need to do is pass the value from each match to the voice object. The other benefit is that I can include a parameter to also display the text as it is being read.
for ($i=0 ;$i -lt $count;$i++) {
$text = $sentences[$i].Value
if ($ShowText) {
write-host "$text " -NoNewline
}
$voice.speak($text)
}
That's all there is to it! If you want to try out all of the options here's a sample command:
$paramHash = @{
Url = "http://blogs.msdn.com/b/powershell/archive/2014/08/27/powershell-summit-europe-2014.aspx"
Count = 5
Gender = "Female"
ShowText = $True
Verbose = $True
}
Invoke-BlogReader @paramHash
Where this can get really fun is using another function, which I'm not sure I ever posted here, to get items from an RSS feed.
#requires -version 3.0
Function Get-RSSFeed {
Param (
[Parameter(Position=0,ValueFromPipeline=$True,ValueFromPipelineByPropertyName=$True)]
[ValidateNotNullOrEmpty()]
[ValidatePattern("^http")]
[Alias('url')]
[string[]]$Path="http://feeds.feedburner.com/JeffsScriptingBlogAndMore"
)
Begin {
Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"
} #begin
Process {
foreach ($item in $path) {
$data = Invoke-RestMethod -Uri $item
foreach ($entry in $data) {
#create a custom object
[pscustomobject][ordered]@{
Title = $entry.title
Published = $entry.pubDate -as [datetime]
Link = $entry.origLink
} #hash
} #foreach entry
} #foreach item
} #process
End {
Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
} #end
} #end Get-RSSFeed
Putting it all together you can get the feed, pipe it to Out-Gridview, select one and have the blog read to you!
get-rssfeed | out-gridview -OutputMode Single | foreach { Invoke-BlogReader $_.link -Count 5}
So now you can have your blog and listen to! There are probably a number of ways this could be enhanced. Or perhaps you want to take some of these concepts and techniques in another direction. If so, I hope you'll let me know where you end up. Have a great weekend.

The wife had a blast as I started making the computer read off things to her, like $voice.Speak(“What’s for dinner!!”)
She asks; can you make it do the dishes!? 😀
Very nice – I think I will wrap in a form with little buttons to click. I can then stage up a number of bogs articles and listen while making dinner or exercising.
Thanks.