用AI将技术分享录音快速转成文字稿

807 阅读4分钟

在做一些演讲或分享的时候,为了扩大传播效果,最好除了PPT之外,还能提供文字版。但是,若是花时间去从头开始把讲的内容都重新写成文字稿一遍,未免太费时,这时,比较有效的办法是考虑做个录音,然后通过AI语音转写接口将语音转成文字。

找来找去,还是发现讯飞的转写准确率比较高。

首先,你需要去讯飞开放平台注册一个账号。

然后,点击下图所示的“创建新应用”按钮注册一个新应用。

之后再到console.xfyun.cn/services/lf… 中申请5小时的免费体验包,可供转换5小时的语音时长,在30天内有效。若时长不够可以继续购买。价格如下图所示:

另外,你需要准备好待转换的语音文件。为了避免浪费时长,你需要找一个非常短的语音文件来测试一下,没有这样的文件可以临时录制一个,至于其内容,简短地随便说几个字即可。

等一切都准备妥当,就该上代码了:

// ./index.js
const CryptoJS = require('crypto-js')
var rp = require('request-promise')
var log = require('log4node')
var fs = require('fs')
var path = require('path')

// 系统配置
const config = {
    // 请求地址
    hostUrl: "http://raasr.xfyun.cn/api/",
    // 在控制台-我的应用-语音转写获取
    appId: "xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    // 在控制台-我的应用-语音转写获取
    secretKey: "yyyyyyyyyyyyyyyyyyyyyyyy",
    // 音频文件地址
    filePath: "./zzzzzzzzzzzzzzzzzzzzzzz.mp3"
}

// 请求的接口名
const api = {
    prepare: 'prepare',
    upload: 'upload',
    merge: 'merge',
    getProgress: 'getProgress',
    getResult: 'getResult'
}

// 文件分片大小 10M
const FILE_PIECE_SICE = 10485760

// ——————————————————转写可配置参数————————————————
// 参数可在官网界面(https://doc.xfyun.cn/rest_api/%E8%AF%AD%E9%9F%B3%E8%BD%AC%E5%86%99.html)查看,根据需求可自行在gene_params方法里添加修改
// 转写类型
let lfasr_type = 0
// 是否开启分词
let has_participle = 'false'
let has_seperate = 'true'
// 多候选词个数
let max_alternatives = 0

// 鉴权签名
function getSigna(ts) {
    let md5 = CryptoJS.MD5(config.appId + ts).toString()
    let sha1 = CryptoJS.HmacSHA1(md5, config.secretKey)
    let signa = CryptoJS.enc.Base64.stringify(sha1)
    return signa
}

// slice_id 生成器
class SliceIdGenerator {
    constructor() {
        this.__ch = 'aaaaaaaaa`'
    }

    getNextSliceId() {
        let ch = this.__ch
        let i = ch.length - 1
        while (i >= 0) {
            let ci = ch[i]
            if (ci !== 'z') {
                ch = ch.slice(0, i) + String.fromCharCode(ci.charCodeAt(0) + 1) + ch.slice(i + 1)
                break
            } else {
                ch = ch.slice(0, i) + 'a' + ch.slice(i + 1)
                i--
            }
        }
        this.__ch = ch
        return this.__ch
    }
}

class RequestApi {
    constructor({ appId, filePath }) {
        this.appId = appId
        this.filePath = filePath
        this.fileLen = fs.statSync(this.filePath).size
        this.fileName = path.basename(this.filePath)
    }

    geneParams(apiName, taskId, sliceId) {
        // 获取当前时间戳
        let ts = parseInt(new Date().getTime() / 1000)

        let { appId, fileLen, fileName } = this,
            signa = getSigna(ts),
            paramDict = {
                app_id: appId,
                signa,
                ts
            }

        switch (apiName) {
            case api.prepare:
                let sliceNum = Math.ceil(fileLen / FILE_PIECE_SICE)
                paramDict.file_len = fileLen
                paramDict.file_name = fileName
                paramDict.slice_num = sliceNum
                break
            case api.upload:
                paramDict.task_id = taskId
                paramDict.slice_id = sliceId
                break
            case api.merge:
                paramDict.task_id = taskId
                paramDict.file_name = fileName
                break
            case api.getProgress:
            case api.getResult:
                paramDict.task_id = taskId
        }

        return paramDict
    }

    async geneRequest(apiName, data, file) {
        let options
        if (file) {
            options = {
                method: 'POST',
                uri: config.hostUrl + apiName,
                formData: {
                    ...data,
                    content: file
                },
                headers: {
                    'Content-Type': 'multipart/form-data'
                }
            }

        } else {
            options = {
                method: 'POST',
                uri: config.hostUrl + apiName,
                form: data,
                headers: {
                    'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
                }
            }
        }

        try {
            let res = await rp(options)
            res = JSON.parse(res)

            if (res.ok == 0) {
                log.info(apiName + ' success ' + JSON.stringify(res))
            } else {
                log.error(apiName + ' error ' + JSON.stringify(res))
            }

            return res
        } catch (err) {
            log.error(apiName + ' error' + err)
        }
    }

    prepareRequest() {
        return this.geneRequest(api.prepare, this.geneParams(api.prepare))
    }

    uploadRequest(taskId, filePath, fileLen) {
        let self = this

        return new Promise((resolve, reject) => {
            let index = 1,
                start = 0,
                sig = new SliceIdGenerator()

            async function loopUpload() {
                let len = fileLen < FILE_PIECE_SICE ? fileLen : FILE_PIECE_SICE,
                    end = start + len - 1

                // fs.createReadStream() 读取字节时,start 和 end 都包含在内
                let fileFragment = fs.createReadStream(filePath, {
                    start,
                    end
                })

                let res = await self.geneRequest(api.upload,
                    self.geneParams(api.upload, taskId, sig.getNextSliceId()),
                    fileFragment)

                if (res.ok == 0) {
                    log.info('upload slice ' + index + ' success')
                    index++
                    start = end + 1
                    fileLen -= len

                    if (fileLen > 0) {
                        loopUpload()
                    } else {
                        resolve()
                    }
                }
            }

            loopUpload()
        })
    }

    mergeRequest(taskId) {
        return this.geneRequest(api.merge, this.geneParams(api.merge, taskId))
    }

    getProgressRequest(taskId) {
        let self = this

        return new Promise((resolve, reject) => {
            function sleep(time) {
                return new Promise((resolve) => {
                    setTimeout(resolve, time)
                });
            }

            async function loopGetProgress() {
                let res = await self.geneRequest(api.getProgress, self.geneParams(api.getProgress, taskId))

                let data = JSON.parse(res.data)
                let taskStatus = data.status
                log.info('task ' + taskId + ' is in processing, task status ' + taskStatus)
                if (taskStatus == 9) {
                    log.info('task ' + taskId + ' finished')
                    resolve()
                } else {
                    sleep(20000).then(() => loopGetProgress())
                }
            }

            loopGetProgress()
        })
    }

    async getResultRequest(taskId) {
        let res = await this.geneRequest(api.getResult, this.geneParams(api.getResult, taskId))

        let data = JSON.parse(res.data),
            result = ''
        data.forEach(val => {
            result += val.onebest
        })
        log.info(result)
        fs.writeFileSync(`./result.txt`, result)
    }

    async allApiRequest() {
        try {
            let prepare = await this.prepareRequest()
            let taskId = prepare.data
            await this.uploadRequest(taskId, this.filePath, this.fileLen)
            await this.mergeRequest(taskId)
            await this.getProgressRequest(taskId)
            await this.getResultRequest(taskId)
        } catch (err) {
            log.error(err)
        }
    }
}

let ra = new RequestApi(config)
ra.allApiRequest()

这是一个Node.js脚本。其中,filePath是待转换的音频文件的路径,appId和secretKey是你刚才新建的应用的id和密钥,可在console.xfyun.cn/services/lf… 中查到。

我们先执行npm i crypto-js request-promise log4node把相应的依赖安装上,然后执行node ./index.js,就会将转换的结果输出到./result.txt文件中。

但是,经过反复的测试,笔者发现一般的录音中会有大量的语气词。于是,我想起了之前用过的网易见外工作台提供的语气词过滤功能,如下图所示:

我们只需要将一些常见的语气词根据你的需要在上述代码的fs.writeFileSync(./result.txt, result)语句之前将其过滤掉即可。不过,如果你完全按照见外工作台的这种方法把这些语气词通通过滤掉可能会导致语义上的损害,因为这里面有些词是有实际意义的。所以,需要你选择性地过滤,比如,我就过滤了嗯、呃、uh、额、呢、啊等这几个。

最终的效果还是很不错的,省去了不少码字的时间。