V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
ssttm169
V2EX  ›  前端开发

Web 端声纹识别

  •  
  •   ssttm169 ·
    ssttm169 · 2018-06-04 10:40:21 +08:00 · 2161 次点击
    这是一个创建于 2365 天前的主题,其中的信息可能已经有所发展或是发生改变。

    最近做一个微信的口令红包的功能,准备都要投入使用了, 老板突然发愁,他说 现在的羊毛党这么猖狂,一不小心,10 万的推广红包,会不会两天就挨刷完了? ....那我们能否做一个功能校验一下是否同一个人来领取红包,不就得了吗? 他一拍脑袋,接着说,Tom 你给我们做一个声纹识别吧!


    说干就干,在寻找 声纹识别服务商,发现什么科大讯飞,还什么 BAT 等许多大厂都没有支持 Web 端的,后来找到一个不知名的小厂。。

    具体的流程如下:

    具体流程


    声纹注册用户(最终效果图)

    注册效果图


    声纹登录(最终效果图)

    登陆效果图


    上传文件识别:

    上传文件识别


    pm2 线程

    pm2 线程

    服务端

    因为声纹识别服务商 不能直接使用客户端直接调用 和 音频不支持的问题,要开发自己的服务端来对接。

    技术栈 koa + co-wecaht-api + mysql + ffmpeg + pm2 + knex

    注:因服务商不支持微信 amr 文件, 要用 ffmpeg 把微信的音频 amr 文件转码成 wav。

    以下是一些相关的代码,,开撸。。

    微信 jssdk 开发 如果你微信 API 这一块已经很熟悉了,跳到下一节

    获取微信 token

    var api = await new WechatAPI(
    	config.appid,
    	config.appsecret,
    	async () => {
    		// 传入一个获取全局 token 的方法
    		var txt = await fs.readFile("./token/access_token.txt", "utf8");
    		return JSON.parse(txt);
    	},
    	async token => {
    		// 请将 token 存储到全局,跨进程、跨机器级别的全局,比如写到数据库、redis 等
    		// 这样才能在 cluster 模式及多机情况下使用,以下为写入到文件的示例
    		await fs.writeFile("./token/access_token.txt", JSON.stringify(token));
    	}
    );
    

    注:如果报读取不了 token 文件,就手动在相应的目录,新建的文本文件, 比如 access_token.txt


    获取微信签名

    var jsapi_ticket = await api.getLatestTicket();
    let nonce_str = 'abcdefg';    // 密钥,字符串任意,可以随机生成
    let timestamp = parseInt(new Date().getTime() / 1000) + '';  // 时间戳
    let url = ctx.request.body.url;   // 使用接口的 url 链接,不包含#后的内容
    let str = 'jsapi_ticket=' + jsapi_ticket.ticket + '&noncestr=' + nonce_str + '&timestamp=' + timestamp + '&url=' + url;
    let signature = sha1(str);
    
    ctx.body = {
        appId: config.appid,
        timestamp: timestamp,
        nonceStr: nonce_str,
        signature: signature
    }
    

    跨域请求

    const Koa = require("koa");
    const app = new Koa();
    const cors = require("koa-cors");
    .....
    app.use(
    	cors({
    		origin: "http://www.xxxx.com",
    		maxAge: 5,
    		credentials: true,
    		allowMethods: ["OPTIONS", "GET", "POST", "DELETE"],
    		allowHeaders: ['Content-Type', 'Accept']
    	})
    );
    

    ffmpeg 转码

    const ffmpeg = require('fluent-ffmpeg');
    ....
    var command = ffmpeg(_delPath.amr)
    .audioBitrate('16k')  //16k 音频采样率
    .audioFrequency(16000)  //16 比特音频信号
    .audioQuality(10)   //音频质量
    .on('end', function() {
    	console.log('file has been converted succesfully');
    	resolve();
    })
    .on('error', function(err) {
    	reject(err.message)
    	console.log('an error happened: ' + err.message);
    })
    .save(_delPath.fix);
    

    提交声纹服务器

    const rp = require("request-promise");
    .....
    var vprData = {
        method: "POST",
        url: "http://www.xxxx.com",
        headers: {
            "cache-control": "no-cache",
            "x-udid": "xxxxxx",
            "x-session-key": "xxxx",
            "x-task-config": "xxxxxx",
            "x-request-date": "xxxxxx",
            "x-sdk-version": "5.1",
            "x-app-key": "xxxxxxx"
        },
        formData: {
            // Like <input type="file" name="file">
            file: {
                value: fs.createReadStream(soundData.path),
                options: {
                    filename: soundData.name,
                    contentType: soundData.type //mp3 = audio/mpeg, wav = audio/wav
                }
            }
        }
    };
    var xml = await rp(vprData);
    
    //xml to json
    var resJson = {};
    var parseString = require('xml2js').parseString;
    await new Promise((resolve, reject) => {
        parseString(xml.toString(), async (err, result) => {
            resJson = result.ResponseInfo;
           	//do something
            resolve();
        });
    });
    



    客户端

    技术栈 vue + vue-router + axios。

    去掉微信 长按 弹出复制的按钮

    mounted() {
    	document.oncontextmenu = function(e) {
    		e.preventDefault();
    	};
    
        //初始化 微信 jssdk
    	vm.wx_init();
    }
    

    获取微信签名,注册事件

    wx.config({
    	debug: false, // 开启调试模式,调用的所有 api 的返回值会在客户端 alert 出来,若要查看传入的参数,可以在 pc 端打开,参数信息会通过 log 打出,仅在 pc 端时才会打印。
    	appId: res.appId, // 必填,公众号的唯一标识
    	timestamp: res.timestamp, // 必填,生成签名的时间戳
    	nonceStr: res.nonceStr, // 必填,生成签名的随机串
    	signature: res.signature, // 必填,签名,见附录 1
    	jsApiList: [
    		"onMenuShareTimeline",
    		"onMenuShareAppMessage",
    		"uploadVoice",
    		"startRecord",
    		"playVoice",
    		"stopRecord",
    		"onVoicePlayEnd"
    	] // 必填,需要使用的 JS 接口列表,所有 JS 接口列表见附录 2
    });
    

    提前提示用户授权录音功能, 为了避免 正式开始录音时,同时提示授权,此时录音功能状态已经失控。

    if (!localStorage.rainAllowRecord || localStorage.rainAllowRecord !== "true" ) {
    	wx.startRecord({
    		success: function() {
    			localStorage.rainAllowRecord = "true";
    			wx.stopRecord();
    		},
    		cancel: function() {
    			alert("用户拒绝授权录音");
    		}
    	});
    }
    

    好了,talk is cheap, show you the code.

    Github 源代码在此, 给星星的人都很美~

    yimity
        1
    yimity  
       2018-06-04 10:55:49 +08:00
    我想知道的是,正确率有多少。
    不过还是要赞美一下。很棒。
    takato
        2
    takato  
       2018-06-04 11:09:43 +08:00 via iPhone
    现在的情况是:敢用模型对方就敢用 GAN
    ∠( ᐛ 」∠)_
    paparika
        3
    paparika  
       2018-06-04 11:39:02 +08:00
    领过之后可以在后台记录 ta 的微信 id 防止重复领吧,还是我理解错了
    EchoChan
        4
    EchoChan  
       2018-06-04 12:32:24 +08:00
    能识别合成的声音?
    1stPLACE
        5
    1stPLACE  
       2018-06-04 12:32:36 +08:00
    我记得某个区块链交易所的实名认证就是用了微信端网页语音认证。
    SingeeKing
        6
    SingeeKing  
       2018-06-04 16:25:07 +08:00
    @paparika #3 意思应该是防止羊毛党「一个人多个微信账号领取」
    ssttm169
        7
    ssttm169  
    OP
       2018-06-04 16:45:06 +08:00
    @1stPLACE 是哪个区块链交易所?还记得不? 发过来我体验一下。。~~谢谢。
    ssttm169
        8
    ssttm169  
    OP
       2018-06-04 16:46:26 +08:00
    @yimity 准确率不是很高, 我现在设定是录音时间是 5 秒,,如果录音时间长一点的话就准确率就高一点。
    ssttm169
        9
    ssttm169  
    OP
       2018-06-04 16:47:36 +08:00
    @takato GAN,没有研究过这个,谢谢你提醒。。
    ssttm169
        10
    ssttm169  
    OP
       2018-06-04 16:49:21 +08:00
    @paparika 我设置每个微信 ID 只能领取一个红包,,但是专业的羊毛党,一般是一个控制成千上万的 ID,但如果有声纹识别,他就没戏了,不过这个功能,还在实验当中。。。
    ssttm169
        11
    ssttm169  
    OP
       2018-06-04 16:50:08 +08:00
    @EchoChan 估计不能,,现在技术还没有这么先进吧~~
    takato
        12
    takato  
       2018-06-04 16:56:28 +08:00
    @ssttm169 相关资料可以通过搜索这个关键词找到 Generative adversarial network
    EchoChan
        13
    EchoChan  
       2018-06-04 17:12:56 +08:00
    @ssttm169 如果不能识别合成的声音,那就防不住专业的羊毛党了。
    ssttm169
        14
    ssttm169  
    OP
       2018-06-04 17:17:17 +08:00
    @EchoChan 恩,,是的,这个功能还在实验阶段,,还没有正式投入使用呢~~
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5340 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 27ms · UTC 01:30 · PVG 09:30 · LAX 17:30 · JFK 20:30
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.