{ This is a sample IVR application written in Delphi. Delphi IVR applications are implemented as DLLs exporting a function to handle a call, typically named Answer(). This one speaks the current weather condition to a caller who has identified the locality whose weather he wants by speaking the digits of its ZIP code. Please note that while the sample may not be considered short, that has to do with what the sample does and not the form of its user interface. To put that another way, the entirety of the voice user interface code is held in the short (3 dozen executable lines) subroutine named Answer() which is below. Mostly because we don't want to be tied to a specific version of MSXML and to a lesser extent because we are lazy, we use Automation (IDispatch) rather than early binding to access the XML control. } unit DelphiWeatherSRApp; interface uses ComObj, SysUtils, TClientCallImpl; function DoRequest(call : TClientCall; zip : string) : string; function TranslateXMLtoForecast(zip : string; xmlDoc : Variant) : string; function GetCity(xmlDoc : Variant) : string; function GetState(xmlDoc : Variant) : string; function GetWind(xmlDoc : Variant) : string; function GetOutlook(xmlDoc : Variant) : string; function GetTemperature(xmlDoc : Variant) : string; function ExpandState(abbrev : string) : string; function Direction(degrees : string) : string; procedure NoRecognition(rec : TRecognizer; syn : TSynthesizer); implementation { The data common to all instances of the application } const wait : string = 'Please wait ...'; prompt : string = 'Please tell me the ZIP code for the town whose weather forecast you want to hear.'; welcome : string = 'Welcome to the weather demonstration application. ' + 'Say good bye or hang up when you are done.'; regrets : string = 'We are sorry but we had trouble determining the weather for that ZIP code. ' + 'If the ZIP code is valid you can try again later.'; sayWhat : string = 'I''m sorry, but I didn''t get that.'; baseQuery : string = 'http://weather.yahooapis.com/forecastrss?p='; nameSpace : string = 'xmlns:yweather=''http://xml.weather.yahoo.com/ns/rss/1.0'''; { We use oddball numbers to make the point that grammar IDs are arbitrary } ZipGrammarId = 22; QuitGrammarId = 67; { In this application we use 65% as a confidence threshold for recognition. You can use whatever makes sense in your application. A more robust application might have two thresholds. Confidence scores below the lower number might cause the application to prompt again, confidence scores above the higher number might cause the application to accept the recognition, and scores between the two numbers might require confirmation } ReqdConfidence = 6500; { Each digit is a property of the whole 'phrase' to be recognized } properties : array[0..4] of string = ( 'Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5' ); { 50 states, DC and PR } states : array [0..51, 0..1] of string = ( ('AL', 'Alabama'), ('AK', 'Alaska'), ('AZ', 'Arizona'), ('AR', 'Arkansas'), ('CA', 'California'), ('CO', 'Colorado'), ('CT', 'Connecticut'), ('DE', 'Delaware'), ('DC', 'D C'), ('FL', 'Florida'), ('GA', 'Georgia'), ('HI', 'Hawaii'), ('ID', 'Idaho'), ('IL', 'Illinois'), ('IN', 'Indiana'), ('IA', 'Iowa'), ('KS', 'Kansas'), ('KY', 'Kentucky'), ('LA', 'Louisiana'), ('ME', 'Maine'), ('MD', 'Maryland'), ('MA', 'Massachusetts'), ('MI', 'Michigan'), ('MN', 'Minnesota'), ('MS', 'Mississippi'), ('MO', 'Missouri'), ('MT', 'Montana'), ('NE', 'Nebraska'), ('NV', 'Nevada'), ('NH', 'New Hampshire'), ('NJ', 'New Jersey'), ('NM', 'New Mexico'), ('NY', 'New York'), ('NC', 'North Carolina'), ('ND', 'North Dakota'), ('OH', 'Ohio'), ('OK', 'Oklahoma'), ('OR', 'Oregon'), ('PA', 'Pennsylvania'), ('PR', 'Puerto Rico'), ('RI', 'Rhode Island'), ('SC', 'South Carolina'), ('SD', 'South Dakota'), ('TN', 'Tennessee'), ('TX', 'Texas'), ('UT', 'Utah'), ('VT', 'Vermont'), ('VA', 'Virginia'), ('WA', 'Washington'), ('WV', 'West Virginia'), ('WI', 'Wisconsin'), ('WY', 'Wyoming') ); { This function is called once when the application is loaded. Any application-wide initialization steps can be taken here. If no initialization is required it may be removed. We show it here just for the sake of completeness. } function Initialize(shutdown : integer) : Boolean; stdcall; begin; result := true; end; { The required Answer() method always comprises the entire Voice User Interface for an IVR application. } procedure Answer(call : TClientCall); stdcall; var i : Integer; zip : string; rec : TRecognizer; syn : TSynthesizer; conf : Integer; done : Boolean; forecast : string; begin try { We want an exception if the user disconnects } call.hangupThrows(true); { Get an instance of the recognizer Load a grammar that handles the request for the ZIP code Load another that handles the request to quit the application } rec := call.getRecognizer(); rec.loadGrammar('ZIP', ZipGrammarId); rec.loadGrammar('Quit', QuitGrammarId); { Get an instance of the synthesizer and greet the caller We should check for a nil object which indicates failure } syn := call.getSynthesizer(); syn.speak(welcome); syn.wait(); done := false; while ( not call.isDisconnected() and not done ) do begin { Start listening first and then prompt the caller } rec.listen(); syn.speak(prompt); { Wait for the recognizer to tell us how it did. We will wait at most 10 seconds for the user to begin to speak. In no case will we wait more than 30 seconds. } conf := rec.wait(10000, 30000); { Stop listening } rec.stopListening(); { Dump the state of the recognition for debugging } rec.dump(); { On an error or disconnect we are done } if conf < 0 then done := true; { We need at least minimum confidence to proceed } if ( conf >= ReqdConfidence ) then begin { If we recognized a phrase in Quit grammar then we are done } if rec.getGrammarId() = QuitGrammarId then done := true { Otherwise we need a five digit ZIP code } else begin zip := ''; for i := 0 to 5 do zip := zip + rec.getPropertyText( properties[i] ); if Length(zip) <> 5 then conf := 0; end; end; { Apologize if we didn't understand } if (not done) and (conf < ReqdConfidence) then NoRecognition(rec, syn) { If all is well, we proceed } else if not done then begin { Tell the caller to be patient } syn.speak(wait); { Amuse him with "music on hold" } call.loopMessage('strumming'); { Get his forecast while he listens to music } forecast := DoRequest(call, zip); { Stop playing music and put a delay between the music and the speech } call.stopPlaying(); call.wait(500); { Speak the forecast } syn.speak(forecast); { Log the input and output to the call detail records table } call.cdrStatusMessage(0, zip); call.cdrStatusMessage(1, forecast); { Let him hear the forecast before we ask if he wants to go again } syn.wait(); end; end; { Be polite and say goodbye before hanging up } syn.speak('Good bye.'); syn.wait(); except { Nothing serious here; caller hung up } on t : TClientCallTermination do Writeln(t.Message); { This might just be serious } on e : Exception do Writeln(e.Message); end; end; { Access a web service to retrieve the weather for the ZIP code } function DoRequest(call : TClientCall; zip : string) : string; var query : string; xmlReq : Variant; xmlDoc : Variant; begin { Complete the query string and use XML HTTP to send it } query := baseQuery + zip; xmlReq := CreateOLEObject('Msxml2.XMLHTTP'); xmlReq.open('GET', query, false); xmlReq.send(); { Apologize if the web service did not return the weather } if xmlReq.Status <> 200 then begin result := regrets; { Purely for diagnostic purposes we display any failure code } Write('The weather service fails us: '); Write(xmlReq.statusText); Write(' ( '); Write(xmlReq.Status); WriteLn(' )'); end { On success we need to parse the XML } else xmlDoc := CreateOLEObject('Msxml2.DomDocument.4.0'); xmlDoc.async := false; xmlDoc.loadXML( xmlReq.responseText ); xmlDoc.setProperty('SelectionLanguage', 'XPath'); xmlDoc.setProperty('SelectionNamespaces', nameSpace); result := TranslateXMLtoForecast(zip, xmlDoc); end; { Crack open the XML document and build a two sentence forecast } function TranslateXMLtoForecast(zip : string; xmlDoc : Variant) : string; var city : string; wind : string; state : string; outlook : string; temperature : string; begin { For now we are interested in the city and state, wind speed and direction, current condition and temperature } city := GetCity(xmlDoc); state := GetState(xmlDoc); wind := GetWind(xmlDoc); outlook := GetOutlook(xmlDoc); temperature := GetTemperature(xmlDoc); { Build two sentences from the four components above } result := 'In ' + city + ' ' + state + ', the weather is ' + outlook + ' with a temperature of ' + temperature + '. The winds are ' + wind + '.'; end; { Pull the city from the document } function GetCity(xmlDoc : Variant) : string; var node : Variant; begin node := xmlDoc.selectSingleNode('//channel/yweather:location'); result := node.attributes.getNamedItem('city').text; end; { Pull the state from the document } function GetState(xmlDoc : Variant) : string; var node : Variant; temp : string; begin node := xmlDoc.selectSingleNode('//channel/yweather:location'); temp := node.attributes.getNamedItem('region').text; result := ExpandState(temp); end; { Pull the wind speed and direction from the document } function GetWind(xmlDoc : Variant ) : string; var speed : string; degrees : string; node : Variant; begin node := xmlDoc.selectSingleNode('//channel/yweather:wind'); speed := node.attributes.getNamedItem('speed').text; degrees := node.attributes.getNamedItem('direction').text; result := 'from the ' + Direction(degrees) + ' at ' + speed + ' miles per hour'; end; { Pull the current condition from the document } function GetOutlook(xmlDoc : Variant) : string; var node : Variant; begin node := xmlDoc.selectSingleNode('//channel/item/yweather:condition'); result := node.attributes.getNamedItem('text').text; end; { Pull the current temperature from the document } function GetTemperature(xmlDoc : Variant) : string; var node : Variant; temp : string; begin node := xmlDoc.selectSingleNode('//channel/item/yweather:condition'); temp := node.attributes.getNamedItem('temp').text; result := temp + ' degrees Fahrenheit'; end; { Expands a two-character abbreviation for states D.C. and PR to a full name } function ExpandState(abbrev : string) : string; var i : Integer; label found; begin result := abbrev; for i := 0 to 51 do begin if abbrev = states[i, 0] then begin result := states[i, 1]; goto found; end; end; found: end; { Translates a numeric wind heading to compass direction } function Direction(degrees : string) : string; var num : Integer; begin num := StrToInt(degrees); if num <= 11 then result := 'north' else if num <= 33 then result := 'north northeast' else if num <= 56 then result := 'northeast' else if num <= 78 then result := 'east northeast' else if num <= 101 then result := 'east' else if num <= 123 then result := 'east southeast' else if num <= 146 then result := 'southeast' else if num <= 168 then result := 'south southeast' else if num <= 191 then result := 'south' else if num <= 213 then result := 'south southwest' else if num <= 236 then result := 'southwest' else if num <= 258 then result := 'west southwest' else if num <= 281 then result := 'west' else if num <= 303 then result := 'west northwest' else if num <= 326 then result := 'northwest' else if num <= 348 then result := 'north northwest' else result := 'north'; end; { We apologize if we don't understand the user } procedure NoRecognition(rec : TRecognizer; syn : TSynthesizer); begin rec.reset(); rec.listen(); syn.speak(sayWhat); syn.wait(); end; exports { Export Initialize() and Answer() so that the server and simulator can find them } Initialize, Answer; end.