Build a Speech Recognition Stock Quote App
with .NET and SAPI 5.1

By Peter A. Bromberg, Ph.D.
Printer - Friendly Version

Peter Bromberg

SAPI 5.1 comes with several samples for the .NET platform that use the SpechLib COM wrapper type library. While playing around with one of these and some information in Access databases, I realized it would be fairly trivial to create a speech - enabled Stock quote app using some code I had written for a previous article, "Screenscraping RealTime Quotes".



All I needed was a database of all the stock symbols and company names so that I could take the RecoContext_Recognition result phrase and use it in a simple SQL Statement to lookup a valid stock symbol from either a company name or spelled stock symbol uttered by the user. Then I could get the quote from MSN or Yahoo very easily, present it in a textbox, and have SAPI speak the information to the user.

After searching the .NET for a while, I finally landed on a web page that had a listing of all the US Stock symbols and their respective company names. While not perfect, it was "good enough", so I prepared it for import into an Access database by cleaning up all the rows, putting in delimiters, and so on, in order to be able to bring it into MSAcess correctly.

With that completed, all I really needed to do was some preprocessing cleanup such as removing spaces and punctuation so the database record would be found, and I had an app that would let you speak a company name or symbol. If the match is found in the database, we use the code from my Screenscraping article to go and retrieve the current realtime stock quote info from Yahoo finance, and speak it back to the user. I present the key methods below, and you can download the entire app and source code including the Access database of all US stock symbols below.

Before I get started with this, there is one caveat: You must have the SAPI 5.1 runtime installed for this to work. The only way to distribute SAPI 5.1 with your app is to include the SAPI MSM merge modules, and that's just a little too "large" for an article with a downloadable installer file here. So, no SAPI, no speakie! To get SAPI 5.1 , go here.

The key method we use to capture the recognition event and kick off the SQL query is as follows:
public void RecoContext_Recognition(int StreamNumber, 
            object StreamPosition, 
            SpeechRecognitionType RecognitionType,
            ISpeechRecoResult Result)
{
    string word=Result.PhraseInfo.GetText(0, -1, true);
    if(word.ToUpper()=="CLEAR") 
    {
        recognition_textbox.Clear();
        return;
    }        
DisableSpeechRecognition();
SpeakText(word + "  " + "One moment please.");
 recognition_textbox.AppendText(GetStockQuote(word));
SpeakText(recognition_textbox.Text);
EnableSpeechRecognition();

}        

You can see that when we have a recognition event, we use the Result.PhraseInfo.GetText method to isolate the words the user has uttered as the SAPI engine understands it / them. We fire the DisableSpeechRecognition method to "stop listening" so we don't get any more garbage while we look up the stock symbol, and then we SpeakText the formatted stock quote results from the GetStockQuote method.

GetStockQuote, which also calls GetQuote, looks like this:
 private string GetStockQuote (string word)
 {
   string path=Environment.CurrentDirectory ;
    string strConn="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" +path+ "\\" +"stocks.mdb" ;
    OleDbConnection myConn = new OleDbConnection(strConn) ;
    //Make a Select Command
         string result=String.Empty;
    try
    {
    string strCom = "Select * from Companies where name like '" + word  +"%' or symbol ='" 
+ word + "'"; OleDbCommand myCommand =new OleDbCommand(strCom,myConn); myConn.Open(); System.Data.OleDb.OleDbDataReader reader; //Execute the command and get the Data into "reader" reader=myCommand.ExecuteReader( ) ; // for now, we'll just do the first record. //while(reader.Read()) reader.Read(); { result += reader.GetValue(1).ToString() + " " + GetQuote(reader.GetValue(0).ToString()); } } catch (Exception ex) { SpeakText("There was an error. Please try again."); } return result + "\n\r"; }

Finally, "GetQuote" is my custom code to go to Yahoo and bring back the information into an XmlDocument:

public string GetQuote(string sSymbol)
{
XmlDocument xmlDoc=GetXmlYahoo(sSymbol);        
string  strSymbol = xmlDoc.SelectSingleNode("//StockQuotes/Quote/Symbol").InnerText ;
string  strPrice = xmlDoc.SelectSingleNode("//StockQuotes/Quote/Price").InnerText ;
 string  strChange = xmlDoc.SelectSingleNode("//StockQuotes/Quote/Change").InnerText ;
string strTime = xmlDoc.SelectSingleNode("//StockQuotes/Quote/Time").InnerText ;
    if(Convert.ToDouble(strChange) >0)
    {
        strChange="up " + strChange;
    }
    else
    {
        strChange="down " + strChange;
    }

string strResult= " symbol " + strSymbol + " Current Trade Price " + strPrice + " Change " + 
strChange + " at " + strTime; return strResult; } public XmlDocument GetXmlYahoo(string symbolList ) { string url="http://finance.yahoo.com/q?s="; url+=symbolList; url+="&d=e"; WebRequest webRequest = WebRequest.Create(url); //this.url = url; string beginStr = ""; try { WebResponse webResponse = webRequest.GetResponse(); beginStr = new StreamReader(webResponse.GetResponseStream(), Encoding.Default).ReadToEnd(); webResponse.Close(); // clean up some YHOO finance "junk" first so Regex matches won't fail beginStr = beginStr.Replace("\n", ""); beginStr=beginStr.Substring(beginStr.IndexOf("Order Books")); beginStr=beginStr.Replace("<font color=ff0020>",""); beginStr=beginStr.Replace("</font></font>","</font>"); beginStr=beginStr.Replace("<b>","").Replace("</b>",""); beginStr=beginStr.Replace("<i>","").Replace("</i>",""); } catch (Exception) { beginStr = ""; } XmlDocument xmlDocument = new XmlDocument(); XmlElement elemQuotes = xmlDocument.CreateElement("StockQuotes"); xmlDocument.AppendChild(elemQuotes); // match string for our Regex Matches collection string mainStr =
"<td nowrap align=left><font face=arial size=-1><a href=\"(?<href>[^\"]+)\">
(?<symbol>[^<]+)</a></font></td><td nowrap align=center><font face=arial size=-1>
(?<time>[^<]+)</font></td><td nowrap><font face=arial size=-1>(?<price>[^>]+)
</font></td><td nowrap><font face=arial size=-1>(?<change>[^<]+)</font></td>"
; new Regex(mainStr, RegexOptions.Compiled); IEnumerator iEnumerator = Regex.Matches(beginStr, mainStr).GetEnumerator(); try { iEnumerator.MoveNext(); { Match match = (Match)iEnumerator.Current; XmlElement elemQuote = xmlDocument.CreateElement("Quote"); XmlElement elemSymbol = xmlDocument.CreateElement("Symbol"); XmlElement elemTime = xmlDocument.CreateElement("Time"); XmlElement elemPrice = xmlDocument.CreateElement("Price"); XmlElement elemChange = xmlDocument.CreateElement("Change"); elemSymbol.InnerText = match.Groups["symbol"].Value; elemPrice.InnerText = match.Groups["price"].Value.Replace(",", "."); elemTime.InnerText=match.Groups["time"].Value.Replace(",", "."); elemChange.InnerText = match.Groups["change"].Value.Replace(",", "."); elemQuote.AppendChild(elemSymbol); elemQuote.AppendChild(elemPrice); elemQuote.AppendChild(elemChange); elemQuote.AppendChild(elemTime); xmlDocument.DocumentElement.AppendChild(elemQuote); } } catch(Exception ex) { SpeakText("There was an error. Try Again please.");} return xmlDocument; }

The rest of the methods, which you can find in the downloadable Visual Studio.NET 2003 Solution, are utility methods and are more or less "self-documenting". While this is not feature complete (a more complete app would probably use a custom compiled grammar for recognition, rather than the Dictation class) its really not too bad for a first stab. Hope you enjoy it.

Download the code that accompanies this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform. Pete's samples at GotDotNet.com have been downloaded over 41,000 times. You can read Peter's UnBlog Here.  --><--NOTE: Post QUESTIONS on FORUMS!

Do you have a question or comment about this article? Have a programming problem you need to solve? Post it at eggheadcafe.com forums and receive immediate email notification of responses.