What is SpeakRight?

SpeakRight is an open-source Java framework for writing speech recognition applications in VoiceXML.Unlike most proprietary speech-app tools, SpeakRight is code-based. Applications are written in Java using SpeakRight's extensible classes. Java IDEs such as Eclipse provide great debugging, fast Java-aware editing, and refactoring. Dynamic generation of VoiceXML is done using the popular StringTemplate templating framework. Read more...

See Getting Started, Tutorial, and Table of Contents
Powered By Blogger

Wednesday, May 2, 2007

Prompt Ids and Prompt XML Files

Tuning a speech application often involves changing prompts, to re-word a question or improve an error message. This should be possible without having to rebuild the app. Speakright uses prompt ids to provide this feature.

A prompt id is a ptext item beginning with "id:", such as "id:outOfRange". When the prompt is rendered, a set of XML files are searched to find an entry for that id. The entry looks like this:
<prompt name="outOfRange" def="true">That value is out of range. </prompt>
The prompt text for the id is a full PText; it can contain references to other ids, for example. It's an error if a prompt id cannot be found in any of the XML files.

Which set of XML files? Well you get to define them using SRRunner.registerPromptFile, usually one per app. The framework itself may register some; each SRO has its own prompt XML. The registration may be permanent (for the life of the app), or temporary (for the current flow object execution). The list of XML files is searched in reverse order so that your XML files are searched first, and framework XML files searched last.

Prompt Groups

Another useful feature is the ability to build an app using rough prompts, and then finalize the prompts later without having to do any code changes. Prompt Groups does this. Each flow object has a prompt group. The default value is the flow object's name. Prompt ids are looked up twice. First the prefix is added, so for an id "id:outOfRange" in a flow object "MyMenu" the first lookup is "id:MyMenu.outOfRange". If this prompt id is found, the value in the XML file is used. If not, then a second lookup without the prefix is done, which for our example would be "id:outOfRange".

All SROs use prompt ids with default values (see Prompts in SROs). The default prompts are usually good enough to get your app logic up and tested. Then you can create an app-specific prompt XML file and register it (using SRRunner.registerPromptFile). Now you can define the prompt text for all the flow objects at your leisure. No code changes needed!

Thursday, April 26, 2007

Simpsons Demo

SimpsonsDemo is a simple voting application. It's a working speech application that demonstrates the use of SpeakRight in a non-trivial app. In SimpsonsDemo, users can call in and vote for a character from the Simpsons TV show, one vote per phone call. After voting they can hear the current standings and other information about the application. In order to make the application more complex, each character is assumed to have a related character who they are closest to. For Mister Burns this might be Smithers (and vice versa!).

Development Process

Productivity of developers is important. When code is quick to create, it's quick to change (and test!). This encourages an agile development process.

A SpeakRight application is developed in stages.

Stage 1. The application flow objects are created and wired together. Concentrate on the Model and the overall callflow. Don't worry about grammars and prompts at this point. Use inline grammars and default prompts. Do all testing using the keyboard-based interactive tester (and unit tests). The goal at this stage is to get the callflow logic working. External data access can be done at this stage, or mocked out, and done later in parallel with other stages.

Stage 2. Define the grammars and prompts. Use external grammars that take into consideration pre-amble, post-amble, and various ways of saying things ("LA" and "Los Angeles"). An application prompt XML file should be created that defines the main and error prompts for your flow objects. Deploy the app as a servlet and test using the HTML mode, where the app can be executed using a web browser. This will flush out missing files and other errors.

Stage 3. Deploy the app to the VoiceXML platform. At this point the callflow logic should be already complete and well tested, and the prompts and grammars defined. All that remains is to listen for mis-pronounciations, poor prosody, and other VUI-level errors.

The idea is to get as much testing done before deploying to the VoiceXML platform, where testing becomes much slower and more difficult (especially automated testing). Of course, it's not a fixed waterfall approach; you may, for example, need to prototype some VUI design issues on the VoiceXML platform before tackling stage 1.

Description of the Call Flow
When a user calls in they choose a character by saying the character's name, such as "Mister Burns". The application plays a short description of the character and asks if the user wishes to vote for this character. If yes, the vote is recorded and the user is taken to the main menu. Otherwise the user is asked if they want to hear about the related character. If the user says yes, the related character's description is played and the user has the opportunity to vote for the
character as before. If the user chooses not to hear about the related character then they are asked to choose another character.

The main menu has four options.
  • choose character select a Simpsons character and vote. described above.
  • voting results. Hear the voting results. Results are played in sets of three. The top three characters are listed. If the user says 'next', then the next three characters are listed.
  • call statistics. Lists # of calls, average call duration, and other statistics.
  • speakright. Plays a description of the SpeakRight framework.
Sample Call
(For brevity we've left off examples of error handling for silence and no-input errors)

Computer: Welcome to the Simpsons Demo where you can vote for your favorite Simpson's character. {pause} Please say the name of a character, such as "Mister Burns".
Human: Homer
C: Homer Simpson is the show's main character. His signature annoyed grunt "D'oh!" has been included in the Oxford English Dictionary. Do you want to vote for Homer?
H: No
C: Do you want to hear about Marge, Homer's wife?
H: Yes
C: Marge Simpson is the well-meaning and patient wife of Homer. Do you want to vote for Marge?
H: Yes.
C: Vote recorded. {pause} Main Menu. You can say 'choose character', 'voting results', 'call statistics', or 'hear about speakright'.
H: call statistics
C: There have been 413 calls with average length of 65 seconds. The average completion is...
H: Hangup


Pseudo-Code for the Call Flow

Let's write the call flow logic as a series of actions with some basic pseudo-code to represent branching and looping. Labels are marked in bold.

Welcome
A: ChooseCharacter
B: SayCharacterInfo
VoteForCharacterYesNo
if yes then goto MainMenu
HearAboutRelatedCharacterYesNo
if yes then goto B else goto A

MainMenu: MainMenu
if 'character' then goto A
else if 'results' then SayVotingResults
else if 'statistics' then SayCallStatistics
else if 'speakright' then SaySpeakRightInfo

Writing the Call Flow in Java
In SpeakRight, the pseudo-code for a callflow can be converted into Java code in a fairly simple way. Each action becomes a flow object, and is represented by a class derived from one of the SpeakRight base classes, such as a PromptFlow that plays some audio output. A series of flow objects are executed in sequence.

Where branching is required, the getNext method of a flow object can be overridden to add the branching logic. getNext returns either a flow object to be executed, or an event object which causes execution to jump to a previous point. Event objects and event handlers act like "throw" and "catch" respectively.

The application's data is stored in the model, a special class generated by the SpeakRight tool MGen. The model can be used to hold user input (such as the currently selected Simpson's character) and retrieved data (eg. from a database). It can also hold control data that is used to control the execution path.

Let's get started. The outermost flow object represents the entire callflow, and is always derived from SRApp.


public static class SimpsonsDemo extends SRApp
{
public SimpsonsDemo()
{
addPromptFlow("Welcome");
add(new MainLoop());
}
}

The constructor adds two sub-flows: a welcome prompt and a loop flow object. The welcome is only played once and the remainder of the callflow is done in a loop since the user can go back and forth from selecting a character to the main menu as many times as he or she likes.

The MainLoop class defines a model variable M. By convention, SpeakRight will inject a value for M at runtime automatically.

public static class MainLoop extends BranchFlow
{
public Model M;

The first method called in a flow object is its onBegin method. Here we initialize the two main model values. nextAction is used to control what MainLoop does next, and currentCharacterId is the currently chosen Simpsons' character.


@Override
public void onBegin() {
M.nextAction().set("A");
M.CurrentCharacterId().clear();
}

The next method to be called is getFirst. If nextAction is set to choose a character then we build a sequence of sub-flows for selecting and voting for a character. Otherwise we return the main menu flow object.

 
@Override
public IFlow branch() {
if (M.nextAction().get() == "A") {
BasicFlow flow = new BasicFlow();
if (M.getCurrentCharacterId() == 0) { //no char selected?
flow.add(new AskCharacter());
}
flow.add(new new PromptFlow("{$M.CurrentCharacterId}"));
flow.add(new VoteYesNo());
flow.add(new RelatedCharacterYesNo());
return flow;
}
else {
MainMenu menu = new MainMenu();
return menu;
}
}

AskCharacter is a flow object that asks the user to enter a character name.


public static class AskCharacter extends BaseSROQuestion {

public AskCharacter() {
super("character");

m_main1Prompt = "Say the name of a Simpson's character";
m_slotName = "x";
m_modelVar = "currentCharacterId";
}
}

The voting flow object is a yes/no question. The VoteYesNo class asks the question and then it's getNext method handles the result. If 'yes' is input then record the vote and jump to the main menu. The GotoMainLoopEvent event object is used to do this.

public static class VoteYesNo extends SROYesNo {
public VoteYesNo()
{
m_main1Prompt = "Do you want to vote for {$M.CharacterName}";
}

@Override
public IFlow onYes() {
return new MainLoop.GotoEvent(MainLoop.BRANCH_MAIN_MENU);
}
}


The GotoMainLoopEvent is caught by MainLoop in its onCatchGotoBranchEventmethod. Here we set the branching condition M.nextAction.

protected void onCatchGotoBranchEvent(GotoBranchEvent ev)
{
log("branch. action: " + ev.m_action);
M.nextAction().set(ev.m_action);
}

MainLoop is a BranchFlow with it's loop-forever set. MainLoop will be executed again, and depending on nextAction do either the main menu or choose-a-character.

SimpsonsDemo has an interactive tester (for keyboard testing), and an auto-tester (see Automated Testing).

Thursday, April 19, 2007

SpeakRight Reusable Objects (SROs)

One of the biggest challenges for a speech app framework is to maximise the reuse of common VUI dialogs. Collection of comon data elements should be reusable. This includes as time, date, currency, numbers, phone numbers, and zip code. Another area of commonality is user interface elements: login, main menu, "hot word" commands, list traversal, the enter-or-cancel pattern, and confirmation.

SpeakRight provides a set of reusable speech objects called SROs. They are configurable and extensible. Here is a list of the ways SROs can be "tweaked":

  • SROs have a full set of prompts, with main, silence, no-reco, and help prompts. Up to four escalations of each can be defined.
  • any or all prompts can be replaced. Each SRO has a subject, a word such as "flights", which is used to build prompts. Changing the subject word is the simplest way to adjust the prompts. There is an extension point for handling the plurality of subject words ("flight", "flights"). Or the entire prompt can be replaced.
  • prompts can be conditional, such as a prompt that only plays the first time an SRO is executed.
  • prompts can be defined at compile time, at runtime in code, or in external XML files.
  • grammars are replaceable. Inline grammars or grammar files can be used. The only restriction on a grammar is that it uses the slot names required by the SRO.
  • validation code can be added. This server-side code inspects use input and either accepts it or causes the SRO to re-execute.
  • Model binding. An SRO has a model variable name. When set, the user input results are bound to the model (i.e. stored in the model for later use by the app)
  • Command phrases can be added to an SRO. A common data entry pattern is the enter-data-or-say-cancel pattern. SROs have a list of command phrases that you can add to.
  • confirmation can be added. An SRO has a confirmation plug-in that can be used to add various forms of confirmation (explicit, implicit, or confirm-and-correct).


The current list of SROs is:
  • SROCancelCommand
  • SROChoice
  • SROConfirmYesNo
  • SRODigitString
  • SROListNavigator
  • SRONumber
  • SROOrdinalItem
  • SROTransferCall
  • SROYesNo
Adding new SROs is simple. Code-generation (using StringTemplate) generates a base class (eg. genSRONumber) that manages prompts and grammars. You only need derive the actual SRO class to add specific logic.

Thursday, March 29, 2007

Prompts in SROs

SpeakRight speech objects (SROs) offer a highly-reusable approach to prompts. Recall that SRO classes are generated by the SROGen tool. Each SRO has an XML file that defines what code will be generated. The XML file contains the grammars and prompts for the SRO. You can modify this file and regenerate an SRO.

SROGen also generates an XML file holding the default prompts. This file is deployed and is read at runtime to load the SRO prompt fields. You can modify this file on a production system to modify the default prompts for an SRO; no re-compile (or restart) is necessary.

Here's what a prompt definition in the XML file looks like:

<prompt name="main1" def="no">What {%subject%} would you like?</prompt>

The prompt id main1, has a corresponding field in the SRO class called m_main1Prompt. A derived class, or a SR app, can modify the field using get/set methods.

This works fairly well with the PText feature {%fieldName%} for extracting values from a field.

Sub-Prompts
The one thing missing so far is the ability to define conditional prompts, such as a play-once prompt. You can do it in code of course, but that's not very flexible

if (executionCount() == 1) {
m_main1Prompt = "Let's get started. " + m_main1Prompt;
}

To remedy this, SROs support multiple sub-prompts to be defined for a single prompt, such as the MAIN prompt.

<prompt name="main1welc" group="MAIN" cond="once_ever">Let's get started.</prompt>
<prompt name="main1" group="MAIN" def="no">What {%subject%} would you like?</prompt>

Each prompt tag results in a field being created. Multiple prompts in the same group are rendered as a single VoiceXML prompt, in the order they occur in the XML file. The neat thing is that conditions can now be applied to individual sub-prompts. The cond attribute defines a condition. Here, "once_ever" means a play-once-ever condition. The first time the SRO is executed, the prompt will be: "Let's get started. What flight would you like?". On subsequent executions, the prompt is "What flight would you like?".

Implementation note: Sub-prompts are implemented using the m_subIndex field of Prompt. When prompts are rendered (in a PromptSet), all the rendered items are gathered together in the first sub-prompt. But since each sub-prompt is an independent Prompt object, its rendering can be enabled or disabled by its condition.

Tuesday, March 6, 2007

Release 0.0.2 is out

This second release is a bit more real. First two SROs (SRONumber and SROConfirmYesNo) are available. And confirmation and validation are working.

Here's a simple app to ask for the number of tickets in a travel application

SRApp flow = new SRApp();

SRONumber sro = new SRONumber("tickets", 1, 10);
sro.setConfirmer(new SROConfirmYesNo("tickets"));

flow.add(sro);
flow.addPrompt("You said {$INPUT}");

From this, the following dialog is possible

Computer: How many tickets would you like?
Human:
Computer: I didn't hear you. How many tickets would you like?
Human:
Computer: I still didn't hear you. How many tickets would you like?
Human: twelve
Computer: Sorry, I'm looking for a number between one and ten. How many tickets would you like?
Human: two
Computer: Do you want two?
Human: yes
Computer: You said two.

As you can see, escalating error prompts are given and user input is validated against the range given to the SRONumber flow object. Utterances below 80% confidence are confirmed, and finally the user's input is played back to them.

This release uses extremely simple grammar in SRONumber (a built-in grammar for one to nine). You're free to replace it with your own, and of course the next release will improve on this.

Enjoy :)

Saturday, March 3, 2007

Testing

Testing is an important part of SpeakRight. Running applications on an VoiceXML platform is not easy or quick to do. It's essential to be able to test your app under more programmer-friendly conditions.

Unit Tests

Both JUnit and XMLUnit are supported. Here is a basic test that creates an app and runs it, feeding the required user input. Notice the use of TrailWrapper, a flow object that tracks execution in a trail of flow object names. The test shown below is in org.speakright.sro.tests and uses the MockSpeechPageWriter. This mock object remembers the rendered SpeechPage object which can then be checked to see if the expected grammars and prompts are there.

@Test public void testConfirmNo()
{
log("--------testConfirmNo--------");
SRApp flow = createApp(true);
TrailWrapper wrap1 = new TrailWrapper(flow);

SRInstance run = StartIt(wrap1);
Proceed(run, "2", "num", 40); //question with low confidence
Proceed(run, "no"); //reject the confirmation
Proceed(run, "8", "num"); //question again
Proceed(run, ""); //you said...

assertEquals("fail", false, run.isFailed());
assertEquals("fin", true, run.isFinished());
assertEquals("start", true, run.isStarted());

ChkTrail(run, "SROQuantity;ConfYNFlow;SROQuantity;PFlow");
assertEquals("city", "8", M.city().get());
}

XMLUnit can also be used. See the TestRender.java file in org.speakright.core.tests. XML comparison is more fussy (and slower), but you can check the actual VoiceXML.

ISRInteractiveTester

Next up is the ISRInteractiveTester class. Use it in a console-app for executing a SpeakRight app interactively from the keyboard. See InteractiveTester.java in org.speakright.core.tests for an example. Run this file as a Java application.

Here are the commands it uses:

SpeakRight ITester..
RUNNING App2.........
1> ???
available cmds:
q -- quit
go -- run or proceed
bye -- simulate a DISCONNECT
echo -- toggle echo of generated content
version -- display version
status -- show interpreter status
out -- turn on file output of each page in tmpfiles dir
ret -- set return url
gramurl -- gram base dir
prompturl -- prompt base dir
html -- switch to HTML output
vxml -- switch to VXML output

The out command causes each rendered VoiceXML page to be output as a file (page1.vxml, page2.vxml, etc).

Running Servlet in HTML mode

Once you're satisfied with your app, it's time to test it inside a real servlet. Write your servlet; the one that will output VoiceXML. However when you run it, add the CGI param "mode=html", like this:

http:localhost:8080/MyServlet3/App1?mode=html

MyServlet3
is the name of your dynamic web project, and App1 is the servlet inside it.

SpeakRight will render into HTML inside VoiceXML. A sample page is shown on the left. Pressing the Next or Submit button simulates the VoiceXML platform returning results.



















Running Servlet in VoiceXML mode


OK, time for the real thing. Point your VoiceXML platform at your servlet's URL, like this:

http://www.someplace.com/MyServlet3/App1

Some platforms (such as Voxeo's) have a real-time debugger that shows events and log messages as they occur.

You can also use the log4j log file that SpeakRight writes. Here's a sample:

03-03 11:32:12.015 [or24] INFO srf - SR: startApp
03-03 11:32:12.015 [or24] INFO srf - START: MyApp
03-03 11:32:12.015 [or24] INFO srf - push MyApp
03-03 11:32:12.015 [or24] INFO srf - push PFlow
03-03 11:32:12.015 [or24] INFO srf - EXEC PFlow
03-03 11:32:12.031 [or24] DEBUG srf - prompt (1 items): Welcome to Joe's Pizza
03-03 11:32:12.359 [or24] INFO srf - SR: writing content.
03-03 11:32:12.375 [or24] INFO srf - SR: saving state.
03-03 11:32:12.375 [or24] INFO srf - SR: done.
03-03 11:32:14.109 [or25] INFO srf - SR: doing POST

Automated Testing

See Automated Testing.

Friday, March 2, 2007

Servlets

Let's consider how to enclose a SpeakRight application in a Java servlet. Servlets are a portable API for responding to HTTP requests. Tomcat or other types of web servers (such as TBD) support servlets.

SpeakRight provides a class, SRServletRunner, that does most of the work. Here's how to use it in the doGet method of a servlet

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
SRServletRunner runner = new SRServletRunner(new AppFactory(), this, request, response, "GET");

if (runner.isNewSession()) {
SRRunner run = runner.createNewSRRunner(this);
IFlow flow = new App();
runner.startApp(flow);
} else {
runner.logger().log("contine in GET!!");
runner.continueApp();
}
}

First we pass the request and response objects into SRServletRunner, along with a string (used for logging) and our app factory (see Initialization). Then we check if this is a new session. If it is then we create our SpeakRight application and call startApp; otherwise we call continueApp. SRServletRunner manages passifying and re-activating the SpeakRight runtime between HTTP requests.

Logging is done using log4j.