微信小程式個人開發全過程
準備工具:Eclipse、FileZilla、微信開發者工具、一個配置好SSL證書(https)的有域名的伺服器
所需知識:SpringMVC框架、Java+HTML+CSS+JS、檔案上傳技術、Tomcat虛擬目錄、介面呼叫與釋出
成品介紹:將中文語音直接轉化成英文語音。好久不用現在已下線。。。可以在微信搜我另外一個作品“壹曲覓知音”玩玩,部落格地址:
一、服務端
基本思路
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;'>作者郵箱: [email protected]</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的演算法做一個能很好地滿足使用者的程式,又或者是你手頭有很好又很難找到的合法的資源都可以做個不錯的個人小程式,我這個小程式實際上毫無軟用,體驗一下小程式開發罷了。總之我覺得有創新的思路還是最重要的,技術其次,可以慢慢學。