共计 6939 个字符,预计需要花费 18 分钟才能阅读完成。
京东云上提供了足够多的人工智能 api,并且都使用了 http 的方式进行了封装,用户可以方便在自己的系统中接入京东云的 ai 能力。今天就是介绍一下如何编写很少的代码就能使用京东云的语音合成 api 在网页中实现文字朗读,最终实现效果,延迟小,支持主流设备,声调优美,还能男女生切换。
最终效果
最终效果,微信打开链接,点击播放按钮则可以进行文字朗读。
Api 介绍
京东云 AI API 使用 Restful 接口风格,同时提供了 java 和 python 的 sdk,使用 sdk 能够方便的封装参数,调用 api 获得数据。
为了提升调用方的响应速度,语音合成 api 采用了分段合成的模式,所以调用的时候在后端逻辑中按顺序多次调用,将音频数据以数据流的形式回写给前端。
获取 AK/SK
访问京东云 api 需要获取 ak sk,配合 sdk 使用;
进入京东云控制台 - 账号管理 -Access Key 管理,创建并获取 Access Key。
后端音频流合成
这里给出后端部分源码,实现一个 controller,开发一个 get 请求方法,参数封装的逻辑全都提炼出单独的方法,代码逻辑结构简单易懂。代码使用 fastJson 处理参数,另外引用了京东云 sdk,其余都是 jdk 自带的 api,依赖很少。
1 import com.alibaba.fastjson.JSON; | |
2 import com.alibaba.fastjson.JSONObject; | |
3 import com.wxapi.WxApiCall.WxApiCall; | |
4 import com.wxapi.model.RequestModel; | |
5 | |
6 import org.springframework.stereotype.Controller; | |
7 import org.springframework.web.bind.annotation.GetMapping; | |
8 import org.springframework.web.bind.annotation.RequestHeader; | |
9 | |
10 import javax.servlet.http.HttpServletRequest; | |
11 import javax.servlet.http.HttpServletResponse; | |
12 import java.io.IOException; | |
13 import java.io.OutputStream; | |
14 import java.util.Base64; | |
15 import java.util.HashMap; | |
16 import java.util.Map; | |
17 | |
18 @Controller | |
19 public 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> | |
177 import {Howl, Howler} from 'howler' | |
178 export 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 专场”!现场将会有技术专家为大家答疑解惑。
点击“阅读原文”即可免费报名参加哦!
正文完