记录小程序后端支持中语音文字识别等有意思功能的实现思路
[id_35494429[id_995673373]]

先放几张设计图看下大概功能:
[id_224050024]
从图片中可以观察到,其中包含了一些相对较为复杂的功能模块,例如语音文字识别技术、红包分配的计算算法以及周边红包的计算算法等;而其他部分则主要是基本的增删改查操作。我花费了近一周的时间进行编码和测试,接下来我将简要介绍各个功能点的实现思路和方法。
语音识别
在使用该功能时,A用户设定了一个中文口令红包,而B用户在领取该红包时必须通过语音准确复述该口令,只有当复述内容与设定的口令完全一致,B用户才能获得红包中相应比例的金额。
录音操作是通过调用小程序内置的原生接口来完成的,但在此过程中,遇到了一个问题,那就是微信平台所采用的录音文件格式为.silk。为了解决这个问题,网上提供的方法是先将这种.silk格式的文件转换成wav或MP3格式,之后才能利用各大云服务提供商的接口来执行语音识别任务。
此处采用了该库进行wav格式的转换,随之后又借助百度提供的语音识别公开接口对语音数据进行识别。
业务实现步骤如下:
实现前端录音功能,通过upload接口将silk格式的语音文件上传至数据库,系统自动启动语音识别任务,并向前端异步发送成功反馈,随后前端持续查询识别结果。
由于上传至识别并获取结果这一过程耗时较长,因此建议将识别环节设置为异步处理方式。(第三步)
upload语音接口部分代码:
// ... 业务代码略
获取由本对象生成的语音内容,赋值给变量$voice。// 上传并入库
$this->identifyVoice($voice); // 触发语音识别task
// ...
public function identifyVoice($voice)
{
WorkerUtil::sendTaskByRouteAndParams('task/detectvoice', ['voiceid' => $voice->id, 'type' =>[id_160723[id_1337818091]]]);
}
显而易见,一条信息已被发送至后端task服务,其中包含着指向语音文件的记录标识符及其类别。
后端task服务处理如下:
class DetectVoice extends Action
{
public function run($voiceid, $type = 'redpack')
{
if ($type == 'redpack') {
$voice = Voices::findOne($voiceid);
$url赋值为$voice对象中的voice属性值。
$saveName = '/runtime/redpack-'.$voiceid.'.silk';
$convertName = '/runtime/redpack-'.$voiceid.'.wav';
}
执行保存操作,将远程语音链接中的内容按照指定的本地文件名保存至本地。
$cfg = [
'appKey' => 'xxx',
'appSecret' => 'xxx',
'appId' => 'xxx',
];
$util = new BaiduVoiceUtil($cfg);
$code = exec(执行命令bash,进入路径/www/silk-v3-decoder/,运行脚本converter.sh,并传入参数{$saveName},输出格式为wav。);
if ($code == 0) {
$result等于$util对象调用asr方法,传入转换后的名称作为参数所返回的结果。
if ($result['err_no'] == 0) {
$voicesResult$ 等于将 $result$ 数组转换成 JSON 格式的编码结果。'result'], JSON_UNESCAPED_UNICODE);
将结果赋值给voice对象的result属性,使其等于voicesResult。
执行保存操作,针对变量voice所指向的对象。
@unlink($saveName);
@unlink($convertName);
}
}
}
...
}
task服务的处理流程明确:首先接收待识别的voiceid,接着检索相关记录,将语音文件下载至本地的tmp目录,然后使用shell命令进行格式转换,转换完成后,利用百度的语音识别接口进行识别操作,并将识别结果存入数据库。
voice表结构如下:
如此,便完成了语音识别功能。
红包分配
应用场景:创建红包时
拆分红包通常有两种方式,其一是在红包创建之初就预先设定好每一份的具体额度,其二是在红包开启时进行实时分配。本次我们采用的是第一种方式。
具体讨论可在知乎: 找到。
坦白讲,阅读完这份答案后,我确实收获颇丰,比如对微信红包的构建方式、分配算法等方面有了更深入的理解。
由于我们的应用规模不及微信,故无需过多关注(如负载、并发等问题),产品需求仅是金额分配需采用类似微信红包的分配方式。鉴于此,在考虑扩展性、性能和时间成本后,我直接采纳了陈鹏提供的解决方案,并将其改编为PHP版本。同时,我还选用了redis来存储红包份额,并作为处理潜在并发问题的方案。
先上代码(redpack/create):
红色红包对象是通过调用getCreatedRedPackByBody方法获取的。
// ... 业务逻辑代码略
// 设置随机红包份额
本对象将红包开启概率设定为指定红包参数。
protected function setRedPackOpenOdds($rp)
{
$remainNum = $rp->num;
$remainMoney = $rp->fee;
$key = 'redpack:'.$rp->id;
$redis = yii::$app->redis;
while (!empty($remainNum)) {
此处的金额是通过调用`getRandomMoney`方法,基于剩余数量和剩余金额计算得出的,即:$money = $this->getRandomMoney($remainNum, $remainMoney);。
Redis执行命令,$redis->executeCommand('RPUSH', [$key, $money]);
}
$redis->executeCommand('expire', [$key, 259200]);
}
protected function执行获取随机金额操作,并将剩余数量和剩余金额的引用传递给相应变量。
{
if ($remainNum == 1) {
$remainNum--;
return $remainMoney;
}
$randomNum = StringUtil::getRandom(6, 1);
$seed = $randomNum / 1000000;
$min = 1;
$remainMoney除以$remainNum得到的结果,再乘以系数,即得最大值$max。2;
$money = $seed * $max;
$money = $money <= $min ? $min : ceil($money);
$remainNum--;
$remainMoney -= $money;
return $money;
}
这部分代码逻辑也相对简单,主要就是:
将当前金额与数量数据输入至函数“getRandomMoney”,计算得出本次随机金额后,将该金额存入redis列表中,键名为“redpack:id”。同时,对总金额与总数量进行相应扣除,直至完全扣除完毕。
有几点值得注意的地方:
在原答案中,采用了基于java.math.BigDecimal的随机数生成方法,然而在PHP中,并没有找到与之对应的函数,而且PHP自带的随机数生成功能也并不理想。这里采用了本人独创的随机数生成技术,通过生成六位数的随机数字,再将其除以数字的位数,从而得到如0.608948般的随机数值。此外,每个红包的份额都设定了24小时的过期期限,这一设置旨在实现红包自动过期的功能。
redis里的结果(单位为分):
10元分配15个
100元分配7个:
50元分配25个:
可以看到基本实现了随机分配,也兼顾了手气最佳的要求。
操作简便,只需在领取红包并分配份额时,按照该清单左侧的顺序逐一进行出栈操作即可。
红包地图
应用场景:查看周围发布的红包
实现这一功能的核心在于周边定位的计算方法。首先,必须确保在创建红包时能够获取到精确的经纬度信息,这部分工作由前端负责完成,而我们只需负责记录相关信息。
在调用该接口时,需将用户当前的地理位置坐标传递。依据这些坐标,计算周边区域,进而筛选出表中位于该区域内的相关记录。
代码如下:
/**
*
* @param double $lng 经度
* @param double $lat 纬度
* @param integer $radius 范围
* @return array
*/
public function run($lng, $lat, $radius = 500)
{
此操作通过调用方法getAroundByCoordinates,根据经度$lng和纬度$lat,以及指定的半径$radius,获取周边相关坐标信息。
$field = 'id,lat,lng';
$data = (new Query())
->select($field)
->from('{{app_redpack}}')
->where(sprintf(限制条件为:经度值位于%f至%f之间,纬度值介于%f至%f之间,同时`ishandle`字段等于1,且`isexpire`字段为0。, $coordinates[0], $coordinates[2], $coordinates[1], $coordinates[3]))
->all();
return ResponseUtil通过代码和数据获取输出数组。Api::SUCCESS, $data);
}
/**
* 地球的圆周是24901英里。
* 24,901/360度 = 69.17 英里 / 度
* @param double $longitude 经度
* @param double $latitude 纬度
参数为整数型,代表范围,计量单位为米。
* @return array
*/
public function通过坐标值来获取位置信息($longitude,$latitude),并设定搜索范围($radius)。
{
(double) $degree = (24901 * 1609) / 360.0;
(double) $dpmLat = 1 / $degree;
(double半径值 $radiusLat$ 等于 $dpmLat$ 乘以 $raidus$。
(double计算得到的最小纬度值为:纬度坐标减去纬度半径。
(double计算得到的最大纬度值为:纬度坐标加上纬度半径。
(double$mpdLng$ 的值等于 $degree$ 乘以余弦函数,该函数的参数是 $latitude$ 乘以 $\pi$ 除以 $180$ 的结果。180));
(double) $dpmLng = 1 / $mpdLng;
(double半径长度等于单位长度的经度值乘以半径。
(double计算出的最小经度值为:经度坐标减去搜索半径。
(double计算出的最长经度值为:经度坐标加上搜索半径。
return指定坐标范围分别为:最小纬度点为 $minLat,最小经度点为 $minLng,最大纬度点为 $maxLat,最大经度点为 $maxLng。
}
这个算法的核心是getAroundByCoordinates,它能够依据输入的经纬度数据以及指定的范围尺寸,精确计算出包括左上、左下、右上、右下在内的四个角落的坐标值。若将这些坐标点在地图上标注出来,便形成了一个矩形区域的轮廓。
有兴趣的可以尝试使用这个工具,随意选取一个坐标点,按照既定方法计算出四个角的位置,观察这四个角是否正好位于radius所规定的区域内。
必须指出的是,这项技术并非出自我的笔下,我实在想不起它的来源是何方。不过,我确实记得将Java的执行方案转换为了PHP。对于原作者,我表示诚挚的歉意。