欢迎来到爱学习爱分享,在这里,你会找到许多有趣的技术 : )

5 分钟 Serverless 实践:构建无服务器的敏感词过滤后端系统

开发者头条 1021℃
前  言

本文将以敏感语音屏蔽为例,介绍了如何构建一个敏感词过滤的无服务器Web应用。

函数工作流

函数工作流(FunctionGraph,FGS)是一项基于事件驱动的函数托管计算服务,托管函数具备以毫秒级弹性伸缩、免运维、高可靠的方式运行。通过函数工作流,开发者无需配置和管理服务器,只需关注业务逻辑,编写函数代码,以无服务器的方式构建应用,便能开发出一个弹性高可用的后端系统,并按实际运行消耗的资源计费。极大地提高了开发和运维效率,减小了运作成本。

相比于传统的架构,函数工作流构建的无服务器架构具有如下优点:

  1. 无需关注任何服务器,只需关注核心业务逻辑,提高开发和运维效率。

  2. 函数运行随业务量弹性伸缩,按需付费,执行才计费,对于负载波峰波谷非常明显的场景可以减少大量成本。

  3. 通过简单的配置即可连通函数工作流和其它各云服务,甚至云服务和云服务;

构建无服务器的语音敏感词过滤Web应用

为了进一步让大家感受函数工作流的优势,我们将介绍如何通过函数工作流快速构建一个无服务器的语音检测和屏蔽系统。如下图,该系统会识别用户上传的语音是否包含敏感信息(如色情、暴力、恐吓等),并将语音转换为对应的文字,且过滤掉敏感词。

试想,如果我们通过传统的模式开发此应用,需要如何开发?

即使是基于现在的云平台,我们也仍需要购买云服务器,关注其规格、镜像、网络等各指标的选型和运维,然后在开发过程中可能还需要考虑与其他云服务的集成使用问题,使代码中耦合大量非业务代码,并且服务器等资源也并非是按需的,特别是对于访问量波峰波谷非常明显的场景,会造成大量多余的费用。

现在我们可以通过函数工作流服务来快速构建这个系统,并且完全无需关注服务器,同时弹性伸缩运行、按需计费,如图:

创建函数,在函数中调用华为云语音服务提供的短语音识别接口,将语音转换为文字,然后再调用内容检测服务的文本检测接口,实现文本的敏感词检测,进而判断语音是否合规,并将不合规的词语屏蔽掉。再为该函数配置一个APIG触发器,这样便可以对外提供一个语音敏感词检测的API,再部署前端页面到OBS,托管为静态网站,从而构建出一个完整的语音敏感词检测的无服务器Web应用。页面调用API,他会自动触发函数执行,而开发者编写的函数只需实现接收到语音之后如何处理语音的逻辑即可,最后将结果返回给页面。

接下来,我们将介绍如何完整地将此无服务器Web应用构建出来。

1. 准备工作

进入华为云内容检测服务,申请开通语音服务的短语音检测和内容检测服务的文本内容检测,成功申请后便可以调用语音识别和文本检测接口了。

2. 创建函数

进入函数工作流服务页面,创建函数,实现语音识别、文本检测的接口调用和敏感词过滤,代码如下(Nodejs 8.10):

const https = require('https');
const _ = require('lodash');

exports.handler = async (event, context) => {
    // 跨域验证
    if (event.httpMethod === 'OPTIONS') {
        console.log('OPTIONS request, pass.');
        const result = constructResponse('', 200);
        return result;
	} else if (event.httpMethod !== 'POST') {
        console.log('Get error http method: ', event.httpMethod);
        return constructResponse('Only POST method allowed', 401);
    }
    
    CONTEXT = context;
    const body = getBody(event);
    if (body.voice) {
        // 分析语音
        console.log('Start check the voice...');
        let error = '';
        const voiceResult = await checkVoice(body).catch((ex) => {
            error = ex;
        });
        return error ? constructResponse(error, 500) : constructResponse(voiceResult, 200);
    } else if (body.text) {
        // 分析文本
        console.log('Start check the text...');
        let error = '';
        const textResult = await checkText(body).catch((ex) => {
            error = ex;
        });
        return error ? constructResponse(error, 500) : constructResponse(textResult, 200);
    } else {
        console.log('Input parameter error: empty content');
        return constructResponse('Input parameter error: empty content', 401);
    }
}

function constructVoicePayload(body) {
    return {
        "data": body.voice || '',
        "encode_type": "wav",
        "sample_rate": body.sample_rate || '16k'    // '8k', '16k'
    };
}

function constructTextPayload(body, text = '') {
    const payload = {
        categories: ['porn','ad','politics','abuse','contraband'],
        items: [{type: 'content','text': ''}]
    };
    payload.items[0].text = text ? text : (body.text || '');
    return payload;
}

function getBody(event) {
    const bodyStr = new Buffer(event.body, 'base64').toString('ascii');
    let body = {};
    try {
     	body = JSON.parse(bodyStr);
    } catch(ex) {
        console.log('Parse body failed: ', ex);
    }
    return body;
}

async function checkVoice(body) {
    const voiceText = await voiceToText(body);
    return voiceText ? await checkText(body, voiceText) :  {suggestion: 'pass', text: ''};
}

async function voiceToText(body) {
    const options = {
        hostname: 'ais.cn-north-1.myhwclouds.com',
        port: 443,
        path: '/v1.0/voice/asr/sentence',
        headers: {
            'Content-Type': 'application/json;charset=utf8',
        }
    };
    const payload = constructVoicePayload(body);
    const result = await post(payload, options);
    const { status, data } = result;
    if (status === 200) {
        console.log('Translate voice to text successfully, text:', JSON.parse(data).result.words);
        return JSON.parse(data).result.words || '';
    } else {
        throw(`Failed translate voice to text: ${status} ${data}`);    
    }
}

async function checkText(body, text = '') {
    const options = {
        hostname: 'ais.cn-north-1.myhwclouds.com',
        port: 443,
        path: '/v1.0/moderation/text',
        headers: {
            'Content-Type': 'application/json;charset=utf8',
        }
    };
    const payload = constructTextPayload(body, text);
    const result = await post(payload, options);
    const { status, data } = result;
    
    if (status === 200) {
        const checkResult = JSON.parse(data).result || {};
        const { suggestion, detail: { 
            porn: pornList,
            ad: adList,
            politics: politicsList,
            abuse: abuseList,
            contraband: contrabandList
        }} = checkResult;
        const filterTextList = suggestion === 'pass' ? []: _.merge(pornList || [], adList || [], politicsList || [], abuseList || [], contrabandList || []);

        let filtedText = text || body.text || '';
        filterTextList.forEach((filterText) => {
            filtedText = filtedText.replace(filterText, '*');
        });
        
        console.log(`Successful check the text, suggestion is ${suggestion}, block text:`, filterTextList);
        
        return {suggestion: suggestion || 'pass' , text: filtedText};
    } else {
        throw(`Failed check text: ${status} ${data}`);
    }
}

function post(payload, options) {
    return new Promise((resolve, reject) => {
        const token = CONTEXT.getToken();
        if (token) {
            const requestOptions = _.merge(options, { method: 'POST', headers: { 'X-Auth-Token': token } });
            const req = https.request(options, (res) => {
                const status = res.statusCode;
                res.on('data', (data) => {
                    resolve({
                        status,
                        data
                    });
                });
            });
            req.on('error', (e) => {
                reject('Post request error:', e);
            });
            req.write(typeof payload === 'object' ? JSON.stringify(payload) : (payload + ''));
            req.end();
        } else {
            reject('Post request failed, can not get token, please set agency with iam.');
        }
    });
}

function constructResponse(data = '', code = 200) {
    return {
        body: typeof data === 'object' ? JSON.stringify(data) : (data + ''),
        headers: {
            "Content-Type":"application/json",
            "Access-Control-Allow-Origin": "*",
            "Access-Control-Allow-Headers": "Content-Type,Accept",
            "Access-Control-Allow-Methods": "GET,POST,PUT,DELETE"
        },
        statusCode: code,
        isBase64Encoded: false
    };
}

函数创建完成之后,为其配置具有IAM访问权限的委托,因为本函数代码中获取用户的ak、sk需要拥有访问IAM的权限。

3. 创建APIG触发器

为函数配置一个APIG触发器,这样便得到一个调用该函数的HTTP(S) API,供外部调用。

创建成功后,API的URL可以在函数详情页面的“触发器”栏看到:

至此,我们就完整地构建了一个无服务器的语音敏感词过滤的Web应用。

4. 搭建前端页面

方法一:

我们已经提供了web应用的前端页面,并已经部署,可以供大家自由使用。访问地址:https://fgs-moderation-app.obs-website.cn-north-1.myhuaweicloud.com?rest-api-moderation={apig url}

其中apig url为您上一步中创建的APIG触发器的url,该页面会发送请求到该url,进而触发您的函数执行。

在这个寒冷的时节里

因为有你的关注

而变得温暖

点击“阅读原文”,快来体验吧!

转载请注明:爱学习爱分享 » 5 分钟 Serverless 实践:构建无服务器的敏感词过滤后端系统

喜欢 (0)or分享 (0)