最近项目在做一个微信公众号,但是用的jsp+js+java,没法调用wx自带的接口,所以就找了科大讯飞的接口调用,遇到了不少波折,这里记录一下过程
注册科大讯飞账户,得到认证信息
科大讯飞语音转换文字
下载demo
demo、webapi文档
1、下载好demo文档、音频样例之后,用IDEA或者Eclipse启动就能成功
2、html的话原先在百度找了个浏览器支持MediaRecorder API的技术,最后发现用这个技术得到的音频文件科大讯飞的webapi不能识别,好难受~~~
3、最后好不容易找到了HZRecorder.js
4、直接把这俩文件拷贝出去放在一起就能实现浏览器录音
html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title></title>
</head>
<body>
<div>
<audio controls autoplay></audio>
<input onclick="startRecording()" type="button" value="录音" />
<input onclick="stopRecording()" type="button" value="停止" />
<input onclick="playRecording()" type="button" value="播放" />
<input onclick="uploadAudio()" type="button" value="提交" />
</div>
<script type="text/javascript" src="HZRecorder.js"></script>
<script>
var recorder;
var audio = document.querySelector('audio');
function startRecording() {
HZRecorder.get(function (rec) {
recorder = rec;
recorder.start();
});
}
function stopRecording() {
recorder.stop();
}
function playRecording() {
recorder.play(audio);
}
function uploadAudio() {
recorder.upload("Handler1.ashx", function (state, e) {
switch (state) {
case 'uploading':
var percentComplete = Math.round(e.loaded * 100 / e.total) + '%';
break;
case 'ok':
alert("上传成功");
break;
case 'error':
alert("上传失败");
break;
case 'cancel':
alert("上传被取消");
break;
}
});
}
</script>
</body>
</html>
js
(function (window) {
window.URL = window.URL || window.webkitURL;
navigator.getUserMedia = navigator.getUserMedia || navigator.webkitGetUserMedia || navigator.mozGetUserMedia || navigator.msGetUserMedia;
var HZRecorder = function (stream, config) {
config = config || {};
config.sampleBits = config.sampleBits || 16; //采样数位 8, 16
config.sampleRate = config.sampleRate || (16000); // 采样率(1/6 44100)
var context = new (window.webkitAudioContext || window.AudioContext)();
var audioInput = context.createMediaStreamSource(stream);
var createScript = context.createScriptProcessor || context.createJavaScriptNode;
var recorder = createScript.apply(context, [4096, 1, 1]);
var audioData = {
size: 0 // 录音文件长度
, buffer: [] // 录音缓存
, inputSampleRate: context.sampleRate // 输入采样率
, inputSampleBits: 16 // 输入采样数位
, outputSampleRate: config.sampleRate // 输出采样率
, oututSampleBits: config.sampleBits // 输出采样数位
, input: function (data) {
this.buffer.push(new Float32Array(data));
this.size += data.length;
}
, compress: function () {
//合并
var data = new Float32Array(this.size);
var offset = 0;
for (var i = 0; i < this.buffer.length; i++) {
data.set(this.buffer[i], offset);
offset += this.buffer[i].length;
}
//压缩
var compression = parseInt(this.inputSampleRate / this.outputSampleRate);
var length = data.length / compression;
var result = new Float32Array(length);
var index = 0, j = 0;
while (index < length) {
result[index] = data[j];
j += compression;
index++;
}
return result;
}
, encodeWAV: function () {
var sampleRate = Math.min(this.inputSampleRate, this.outputSampleRate);
var sampleBits = Math.min(this.inputSampleBits, this.oututSampleBits);
var bytes = this.compress();
var dataLength = bytes.length * (sampleBits / 8);
var buffer = new ArrayBuffer(44 + dataLength);
var data = new DataView(buffer);
var channelCount = 1; // 单声道
var offset = 0;
var writeString = function (str) {
for (var i = 0; i < str.length; i++) {
data.setUint8(offset + i, str.charCodeAt(i));
}
}
// 资源交换文件标识符
writeString('RIFF'); offset += 4;
// 下个地址开始到文件尾总字节数,即文件大小-8
data.setUint32(offset, 36 + dataLength, true); offset += 4;
// WAV文件标志
writeString('WAVE'); offset += 4;
// 波形格式标志
writeString('fmt '); offset += 4;
// 过滤字节,一般为 0x10 = 16
data.setUint32(offset, 16, true); offset += 4;
// 格式类别 (PCM形式采样数据)
data.setUint16(offset, 1, true); offset += 2;
// 通道数
data.setUint16(offset, channelCount, true); offset += 2;
// 采样率,每秒样本数,表示每个通道的播放速度
data.setUint32(offset, sampleRate, true); offset += 4;
// 波形数据传输率 (每秒平均字节数) 单声道×每秒数据位数×每样本数据位/8
data.setUint32(offset, channelCount * sampleRate * (sampleBits / 8), true); offset += 4;
// 快数据调整数 采样一次占用字节数 单声道×每样本的数据位数/8
data.setUint16(offset, channelCount * (sampleBits / 8), true); offset += 2;
// 每样本数据位数
data.setUint16(offset, sampleBits, true); offset += 2;
// 数据标识符
writeString('data'); offset += 4;
// 采样数据总数,即数据总大小-44
data.setUint32(offset, dataLength, true); offset += 4;
// 写入采样数据
if (sampleBits === 8) {
for (var i = 0; i < bytes.length; i++, offset++) {
var s = Math.max(-1, Math.min(1, bytes[i]));
var val = s < 0 ? s * 0x8000 : s * 0x7FFF;
val = parseInt(255 / (65535 / (val + 32768)));
data.setInt8(offset, val, true);
}
} else {
for (var i = 0; i < bytes.length; i++, offset += 2) {
var s = Math.max(-1, Math.min(1, bytes[i]));
data.setInt16(offset, s < 0 ? s * 0x8000 : s * 0x7FFF, true);
}
}
return new Blob([data], { type: 'audio/wav' });
}
};
// 开始录音
this.start = function () {
audioInput.connect(recorder);
recorder.connect(context.destination);
}
// 暂停
this.stop = function () {
recorder.disconnect();
}
// 获取音频文件
this.getBlob = function () {
this.stop();
return audioData.encodeWAV();
}
// 播放
this.play = function (audio) {
audio.src = window.URL.createObjectURL(this.getBlob());
}
// 上传
this.upload = function (url, callback) {
var fd = new FormData();
fd.append("audioData", this.getBlob());
var xhr = new XMLHttpRequest();
if (callback) {
xhr.upload.addEventListener("progress", function (e) {
callback('uploading', e);
}, false);
xhr.addEventListener("load", function (e) {
callback('ok', e);
}, false);
xhr.addEventListener("error", function (e) {
callback('error', e);
}, false);
xhr.addEventListener("abort", function (e) {
callback('cancel', e);
}, false);
}
xhr.open("POST", url);
xhr.send(fd);
}
// 音频采集
recorder.onaudioprocess = function (e) {
audioData.input(e.inputBuffer.getChannelData(0));
}
};
// 抛出异常
HZRecorder.throwError = function (message) {
alert(message);
throw new function () { this.toString = function () { return message; } }
}
// 是否支持录音
HZRecorder.canRecording = (navigator.getUserMedia != null);
// 获取录音机
HZRecorder.get = function (callback, config) {
if (callback) {
if (navigator.getUserMedia) {
navigator.getUserMedia(
{ audio: true } // 启用音频
, function (stream) {
var rec = new HZRecorder(stream, config);
callback(rec);
}
, function (error) {
switch (error.code || error.name) {
case 'PERMISSION_DENIED':
case 'PermissionDeniedError':
console.log('用户拒绝提供信息。');
break;
case 'NOT_SUPPORTED_ERROR':
case 'NotSupportedError':
console.log('浏览器不支持硬件设备。');
break;
case 'MANDATORY_UNSATISFIED_ERROR':
case 'MandatoryUnsatisfiedError':
console.log('无法发现指定的硬件设备。');
break;
default:
console.log('无法打开麦克风。异常信息:' + (error.code || error.name));
break;
}
});
} else {
console.log('当前浏览器不支持录音功能。');
return;
}
}
}
window.HZRecorder = HZRecorder;
})(window);
5、上传至服务端
6、var xhr = new XMLHttpRequest(); 回显
var jsondata = JSON.parse(xhr.responseText);
7、服务端接收上传文件
@RequestMapping(value = "/getAnswerInfo")
@ResponseBody
public Object getAnswerInfo(HttpServletRequest request, HttpServletResponse response) throws Exception {
//接收参数
Map<String,Object> map = new HashMap<>();
String openo = request.getParameter("openo");
String personid = request.getParameter("personid");
String recid = get32UUID();
String flag = request.getParameter("flag");
File file = null;
String keywords = "";
//判断
if (!StringUtil.isEmpty(flag) && "1".equals(flag)) {
keywords = request.getParameter("keywords");
map = lybZnwdService.selectByExampleByLybZsk(keywords,openo,recid,personid,getUser());
} else if (!StringUtil.isEmpty(flag) && "2".equals(flag)){
//这一段是接收音频文件,然后转成File文件
MultipartHttpServletRequest multiRequest = (MultipartHttpServletRequest) request;
Iterator<String> iter = multiRequest.getFileNames();
while (iter.hasNext()) {
List<MultipartFile> files = multiRequest.getFiles(iter.next());
if (files != null && files.size() > 0) {
for (MultipartFile multipartFile : files) {
file = WebIATWS.transferToFile(multipartFile);
keywords = WebIATWS.sendStart(file);
if (!StringUtil.isEmpty(keywords)) {
map = lybZnwdService.selectByExampleByLybZsk(keywords.replace("。",""),openo,recid,personid,getUser());
}
System.out.println("result=="+keywords);
}
}
}
}
return map;
}
8、音频文件转成文字
package com.yawei.es.wechat.Utils;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import okhttp3.*;
import org.springframework.web.multipart.MultipartFile;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.net.URL;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* 语音听写流式 WebAPI 接口调用示例 接口文档(必看):https://doc.xfyun.cn/rest_api/语音听写(流式版).html
* webapi 听写服务参考帖子(必看):http://bbs.xfyun.cn/forum.php?mod=viewthread&tid=38947&extra=
* 语音听写流式WebAPI 服务,热词使用方式:登陆开放平台https://www.xfyun.cn/后,找到控制台--我的应用---语音听写---个性化热词,上传热词
* 注意:热词只能在识别的时候会增加热词的识别权重,需要注意的是增加相应词条的识别率,但并不是绝对的,具体效果以您测试为准。
* 错误码链接:https://www.xfyun.cn/document/error-code (code返回错误码时必看)
* 语音听写流式WebAPI 服务,方言或小语种试用方法:登陆开放平台https://www.xfyun.cn/后,在控制台--语音听写(流式)--方言/语种处添加
* 添加后会显示该方言/语种的参数值
* @author iflytek
*/
public class WebIATWS extends WebSocketListener {
private static final String hostUrl = "https://iat-api.xfyun.cn/v2/iat"; //中英文,http url 不支持解析 ws/wss schema
// private static final String hostUrl = "https://iat-niche-api.xfyun.cn/v2/iat";//小语种
private static final String apiKey = "xxx"; //在控制台-我的应用-语音听写(流式版)获取
private static final String apiSecret = "xxx"; //在控制台-我的应用-语音听写(流式版)获取
private static final String appid = "xxx"; //在控制台-我的应用获取
// private static final String file = "C:\\Users\\lihao\\Desktop\\luyin\\777.wav"; // 中文 resource\\iat\\16k_10.pcm
private static File file = null;
public static final int StatusFirstFrame = 0;
public static final int StatusContinueFrame = 1;
public static final int StatusLastFrame = 2;
public static final Gson json = new Gson();
Decoder decoder = new Decoder();
// 开始时间
private static Date dateBegin = new Date();
// 结束时间
private static Date dateEnd = new Date();
private static final SimpleDateFormat sdf = new SimpleDateFormat("yyy-MM-dd HH:mm:ss.SSS");
private static String result = "";
/**
* 连接
* @param webSocket
* @param response
*/
@Override
public void onOpen(WebSocket webSocket, Response response) {
super.onOpen(webSocket, response);
new Thread(()->{
//连接成功,开始发送数据
int frameSize = 1280; //每一帧音频的大小,建议每 40ms 发送 122B
int intervel = 40;
int status = 0; // 音频的状态
try (FileInputStream fs = new FileInputStream(file)) {
byte[] buffer = new byte[frameSize];
// 发送音频
end:
while (true) {
int len = fs.read(buffer);
if (len == -1) {
status = StatusLastFrame; //文件读完,改变status 为 2
}
switch (status) {
case StatusFirstFrame: // 第一帧音频status = 0
JsonObject frame = new JsonObject();
JsonObject business = new JsonObject(); //第一帧必须发送
JsonObject common = new JsonObject(); //第一帧必须发送
JsonObject data = new JsonObject(); //每一帧都要发送
// 填充common
common.addProperty("app_id", appid);
//填充business
business.addProperty("language", "zh_cn");
//business.addProperty("language", "en_us");//英文
//business.addProperty("language", "ja_jp");//日语,在控制台可添加试用或购买
//business.addProperty("language", "ko_kr");//韩语,在控制台可添加试用或购买
//business.addProperty("language", "ru-ru");//俄语,在控制台可添加试用或购买
business.addProperty("domain", "iat");
business.addProperty("accent", "mandarin");//中文方言请在控制台添加试用,添加后即展示相应参数值
//business.addProperty("nunum", 0);
//business.addProperty("ptt", 0);//标点符号
//business.addProperty("rlang", "zh-hk"); // zh-cn :简体中文(默认值)zh-hk :繁体香港(若未授权不生效,在控制台可免费开通)
//business.addProperty("vinfo", 1);
business.addProperty("dwa", "wpgs");//动态修正(若未授权不生效,在控制台可免费开通)
//business.addProperty("nbest", 5);// 句子多候选(若未授权不生效,在控制台可免费开通)
//business.addProperty("wbest", 3);// 词级多候选(若未授权不生效,在控制台可免费开通)
//填充data
data.addProperty("status", StatusFirstFrame);
data.addProperty("format", "audio/L16;rate=16000");
data.addProperty("encoding", "raw");
data.addProperty("audio", Base64.getEncoder().encodeToString(Arrays.copyOf(buffer, len)));
//填充frame
frame.add("common", common);
frame.add("business", business);
frame.add("data", data);
webSocket.send(frame.toString());
status = StatusContinueFrame; // 发送完第一帧改变status 为 1
break;
case StatusContinueFrame: //中间帧status = 1
JsonObject frame1 = new JsonObject();
JsonObject data1 = new JsonObject();
data1.addProperty("status", StatusContinueFrame);
data1.addProperty("format", "audio/L16;rate=16000");
data1.addProperty("encoding", "raw");
data1.addProperty("audio", Base64.getEncoder().encodeToString(Arrays.copyOf(buffer, len)));
frame1.add("data", data1);
webSocket.send(frame1.toString());
// System.out.println("send continue");
break;
case StatusLastFrame: // 最后一帧音频status = 2 ,标志音频发送结束
JsonObject frame2 = new JsonObject();
JsonObject data2 = new JsonObject();
data2.addProperty("status", StatusLastFrame);
data2.addProperty("audio", "");
data2.addProperty("format", "audio/L16;rate=16000");
data2.addProperty("encoding", "raw");
frame2.add("data", data2);
webSocket.send(frame2.toString());
System.out.println("sendlast");
break end;
}
Thread.sleep(intervel); //模拟音频采样延时
}
System.out.println("all data is send");
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
/**
* 结果
* @param webSocket
* @param text
*/
@Override
public void onMessage(WebSocket webSocket, String text) {
super.onMessage(webSocket, text);
//System.out.println(text);
ResponseData resp = json.fromJson(text, ResponseData.class);
if (resp != null) {
if (resp.getCode() != 0) {
System.out.println( "code=>" + resp.getCode() + " error=>" + resp.getMessage() + " sid=" + resp.getSid());
System.out.println( "错误码查询链接:https://www.xfyun.cn/document/error-code");
return;
}
if (resp.getData() != null) {
if (resp.getData().getResult() != null) {
Text te = resp.getData().getResult().getText();
//System.out.println(te.toString());
try {
decoder.decode(te);
System.out.println("中间识别结果 ==》" + decoder.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
if (resp.getData().getStatus() == 2) {
// todo resp.data.status ==2 说明数据全部返回完毕,可以关闭连接,释放资源
System.out.println("session end ");
dateEnd = new Date();
System.out.println(sdf.format(dateBegin) + "开始");
System.out.println(sdf.format(dateEnd) + "结束");
System.out.println("耗时:" + (dateEnd.getTime() - dateBegin.getTime()) + "ms");
System.out.println("最终识别结果 ==》" + decoder.toString());
System.out.println("本次识别sid ==》" + resp.getSid());
result = decoder.toString();
System.out.println("resultweb--->"+result);
decoder.discard();
webSocket.close(1000, "");
} else {
// todo 根据返回的数据处理
}
}
}
}
/**
* 异常
* @param webSocket
* @param t
* @param response
*/
@Override
public void onFailure(WebSocket webSocket, Throwable t, Response response) {
super.onFailure(webSocket, t, response);
try {
if (null != response) {
int code = response.code();
System.out.println("onFailure code:" + code);
System.out.println("onFailure body:" + response.body().string());
if (101 != code) {
System.out.println("connection failed");
System.exit(0);
}
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
/**
* main方法
* @param args
* @throws Exception
*/
public static void main(String[] args) throws Exception {
String authUrl = getAuthUrl(hostUrl, apiKey, apiSecret);
OkHttpClient client = new OkHttpClient.Builder().build();
String url = authUrl.toString().replace("http://", "ws://").replace("https://", "wss://");
Request request = new Request.Builder().url(url).build();
WebSocket webSocket = client.newWebSocket(request, new WebIATWS());
}
/**
* 鉴权URL
* @param hostUrl
* @param apiKey
* @param apiSecret
* @return
* @throws Exception
*/
public static String getAuthUrl(String hostUrl, String apiKey, String apiSecret) throws Exception {
URL url = new URL(hostUrl);
SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z", Locale.US);
format.setTimeZone(TimeZone.getTimeZone("GMT"));
String date = format.format(new Date());
StringBuilder builder = new StringBuilder("host: ").append(url.getHost()).append("\n").//
append("date: ").append(date).append("\n").//
append("GET ").append(url.getPath()).append(" HTTP/1.1");
System.out.println(builder);
Charset charset = Charset.forName("UTF-8");
Mac mac = Mac.getInstance("hmacsha256");
SecretKeySpec spec = new SecretKeySpec(apiSecret.getBytes(charset), "hmacsha256");
mac.init(spec);
byte[] hexDigits = mac.doFinal(builder.toString().getBytes(charset));
String sha = Base64.getEncoder().encodeToString(hexDigits);
//System.out.println(sha);
String authorization = String.format("api_key=\"%s\", algorithm=\"%s\", headers=\"%s\", signature=\"%s\"", apiKey, "hmac-sha256", "host date request-line", sha);
System.out.println(authorization);
HttpUrl httpUrl = HttpUrl.parse("https://" + url.getHost() + url.getPath()).newBuilder().//
addQueryParameter("authorization", Base64.getEncoder().encodeToString(authorization.getBytes(charset))).//
addQueryParameter("date", date).//
addQueryParameter("host", url.getHost()).//
build();
return httpUrl.toString();
}
public static class ResponseData {
private int code;
private String message;
private String sid;
private Data data;
public int getCode() {
return code;
}
public String getMessage() {
return this.message;
}
public String getSid() {
return sid;
}
public Data getData() {
return data;
}
}
public static class Data {
private int status;
private Result result;
public int getStatus() {
return status;
}
public Result getResult() {
return result;
}
}
public static class Result {
int bg;
int ed;
String pgs;
int[] rg;
int sn;
Ws[] ws;
boolean ls;
JsonObject vad;
public Text getText() {
Text text = new Text();
StringBuilder sb = new StringBuilder();
for (Ws ws : this.ws) {
sb.append(ws.cw[0].w);
}
text.sn = this.sn;
text.text = sb.toString();
text.sn = this.sn;
text.rg = this.rg;
text.pgs = this.pgs;
text.bg = this.bg;
text.ed = this.ed;
text.ls = this.ls;
text.vad = this.vad==null ? null : this.vad;
return text;
}
}
public static class Ws {
Cw[] cw;
int bg;
int ed;
}
public static class Cw {
int sc;
String w;
}
public static class Text {
int sn;
int bg;
int ed;
String text;
String pgs;
int[] rg;
boolean deleted;
boolean ls;
JsonObject vad;
@Override
public String toString() {
return "Text{" +
"bg=" + bg +
", ed=" + ed +
", ls=" + ls +
", sn=" + sn +
", text='" + text + '\'' +
", pgs=" + pgs +
", rg=" + Arrays.toString(rg) +
", deleted=" + deleted +
", vad=" + (vad==null ? "null" : vad.getAsJsonArray("ws").toString()) +
'}';
}
}
//解析返回数据,仅供参考
public static class Decoder {
private Text[] texts;
private int defc = 10;
public Decoder() {
this.texts = new Text[this.defc];
}
public synchronized void decode(Text text) {
if (text.sn >= this.defc) {
this.resize();
}
if ("rpl".equals(text.pgs)) {
for (int i = text.rg[0]; i <= text.rg[1]; i++) {
this.texts[i].deleted = true;
}
}
this.texts[text.sn] = text;
}
public String toString() {
StringBuilder sb = new StringBuilder();
for (Text t : this.texts) {
if (t != null && !t.deleted) {
sb.append(t.text);
}
}
return sb.toString();
}
public void resize() {
int oc = this.defc;
this.defc <<= 1;
Text[] old = this.texts;
this.texts = new Text[this.defc];
for (int i = 0; i < oc; i++) {
this.texts[i] = old[i];
}
}
public void discard(){
for(int i=0;i<this.texts.length;i++){
this.texts[i]= null;
}
}
}
public static String sendStart (File file_keywords) throws Exception {
file = file_keywords;
// 构建鉴权url
String authUrl = getAuthUrl(hostUrl, apiKey, apiSecret);
OkHttpClient client = new OkHttpClient.Builder().build();
//将url中的 schema http://和https://分别替换为ws:// 和 wss://
String url = authUrl.toString().replace("http://", "ws://").replace("https://", "wss://");
Request request = new Request.Builder().url(url).build();
// System.out.println(client.newCall(request).execute());
WebSocket webSocket = client.newWebSocket(request, new WebIATWS());
Thread.sleep(5000);
return result;
}
/**
* MultipartFile 转 File
* @param multipartFile
* @return
*/
public static File transferToFile(MultipartFile multipartFile) {
// 选择用缓冲区来实现这个转换即使用java 创建的临时文件 使用 MultipartFile.transferto()方法 。
File file = null;
try {
String originalFilename = multipartFile.getOriginalFilename();
if ("blob".equals(originalFilename)) {
file = File.createTempFile(UUID.randomUUID().toString(), ".wav");
}
multipartFile.transferTo(file);
file.deleteOnExit();
} catch (IOException e) {
e.printStackTrace();
}
return file;
}
}
好啦,到此就结束啦,有疑问再沟通