蓝星际语音平台 脚本例子
- 这些例子都比较简单, 有些比较有趣,您很容易看明白,仔细阅读源程序可以让您迅速入门,也会提高您的水平。
- 每个例子都有一个说明文档
,请您在运行例子之前阅读它。 - 请将蓝星际平台程序拷贝到相应的目录之下,然后运行。因为有些脚本使用了相对路径,所以建议脚本文件和蓝星际平台程序放在相同的目录。
地铁热线IVR的实现:脚本中的脚本
某些客户其语音流程经常会发生变化,虽然用脚本来表达其变化也很容易,但每次进行重复劳动毕竟是很烦琐的事情。最近深圳地铁热线要大规模地修改语音流程,其语音菜单层次繁多,但其操作仅仅是播放某个提示语音,不需要从后台数据库中取数据,便考虑给客户提供更方便的界面,比如采用配置文件的方式,这样完全不懂编程的用户可以自行调整流程,重录语音,增加、减少、合并、修改语音菜单,既灵活方便,也省去我们维护的麻烦。
那么,如何设计一个好的配置文件,既简单明白,又能够表现语音菜单的层次结构呢?
请看这个完整的配置文件:
#地铁流程配置文件:Menu.cfg
#注释以#开始,可以有空行
#每一行的格式为: [空格]文件名[,类型信息]
# 空格: 表示菜单的层次,根目录没有空格, 下一层与上一层之间的间隔是1个空格
# 类型信息: F-传真, R-留言, P-播放语音(缺省值,可不设置)
#返回上一层菜单和转人工座席不需要设置
#注意,用空格表示菜单的层次,而不是文件名,当然取有意义的文件名可以增加可读性,便于理解流程
#设计者:bluesen@sina.com, 2006.11
StartFile #开始菜单
1_1 #失物查询
2_1 #首末班时间
2_1_1 #1号线
2_1_2 #4号线
3_1 #车站信息
3_1_1 #世界之窗...
3_1_1_1
3_1_1_2
3_1_1_3
3_1_2 #竹子林...
3_1_2_1
3_1_2_2
3_1_2_3
3_1_3 #香蜜湖...
3_1_3_1
3_1_3_2
3_1_3_3
3_1_4 #华强路...
3_1_4_1
3_1_4_2
3_1_4_3
3_1_5 #老街...
3_1_5_1
3_1_5_2
3_1_5_3
3_1_6 #少年宫...
3_1_6_1
3_1_6_2
3_1_6_3
4_1 #票务政策
4_1_1 #车票介绍
4_1_2 #票价政策
4_1_2_1 #票价查询
4_1_2_2 #车牌优惠政策
4_1_2_2_1 #老年人
4_1_2_2_2 #残疾人
4_1_2_2_3 #学生
5_1 #传真
5_1_1.txt, F #传真
5_1_2.txt, F #传真
5_1_3.txt, F #传真
6_1, R #留言
#配置结束
我认为这个配置文件体现了简单明白的原则,允许空行,也允许#开始的注释,缺省的操作属性值可不需要配置。用缩进的空格来表示菜单的层次,比较直观,从配置文件就可以知道流程的结构。
这样的配置文件,不懂编程的地铁热线小姐,完全可以看懂并自行修改。
我设计的这个配置文件格式,按Eric S.Raymond在<Unix程序设计的艺术>一书中的说法,是属于微型语言,是最简单的脚本语言,既然这个微型语言要用Koodoo脚本语言实现,那就不妨戏称为“脚本中的脚本”。
那么剩下的问题就是如何来读取这个配置文件并运行了。
Koodoo语言支持文件读写操作,可以一行一行地读取;看来问题的关键是如何表现语音菜单流程,用怎么样的数据结构?语音菜单流程在内存中是颗树,在Koodoo语言中用数组或散列表就可以轻松表达,主要的数据结构是树中的节点,描述如下:
m["File"] = "2_1_1"; // 文件名
m["Type"] = "P"; // 操作类型: F-传真, R-留言, P-播放语音(缺省值,可不设置)
m["SubNum"] = 2; // 子节点个数, 如为0表示叶节点
m["Level"] = 0; // 本节点所在层数
m["Parent"] = NULL; // 父节点的下标, NULL表示没有父节点
m[1] = 2; // 按1对应的子节点下标
m[2] = 5; // 按2对应的子节点下标
...
所有的节点放在全局数组root[]中,本质上root描述了整个语音菜单树。
好了,“代码不会骗人”,还是看看具体的脚本代码实现的吧(代码并不长,我加了详细的注释):
// 全局常量和变量定义
const LOOP_NUM = 5;
root = 0; // 用这个数组变量描述流程树
// 自定义函数
// 处理配置文件的每一行
// 输入参数:txt - 本行内容,字符串
// 输出参数:level - 本节点所在层次, 0为根节点
// file - 本节点的文件名
// type - 本节点类型,"P"-播音, "F"-传真, "R"-留言
// 返回值:true-是有效的节点, false-无效节点
function ProceTxtLine(txt, level, file, type)
{
level = 0;
file = "";
type = "";
isFirst = true;
isType = false;
// 扫描字符串中的每个字节
for( ch in txt )
{
if( ch==" " || ch=="\t" ) // 如果是空格或制表符
{
if( isFirst ) // 前导空格表示节点的层次级别
level ++;
}
else if( ch=="#" ) // 注释将被忽略
break;
else if( ch=="," ) // 逗号后面是类型信息
isType = true;
else
{
isFirst = false;
if( isType )
type = type + ch;
else
file = file + ch;
}
}
if( file=="" )
return(false);
else
return(true);
}
// 读取菜单配置文件
// 配置文件"Menu.cfg"必须在当前目录下
// 读取后将生成一系列的节点到全局的数组变量root中
// 返回值:0-打不开文件, >0 - 节点数
function ReadMenuCfg()
{
hd = -1; // 文件句柄
FileOpen("Menu.cfg", "rt", hd); // 打开菜单配置文件
if( hd < 0 )
{
DispInfo(0, "Open 'Menu.cfg' Failed!");
return(0);
}
s = "";
f = "";
t = "";
lvl = 0;
count = 0; // 节点计数
while(true)
{
// 读取一行
ret = FileReadLine(hd, s);
if( ret<0 ) // 文件结束了
break;
// 解析本行数据为: 节点层次,文件名,节点类型
ok = ProceTxtLine(s, lvl, f, t);
if( ok )
{
// 构造一个节点
m["File"] = f; // 文件名
m["Type"] = t; // 操作类型: F-传真, R-留言, P或""-播放语音(缺省值,可不设置)
m["Level"] = lvl; // 本节点所在层数
m["SubNum"] = 0; // 子节点个数, 如为0表示叶节点, 子节点项: 为指针列表,如m[1], m[2]...
m["Parent"] = NULL; // 父节点的下标, 如为空则表示为根结点
root[count] = m;
if( lvl>0 ) // 如果不是根节点, 需计算其父节点
{
front = root[count-1]; // 取前面一项
if( lvl == front["Level"] ) // 如果前面一项是自己的兄弟, 则他们有相同的父亲
m["Parent"] = front["Parent"];
else if( lvl == front["Level"]+1 ) // 如果前面的节点比自己少一层,则它是自己的父亲
m["Parent"] = count-1;
else
{
// 其它情况, 要往前搜索自己的兄弟
for(i=count-2; i>0; i--)
{
up = root[i];
if( up["Level"]==lvl )
{
m["Parent"] = up["Parent"];
break; // 找到就退出
}
up = 0;
}
}
front = 0;
// 修改父节点的"SubNum"属性, 即增加子节点个数
parent = root[m["Parent"]];
if( parent!=NULL )
{
n = parent["SubNum"] + 1;
parent["SubNum"] = n;
parent[n] = count; // 增加子节点索引
}
parent = 0;
}
m = 0;
count ++;
}
}
FileClose(hd);
return(count);
}
// 功能说明: 提示菜单选择,接收用户按键,判断是否正确(0)
// 入口参数: voc-提示语音,k-得到的按键码,keysSet-所容许的按键码
function SelMenu(voc, k, keysSet)
{
for(i=0; i<LOOP_NUM; i++)
{
Play(voc);
k = "";
GetKeys(k, 1, 10);
if( k!="" )
{
keysLog = keysLog + k + ","; // 按键记录
in = false;
Strstr(keysSet, k, in);
if( keysSet=="" || in )
{
return(0);
}
}
}
Hangup();
}
// 运行某个节点
// 参数: idx - 节点下标, 0为起始节点也就是根节点
function RootRun(idx)
{
m = 0;
m = root[idx];
n = m["SubNum"]; // 子节点个数, 如为0表示叶节点
f = m["File"]; // 文件名
t = m["Type"]; // 操作类型: F-传真, R-留言, P或""-播放语音(缺省值,可不设置)
lvl = m["Level"]; // 本节点所在层数
parent = m["Parent"]; // 父节点的下标, 如为空则表示为根结点
if( n==0 ) // 叶节点
{
m = 0;
// 根据节点类型进行具体的操作
if( t=="P" || t=="" )
Play(f);
else if( t=="F" )
DispInfo(0, "Fax..."); // Fax(f);
else if( t=="R" )
DispInfo(0, "Record..."); // RecordMail(f);
return(parent);
}
else // 菜单节点
{
k = "";
// 生成本菜单的允许按键序列
kSet = "";
for(i=1; i<=n;i++)
kSet = kSet + i;
kSet = kSet + "90"; // 总是需要:9-返回上一层菜单,0-转人工
SelMenu(f, k, kSet); // 让用户选择菜单
Int(k, i);
if( i==0 )
{
DispInfo(0, "Agent..."); // 转人工
return(idx);
}
else if( i==9 ) // 返回上一层菜单
{
m = 0;
DispInfo(0, "Up Menu...");
if( parent!=NULL )
return(parent);
else
return(idx);
}
else
{
ret = m[i]; // 取得按键对应的子节点索引
return(ret);
}
}
}
// 自定义函数结束
// --------------------
// 读取系统配置项
pthDir = ".\\Voice"; // 普通话路径
gzhDir = pthDir; // 广州话路径
SetSndDir(pthDir, gzhDir, "");
SelectLan(0); // 选择普通话
SetEndKey("#"); // 不足位数按#号结束
SetBreak(true); // 可以打断
SetSndExt(".wav");
SetKeyTime( 8 ); // 设置两个按键之间的最大间隔
ret = ReadMenuCfg();
if( ret<=0 )
return(0);
// --------------------
// 主循环
while(true)
{
WaitRing(2);
Play("Welcome"); // "欢迎播打地铁服务热线"
// 执行IVR流程
idx = 0; // 从根节点开始
while(true)
{
idx = RootRun(idx); // 循环执行
}
Hangup()
OnDisconn(); // 响应用户挂机
Sleep(2);
}
OnSysQuit(); // 响应系统退出
root = 0;
return(0);
我曾经在“图形化误区--再论IVR语音平台的选择”一文中,指出过:
“ 回到IVR流程设计,我认为应该对各阶段的人群进行这样的划分:
1.IVR流程设计者,也就是采用语音平台开发工具的人,他们可能是集成商,也可能的用户单位电脑中心的开发人员,总之是懂电脑的人;
2.IVR系统的日常维护人员,可能很懂电脑,大部分是一般的应用水平;
3.IVR系统的使用者,他们是最终用户,在电话机上操作。”
本文中采用的简单配置文件,就是给“2.IVR系统的日常维护人员,可能很懂电脑,大部分是一般的应用水平;”使用的,也算是对我自己提出的开发原则的一种实践吧。
下载: