An Interesting Opportunity
Voice recognition and text to speech technologies make a good combo for future user interfaces. Imagine browsing the web by voice and listening to blogs like you listen to podcasts. Your mobile phone will be able to provide these features in the near future. Voice web browsing is not only useful for the visually impaired, but also for users who wish to do other things while surfing the web. Voice recognition and text to speech feature prominently on the iPhone 4S, but voice web browsing is not incorporated.
This blog post includes code which allows you to extract the main text content from a web page and convert it to a playable audio file. The process is triggered by simple voice commands such as “receive hacker news”, “read article 7” or “save article 19”. The resulting audio file can also be synchronized to other services such as DropBox, Amazon Cloud Drive, and Apple iCloud to be played later. We use IKVM to allow us to run the boilerpipe library, which is written in Java, over .NET. We choose .NET because it comes with “batteries included”: ready to use voice recognition and text to speech capabilities.
The present article demonstrates the application of main text content extraction. For methods of MTCE see our article: Extraction of Main Text Content Using the Google Reader NoAPI. For further exploration of voice recognition and text to speech .NET capabilities consult the following links:
- .NET Framework System.Speech.Recognition Namespace
- StackOverflow Q&A about TTS under .NET
- StackOverflow Q&A about voice recognition under .NET
Good voice recognition and text to speech systems are expensive and require training. Companies which provide these services do not offer a more granular way to access a web service or run a local engine. For example, AT&T Natural Voice for TTS sounds good, but their licensing terms are prohibitive for small companies and startups. On the voice recognition side of the equation, Nuance has been accumulating patents to strengthen their market position, making it difficult for others to compete. Although many other companies offer voice recognition, most of them actually use Nuance’s technology. See for example: Siri, Do You Use Nuance Technology? Siri: I’m Sorry, I Can’t Answer That. Voice recognition systems require training to improve their accuracy. SpinVox, a Nuance subsidiary, used “conversion experts”. They built teams which listened to audio messages and manually converted them to text. If you want to use this approach, you’ll need to wait for Amazon’s Mechanical Turk to offer micro jobs in real time.
What is missing here? None of the leading companies offer good quality voice recognition on a charge per use basis. Google seems to be actively researching voice recognition, and has achieved impressive results with “experimental” speech recognition technology. Sadly, Google’s voice recognition and text to speech APIs can not be used to develop all desktop and server applications. Their use is restricted to Android phones, Chrome’s beta html5 support, and Chrome’s extensions. It would be nice for Google to remove this restriction and include this service on their web APIs Console.
Our code demonstrates that it is possible to use voice recognition and text to speech while avoiding the licensing, patent and API conundrum.
Using VR and TTS under .NET
Prerequisites
If you use our VoiceWebBrowsing code, also available on GitHub, you will just need Microsoft Visual Studio 2010. However if there is a new version of boilerpipe you will have to generate the boilerpipe .NET assemblies yourself as follows:
- Have Microsoft Visual Studio 2010
- Download boilerpipe from http://code.google.com/p/boilerpipe/
- Download and install IKVM from http://www.ikvm.net/
- Run boilerpipe library and dependencies through ikvmc: ikvmc -nojni -target:library boilerpipe-1.2.0.jar lib\nekohtml-1.9.13.jar lib\xerces-2.9.1.jar
- Use the resulting boilerpipe-1.2.0.dll .NET assembly from ikvmc.
Code
using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; using System.Speech.Recognition; using System.Speech.Synthesis; using System.Speech.AudioFormat; using System.Net; using System.IO; using System.Xml; namespace VoiceWebBrowsing { public partial class MainForm : Form { #region Constants //const string _bogusRSSFeed = "<items><item><title>First title</title><link>http://</link></item><item><title>Second title</title><link>http://</link></item></items>"; const string _bogusRSSFeed = null; const string downloadPath = @"..\..\..\..\Download"; #endregion #region Private Variables SpeechRecognizer _speechRecognizer = new SpeechRecognizer(); SpeechSynthesizer _ttsVoice = new SpeechSynthesizer(); SpeechAudioFormatInfo formatInfo = new SpeechAudioFormatInfo(8000, AudioBitsPerSample.Sixteen, AudioChannel.Mono); Queue<string> _queue = new Queue<string>(); List<string> articleList = new List<string>(); HashSet<SpeechSynthesizer> tts2FileTasks = new HashSet<SpeechSynthesizer>(); #endregion #region Private Methods private void InitGrammar() { GrammarBuilder readGrammar = new Choices(new string[] { "read article" }); Choices articleChoice = new Choices(); for (int i = 1; i <= 30; i++) { articleChoice.Add(i.ToString()); } readGrammar.Append(articleChoice); GrammarBuilder saveGrammar = new Choices(new string[] { "save article" }); saveGrammar.Append(articleChoice); GrammarBuilder otherGrammar = new Choices(new string[] { "receive hacker news", "stop", "test" }); //GrammarBuilder commands = new Choices(new string[] { "receive hacker news", "stop", "test" }); Choices commands = new Choices(); commands.Add(new Choices(new GrammarBuilder[] { readGrammar, saveGrammar, otherGrammar })); var grammar = new Grammar(commands); this._speechRecognizer.LoadGrammar(grammar); } private void Say(string text) { this._ttsVoice.SpeakAsync(text); } private void ReadHackerNewsFeed() { string hackerNewsRSSUrl = "http://news.ycombinator.com/rss"; using (WebClient client = new WebClient()) // WebClient class inherits IDisposable { // // an app.config is added to surpress: The server committed a protocol violation. Section=ResponseStatusLine // string rssXmlStr = null; if (_bogusRSSFeed == null) rssXmlStr = client.DownloadString(hackerNewsRSSUrl); else rssXmlStr = _bogusRSSFeed; XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml(rssXmlStr); XmlNodeList items = xmlDoc.SelectNodes("//item"); int counter = 1; articleList.Clear(); foreach (XmlNode item in items) { string title = item.SelectSingleNode("title").InnerText; string link = item.SelectSingleNode("link").InnerText; articleList.Add(link); Say("article " + counter.ToString() + " " + title); System.Diagnostics.Debug.WriteLine(title); counter++; } } } private void ReceiveHackerNewsButton_Click(object sender, EventArgs e) { ReceiveHackerNewsCommand(); } private void SaveArticle(string link, string article) { SpeechSynthesizer tts2File = new SpeechSynthesizer(); tts2File.SpeakStarted += new EventHandler<SpeakStartedEventArgs>(tts2File_SpeakStarted); tts2File.SpeakCompleted += new EventHandler<SpeakCompletedEventArgs>(tts2File_SpeakCompleted); System.Security.Cryptography.SHA1Managed hashAlgorithm = new System.Security.Cryptography.SHA1Managed(); hashAlgorithm.Initialize(); byte[] buffer = Encoding.UTF8.GetBytes(link); byte[] hash = hashAlgorithm.ComputeHash(buffer); string fileName = BitConverter.ToString(hash).Replace("-", string.Empty) + ".wav"; string executionPath = System.Reflection.Assembly.GetExecutingAssembly().Location; string fullPath = Path.Combine(executionPath, downloadPath, fileName); hashAlgorithm.Clear(); tts2File.SetOutputToWaveFile(fullPath, formatInfo); tts2File.SpeakAsync(article); this.tts2FileTasks.Add(tts2File); } #endregion #region Constructor public MainForm() { InitializeComponent(); _speechRecognizer.Enabled = true; _speechRecognizer.SpeechRecognized += new EventHandler<SpeechRecognizedEventArgs>(_speechRecognizer_SpeechRecognized); } #endregion #region Events void _ttsVoice_SpeakCompleted(object sender, SpeakCompletedEventArgs e) { this._ttsVoice.SetOutputToNull(); // Needed for flushing file buffers. } void _speechRecognizer_SpeechRecognized(object sender, SpeechRecognizedEventArgs e) { string command = e.Result.Text; CommandTextBox.Text = command; if (command == "test") { return; } if (command == "stop") { StopCommand(); return; } if (command == "receive hacker news") { ReceiveHackerNewsCommand(); return; } if (command.Contains("read article")) { string[] words = command.Split(' '); ReadArticleCommand(Decimal.Parse(words[2])); return; } if (command.Contains("save article")) { string[] words = command.Split(' '); SaveArticleCommand(Decimal.Parse(words[2])); return; } } private void MainForm_Load(object sender, EventArgs e) { InitGrammar(); BackgroundWorker.RunWorkerAsync(); } void tts2File_SpeakStarted(object sender, SpeakStartedEventArgs e) { } void tts2File_SpeakCompleted(object sender, SpeakCompletedEventArgs e) { SpeechSynthesizer tts2File = (SpeechSynthesizer)sender; tts2File.SetOutputToNull(); this.tts2FileTasks.Remove(tts2File); } private void StopButton_Click(object sender, EventArgs e) { StopCommand(); } private void ReadArticleButton_Click(object sender, EventArgs e) { ReadArticleCommand(ArticleNumberUpDown.Value); } private void SaveArticleButton_Click(object sender, EventArgs e) { SaveArticleCommand(ArticleNumberUpDown.Value); } private void BackgroundWorker_DoWork(object sender, DoWorkEventArgs e) { while (true) { System.Threading.Thread.Sleep(125); lock(this) { if(this._queue.Count > 0) { string cmd = this._queue.Dequeue(); if (cmd != null) { System.Uri uri = new Uri(cmd); if (uri.Scheme == "voicewebbrowsing") { if (uri.Host == "receivehackernews") { ReadHackerNewsFeed(); } else if (uri.Host == "stop") { this._ttsVoice.SpeakAsyncCancelAll(); } else if (uri.Host == "readarticle" || uri.Host == "savearticle") { string articleNumberStr = System.IO.Path.GetFileName(uri.AbsolutePath); int articleNumber = int.Parse(articleNumberStr); if (articleNumber > articleList.Count) { Say("please retrieve hacker news articles first"); } else { articleNumber--; // 0-based index string link = articleList[articleNumber]; java.net.URL url = new java.net.URL(link); string article = de.l3s.boilerpipe.extractors.ArticleExtractor.INSTANCE.getText(url); if (uri.Host == "readarticle") { Say(article); } else if (uri.Host == "savearticle") { SaveArticle(link, article); } } } } } } } } } #endregion #region Commands private void ReceiveHackerNewsCommand() { StopCommand(); lock (this) { this._queue.Enqueue("voicewebbrowsing://receivehackernews"); } } private void StopCommand() { lock (this) { this._queue.Enqueue("voicewebbrowsing://stop"); } } private void ReadArticleCommand(Decimal article) { StopCommand(); lock (this) { this._queue.Enqueue(String.Format("voicewebbrowsing://readarticle/{0}", article.ToString())); } } private void SaveArticleCommand(decimal article) { lock (this) { this._queue.Enqueue(String.Format("voicewebbrowsing://savearticle/{0}", article.ToString())); } } #endregion } }
See Also
Where can you go from here?
- You can write a continuous Hacker News front page reader which constantly checks the feed and reads you new titles.
- You can write a voice oriented mobile web browser for Windows Phone, Google Android, and for Google Chrome using their APIs. To write a mobile browser for iPhones, you will have to wait for iOS Siri API.
- You can write a cloud service to provide a web page to a podcast converter service, store the audio file in the cloud, and automatically convert Google Reader’s starred items.
- Finally, you can research how to improve TTS and VR technologies.
Resources
- Dragon Speech Recognition Software
- Publications by Googlers in Speech Processing
- Patent case seeks to silence Nuance voice recognition
- Nuance Loses First Patent Fight with Vlingo, Others to Follow
- eSpeak: Open Source Text to Speech
- CMU Sphinx: Speech Recognition Open Source Toolkit
- SAM: The First Commercial voice synthesis program for Commodore 64, Apple and Atari computers.
- Siri
- Microsoft Tellme
- The Mobile Challenge: My Personal Rants
- List of speech recognition software
- What is the difference between System.Speech.Recognition and Microsoft.Speech.Recognition?
- Siri for everyone, with Pioneer’s Zypr API
- Reverse Engineering and Cracking Apple Siri with SiriProxy
Had you attempted a port of BoilerPipe ? Its on my list of things to do in the new year. There was a previous attempt by https://github.com/Soliah but the code was unfortunately lost/deleted.
Happily you don’t need to port it. Boilerpipe can be used under .NET “via” IKVM. This article explains how to do that.
Understood. I just prefer not to incur the weight of IKVM.
In that case a pure .NET solution is needed, I don’t know a .NET content extraction library with the quality of boilerpipe.
If the IKVM issue is related with performance an obvious alternative is to run Java and .NET and communicate both processes.
Today has been my first day with Eclipse and Sharpen… (I think too much for one day). I downloaded the latest svn code from boilerpipe that includes the ImageExtractor, I compiled it to the jar and then I Ikvmc as you great comment in the article. The issue is that in the Xamarin Android Project when I try to compile appears. System.IO.FileNotFoundException: Could not load assembly ‘System.Drawing. How can I know which class makes this dependency? After all the effort and now stucked with Windows forms libraries. Can you give me some advice?