• 干货 | 调用AI api 实现网页文字朗读


    京东云上提供了足够多的人工智能api,并且都使用了http的方式进行了封装,用户可以方便在自己的系统中接入京东云的ai能力。今天就是介绍一下如何编写很少的代码就能使用京东云的语音合成api在网页中实现文字朗读,最终实现效果,延迟小,支持主流设备,声调优美,还能男女生切换。

    最终效果

    最终效果,微信打开链接,点击播放按钮则可以进行文字朗读。

    Api介绍

    京东云AI API使用Restful接口风格,同时提供了java和python的sdk,使用sdk能够方便的封装参数,调用api获得数据。

    为了提升调用方的响应速度,语音合成api采用了分段合成的模式,所以调用的时候在后端逻辑中按顺序多次调用,将音频数据以数据流的形式回写给前端。

    获取AK/SK

    访问京东云api需要获取 ak sk ,配合sdk使用;
    进入京东云控制台-账号管理-Access Key管理,创建并获取Access Key。

    image

    后端音频流合成

    image

    这里给出后端部分源码,实现一个controller,开发一个get请求方法,参数封装的逻辑全都提炼出单独的方法,代码逻辑结构简单易懂。代码使用fastJson处理参数,另外引用了京东云sdk,其余都是jdk自带的api,依赖很少。

      1import com.alibaba.fastjson.JSON;
      2import com.alibaba.fastjson.JSONObject;
      3import com.wxapi.WxApiCall.WxApiCall;
      4import com.wxapi.model.RequestModel;
      5
      6import org.springframework.stereotype.Controller;
      7import org.springframework.web.bind.annotation.GetMapping;
      8import org.springframework.web.bind.annotation.RequestHeader;
      9
     10import javax.servlet.http.HttpServletRequest;
     11import javax.servlet.http.HttpServletResponse;
     12import java.io.IOException;
     13import java.io.OutputStream;
     14import java.util.Base64;
     15import java.util.HashMap;
     16import java.util.Map;
     17
     18@Controller
     19public class TTSControllerExample {
     20    //url appkey secretkey
     21    private static final String url = "https://aiapi.jdcloud.com/jdai/tts";
     22    private static final String appKey = "";
     23    private static final String secretKey = "";
     24
     25    @GetMapping("/tts/stream/example")
     26    public void ttsStream(
     27            @RequestHeader(value = "Range", required = false) String range,
     28            HttpServletRequest req,
     29            HttpServletResponse resp) {
     30
     31        //应对safari的第一次确认请求携带header Range:bytes=0-1,此时回写1byte数据,防止错误
     32        if ("bytes=0-1".equals(range)) {
     33            try {
     34                byte[] temp = new byte['a'];
     35                resp.setHeader("Content-Type", "audio/mp3");
     36                OutputStream out = resp.getOutputStream();
     37                out.write(temp);
     38} catch (IOException e) {
     39                e.printStackTrace();
     40            }
     41            return;
     42        }
     43        //封装输入参数
     44        Map queryMap = processQueryParam(req);
     45        String text = req.getParameter("text");
     46//封装api调用请求报文
     47        RequestModel requestModel = getBaseRequestModel(queryMap, text);
     48        try {
     49//回写音频数据给前端
     50            writeTtsStream(resp, requestModel);
     51} catch (IOException e) {
     52            e.printStackTrace();
     53        }
     54    }
     55
     56    /**
     57     * 将前端输入参数封装为api调用的请求对象,同时设置url appkey secaretKey
     58     * @param queryMap
     59     * @param bodyStr
     60     * @return
     61     */
     62    private RequestModel getBaseRequestModel(Map queryMap, String bodyStr) {
     63        RequestModel requestModel = new RequestModel();
     64        requestModel.setGwUrl(url);
     65        requestModel.setAppkey(appKey);
     66        requestModel.setSecretKey(secretKey);
     67        requestModel.setQueryParams(queryMap);
     68        requestModel.setBodyStr(bodyStr);
     69        return requestModel;
     70    }
     71
     72    /**
     73     * 流式api调用,需要将sequenceId 依次递增,用该方法进行设置请求对象sequenceId
     74     * @param sequenceId
     75     * @param requestModel
     76     * @return
     77     */
     78    private RequestModel changeSequenceId(int sequenceId, RequestModel requestModel) {
     79        requestModel.getQueryParams().put("Sequence-Id", sequenceId);
     80        return requestModel;
     81    }
     82
     83    /**
     84     * 将request中的请求参数封装为api调用请求对象中的queryMap
     85     * @param req
     86     * @return
     87     */
     88    private Map processQueryParam(HttpServletRequest req) {
     89        String reqid = req.getParameter("reqid");
     90        int tim = Integer.parseInt(req.getParameter("tim"));
     91        String sp = req.getParameter("sp");
     92
     93        JSONObject parameters = new JSONObject(8);
     94        parameters.put("tim", tim);
     95        parameters.put("sr", 24000);
     96        parameters.put("sp", sp);
     97        parameters.put("vol", 2.0);
     98        parameters.put("tte", 0);
     99        parameters.put("aue", 3);
    100
    101        JSONObject property = new JSONObject(4);
    102        property.put("platform", "Linux");
    103        property.put("version", "1.0.0");
    104        property.put("parameters", parameters);
    105
    106        Map<String, Object> queryMap = new HashMap<>();
    107//访问参数
    108        queryMap.put("Service-Type", "synthesis");
    109        queryMap.put("Request-Id", reqid);
    110        queryMap.put("Protocol", 1);
    111        queryMap.put("Net-State", 1);
    112        queryMap.put("Applicator", 1);
    113        queryMap.put("Property", property.toJSONString());
    114
    115        return queryMap;
    116    }
    117
    118    /**
    119     * 循环调用api,将音频数据回写到response对象
    120     * @param resp
    121     * @param requestModel
    122     * @throws IOException
    123     */
    124    public void writeTtsStream(HttpServletResponse resp, RequestModel requestModel) throws IOException {
    125        //分段获取音频sequenceId从1递增
    126        int sequenceId = 1;
    127        changeSequenceId(sequenceId, requestModel);
    128        //设置返回报文头内容类型为audio/mp3
    129        resp.setHeader("Content-Type", "audio/mp3");
    130        //api请求sdk对象
    131        WxApiCall call = new WxApiCall();
    132        //获取输出流用于输出音频流
    133        OutputStream out = resp.getOutputStream();
    134        call.setModel(requestModel);
    135        //解析返回报文,获得status
    136        String response = call.request();
    137        JSONObject jsonObject = JSON.parseObject(response);
    138        JSONObject data = jsonObject.getJSONObject("result");
    139        //第一次请求增加校验,如果错误则向前端回写500错误码
    140        if (data.getIntValue("status") != 0) {
    141            resp.sendError(500, data.getString("message"));
    142            return;
    143        }
    144        //推送实际音频数据
    145        String audio = data.getString("audio");
    146        byte[] part = Base64.getDecoder().decode(audio);
    147        out.write(part);
    148        out.flush();
    149        //判断是否已结束,多次请求对应多个index,index<0 代表最后一个包
    150        if (data.getIntValue("index") < 0) {
    151            return;
    152        }
    153        //循环推送剩余部分音频
    154        while (data.getIntValue("index") >= 0) {
    155            //sequenceid 递增
    156            sequenceId = sequenceId + 1;
    157            changeSequenceId(sequenceId, requestModel);
    158            //请求api获得新的音频数据
    159            call.setModel(requestModel);
    160            response = call.request();
    161            jsonObject = JSON.parseObject(response);
    162            data = jsonObject.getJSONObject("result");
    163            audio = data.getString("audio");
    164            part = Base64.getDecoder().decode(audio);
    165            //回写新的音频数据
    166            out.write(part);
    167            out.flush();
    168        }
    169    }
    170
    171
    172
    173前端audio播放朗读
    174前端部分给出在vue 模块化开发中的script部分,由于采用html5的audio进行语音播放,为了兼容性需要引用howler.js (npm install howler),主要逻辑为根据设置的参数和待朗读的文字拼接一个url,调用howler.js 中的api进行播放。
    175
    176<script>
    177import {Howl, Howler} from 'howler'
    178export default {
    179  data() {
    180    return {
    181      news: { // 新闻内容
    182        ……
    183      },
    184      role: 1, // 0女声,1男声
    185      speed: 1, // 播放速度
    186      curIndex: -1, // 播放的段落在所有段落中的顺序,与用户交互显示相关,与流式播放无关
    187      sound: null, // 页面唯一的指向howler实例的变量
    188      status: 'empty' // load,pause,stop,empty 仅与用户交互显示相关,与流式播放显示无关
    189    }
    190  },
    191  methods: {
    192    generateUUID () { // 生成uuid
    193      let d = Date.now()
    194      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {
    195        let r = (d + Math.random() * 16) % 16 | 0
    196        d = Math.floor(d / 16)
    197        return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16)
    198      })
    199    },
    200    audioSrc (txt) { // 生成获取音频的链接
    201      let content = encodeURI(txt) // 文字编码
    202      return `http://neuhubdemo.jd.com/api/tts/streamv2?reqid=${
    203          this.generateUUID() // requestID
    204        }&text=${
    205          content // 编码后的文字内容
    206        }&tim=${
    207          this.role // 男声 or 女声
    208        }&sp=${
    209          this.speed // 播放速度
    210        }`
    211    },
    212    /** 
    213     * 获取文案对应的流式音频
    214     * 
    215     * 使用howler能够解决部分手机浏览器(eg:UC)的兼容问题,
    216     * 但解决ios上微信和safari的兼容问题,
    217     * 需要后端通过{range:bytes=0-1}这个header字段对请求进行控制
    218     *  @param {String 待转音频的文案} txt
    219    */
    220    howlerPlay(txt) { 
    221      if (this.sound) {
    222        this.sound.unload() // 若sound已有值,则销毁原对象
    223      }
    224      let self = this
    225      this.status = 'load'
    226      this.sound = new Howl({
    227        src: `${this.audioSrc(txt)}`,
    228        html5: true, // 必须!A live stream can only be played through HTML5 Audio.
    229        format: ['mp3', 'aac'],
    230        // 以下onplay、onpause、onend均为控制显示相关
    231        onplay() {
    232          self.status = 'pause'
    233        },
    234        onpause: function() {
    235          self.status = 'stop'
    236        },
    237        onend: function() {
    238          self.status = 'stop'
    239        }
    240      });
    241      this.sound.play()
    242    },
    243    // 控制用户交互
    244    play (txt, index) {
    245      if (this.curIndex === index) {
    246        if (this.status === 'stop') {
    247          this.sound.play()
    248        } else {
    249          this.sound.pause()
    250        }
    251      } else {
    252        this.curIndex = index
    253        this.howlerPlay(txt)
    254      }
    255    }
    256  }
    257}
    258</script>
    

    看完这个操作文档是不是跃跃欲试?对AI也想了解更多?

    本周六我们为大家准备了【从“智慧零售”到“无人仓储”,揭秘京东人工智能技术的实践与应用】“京东云技术沙龙AI专场 ”!现场将会有技术专家为大家答疑解惑。

    欢迎点击“链接”了解更多精彩内容

    阅读原文

  • 相关阅读:
    【古曲】流水-古琴曲
    【文献阅读】基于特征的非局部均值图像去噪算法研究毕业论文
    【名言】后生看经书,须着看注疏及先儒解释,不然,执己见议论,恐入自是之域,便轻视古人。
    马氏距离(Mahalanobis distance)
    广义高斯分布(GGD)
    Lenna图-莱娜·瑟德贝里
    【机器学习】WIFI室内定位
    【matlab】生成列是0-255渐变的图像
    少笔画生僻字
    Gabor变换、Gabor滤波器
  • 原文地址:https://www.cnblogs.com/jdclouddeveloper/p/11210054.html
Copyright © 2020-2023  润新知