|
Speak-n-Listen Column
Q. I'm using the builtin date grammar. According to the VoiceXML 2.0 specification and my testing, the results I get back are in the format "yyyymmdd". If I output the result using the value element, the text to speech engine reads it as a number rather than as a date. For example, if I say "August 14th, 2002", the builtin date grammar returns "20020814", and the TTS engine reads this back as "twenty-million, twenty-thousand, eight-hundred and fourteen". How can I get the TTS engine to read this back as a date?
A: You have a few options:
- Use the Speech Synthesis Markup Language (SSML) say-as element.
- Parse the return value and translate it to the string form of a date.
- Use a pre-recorded audio library and map the return value to a sequence of audio elements.
The SSML solution is by far the simplest, but you must first determine if your VoiceXML interpreter supports the say-as element, described in section 2.1.4 of the Speech Synthesis Markup Language (SSML) specification ( http://www.w3.org/TR/speech-synthesis/#S2.1.4). The say-as element requires a type attribute where you specify the data type and optional format of the content contained by the element. To get a conforming VoiceXML interpreter to read back a date in a format "similar" to what the date grammar returns, you'd set the type attribute to "date:ymd".
<say-as type="date:ymd"><value expr="theDate"/></say-as>
To determine if your VoiceXML interpreter supports SSML say-as, try a simple example:
<vxml version="2.0" xmlns="http://www.w3.org/2002/vxml">
<form>
<block>
<var name="theDate" expr="'2002/08/14'"/>
<prompt>
<say-as type="date:ymd"><value expr="theDate"/></say-as>
</prompt>
</block>
</form>
</vxml> |
Observe that the SSML specification implies (from the included example) that the individual parts of the date must be separated by forward slashes. Thus, you'll need to perform a small transformation on the return value from the builtin grammar prior to outputting it using say-as. That transformation follows:
theDate.replace(/^(\d{4})(\d{2})(\d{2})$/, '$1/$2/$3')
Since the variable theDate stores a string, you can use the ECMAScript replace method to substitute a pattern with other characters. The pattern specified here as the first argument is a regular expression as indicated by the leading and trailing slashes ("/"), and it matches the beginning of the line (^) followed by four digits followed by two digits followed by another two digits followed by the end of the line ($). Each of the digit sequences are "captured" as indicated by the parenthesis. The ECMAScript interpreter makes these available in the second argument to the replace method via special variables "$n" where n corresponds to the order in which the capturing parentheses occur in the regular expression. In our pattern, $1 corresponds to the year, $2 to the month, and $3 to the day.
Regular expressions are a very powerful way to perform string processing, and ECMAScript provides a fairly robust implementation. You can learn more about the regular expression syntax supported by ECMAScript by reading section 15.10 of the ECMA-262 specification ( http://www.ecma.ch/ecma1/STAND/ECMA-262.HTM). If that's hard to digest (I think so), try Netscape's regular expression topic in the "Client-side JavaScript Guide" (http://developer.netscape.com/docs/manuals/js/client/jsguide/regexp.htm). Many VoiceXML intepreter vendors use the open source ECMAScript engine provided by Netscape, so the functionality of the intrinsic objects including regular expressions (RegExp) and dates (Date) is the same. Of course, a list of recommended reading about regular expressions would not be complete without referencing Jeffrey Friedl's book, "Mastering Regular Expressions". The second edition was just published by O'Reilly and Associates in July ( http://www.oreilly.com/catalog/regex2/).
Here are a couple of additional notes about this solution:
According to the builtin date grammar, if the user doesn't specify a part of the date such as the year, the corresponding part of the return value will contain question marks (?). The regular expression above doesn't take that into account, but you should check for the question marks and either reprompt the user for a complete date, prompt the user for the missing part, or assume the missing part is the same as the current date. You can determine the current date by constructing an ECMAScript Date object without passing the constructor any arguments. You can extract the year, month, and day from a Date object using the getFullYear, getMonth, and getDate methods respectively.
Rather than using regular expressions, you could have just as easily used the substring and indexOf methods of the ECMAScript String object. Regular expressions are much cooler though, and they can be more efficient too, especially if you use them multiple times.
If your VoiceXML interpreter doesn't support SSML, you still have options: translate the return value of the grammar to a string, or use a pre-recorded audio library. While the latter is the most preferable from an application quality perspective, let's explore the former option to exercise our ECMAScript muscles. It's also good to have TTS as backup if ever the Web server hosting our application's audio should fail.
We'll implement a simple user-defined class, CBuiltinDateReader. The class exposes a method GetTTS that, given a date in the format returned by the builtin date grammar, returns a string that should be readable as a date by most TTS engines. We implement the translator as a class rather than as a simple function with the assumption that it will be called multiple times within a voice application and to encapsulate the mapping tables (months, days, days_in_months, and centuries) in the class so as not to conflict with other variables in the application.
Here's the code:
// Simple Date to TTS converter function CBuiltinDateReader() { this.months = ["January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"];
this.days = ["", "first", "second", "third", "fourth", "fifth",
"sixth", "seventh", "eighth", "ninth", "tenth",
"eleventh", "twelfth", "thirteenth", "fourteenth",
"fifteenth", "sixteenth", "seventeenth", "eighteenth",
"nineteenth", "twentieth", "twenty-first", "twenty-second",
"twenty-third", "twenty-fourth", "twenty-fifth", "twenty-sixth",
"twenty-seventh", "twenty-eighth", "twenty-ninth",
"thirtieth", "thirty-first"];
/*ja fe ma ap ma ju ju au se oc no de */
this.days_in_month = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
// more robust code should check for leap year
// used to convert years to tts
this.centuries = {"y11" : "eleven", "y12" : "twelve", "y13" : "thirteen",
"y14" : "fourteen", "y15" : "fifteen", "y16" : "sixteen", "y17" : "seventeen",
"y18" : "eigthteen", "y19" : "nineteen", "y20" : "two-thousand",
"y21" : "twenty-one"};
}
// Convert a string in the format yyymmdd, and convert it to a TTS-readable date
CBuiltinDateReader.prototype.GetTTS = function(sDate)
{
var dWhen = this.Str2JSDate(sDate);
return this.months[dWhen.getMonth()] + " " +
this.days[dWhen.getDate()] + ", " + this.Year2TTS(dWhen.getFullYear());
}
// Convert a string formatted as yyyymmdd to a JS date object
CBuiltinDateReader.prototype.Str2JSDate = function(sDate)
{
var now = new Date(); // to fill-in missing pieces
var sResult = "";
if (/^(.{4})(.{2})(.{2})$/.test(sDate))
{
var y = RegExp.$1;
var m = RegExp.$2;
var d = RegExp.$3;
if (/\?/.test(y))
{
y = now.getFullYear();
}
if (/\?/.test(m))
{
m = now.getMonth()+1;
}
if (/\?/.test(d))
{
d = this.days_in_month[m-1];
}
return new Date(y, m-1, d);
}
else
{
return now;
}
}
// Convert a full year (yyyy) to a string
CBuiltinDateReader.prototype.Year2TTS = function(year)
{
if (year < 1100)
{
return year;
}
if (/(\d{2})(\d{2})/.test(year))
{
var century = RegExp.$1;
var tens = parseInt(RegExp.$2, 10);
var sCentury = this.centuries["y" + century];
if (!sCentury)
{
sCentury = century;
}
if (tens == 0)
{
tens = "hundred";
}
else if (tens < 10)
{
tens = ("oh " + tens);
}
return sCentury + " " + tens;
}
else
{
return year;
}
}
|
Using this class in an application is easy:
- Copy the class into a text file.
- Host the file on a Web server accessible to your application.
- Add a script element to your application root document that references the text file.
- Add an additional script block to your application root document that instantiates the class.
- Call the GetTTS method of the object from the expr attribute of a value element within a prompt tag wherever you want to read back a date returned by the date grammar.
Here's a simple application that asks the user for a date and echoes it back to the user. The CBuiltinDateReader class is assumed to be hosted in the document builtindatereader.js hosted on the same server in the same virtual directory as the VoiceXML document.
<vxml version="2.0" xmlns="http://www.w3.org/2002/vxml">
<script src="builtindatereader.js"/>
<script>
var date_reader = new CBuiltinDateReader();
</script>
<form>
<field name="theDate" type="date">
<prompt>
Say a date.
</prompt>
<catch event="noinput nomatch">
Sorry. Didn't get that.
<reprompt/>
</catch>
<filled>
<prompt>
You said <value expr="date_reader.GetTTS(theDate)"/>
</prompt>
</filled>
</field>
</form>
</vxml>
|
The final option, using a pre-recorded library of audio, will produce by far the most professional results and a superior user experience. The layout of pre-recorded audio libraries are vendor-specific, and once you've identified a vendor, you should ask if they have a JavaScript class or set of functions that accompany the audio library and aid in its usage. We'll investigate a fictitious audio library and develop a JavaScript class to access it in a future column.
back to the top
Copyright © 2001-2002 VoiceXML Forum. All rights reserved.
The VoiceXML Forum is a program of the
IEEE Industry Standards and Technology Organization (IEEE-ISTO).
|