Download

{ 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.