博客链接:http://blog.csdn.net/scarlettzhao0602/article/details/76576836


一、简介:
Olami Calculator是一款在键盘输入算式的普通计算器的基础上,增加了支持语音控制输入算式输出结果的人工智能计算器。此外还增加了多种动画效果,计算结果提示音功能,多元化主题换肤功能,以及保存计算公式,侧滑栏查看收藏记录等功能。网上也有许多语音计算器,但是打开看,只是添加了按钮提示音等,并不能识别我们对着计算器说的内容,而Olami Calculator可以实现不用手动敲击键盘,只需要把想知道结果的算式对着语音计算器说出来,例如三加四乘五、清空等,然后Olami会根据自己的一套语音识别系统帮我们准确识别出来。真正做到一款语音控制的计算器。

二、界面直观化展示



三、Olami SDK的配置
step 1:创建工程与导入sdk
Olami SDK下载地址:https://github.com/olami-developers/olami-sdk-ios.git
下载下来之后我们可以看到sdk-libs文件夹:,把libOlamiRecognizer.a静态库文件和OlamiRecognize.h对外提供接口文件拖入到我们的工程中。配置工程:

step 2 : 创建Olami应用
点击https://cn.olami.ai/open/website/home/home_show注册创建个人账号并登陆。进入后创建新应用,建好之后进入应用管理可以看到下面界面
点击查看key:


step 3:可直接导入的语音模块
点击配置模块选择你需要的语音模块,目前有天气,二十四点,新闻,听书,数学等50~多个模块,
,供大家选择的还是很多的。也可以点击“进入NLI系统”,再点击导入,可以看到如下界面,这里也是已经有的模块有需要的直接导入:


step 4:自定制语音模块

olami平台会为广大开发者提供一些已经写好了的语法模块,如果提供给大家的模块不能满足当下解析录入语音的需求,那么不要慌,下面就是教大家如何定制属于自己的模块。
首先.登录,进入我的应用(没有应用的话记得创建新应用哦),然后点击“进入NLI系统”。下面是点击之后的界面,可以看到右上角有导入和新增

如果没有所需的模块,那么就需要点击”新增“。我们做的是计算器那就给个名字,输入calculate,提交成功后可以看到我的模块里面有了一个新模块,:


点击calculate后面的进入模块。界面中有例句库,grammar,rule,slot,template模板。



现在做的计算器,那需要olami为我们识别出什么呢?
比如:9+8+7 就这个算式而言,我们对着蜜蜜说完,是希望把数字还有符号都给我们识别出来的。

分析:”9”、”+”、”8”、”+”、”7”是我们需要系统帮我们识别并且返回给我们的变量,那就可以在slot设置5个变量,slot有五种类型(这里数字用float、符号用internal),rule是一些临时的中间表达式:[等于|结果是],modifier传递预定义好的信息,不管是slot还是rule都是为grammar服务的,要显示句子要写grammar。
各举一个例子:
grammar:名称:两个数结果等于多少 内容:[<再>][<数字一>][<符号一>][<数字二>][<结果是>|<等于几>]
slot:名称:数字一 类型:float 最长:50 最短:1
rule:名称:结果是 内容:[的]结果[是|等于[多少|几]] (|:或 []:可以省略的)。
要更多的了解点击这里查看OSL 语法描述语言 grammar的简介:https://cn.olami.ai/wiki/?mp=overview&content=quickstart.html。
一切就绪提交成功了之后,就可以测试了,测试无误满足需求,点击“发布”就可以使用啦!

上图:

1.新增grammar:

2.添加语料:写出希望可以识别的一句grammar,测试并提交

3.最后测试无误一定要点发布

4.完成配置
以上都完成了回到应用管理,我们就可以配置自己搭建的模块了!

5.再测试
噔噔噔噔~可以使用了,变量都帮我们识别出来了!


四、代码处实现

先来看下OlamiRecognizer.h为我提供了哪些接口

*返回结果*/-(void)onResult:(NSData*)result;/**取消本次会话*/-(void)onCancel;/**识别失败*/-(void)onError:(NSError*)error;/**音量的大小音频强度范围时0到100*/-(void)onUpdateVolume:(float)volume;/****开始录音*/-(void)onBeginningOfSpeech;/***结束录音**/-(void)onEndOfSpeech;@endtypedefNS_ENUM(NSInteger,LanguageLocalization){LANGUAGE_SIMPLIFIED_CHINESE=0,//简体中文LANGUAGE_TRADITIONA_CHINESE=1//繁体中文};@interfaceOlamiRecognizer:NSObject@property(nonatomic,weak)id<OlamiRecognizerDelegate>delegate;@property(nonatomic,assign,readonly)BOOLisRecording;//是否正在录音-(void)start;//开始录音-(void)stop;//结束录音,开始识别-(void)cancel;//取消本次回话/***设置语系的选项,目前只支持一种,简体中文*/-(void)setLocalization:(LanguageLocalization)location;/***CUSID;//终端用户标识id,用来区分各个最终用户例如:手机的IMEI*appKey;//创建应用的appkey*api;//要调用的API类型。现有3种:语义(nli)和分词(seg)和语音(asr)*appSecret;//加密的秘钥,由应用管理自动生成*/-(void)setAuthorization:(NSString*)appKeyapi:(NSString*)apiappSecret:(NSString*)appSecretcusid:(NSString*)CUSID;-(void)setVADTimeoutFrontSIL:(unsignedint)value;//设置VAD前端点超时范围1000~~10000(ms)默认3000-(void)setVADTimeoutBackSIL:(unsignedint)value;//设置VAD后端点超时范围1000~~10000(ms)默认2000-(void)setInputType:(int)type;//设置是语音输入还是文字输入0为语音1为文字输入-(void)setLatitudeAndLongitude:(double)latitudelongitude:(double)longit;//设置地理位置,参数为经纬度-(void)sendText:(NSString*)text;//发送输入的文字

项目中,首先 初始化Olami语音识别对象并设置代理

/***CUSID;//终端用户标识id,用来区分各个最终用户例如:手机的IMEI*appKey;//创建应用的appkey*api;//要调用的API类型。现有3种:语义(nli)和分词(seg)和语音(asr)*appSecret;//加密的秘钥,由应用管理自动生成*/#defineAppKey@""//查看自己的#defineAppSecret@""#definemacID@""-(void)setupOLAMI{_olamiRecognizer=[[OlamiRecognizeralloc]init];_olamiRecognizer.delegate=self;//此处为OlamiRecognizerDelegate[_olamiRecognizersetAuthorization:AppKeyapi:@"asr"appSecret:AppSecretcusid:macID];//设置语言,目前只支持中文[_olamiRecognizersetLocalization:LANGUAGE_SIMPLIFIED_CHINESE];}12345678910111213141516171819201234567891011121314151617181920

设置一个录音键

#pragmamark--录音键-(IBAction)recordButton:(UIButton*)sender{//设置为语音模式(代理方法:0为语音)[_olamiRecognizersetInputType:0];//开始录音if(_olamiRecognizer.isRecording){//isRecording=YES即为录音模式[_olamiRecognizerstop];//代理方法[_recordButtonsetImage:[UIImagep_w_picpathNamed:@"话筒4.png"]forState:UIControlStateNormal];}else{[_olamiRecognizerstart];//代理方法[_recordButtonsetImage:[UIImagep_w_picpathNamed:@"话筒7.png"]forState:UIControlStateNormal];[_recordButton.layeraddAnimation:[selfshine]forKey:@"shine"];//添加一个动画}}//发光动画-(CABasicAnimation*)shine{CABasicAnimation*animation=[CABasicAnimationanimationWithKeyPath:@"shine"];animation.fromValue=[NSNumbernumberWithFloat:1.0f];animation.toValue=[NSNumbernumberWithFloat:0.0f];animation.autoreverses=YES;animation.duration=0.5;animation.repeatCount=MAXFLOAT;animation.removedOnCompletion=NO;animation.fillMode=kCAFillModeForwards;animation.timingFunction=[CAMediaTimingFunctionfunctionWithName:kCAMediaTimingFunctionEaseIn];returnanimation;}#pragmamark--录音结束(代理方法)-(void)onEndOfSpeech{[_recordButtonsetImage:[UIImagep_w_picpathNamed:@"话筒4.png"]forState:UIControlStateNormal];[_recordButton.layerremoveAnimationForKey:@"shine"];}

识别音量

#pragmamark--NLUdelegate-(void)onUpdateVolume:(float)volume{if(_olamiRecognizer.isRecording){_waveView.present=volume/100;}}waveview:根据sin函数y=Asin(ωx+φ)+b//e.g.:1.CGContextRefcontext=UIGraphicsGetCurrentContext();CGMutablePathRefpath=CGPathCreateMutable();CGContextSetLineWidth(context,3);CGContextSetLineCap(context,kCGLineCapRound);CGContextSetAllowsAntialiasing(context,true);CGContextSetRGBStrokeColor(context,124/255.0,145/255.0,155/255.0,1.0);CGContextBeginPath(context);floaty=(1-_present)*rect.size.height;CGPathMoveToPoint(path,NULL,-10,y);for(floatx=0;x<=rect.size.width;x++){y=sin(3*x/rect.size.width*M_PI+moveX/rect.size.width*M_PI)*maxA+_currentLinePointY;CGPathAddLineToPoint(path,nil,x,y);}CGContextAddPath(context,path);CGContextDrawPath(context,kCGPathStroke);CGPathRelease(path);

界面差不多就这些,主要是看返回来的result
调用代理这个方法-(void)onResult:(NSData*)result; 其语义分析后的结果以一个json字符串的形式回调过来,对这个字符串进行解析,就可以获得想要的变量。

#pragmamark--返回结果-(void)onResult:(NSData*)result{NSError*error;__weaktypeof(self)weakSelf=self;if(error){NSLog(@"erroris%@",error.localizedDescription);}else{NSDictionary*json=[NSJSONSerializationJSONObjectWithData:resultoptions:NSJSONReadingMutableContainerserror:&error];NSLog(@"json=%@",json);if([json[@"status"]isEqualToString:@"ok"]){NSDictionary*asr=[json[@"data"]objectForKey:@"asr"];//如果asr不为空,说明目前是语音输入if(asr){[weakSelfprocessASR:asr];}NSDictionary*nli=[[json[@"data"]objectForKey:@"nli"]objectAtIndex:0];NSDictionary*desc=[nliobjectForKey:@"desc_obj"];intstatus=[[descobjectForKey:@"status"]intValue];if(status!=0){//0说明状态正常,非零为状态不正常NSString*result=[descobjectForKey:@"result"];dispatch_async(dispatch_get_main_queue(),^{_resultLabel.text=result;//输出不正常提示_resultLabel.font=[UIFontsystemFontOfSize:20];[_resultLabelstartAnimation];_showTextView.text=asr[@"result"];AudioServicesPlaySystemSound(soundID);});}else{NSDictionary*semantic=[[nliobjectForKey:@"semantic"]objectAtIndex:0];//对slot和算式的处理结果[weakSelfprocessSemantic:semanticasr:asr];//处理modifierNSArray*modifierArr=[semanticobjectForKey:@"modifier"];[weakSelfprocessModifier:modifierArrresult:desc[@"result"]];}}else{_showTextView.text=@"请说出要计算的公式";}}}#pragmamark--处理ASR语音对话节点-(void)processASR:(NSDictionary*)asrDic{NSString*result=[asrDicobjectForKey:@"result"];if(result.length==0){//如果结果为空,则弹出警告框[selfshowAlert:@"没有接受到语音,请重新输入!"];return;}else{dispatch_async(dispatch_get_main_queue(),^{NSString*str=[resultstringByReplacingOccurrencesOfString:@""withString:@""];//去掉字符中间的空格NSLog(@"answerresult=%@",str);});}}//处理semantic节点返回的slot-(void)processSemantic:(NSDictionary*)semanticDicasr:(NSDictionary*)asr{NSMutableArray*sumArr=[NSMutableArrayarray];for(NSDictionary*dicinsemanticDic[@"slots"]){NSString*nameStr=dic[@"name"];//遍历,然后把slot添加到数组里NSString*textStr=[[sumArrcomponentsJoinedByString:@","]stringByReplacingOccurrencesOfString:@","withString:@""];NSLog(@"textstr=%@",textStr);if(![textStrisEqualToString:@""]){_passString=[selfreplaceInputStrWithPassStr:textStr];if(asr){_lastAnswer=_resultLabel.text;//语音记录上一次记录}else{_lastAnswer=@"";}//第一次运算或者不再加if([_lastAnswerisEqualToString:@"error"]||[_lastAnswerisEqualToString:@""]){if(asr){dispatch_async(dispatch_get_main_queue(),^{_showTextView.text=[[textStrstringByReplacingOccurrencesOfString:@"2√"withString:@"√"]stringByAppendingString:@"="];//计算公式});textStr=[_calcultorcalculatingWithString:_passStringandAnswerString:@"0"];}else{textStr=[_calcultorcalculatingWithString:_passStringandAnswerString:@"0"];}//有结果考虑再运算的步骤}else{//有结果再运算的情况UniCharc=[_passStringcharacterAtIndex:0];if(c=='-'||c=='+'||c=='x'||c=='/'){dispatch_async(dispatch_get_main_queue(),^{_showTextView.text=[[_lastAnswerstringByAppendingString:[textStrstringByReplacingOccurrencesOfString:@"2√"withString:@"√"]]stringByAppendingString:@"="];//计算公式});textStr=[_calcultorcalculatingWithString:[_lastAnswerstringByAppendingString:_passString]andAnswerString:@"0"];//}//有结果但是不想再运算else{dispatch_async(dispatch_get_main_queue(),^{_showTextView.text=[[textStrstringByReplacingOccurrencesOfString:@"2√"withString:@"√"]stringByAppendingString:@"="];//计算公式});textStr=[_calcultorcalculatingWithString:_passStringandAnswerString:@"0"];}}dispatch_async(dispatch_get_main_queue(),^{AudioServicesPlaySystemSound(soundID1);_resultLabel.font=[UIFontsystemFontOfSize:50.0];_resultLabel.text=textStr;});[_resultLabelstartAnimation];}}

后台返回:语音内容是显示在asr字段里,大家可能会有疑问后台怎么识别的我们语音的内容,这是由于我们之前在olami平台创建新应用后导入了一套识别相应内容的grammar,这样olami的语义解析功能会为我们自动识别出想要得到的变量内容。

比如我说:3+6乘九等于几?
对应grammar语法:[<再>][<数字一>]<符号一><数字二><符号二><数字三>[<结果>|<等于>]
返回结果:

json={data={asr={final=1;result="\U4e09\U52a0\U516d\U4e58\U4e5d\U7b49\U4e8e\U51e0";"speech_status"=0;status=0;};nli=({"desc_obj"={status=0;};semantic=({app=calculator;customer=59530feb84aea6f385319c65;input="\U4e09\U52a0\U516d\U4e58\U4e5d\U7b49\U4e8e\U51e0";modifier=();slots=({name=number3;"num_detail"={"recommend_value"=9;type=float;};value="\U4e5d";},{name=number1;"num_detail"={"recommend_value"=3;type=float;};value="\U4e09";},{name=number2;"num_detail"={"recommend_value"=6;type=float;};value="\U516d";},{name=symbol1;value="+";},{name=symbol2;value=x;});});type=calculator;});};status=ok;}

再加三等于几?
对应grammar:[<再>][<数字一>][<符号一>][<数字二>][<结果>|<等于>] 、
后台返回json字段:

json={data={asr={final=1;result="\U518d\U52a0\U4e09\U7b49\U4e8e\U51e0";"speech_status"=0;status=0;};nli=({"desc_obj"={status=0;};semantic=({app=calculator;customer=59530feb84aea6f385319c65;input="\U518d\U52a0\U4e09\U7b49\U4e8e\U51e0";modifier=();slots=({name=again;value=a;},{name=number2;"num_detail"={"recommend_value"=3;type=float;};value="\U4e09";},{name=symbol1;value="+";});});type=calculator;});};status=ok;}

计算过程:涉及到算法,数据结构堆栈问题,大概思路设置优先级,设置两个栈,一个数据栈,一个运算符栈,在运算符栈底添加#方便处理。获取表达式第一个元素如果是数据添加到数据栈中,元素如果是运算符,那么每次都要跟运算符栈定元素比较优先级,如果取得的运算符的优先级大于栈顶元素优先级时,该运算符直接进栈,优先级不大的话,就要取栈顶运算符优先运算,最后碰到#停止。如果有记忆上一轮结果的话,结果需要放到数据栈栈进行下一次处理



代码下载地址:https://github.com/zhaoshihui/calculator_olami_ios.git