利用Olami SDK 实现语音控制计算器(iOS)

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

二、界面直观化展示

图片描述
其中包括结果FMDB收藏、侧滑栏、主题换肤、音量正余弦动画等多种动画、混合运算算法等等。

三、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;
@end

typedef NS_ENUM(NSInteger, LanguageLocalization) {
LANGUAGE_SIMPLIFIED_CHINESE = 0, //简体中文
LANGUAGE_TRADITIONA_CHINESE = 1 //繁体中文
};
@interface OlamiRecognizer : NSObject
@property (nonatomic,weak) id<OlamiRecognizerDelegate> delegate;
@property (nonatomic, assign,readonly) BOOL isRecording;//是否正在录音
-(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
)appKey api:(NSString
)api appSecret:(NSString
)appSecret cusid:(NSString
)CUSID;
-(void)setVADTimeoutFrontSIL:(unsigned int)value;//设置VAD前端点超时范围 100010000(ms) 默认3000
-(void)setVADTimeoutBackSIL:(unsigned int)value;//设置VAD后端点超时范围 1000
10000(ms) 默认2000
-(void)setInputType:(int) type;//设置是语音输入还是文字输入 0 为语音 1为文字输入
-(void)setLatitudeAndLongitude:(double) latitude longitude:(double)longit;//设置地理位置,参数为经纬度
-(void)sendText:(NSString
)text;//发送输入的文字

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

/**
*CUSID;//终端用户标识id,用来区分各个最终用户 例如:手机的IMEI
*appKey;//创建应用的appkey
*api;//要调用的API类型。现有3种:语义(nli)和分词(seg)和语音(asr)
*appSecret;//加密的秘钥,由应用管理自动生成
*/

define AppKey @""//查看自己的

define AppSecret @""

define macID @""

-(void)setupOLAMI{
_olamiRecognizer= [[OlamiRecognizer alloc] init];
_olamiRecognizer.delegate = self;//此处为OlamiRecognizerDelegate

[_olamiRecognizer setAuthorization:AppKey api:@"asr" appSecret:AppSecret cusid:macID];

//设置语言,目前只支持中文
[_olamiRecognizer setLocalization:LANGUAGE_SIMPLIFIED_CHINESE];

}

识别音量

pragma mark--NLU delegate

  • (void)onUpdateVolume:(float)volume {
    if (_olamiRecognizer.isRecording) {
    _waveView.present = volume/100;
    }
    }

waveview: 根据sin函数 y=Asin(ωx+φ)+b
//e.g.:1.
CGContextRef context = UIGraphicsGetCurrentContext();

CGMutablePathRef path = 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);

float y= (1 - _present) * rect.size.height;

CGPathMoveToPoint(path, NULL, -10, y);
for(float x=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字符串的形式回调过来,对这个字符串进行解析,就可以获得想要的变量。

pragma mark --返回结果

  • (void)onResult:(NSData *)result {
    NSError *error;
    __weak typeof(self) weakSelf = self;
    if (error) {
    NSLog(@"error is %@",error.localizedDescription);
    }else{
    NSDictionary *json = [NSJSONSerialization JSONObjectWithData:result
    options:NSJSONReadingMutableContainers
    error:&error];
    NSLog(@"json=%@",json);

      if ([json[@"status"] isEqualToString:@"ok"]) {
    
    
          NSDictionary *asr = [json[@"data"] objectForKey:@"asr"];
    
          //如果asr不为空,说明目前是语音输入
          if (asr) {
              [weakSelf processASR:asr];
          }
    
    
          NSDictionary *nli = [[json[@"data"] objectForKey:@"nli"] objectAtIndex:0];
          NSDictionary *desc = [nli objectForKey:@"desc_obj"];
          int status = [[desc objectForKey:@"status"] intValue];
          if (status != 0) {// 0 说明状态正常,非零为状态不正常
              NSString *result  = [desc objectForKey:@"result"];
              dispatch_async(dispatch_get_main_queue(), ^{
                  _resultLabel.text = result;//输出不正常提示
                  _resultLabel.font = [UIFont systemFontOfSize:20];
                  [_resultLabel startAnimation];
    
                  _showTextView.text = asr[@"result"];
                  AudioServicesPlaySystemSound (soundID);
              });
    
          }else{
              NSDictionary *semantic = [[nli objectForKey:@"semantic"]
                                        objectAtIndex:0];
              //对slot和算式的处理结果
              [weakSelf processSemantic:semantic asr:asr];
              //处理modifier
              NSArray *modifierArr = [semantic objectForKey:@"modifier"];
    
              [weakSelf processModifier:modifierArr result:desc[@"result"]];
          }
    
    
      }else{
          _showTextView.text = @"请说出要计算的公式";
      }
    

    }

}

pragma mark --处理ASR语音对话节点

  • (void)processASR:(NSDictionary*)asrDic {
    NSString *result = [asrDic objectForKey:@"result"];
    if (result.length == 0) { //如果结果为空,则弹出警告框
    [self showAlert:@"没有接受到语音,请重新输入!"];
    return;
    }else{
    dispatch_async(dispatch_get_main_queue(), ^{
    NSString *str = [result stringByReplacingOccurrencesOfString:@" " withString:@""];//去掉字符中间的空格
    NSLog(@"answer result = %@",str);
    });
    }
    }

//处理semantic节点返回的slot

  • (void)processSemantic:(NSDictionary*)semanticDic asr:(NSDictionary *)asr {
    NSMutableArray *sumArr = [NSMutableArray array];
    for (NSDictionary *dic in semanticDic[@"slots"]) {
    NSString *nameStr = dic[@"name"];
    //遍历,然后把slot添加到数组里
    NSString *textStr = [[sumArr componentsJoinedByString:@","] stringByReplacingOccurrencesOfString:@"," withString:@""];
    NSLog(@"textstr=%@",textStr);

    if (![textStr isEqualToString:@""]) {
    _passString = [self replaceInputStrWithPassStr:textStr];

      if (asr) {
          _lastAnswer = _resultLabel.text;//语音记录上一次记录
      }else{
          _lastAnswer = @"";
      }
    
      //第一次运算或者不再加
      if ([_lastAnswer isEqualToString:@"error"]||[_lastAnswer isEqualToString:@""]) {
          if (asr) {
              dispatch_async(dispatch_get_main_queue(), ^{
                  _showTextView.text = [[textStr stringByReplacingOccurrencesOfString:@"2√" withString:@"√"] stringByAppendingString:@"="];//计算公式
              });
              textStr = [_calcultor calculatingWithString:_passString andAnswerString:@"0"];
          }else{
              textStr = [_calcultor calculatingWithString:_passString andAnswerString:@"0"];
          }
    
    
      //有结果考虑再运算的步骤
      }else{
    
          //有结果再运算的情况
          UniChar c = [_passString characterAtIndex:0];
          if (c =='-'|| c == '+'||c == 'x'||c =='/')
          {
              dispatch_async(dispatch_get_main_queue(), ^{
                  _showTextView.text = [[_lastAnswer stringByAppendingString:[textStr stringByReplacingOccurrencesOfString:@"2√" withString:@"√"]] stringByAppendingString:@"="];//计算公式
              });
    
              textStr = [_calcultor calculatingWithString:[_lastAnswer stringByAppendingString:_passString] andAnswerString:@"0"];//
          }
          //有结果但是不想再运算
          else{
              dispatch_async(dispatch_get_main_queue(), ^{
                  _showTextView.text = [[textStr stringByReplacingOccurrencesOfString:@"2√" withString:@"√"] stringByAppendingString:@"="];//计算公式
              });
              textStr = [_calcultor calculatingWithString:_passString andAnswerString:@"0"];
          }
    
      }
    
      dispatch_async(dispatch_get_main_queue(), ^{
              AudioServicesPlaySystemSound (soundID1);
    
              _resultLabel.font = [UIFont systemFontOfSize:50.0];
              _resultLabel.text = textStr;
      });
    
      [_resultLabel startAnimation];
    

    }
    }

后台返回:语音内容是显示在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

您喜欢的话还请多多支持,不明白可以留言也欢迎加qq、微信讨论:121003626

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,723评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,485评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,998评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,323评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,355评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,079评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,389评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,019评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,519评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,971评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,100评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,738评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,293评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,289评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,517评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,547评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,834评论 2 345

推荐阅读更多精彩内容