蓝星际语音平台 脚本例子
- 这些例子都比较简单, 有些比较有趣,您很容易看明白,仔细阅读源程序可以让您迅速入门,也会提高您的水平。
- 每个例子都有一个说明文档
,请您在运行例子之前阅读它。 - 请将蓝星际平台程序拷贝到相应的目录之下,然后运行。因为有些脚本使用了相对路径,所以建议脚本文件和蓝星际平台程序放在相同的目录。
地铁热线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系统的日常维护人员,可能很懂电脑,大部分是一般的应用水平;”使用的,也算是对我自己提出的开发原则的一种实践吧。
下载: