zoukankan      html  css  js  c++  java
  • 干货 | 调用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专场 ”!现场将会有技术专家为大家答疑解惑。

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

    阅读原文

  • 相关阅读:
    前沿技术解密——VirtualDOM
    Ques核心思想——CSS Namespace
    Unix Pipes to Javascript Pipes
    Road to the future——伪MVVM库Q.js
    聊聊CSS postproccessors
    【译】十款性能最佳的压缩算法
    Kafka Streams开发入门(9)
    Kafka Streams开发入门(8)
    【译】Kafka Producer Sticky Partitioner
    【译】99th Percentile Latency at Scale with Apache Kafka
  • 原文地址:https://www.cnblogs.com/jdclouddeveloper/p/11210054.html
Copyright © 2011-2022 走看看