内容


支持语音的 XML,第 3 部分

开发语音 blog 应用程序

用 Java servlet 和 VoiceXML 提交 blog 文章

系列内容:

此内容是该系列 # 部分中的第 # 部分: 支持语音的 XML,第 3 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:支持语音的 XML,第 3 部分

敬请期待该系列的后续内容。

简介

blog 是另一个热门的领域,越来越多的人利用 blog 向公众表达自己的想法。为什么不通过 VoiceXML 用自己的声音与 blog 或 tweet 进行交互呢?在本文中,您将学习如何实现这种非常酷的功能以及:

  • 从远程数据生成动态的 VoiceXML
  • 将内容传递给 VXML
  • 通过 VXML 向 blog 提交请求并接收报告
  • 向 Twitter 提交状态更新

关于本系列

语音(和一般的音频)在网上越来越流行了。这方面的示例包括大量的网络音乐和网络广播。本系列讲解几种结合使用语音和 XML 开发应用程序的方式:

blog 的工作流

对于 blog 作者来说,最糟糕的事情莫过于无法访问自己的 blog,无法提交文章。但是,通过使用 VoiceXML,可以提供另一种与 blog 进行交互的方法。可以构建一个支持 VoiceXML 的应用程序,它允许用户通过电话访问 blog。

但糟糕的是,当前的 VoiceXML(VXML)平台能力有限,无法提交完整的 blog 文章;但是,可以使用 VoiceXML 向现有的 blog 提交一些标准化的消息。

为了实现这个功能,我们将构建一个 Java™ servlet,它将生成 VXML,用输入组装 blog 帖子,并将帖子发送到用户的 blog。目前,我们忽略 blog 选择、用户名和密码的问题,当我们看到 blog 接口时就会明白这么做的原因。

为了实现这个工作流,blog 接口 servlet 将:

  1. 从 blog 获得类别列表
  2. 生成一个 VXML 文件,它输出接收到的短语列表,然后输出接收到的类别列表
  3. VXML 使用输入方法将类别和短语信息提交给 servlet
  4. servlet 组装 blog 帖子并将它发送给 blog 以便提交
  5. 然后 VXML 输出一个 “已经完成” 消息,处理过程结束

在图 1 中可以看到基本结构。

图 1. 语音 blog 结构
语音 blog 结构
语音 blog 结构

在处理 VXML 之前,需要研究一下支持连接 blog 的 blog 接口。

blog 接口

有几种用于发布 blog 文章的解决方案,但是大多数 blog 都支持基于 MetaWeblog/MovableType 或 Blogger API 接口的 XML-RPC(XML Remote Procedure Call)标准。这两者非常相似,但是前者是专为解决后者的一些限制设计的,因此它更加实用。

在考虑建立 Web 帖子之前,需要设置一个能够向用户的 blog 发出 XML-RPC 调用的环境。在与大多数 blog 进行通信时,需要三段信息:

  1. blog 的 XML-RPC URL —— 对于 WordPress 这样的系统,通常由一个特殊的组件处理 XML-RPC 过程。例如,WordPress 提供了 xmlrpc.php 脚本。我们需要完整的 URL,例如 http://mcslp.com/xmlrpc.php。
  2. 用户名 —— blog 用户的用户名。
  3. 密码 —— blog 用户的密码。

对于所有 blog,还需要最后一段信息,blog ID。对于只包含一个 blog 的站点和系统(比如 WordPress),blog ID 总是 1。对于包含多个 blog 的站点和系统,需要 blog 的惟一 ID。MovableType、b2evolution 以及大型内容管理系统中包含的 blog 属于后一类。

显然,无法轻松地通过 VoiceXML 接口交换这一信息。但是,如果设置一个大型服务,那么可以提供一个使用数字的登录服务,让用户能够登录自己的帐户,然后装载他们的服务属性。

为了打开到 blog 的连接,我们围绕 XML-RPC 连接创建一个新的包装器类,它实现两个必需的功能:

  • 获得类别的列表
  • 通过主类的方法发布帖子

因此,主类实例只需设置所需的核心参数并创建一个 XML-RPC 对象的实例,我们将使用这个对象的实例与 blog 系统通信。

为了简化这个过程,我们将使用 Apache 提供的 XML-RPC 库。清单 1 给出 BlogPost 类的核心代码和创建这个类的实例的方法。

清单 1. 设置 BlogPost
import java.util.*;
import java.io.*;
import java.net.URL;
import org.apache.xmlrpc.client.XmlRpcClient;
import org.apache.xmlrpc.client.XmlRpcClientConfigImpl;

public class BlogPost {
    String username;
    String password;
    String blogid;
    String blogurl;
    XmlRpcClientConfigImpl xmlrpcConfig = new XmlRpcClientConfigImpl();
    XmlRpcClient client = new XmlRpcClient();

    public BlogPost(String user,
                   String pass,
                   String id,
                   String url) {
        username = user;
        password = pass;
        blogid = id;
        blogurl = url;

        try {
            xmlrpcConfig.setServerURL(new URL(blogurl));
            client.setConfig(xmlrpcConfig);
        } catch (Exception ex) {
            ex.printStackTrace();
            System.out.println("ERROR: " + ex.getMessage());
        }
    }

清单 1 中,可以看到如何接收输入,然后创建一个基本的 XmlRpcClient 实例,可以通过这个实例与 blog 的 XML-RPC 服务通信。

现在,为了创建 BlogPost 对象,使用清单 2 中的代码提供用户名、密码、URL 和 blog ID。

清单 2. 创建 BlogPost 对象
BlogPost bp = new BlogPost("user",
                           "pass",
                           "1", 
                           "http://myblog.com/xmlrpc.php");

在建立帖子之前,希望调用者能够根据 blog 中当前配置的类别设置一个类别。

获得 blog 类别

基于 XML-RPC 的 weblog API 提供一个 getCategories 函数,这个函数返回指定的 blog 的类别列表。为了获得这一信息,需要在调用中提供 blog ID、用户名和密码。

返回值比较复杂。从数据结构的角度来说,它是一个散列值数组,其中的每个散列值由类别的相关信息组成,包括类别 ID、类别名称、类别的源 blog 引用 URL 和 RSS feed URL。因此,对于每个类别,会获得一个包含表 1 中的信息的散列值。

表 1. 散列值包含的类别信息
categoryId3
htmlUrlhttp://www.thewriting.biz/?cat=3
rssUrlhttp://www.thewriting.biz?feed=rss2&cat=4
categoryNameMaking Money
descriptionMaking Money

其中一些信息是我们不需要的,我们只需要 categoryIdcategoryName。在建立 blog 帖子时,需要用 ID 设置类别。在应用程序的 VoiceXML 端提供类别名称,让用户能够按照名称选择类别。

为了使用这些信息,我们要发送请求、获取返回的信息并将类别 ID 和名称解析为一个 Java 散列表,可以在应用程序的 VoiceXML 部分使用这个散列表生成类别列表。清单 3 给出 BlogPost 对象的 GetCategories() 方法。

清单 3. 将来自 blog 的类别放在散列表中
public Hashtable GetCategories() {
    Hashtable categories = new Hashtable();
    Object[] cats = new Object[] {};

    try {
        cats = (Object [])
            this.client.execute("metaWeblog.getCategories",
                                new Object[] {this.blogid,
                                              this.username, 
                                              this.password});
        
    } catch (Exception ex) {
        ex.printStackTrace();
        System.out.println("ERROR: " + ex.getMessage());
    }

    for(int i = 0; i < cats.length; i++ ) {
        HashMap category = (HashMap)cats[i];
        categories.put(category.get("categoryId"),
                       category.get("categoryName"));
    }

    return categories;
}

在 servlet 中,将使用产生的散列表输出类别列表。

建立 blog 帖子

在建立 blog 帖子时,要执行几个步骤,尤其是在希望追加帖子类别的情况下。对于某些 blog 系统,可以在一次提交中完成所有这些操作;但是,一些 blog 系统在提交帖子时无法接收 blog 类别。因此,更好的解决方案是执行以下步骤:

  1. 添加一个 blog 帖子,从而获得一个惟一的 blog 帖子 ID,但是将帖子标为不自动发布(这是 newPost RPC)。
  2. 使用 blog 帖子 ID 为这个 blog 帖子设置类别(使用 setCategories RPC)。
  3. 装载整个 blog 帖子的细节(使用 getPost RPC)。
  4. 重新发布这个帖子,这一次将帖子标为可发布(使用 editPost RPC)。

清单 4 给出向远程服务发送各种 XML-RPC 调用的操作过程。注意,与类别的情况一样,信息的格式也比较复杂。必须构造一个包含 blog 帖子信息的散列表。为了设置类别,还要创建另一个散列表,然后在提交类别信息时,以数组形式传递这个散列表(采用与 getCategories 的返回值相同的方式,即散列表的数组)。

清单 4. 发布 blog 帖子
public Boolean PostIt(String title,
                      String description,
                      Integer category) {
    
    Hashtable post = new Hashtable();
    if (title != null) post.put("title", title);
    post.put("dateCreated", new Date());
    post.put("description", description);

    Hashtable categories = new Hashtable();
    categories.put("categoryId",category);
    
    String blogpostid = "";
    Boolean result = Boolean.FALSE;

    try {
        blogpostid = (String)
            this.client.execute("metaWeblog.newPost",
                           new Object[] {this.blogid, 
                                         this.username, 
                                         this.password, 
                                         post, 
                                         Boolean.FALSE});

        result = (Boolean) 
            this.client.execute("mt.setPostCategories",
                           new Object[] {blogpostid, 
                                         this.username, 
                                         this.password, 
                                         new Object[] {categories}});

        HashMap postdetail = (HashMap) 
            this.client.execute("metaWeblog.getPost",
                           new Object[] {blogpostid, 
                                         this.username, 
                                         this.password, });

        result = (Boolean) 
            this.client.execute("metaWeblog.editPost",
                           new Object[] {blogpostid, 
                                         this.username, 
                                         this.password,
                                         postdetail,
                                         Boolean.TRUE });

    } catch (Exception ex) {
        ex.printStackTrace();
        System.out.println("ERROR: " + ex.getMessage());
    }
    return(result);
}

实际的 blog 发布过程是对 PostIt() 函数的调用:

bp.PostIt("Title","Content",5);

既然 blog 发布机制和类已经就绪了,就只需要创建一个包装器 Java servlet,它输出所需的 VXML,然后接收输入以生成 blog 帖子。

考虑语法规则

在讨论生成 blog 帖子所需的 VXML 之前,需要研究一下接收用户输入所用的一些规则。

测试的基本语法格式是使用一种由圆括号和方括号组成的格式帮助指定所用的语法。正如前面提到的,还不能任意指定内容,但是可以接收一些基本短语并用在 blog 帖子中。

表 2 列出了最有用的操作符。

表 2. 主要操作符
操作符示例描述
Disjunction[happy sad]可以接受其中的任意单词。
Concatenation(very happy)只接受这个完整的短语。
Optional Phrase(?very happy)问号(?)后面的单词是可选的,所以这个示例接受 “very happy” 和 “happy”。
Positive Closure(+very happy)加号(+)后面的单词必须出现至少一次,所以 “very happy” 和 “very very happy” 都是有效的。
Kleene Closure(*very happy)星号(*)后面的单词是可选的,但是也可以重复。所以 “happy”、“very happy” 和 “very very happy” 都是有效的。

显然,可以组合使用这些操作符,建立复杂的有效短语列表。

这个 blog 应用程序使用的语法见清单 5。

清单 5. blog 主题语法
(eye am [happy {<phrase "I am happy">}
        sad {<phrase "I am sad">}
        traveling {<phrase "I am traveling">}
        (on business){<phrase "I am on business">}
        ] )

首先注意,这里用 “eye” 替代 “I”。大多数 VoiceXML 系统无法区分单词和单一字母,而且某些系统会拒绝接受少于两个字符的单词。因此,必须明确地使用单词的发音,然后使用一个定义的字段值捕捉真正需要的单词。

然后,只需提供可用选项的列表。根据 清单 5 中的语法,以下句子都是有效的输入:

  • I am happy
  • I am sad
  • I am traveling
  • I am on business

接下来,看看用来接收输入的 VXML 源代码。

主要的 VXML 源代码

VXML 源代码的核心部分见清单 6。

清单 6. 主要的 VXML 源代码
<?xml version="1.0" encoding ="UTF-8"?>

<!DOCTYPE vxml PUBLIC "-//W3C//DTD VOICEXML 2.1//EN" 
               "http://www.w3.org/TR/voicexml21/vxml.dtd">

<vxml version="2.1" xmlns="http://www/w3/org/2001/vxml"
    xml:lang="en-US">

<form id="blogging">
 <block>
  <prompt>
     Welcome to the blogging service.
  </prompt>
 </block>

<field name="phrase">
  <prompt>Please state your current status to be used on your blog.</prompt>

    <grammar type="text/gsl">
        <![CDATA[
(eye am [happy {<phrase "I am happy">}
        sad {<phrase "I am sad">}
        traveling {<phrase "I am traveling">}
        (on business) {<phrase "I am on business">}
        ] )

]]>
</grammar>
 </field>

<field name="location">
<prompt>Please state your current location</prompt>
<option>Edinburgh</option>
<option>New York</option>
<option>London</option>
<option>Paris</option>
<option>Stockholm</option>
</field>

  <filled>
  <prompt>
    Your blog post will be:  <value expr="phrase"/>. 
    Current location: <value expr="location"/>
  </prompt>

  <submit name="/VXMLBlogPost/blogpost" 
          namelist="phrase category location"/>
  </filled>

</form>

</vxml>

这个文件产生的基本交互序列与清单 7 相似。

清单 7. 基本序列
Service: Welcome to the blogging service.
S: Please state your current status to be used on your blog.
User: I am happy
S: Please state your current location
U: London
S: Your blog post will be: I am happy. Current location: London

然后,需要动态地输出最后一个字段 —— blog 中配置的类别列表。

输出类别

为了产生类别列表,我们在 Java servlet 中生成 VXML 并插入一个 blog,它提示用户选择一个类别。可以使用 <option> 标记接受用户输入的值并分配一个数字类别 ID,在建立 blog 帖子时需要用这个 ID 指定类别。清单 8 给出输出类别列表的代码片段。

清单 8. 输出类别列表
Hashtable cats = this.bp.GetCategories();

Enumeration keys = cats.keys();
while ( keys.hasMoreElements() ) {
    String key = (String)keys.nextElement();
    String cat = (String)cats.get( key );
    out.println("<option value=\"" + key + "\">" + cat + "</option>");
}

产生的 VXML(包括周围的字段定义)输出清单 9 中的内容。

清单 9. 产生的 VXML
<field name="category">       
  <prompt>Please state the category to be used for this post.</prompt>       
<option value="1">Commentary</option>
<option value="2">Holiday Travel</option>
</field>

现在,可以接收输入了。

解析输入并建立帖子

VXML 中的 submit 标记将字段值作为标准 HTTP 参数传递给一个指定的远程应用程序。在 VXML 中,这个块放在 filled 部分中(见清单 10)。

清单 10. submit 标记
<submit name="/VXMLBlogPost/blogpost" 
        namelist="phrase category location"/>

为了接收输入并建立 blog 帖子,只需解析输入中的这些参数,然后将 blog 帖子的内容细节提交给 BlogPost 类实例的 PostIt() 方法,见清单 11。

清单 11. 解析输入并建立 blog 帖子
public void doPost(HttpServletRequest req, HttpServletResponse res)
    throws ServletException, IOException {

    PrintWriter out = null;
    out = res.getWriter();
    res.setContentType("text/xml");

    this.bp.PostIt( req.getParameter("title"),
                    req.getParameter("title") +
                    ". I am currently located in " +
                    req.getParameter("location"),
                    Integer.parseInt(req.getParameter("category")));

    printHeader(out);
    out.println("<form><block>" + 
         "<prompt>Blog posted</prompt><disconnect/>" +
         "</block></form>");
    printFooter(out);
}

现在,应该能够通过世界上的任何电话访问这个应用程序,说出您选择的句子、位置和类别,从而建立一个 blog 帖子。

现在,看看如何给 Twitter 增加语音功能。

Twitter

Twitter 是一种近来受到许多关注的 Web 2.0 服务。(参见 参考资料 中 Twitter 的链接)。这种服务使用户能够及时了解朋友的动向,这不但可以在网上进行,而且可以通过手机或即时消息传递程序进行。您的朋友也可以了解您的动向。

这种服务的思路是,用户发送简短的文本(少于 140 个字符),让其他人了解他的位置以及马上要做的事情。Twitter 最初的意图是提供简单的状态消息,但是已经发展成一种 “快速 blog”,用户可以输入简单的想法并发送给朋友。

那么,这又跟语音 blog 有什么关系?实际情况是各方面都有关系。但是,实时听写还没有实现,所以目前只能在一系列预先定义的短句子中进行选择。这对于 Twitter 非常合适。如果有一些常用的句子,那么可以预定义这些句子的语音;还可以提供一个 Web 界面,用户可以通过这个界面输入新的句子。

在 Twitter 中支持语音的过程与 blog 中的过程完全相同,但是 Twitter 不使用 XML-RPC。相反,需要在 HTTP POST 请求体中发送消息(即 “tweet”),见清单 12。

清单 12. 使用 HTTP POST 向 Twitter 提交消息
import java.net.*;
import java.io.*;

public class BlogPost {

    public BlogPost(String user,
       String pass, String status) {
    try{
           Authenticator.setDefault(new PostAuthenticator(user, pass));

             URL url = new URL ("http://twitter.com/statuses/update.xml");
             URLConnection conn = url.openConnection();
             conn.setDoInput (true);
             conn.setDoOutput (true);
              conn.setUseCaches (false);

             conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
             DataOutputStream printout = new DataOutputStream (conn.getOutputStream ());
             String requestBody = "status=" + URLEncoder.encode (status);
             printout.writeBytes (requestBody);
             printout.flush ();
             printout.close ();

             DataInputStream input = new DataInputStream (conn.getInputStream ());
             String str;
             while (null != ((str = input.readLine()))){
                    System.out.println (str);
             }
             input.close ();
      } catch (Exception e){
           e.printStackTrace();
      }
     }
 
     public static void main (String args[]){
           BlogPost bp = new BlogPost("myusername", "mypassword", 
     "Playing with voice blogging to the Twitter API!");
     }
	
}

稍后会解释 Authenticator。在 清单 12 中,我们打开一个连接到 Twitter 的简单 HTTP 连接,并告诉系统我们将通过这个连接进行读写。

POST 请求的特征是,并不在 URL 中添加参数(比如 status),而是作为请求体发送参数。

根据发送请求的目标是 update.xml 还是 update.json,会收到一个适当格式的响应;如果必要,可以检查这个响应,但是对于 blog 不需要这样做。

当然,您不希望任何人都能够更新您的 Twitter,所以需要提供身份验证机制。这就是这个类顶部的 Authenticator 的作用。

Authenticator 是一个抽象类,所以需要从它派生出子类,并实现 getPasswordAuthentication() 方法来存储用户名和密码(见清单 13)。

清单 13. 向服务器证明自己的身份
import java.net.Authenticator;
import java.net.PasswordAuthentication;

public class PostAuthenticator extends Authenticator {

	private String user;
	private String pass;
	
	public PostAuthenticator (String user, String pass){
		this.user = user;
		this.pass = pass;
	}
	
	protected PasswordAuthentication getPasswordAuthentication(){
		char[] passwd = new String(this.pass).toCharArray();
        PasswordAuthentication auth = new PasswordAuthentication(this.user, passwd);
		return auth;
	}
	
}

通过返回这个对象,可以将它设置为 Authenticator 类的默认值,当在 URLConnection 中需要执行身份验证时,Java servlet 会检查这些值。

最终结果是可以通过语音更新 Twitter。

结束语

本文介绍了如何构建一个 blog 解决方案,从而使用 VoiceXML 提供输入方法来建立 blog 帖子。但糟糕的是,听写功能还未得到广泛支持,所以还无法进行自由的语音输入。我们只能通过详细的语法规则选择简单的短语,但是这对于 Twitter 是很合适的。还可以声明自己的当前位置。

最终的应用程序是一个 Java servlet,它生成查询信息,对输入进行解析,然后提交帖子。

请务必阅读第 4 部分,即本系列中的最后一篇文章。在第 4 部分中,我们将开发一个应用程序,它接收 VoiceXML 输入,并通过 Yahoo Search API 执行基本 Web 搜索和 Yahoo 本地搜索。


下载资源


相关主题

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=XML
ArticleID=265718
ArticleTitle=支持语音的 XML,第 3 部分: 开发语音 blog 应用程序
publish-date=10302007