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