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

最终效果

java朗读文字 sdk java朗读文字_java

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

Api介绍

java朗读文字 sdk java朗读文字_java_02

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

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

获取AK/SK

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

java朗读文字 sdk java朗读文字_数据_03

后端音频流合成

java朗读文字 sdk java朗读文字_java_04

这里给出后端部分源码,实现一个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);100101        JSONObject property = new JSONObject(4);102        property.put("platform", "Linux");103        property.put("version", "1.0.0");104        property.put("parameters", parameters);105106        Map 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());114115        return queryMap;116    }117118    /**119     * 循环调用api,将音频数据回写到response对象120     * @param resp121     * @param requestModel122     * @throws IOException123     */124    public void writeTtsStream(HttpServletResponse resp, RequestModel requestModel) throws IOException {125        //分段获取音频sequenceId从1递增126        int sequenceId = 1;127        changeSequenceId(sequenceId, requestModel);128        //设置返回报文头内容类型为audio/mp3129        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        //解析返回报文,获得status136        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    }170171172173前端audio播放朗读174前端部分给出在vue 模块化开发中的script部分,由于采用html5的audio进行语音播放,为了兼容性需要引用howler.js (npm install howler),主要逻辑为根据设置的参数和待朗读的文字拼接一个url,调用howler.js 中的api进行播放。175176177import {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 () { // 生成uuid193      let d = Date.now()194      return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => {195        let r = (d + Math.random() * 16) % 16 | 0196        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() // requestID204        }&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 待转音频的文案} txt219    */220    howlerPlay(txt) { 221      if (this.sound) {222        this.sound.unload() // 若sound已有值,则销毁原对象223      }224      let self = this225      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 = index253        this.howlerPlay(txt)254      }255    }256  }257}258