微信小程序:将中文语音直接转化成英文语音
作者:瘟小驹 文章来源《微信小程序个人开发全过程》
准备工作:
准备工具:Eclipse、FileZilla、微信开发者工具、一个配置好SSL证书(https)的有域名的服务器
所需知识:SpringMVC框架、Java+HTML+CSS+JS、文件上传技术、Tomcat虚拟目录、接口调用与发布
成品介绍:将中文语音直接转化成英文语音。好久不用现在已下线。。。可以在微信搜我另外一个作品“壹曲觅知音”玩玩,博客地址:
https://blog.csdn.net/qq_37518622/article/details/81040200
最近新作:python scrapy爬取豆瓣即将上映电影用邮件定时推送给自己
一、服务端
基本思路
1、将汉语语音转化为汉语文字
2、将汉语文字转化为英语文字
3、将英语文字转化为英语语音
步骤
1、注册百度语音账户,创建应用,获取API Key 和Secret Key,参照百度文档中心
2、看百度提供的文档编写识别语音的工具类
import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import javax.xml.bind.DatatypeConverter; import priv.xxf.jgsawvoice.entity.JSONArray; import priv.xxf.jgsawvoice.entity.JSONObject; @SuppressWarnings("restriction") public class VoiceRecogUtil { private static final String serverURL = "http://vop.baidu.com/server_api";// API地址 // 开发密钥 private static final String apiKey = "你的apiKey"; private static final String secretKey = "你的secretKey"; private static final String cuid = "随便定义个字符串"; private static String token = "";// 根据密钥获取的token public static String getTextByVoice(String fileName) throws Exception { getToken();// 获取token // 发送请求得到结果 String string = getResultString(fileName); // 解析json JSONArray jsonArray = new JSONObject(string).getJSONArray("result"); int begin = jsonArray.toString().indexOf("\""); int end = jsonArray.toString().lastIndexOf("\""); String result = jsonArray.toString().substring(begin + 1, end); return result; } private static void getToken() throws Exception { String getTokenURL = "https://openapi.baidu.com/oauth/2.0/token?grant_type=client_credentials" + "&client_id=" + apiKey + "&client_secret=" + secretKey; HttpURLConnection conn = (HttpURLConnection) new URL(getTokenURL).openConnection(); token = new JSONObject(getResponse(conn)).getString("access_token"); } @SuppressWarnings("restriction") private static String getResultString(String fileName) throws Exception { File pcmFile = new File(fileName); int index = fileName.lastIndexOf("."); String suffix = fileName.substring(index + 1); HttpURLConnection conn = (HttpURLConnection) new URL(serverURL).openConnection(); // construct params JSONObject params = new JSONObject(); params.put("format", suffix);// 音频后缀 params.put("rate", 16000);// 比特率 params.put("channel", "1");// 固定值 params.put("token", token);// token params.put("cuid", cuid);// 用户请求的唯一标识 params.put("len", pcmFile.length());// 文件长度 params.put("speech", DatatypeConverter.printBase64Binary(loadFile(pcmFile)));// base64编码后的音频文件 // add request header conn.setRequestMethod("POST"); conn.setRequestProperty("Content-Type", "application/json; charset=utf-8"); conn.setDoInput(true); conn.setDoOutput(true); // send request DataOutputStream wr = new DataOutputStream(conn.getOutputStream()); wr.writeBytes(params.toString()); wr.flush(); wr.close(); return getResponse(conn); } private static String getResponse(HttpURLConnection conn) throws Exception { if (conn.getResponseCode() != 200) { // request error return ""; } InputStream is = conn.getInputStream(); BufferedReader rd = new BufferedReader(new InputStreamReader(is)); String line; StringBuffer response = new StringBuffer(); while ((line = rd.readLine()) != null) { response.append(line); response.append(\'\r\'); } rd.close(); return response.toString(); } private static byte[] loadFile(File file) throws IOException { InputStream is = new FileInputStream(file); long length = file.length(); byte[] bytes = new byte[(int) length]; int offset = 0; int numRead = 0; while (offset < bytes.length && (numRead = is.read(bytes, offset, bytes.length - offset)) >= 0) { offset += numRead; } if (offset < bytes.length) { is.close(); throw new IOException("Could not completely read file " + file.getName()); } is.close(); return bytes; } }
3、创建百度翻译账户,获取securityKey,编写文字翻译工具类
import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.Map; public class LanguageTranslateUtil { private static final String TRANS_API_HOST = "http://api.fanyi.baidu.com/api/trans/vip/translate";// API地址 private static final String appid = "你的appid";// 应用的id private static final String securityKey = "百度翻译的securityKey"//securityKey public static String getTransResultFromChToEn(String query) throws UnsupportedEncodingException { return getTransResult(query, "auto", "en"); } public static String getTransResult(String query, String from, String to) throws UnsupportedEncodingException { Map<String, String> params = buildParams(query, from, to); String string = HttpGet.get(TRANS_API_HOST, params); // 解析json串得到结果 int start = string.indexOf("\"dst\":"); int end = string.lastIndexOf("\""); String result = string.substring(start + 7, end); return result; } private static Map<String, String> buildParams(String query, String from, String to) throws UnsupportedEncodingException { Map<String, String> params = new HashMap<String, String>(); params.put("q", query); params.put("from", from); params.put("to", to); params.put("appid", appid); // 随机数 String salt = String.valueOf(System.currentTimeMillis()); params.put("salt", salt); // 签名 String src = appid + query + salt + securityKey; // 加密前的原文 params.put("sign", MD5.md5(src)); return params; } // public static void main(String[] args) throws // UnsupportedEncodingException { // String en = getTransResultFromChToEn("你好"); // System.out.println(en); // } }
4、下载百度语音合成的SDK,放到WEB-INF下的lib目录,添加至构建路径,编写语音合成工具类,直接调用SDK的API
import java.io.File; import java.io.IOException; import java.util.HashMap; import org.json.JSONObject; import com.baidu.aip.speech.AipSpeech; import com.baidu.aip.speech.TtsResponse; import com.baidu.aip.util.Util; public class TextRecogUtil { // 设置APPID/AK/SK public static final String APP_ID = "你的appID"; public static final String API_KEY = "和语音识别一样的api_key"; public static final String SECRET_KEY = "和语音识别一样的secret_key"; public static final String FILE_ROOT = "你的服务器上放语音文件的根目录"; public static String getVoiceByText(String text, String file) { // 初始化一个AipSpeech AipSpeech client = new AipSpeech(APP_ID, API_KEY, SECRET_KEY); // 可选:设置网络连接参数 client.setConnectionTimeoutInMillis(2000); client.setSocketTimeoutInMillis(60000); // 可选:设置代理服务器地址, http和socket二选一,或者均不设置 // client.setHttpProxy("proxy_host", proxy_port); // 设置http代理 // client.setSocketProxy("proxy_host", proxy_port); // 设置socket代理 // 调用接口 HashMap<String, Object> options = new HashMap<String, Object>(); options.put("spd", "5");// 语速 options.put("pit", "0");// 音调 options.put("vol", "15");// 音量 options.put("per", "3");// 音色 TtsResponse res = client.synthesis(text, "zh", 1, options); byte[] data = res.getData(); JSONObject res1 = res.getResult(); if (data != null) { try { // 若没有文件夹创建文件夹 int index = file.lastIndexOf("/"); String substring = file.substring(0, index); File temp1 = new File(substring); if (!temp1.exists()) { temp1.mkdirs(); } File temp2 = new File(file); temp2.createNewFile(); Util.writeBytesToFileSystem(data, file); } catch (IOException e) { e.printStackTrace(); } } if (res1 != null) { System.out.println(res1.toString(2)); } int path = file.indexOf("项目名"); String suffix = file.substring(path); return FILE_ROOT + "/" + suffix; } }
5、编写上传音频文件的工具类
import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; import java.io.OutputStream; import org.springframework.web.multipart.MultipartFile; public class WeChatFileUploadUtil { private static final String TEMP_DIR = "/root/file/temp";//你服务器的临时文件路径 //返回文件的唯一标识 public static String getUploadFilePath(MultipartFile file) { String result; try { InputStream in = file.getInputStream(); // 真正写到磁盘上 String uuid = IDUtil.generateString(10); String filePath = TEMP_DIR + "/" + uuid + ".mp3"; File temp = new File(filePath); OutputStream out = new FileOutputStream(temp); int length = 0; byte[] buf = new byte[100]; while ((length = in.read(buf)) != -1) { out.write(buf, 0, length); } in.close(); out.close(); result = filePath; } catch (Exception e) { result = TEMP_DIR + "/error.mp3";//全局异常结果 } return result; } public static void deleteTempFile(String file) { File temp = new File(file); if (temp.exists()) { temp.delete(); } } }
6、由于小程序录音的采样率与百度语音识别的采用率不同,还需要在服务器上安装ffmpeg转码。安装教程看这里。然后编写转码工具类
import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Date; import java.util.List; public class FileFormatTransformUtil { // private static final String FFMPEG_LOCATION = // "D:\\Eclipse\\eclipse\\WorkSpace\\src\\main\\resources\\ffmpeg.exe";//Windows下的ffmpeg路径 public static String transformSound(String mp3, String path) { File file = new File(path); if (!file.exists()) { file.mkdirs(); } String uuid = IDUtil.generateString(10); String result = path + "/" + uuid; List<String> commend = new ArrayList<String>(); commend.add("ffmpeg");// 如果在Windows下换成FFMPEG_LOCATION commend.add("-y"); commend.add("-i"); commend.add(mp3); commend.add("-acodec"); commend.add("pcm_s16le"); commend.add("-f"); commend.add("s16le"); commend.add("-ac"); commend.add("1"); commend.add("-ar"); commend.add("16000"); commend.add(result + ".pcm"); StringBuffer test = new StringBuffer(); for (int i = 0; i < commend.size(); i++) test.append(commend.get(i) + " "); System.out.println("转化" + result + ".pcm成功" + new Date()); ProcessBuilder builder = new ProcessBuilder(); builder.command(commend); try { builder.start(); } catch (IOException e) { e.printStackTrace(); } return uuid; } // public static void main(String[] args) { // FileFormatTransformUtil.transformSound("D:\\temp\\1.mp3", // "D:\\temp\\2.pcm"); // } }
7、自定义消息实体类,用于返回请求的json串
public class Message { private String code;// 200正常,404异常 private String msg;// 提示消息 private String content;// 正常返回声音的url,异常返回全局异常声音的url public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } }
8、编写Controller,发布接口以便小程序调用
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.multipart.MultipartFile; import net.sf.json.JSONObject; import priv.xxf.jgsawvoice.entity.Message; import priv.xxf.jgsawvoice.utils.FileFormatTransformUtil; import priv.xxf.jgsawvoice.utils.LanguageTranslateUtil; import priv.xxf.jgsawvoice.utils.TextRecogUtil; import priv.xxf.jgsawvoice.utils.VoiceRecogUtil; import priv.xxf.jgsawvoice.utils.WeChatFileUploadUtil; @Controller public class VoiceTansformContoller { private static final String DEFAULT_USER_ID = "defaultdir";// 如果用户不授权就使用默认文件夹授权就根据用户名创建一个文件夹 private static final String SRC_PATH = "/root/file/source";// 待转化音频 private static final String DST_PATH = "/root/file/destination";// 转化后的音频 private static final String ERROR_RESULT = "https:你的网站域名/资源文件夹/error.mp3";// 全局异常结果 @RequestMapping("/getVoice") @ResponseBody public JSONObject index(@RequestParam(value = "file") MultipartFile file, String userId) throws Exception { // userId标识唯一用户,如果不授权存放至默认文件夹 if (userId == null) { userId = DEFAULT_USER_ID; } Message message = new Message(); try { // 中文的id转成英文 try { userId = Long.parseLong(userId) + ""; } catch (Exception e) { userId = LanguageTranslateUtil.getTransResultFromChToEn(userId).replaceAll(" ", ""); } // 将用户的音频存放在服务器的临时文件夹,并获取唯一文件名 String sourceFile = WeChatFileUploadUtil.getUploadFilePath(file); // 转化成能被百度语音识别的文件 String uuid = FileFormatTransformUtil.transformSound(sourceFile, SRC_PATH + "/" + userId); // 删除临时文件 WeChatFileUploadUtil.deleteTempFile(sourceFile); // 将音频识别成文字 String text = VoiceRecogUtil.getTextByVoice(SRC_PATH + "/" + userId + "/" + uuid + ".pcm"); System.out.println(text); // 翻译 String englishText = LanguageTranslateUtil.getTransResultFromChToEn(text); System.out.println(englishText); // 将文字转化成语音存到对应文件夹 String resultFile = TextRecogUtil.getVoiceByText(englishText, DST_PATH + "/" + userId + "/" + uuid + ".mp3"); message.setCode("200"); message.setMsg("success"); message.setContent(resultFile); } catch (Exception e) { // 异常处理 message.setCode("404"); message.setMsg("fail"); message.setContent(ERROR_RESULT); } JSONObject result = JSONObject.fromObject(message); return result; } }
9、将项目maven install一下,打包成war文件,丢到tomcat的webapp文件夹下,tomcat会自动解包,访问接口是否能调用成功
二、小程序
基本思路
1、调用小程序API录音
2、调用服务端接口转化音频
3、获取接口数据让小程序使用
步骤
1、到微信公众平台注册小程序开发账户并下载微信开发者工具,打开工具,输入项目目录、AppID、项目名,其中项目目录为将要创建的小程序的代码的根目录或是已有的项目的根目录
2、查看小程序API,先将界面绘画出来,编写wxml(相当于html),这里的很多i标签是配合wxss(相当于css)渲染页面用的
<!--index.wxml--> <view class="container"> <view class="wrapper"> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> <i></i> </view> <view class="userinfo"> <button wx:if="{{!hasUserInfo && canIUse}}" open-type="getUserInfo" bindgetuserinfo="getUserInfo"> 获取头像昵称 </button> <block wx:else> <image bindtap="bindViewTap" class="userinfo-avatar" src="{{userInfo.avatarUrl}}" background-size="cover"></image> </block> <text class="author-info" style=\'margin-top:50px;\'>作者邮箱: 1553197252@qq.com</text> </view> <audio poster="{{poster}}" name="{{name}}" author="{{author}}" src="{{src}}" id="myAudio" class=\'audio\'></audio> <view class="usermotto"> <!--<text class="user-motto">{{motto}}</text>--> <button class=\'button blue big rounded\' bindlongtap=\'startRecord\' bindtouchend=\'endRecord\' style=\'margin-top:120px;\'>录音</button> <button class=\'button green big rounded\' style=\'margin-left:20px;margin-top:120px;\' bindtap=\'replayRecord\'>回放</button> </view> </view>
3、编写wxss文件(太长,给出部分)
/**index.wxss**/ .container{ overflow: hidden; background: #3e6fa3; height: 150%; } .userinfo { display: flex; flex-direction: column; align-items: center; } .userinfo-avatar { width: 128rpx; height: 128rpx; margin: 20rpx; border-radius: 50%; } .userinfo-nickname { color: #aaa; } .usermotto { margin-top: 100px; } .btn-record{ width: 120px; height: 60px; font: 13pt; line-height: 3.5em; background-color: green; display: inline-block; } .btn-replay{ width: 120px; height: 60px; font: 13pt; line-height: 3.5em; background-color: white; display: inline-block; } .audio{ display: none; } .wrapper { position: absolute; top: 50%; left: 50%; z-index: 2; -moz-perspective: 500px; -webkit-perspective: 500px; perspective: 500px; } i { display: block; position: absolute; width: 8px; height: 8px; border-radius: 8px; opacity: 0; background: rgba(255, 255, 255, 0.5); box-shadow: 0px 0px 10px white; animation-name: spin; animation-duration: 3s; animation-iteration-count: infinite; animation-timing-function: ease-in-out; } i:nth-child(1) { -moz-transform: rotate(11.6129deg) translate3d(80px, 0, 0); -ms-transform: rotate(11.6129deg) translate3d(80px, 0, 0); -webkit-transform: rotate(11.6129deg) translate3d(80px, 0, 0); transform: rotate(11.6129deg) translate3d(80px, 0, 0); animation-delay: 0.04839s; } i:nth-child(2) { -moz-transform: rotate(23.22581deg) translate3d(80px, 0, 0); -ms-transform: rotate(23.22581deg) translate3d(80px, 0, 0); -webkit-transform: rotate(23.22581deg) translate3d(80px, 0, 0); transform: rotate(23.22581deg) translate3d(80px, 0, 0); animation-delay: 0.09677s; } i:nth-child(3) { -moz-transform: rotate(34.83871deg) translate3d(80px, 0, 0); -ms-transform: rotate(34.83871deg) translate3d(80px, 0, 0); -webkit-transform: rotate(34.83871deg) translate3d(80px, 0, 0); transform: rotate(34.83871deg) translate3d(80px, 0, 0); animation-delay: 0.14516s; } i:nth-child(4) { -moz-transform: rotate(46.45161deg) translate3d(80px, 0, 0); -ms-transform: rotate(46.45161deg) translate3d(80px, 0, 0); -webkit-transform: rotate(46.45161deg) translate3d(80px, 0, 0); transform: rotate(46.45161deg) translate3d(80px, 0, 0); animation-delay: 0.19355s; } i:nth-child(5) { -moz-transform: rotate(58.06452deg) translate3d(80px, 0, 0); -ms-transform: rotate(58.06452deg) translate3d(80px, 0, 0); -webkit-transform: rotate(58.06452deg) translate3d(80px, 0, 0); transform: rotate(58.06452deg) translate3d(80px, 0, 0); animation-delay: 0.24194s; } @keyframes spin { from { opacity: 0.0; } to { opacity: 0.6; transform: translate3d(-4px, -4px, 570px); } } #black { position: absolute; left: 10px; bottom: 10px; color: rgba(255, 255, 255, 0.6); text-decoration: none; } #black:after { content: \'Black & white\'; } #black:target { top: 0; left: 0; width: 100%; height: 100%; z-index: 1; background: #111; cursor: default; } #black:target:after { content: \'xxxx\'; } author-info{ color: #840b2a; }
4、编写js文件
//index.js //获取应用实例 const app = getApp() const recorderManager = wx.getRecorderManager() const options = { duration: 60000, sampleRate: 16000, numberOfChannels: 1, format: \'mp3\', encodeBitRate: 24000 //frameSize: 50 } var result Page({ data: { motto: \'自定义motto\', userInfo: {}, hasUserInfo: false, canIUse: wx.canIUse(\'button.open-type.getUserInfo\') }, //事件处理函数 bindViewTap: function() { wx.navigateTo({ url: \'../logs/logs\' }) }, onLoad: function () { if (app.globalData.userInfo) { this.setData({ userInfo: app.globalData.userInfo, hasUserInfo: true }) } else if (this.data.canIUse){ // 由于 getUserInfo 是网络请求,可能会在 Page.onLoad 之后才返回 // 所以此处加入 callback 以防止这种情况 app.userInfoReadyCallback = res => { this.setData({ userInfo: res.userInfo, hasUserInfo: true }) } } else { // 在没有 open-type=getUserInfo 版本的兼容处理 wx.getUserInfo({ success: res => { app.globalData.userInfo = res.userInfo this.setData({ userInfo: res.userInfo, hasUserInfo: true }) } }) } }, getUserInfo: function(e) { console.log(e) app.globalData.userInfo = e.detail.userInfo this.setData({ userInfo: e.detail.userInfo, hasUserInfo: true }) }, //录音键长按按钮 startRecord: function(e){ recorderManager.start(options) wx.showLoading({ title: \'正在录音\', }) }, endRecord: function (e) { wx.hideLoading() recorderManager.stop() wx.showLoading({ title: \'正在翻译\', }) setTimeout(function () { wx.hideLoading() }, 5000) recorderManager.onStop((res) => {//录音结束后上传音频文件 console.log(\'本地audio路径:\', res.tempFilePath) wx.uploadFile({ url: \'接口地址\', filePath: res.tempFilePath, name: \'file\',//上传文件名(controller参数1) formData: { \'userId\': app.globalData.userInfo != null ? app.globalData.userInfo.nickName :\'defaultdir\'//userId(controller参数2) }, success: function (res) { var data = res.data var start = data.indexOf(\'https\'); var end = data.indexOf(\'mp3\'); result = data.substring(start,end+3) console.log(result) this.audioctx = wx.createAudioContext(\'myAudio\'); this.audioctx.setSrc(result); wx.hideLoading(); // this.audioctx.autoplay=true; this.audioctx.play(); // wx.playBackgroundAudio({ // dataUrl: result, // title: \'丁\', // coverImgUrl: \'啊\' // }) }, fail: function (res) { var data = res.data wx.showToast({ title: \'失败那\', icon: \'none\', duration: 1000 }) } }) }) recorderManager.onError((res) => { console.log(\'recorder stop\', res) wx.showToast({ title: \'录音时间太短\', icon: \'none\', duration: 1000 }) }) }, replayRecord: function(e) {//回放函数 audioctx = wx.createInnerAudioContext(\'myAudio\'); if (result == undefined){ result =\'提示url\';//如果还没录音就点回放 } audioctx.src = result; audioctx.play(); console.log(app.globalData.userInfo != null ? app.globalData.userInfo : \'defaultdir\') } })
5、上传小程序代码,查看审核规范,确保符合规范后再提交审核,等待结果,特别注意UI要符合规范,要提供作者联系方式
6、审核通过前,可以在小程序管理中添加用户并授予体验者权限,发送体验版二维码来访问体验版的小程序,审核通过后就直接能在微信小程序上搜到了
结语
百度还有很多API,都可以调用玩玩啊,还有其他公司的API,也可以尝试自己写个很6的算法做一个能很好地满足用户的程序,又或者是你手头有很好又很难找到的合法的资源都可以做个不错的个人小程序,我这个小程序实际上毫无软用,体验一下小程序开发罢了。总之我觉得有创新的思路还是最重要的,技术其次,可以慢慢学。
———————
作者:瘟小驹
来源:CSDN
原文:https://blog.csdn.net/qq_37518622/article/details/79303140
版权声明:本文为博主原创文章,转载请附上博文链接!
________________________________________