一.目的与实现过程
1.目的:将.wav/.mp3音频文件翻译成文字
2.方式:基于科大讯飞语音转写 WebApi的安卓实现
3.机制:采用自定义计时器轮询。
4.坑点1:科大讯飞当前暂无安卓文档/代码开放,需要自己写网络请求。
坑点2:免费的5小时转写套餐只能用于一个AppId
二.上效果图
音频文件随便选了个.wav文件
三.实现流程
1)上科大讯飞官网智能语音,语音转写查看文档和资费预览。这里用于测试,可以免费领取5小时套餐
2)注册账户,实名认证,领取到免费领取5小时套餐
3)创建应用,得到AppId
4)在语音转写功能下,记下APPID和SecretKey,后面需要用到
5)语音转写API
语音转写WebAPI文档地址
6)官网提供三种语言的webApi接口。由于对安卓不支持(网络协议的问题),所以需要我们进行手撸代码。
下载Demo Java语言。得到其中lib下的依赖包,lfasr-sdk-clientxxxx.jar
四.基于安卓的实现
0.实现原理:
创建一个简单的activity,根据APPID和secriteKey加密得到signa
语音转写经过以下流程:语音文件预处理,语音文件分片,文件合并,轮询结果,查询最后的翻译结果。
1.build.gardle:引入语音转写依赖包和OKHTTP(必须要的网络请求工具)
implementation 'com.squareup.okhttp3:okhttp:3.10.0'//网络请求工具implementation fileTree(dir: 'libs', include: ['*.jar'])implementation 'androidx.legacy:legacy-support-v4:1.0.0'implementation 'androidx.appcompat:appcompat:1.1.0'implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
2.代码展示 Audio2TextActivity
private static String myTag = "讯飞调试";public static final String LFASR_HOST = "/api";/*** TODO 设置appid和secret_key*/public static final String APPID = "xxxxxxx";//public static final String SECRET_KEY = "8c4xxxxxxxxxxxxx";//public static final String PREPARE = "/prepare";public static final String UPLOAD = "/upload";public static final String MERGE = "/merge";public static final String GET_RESULT = "/getResult";public static final String GET_PROGRESS = "/getProgress";public static final int SLICE_SICE = 10485760;// 10M 文件分片大小,可根据实际情况调整private Timer myTimer = null;private TextView tv_myTestInfo;private StringBuffer stringBuffer = null;//用来显示测试信息和结果
OnCreate方法入口:
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_audio2_text);tv_myTestInfo = findViewById(R.id.myTestInfo);if(stringBuffer == null){stringBuffer = new StringBuffer(256);}else{int sb_length = stringBuffer.length();stringBuffer.delete(0,sb_length); }String myPath = Environment.getExternalStorageDirectory()+"/patrol/1576159897660tt.wav";myPrepare(myPath);}
3.对文件进行预处理。这里直接选取既定路径的文件,这里有需要可以自己自定义
/*** 预处理* okHttp* @param path需要转写的音频* @return* @throws SignatureException*/private void myPrepare(final String path){final String myUrl = LFASR_HOST + PREPARE;new Thread() {@Overridepublic void run() {String ts = String.valueOf(System.currentTimeMillis() / 1000L);String signa = "";try {signa = EncryptUtil.HmacSHA1Encrypt(EncryptUtil.MD5(APPID + ts), SECRET_KEY);} catch (SignatureException e) {e.printStackTrace();}File file = new File(path);long fileLenth = file.length();String file_len = fileLenth + "";String file_name = file.getName()+"";String slice_num = (fileLenth/SLICE_SICE) + (fileLenth % SLICE_SICE == 0 ? 0 : 1) + "";// 2.创建文件上传请求对象RequestBody fileBody = RequestBody.create(MediaType.parse("audio/x-wav"), file);// 3.new okHttpOkHttpClient okHttpClient = new OkHttpClient();// 4.创建多媒体 请求对象RequestBody multipartBody = new MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart("app_id", APPID)// 文件上传的参数.addFormDataPart("ts",ts ).addFormDataPart("signa",signa ).addFormDataPart("file", file_name,fileBody)//文件主体.addFormDataPart("file_len", file_len ).addFormDataPart("file_name", file_name ).addFormDataPart("slice_num", slice_num ).build();//5. 创建request对象 //.header("gis4_imei",myImei)final Request request = new Request.Builder().url(myUrl).post(multipartBody).build();Call call = okHttpClient.newCall(request);try{Response response = call.execute();String result = response.body().string();Log.w(myTag,"result:"+result);ApiResultDto resultDto = JSON.parseObject(result, ApiResultDto.class);if (resultDto.getOk() != 0) {throw new RuntimeException("预处理失败!" + response);}else{String taskId = resultDto.getData();Log.w(myTag,"预处理成功");stringBuffer.append("预处理成功");Message message = new Message();message.what = 0;message.obj = taskId;mHandler.sendMessage(message);}}catch (IOException e){Log.w(myTag,"**********上传录音失败了"+e.toString());}}}.start();}
4.这里是异步的,所以采取Handlert异步消息机制。科大讯飞的官方解释是:上传的音频翻译结果在24H内完成,经测试,正常情况下不堵塞,大概3-5s内可以返回。
Handler方法:
Handler mHandler = new Handler() {@Overridepublic void handleMessage(Message message) {super.handleMessage(message);//完成主界面更新,拿到数据switch (message.what) {case 0://预处理完成,执行分片和分片上传final String task_id = (String) message.obj;Log.w(myTag,"收到来自预处理完成后Handler的消息.What:"+message.what+"task_id:"+task_id);tv_myTestInfo.setText(stringBuffer);doMythod(task_id);break;case 1://分片完成,进行合并String task_id1 = (String) message.obj;Log.w(myTag,"收到来自分片处理完成后Handler的消息.What:"+message.what+"task_id:"+task_id1);tv_myTestInfo.setText(stringBuffer);myMerge(task_id1); // 合并文件break;case 2://合并完成,轮询进度final String task_id2 = (String) message.obj;Log.w(myTag,"收到来自合并处理完成后Handler的消息.What:"+message.what+"task_id:"+task_id2);tv_myTestInfo.setText(stringBuffer);if(myTimer == null){myTimer = new Timer();myTimer.schedule(new TimerTask() {@Overridepublic void run() {myProgress(task_id2);}},10*1000,20*1000);}break;case 3://服务器端处理完成状态为9,最后进行查询结果final String task_id3 = (String) message.obj;Log.w(myTag,"收到来自查询进度后Handler的消息.What:"+message.what+"task_id:"+task_id3);tv_myTestInfo.setText(stringBuffer);getMyResult(task_id3);break;case 4://服务器端最后的查询结果final String text = (String) message.obj;Log.w(myTag,"收到来自最后的查询结果的消息.What:"+message.what+" text:"+text);stringBuffer.append("\n查询结果的消息:"+text);tv_myTestInfo.setText(stringBuffer);break;default:break;}}};
在预处理完成后,可以得到任务ID:task_id,然后在消息接收后,执行doSlice(task_id);方法
doSlice方法:
//提取主干方法 预处理后分支 合并 得到进度 上传 回调结果等private void doSlice(String taskId){File audio = new File(Environment.getExternalStorageDirectory()+"/patrol/1576159897660tt.wav");try (FileInputStream fis = new FileInputStream(audio)) {// 已得到预处理后的任务id为参数taskId// 分片上传文件int len = 0;byte[] slice = new byte[SLICE_SICE];SliceIdGenerator generator = new SliceIdGenerator();while ((len =fis.read(slice)) > 0) {// 上传分片if (fis.available() == 0) {slice = Arrays.copyOfRange(slice, 0, len);}myUploadSlice(taskId, generator.getNextSliceId(), slice);}} catch (SignatureException e) {e.printStackTrace();} catch (FileNotFoundException e1) {e1.printStackTrace();} catch (IOException e1) {e1.printStackTrace();}}
myUploadSlice(taskId, generator.getNextSliceId(), slice)方法:
/*** okHttp分片上传** @param taskId 任务id* @param slice 分片的byte数组* @throws SignatureException*/public void myUploadSlice(final String taskId, final String sliceId, final byte[] slice) throws SignatureException {Log.w(myTag,"*****uploadSlice()+sliceId"+sliceId);Map<String, String> uploadParam = getBaseAuthParam(taskId);uploadParam.put("slice_id", sliceId);new Thread() {@Overridepublic void run() {String ts = String.valueOf(System.currentTimeMillis() / 1000L);String signa = "";try {signa = EncryptUtil.HmacSHA1Encrypt(EncryptUtil.MD5(APPID + ts), SECRET_KEY);} catch (SignatureException e) {e.printStackTrace();}OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象。RequestBody requestBody = RequestBody.create(MediaType.parse("multipart/form-data"), slice);RequestBody multipartBody = new MultipartBody.Builder().setType(MultipartBody.FORM).addFormDataPart("app_id", APPID)// 文件上传的参数.addFormDataPart("ts",ts ).addFormDataPart("signa",signa )//.addFormDataPart("file", file_name,fileBody)//文件主体.addFormDataPart("task_id", taskId)//文件主体.addFormDataPart("slice_id", sliceId ).addFormDataPart("content", "fileName",requestBody).build();Request request = new Request.Builder().post(multipartBody)//.addHeader("Content-Type", "multipart/form-data;").url(LFASR_HOST + UPLOAD).build();client.newCall(request).enqueue(new Callback() {@Overridepublic void onResponse(Call call, Response response) throws IOException {if(response.isSuccessful()){//回调的方法执行在子线程。String result = response.body().string();Log.w(myTag,"分片请求发送后的状态回调"+result);ApiResultDto apiResultDto = JSONObject.parseObject(result,ApiResultDto.class);if(apiResultDto.getOk()==0){Log.w(myTag,"分片上传成功, sliceId: " + sliceId + ", sliceLen: " + slice.length);stringBuffer.append("\n分片上传成功, sliceId: " + sliceId + ", sliceLen: " + slice.length);Message message = new Message();message.what = 1;message.obj = taskId;mHandler.sendMessage(message);}else{Log.w(myTag,"分片上传失败! + response + taskId:"+taskId+"/状态码:"+apiResultDto.getErr_no()+"/原因:"+apiResultDto.getFailed());}}}@Overridepublic void onFailure(Call call, IOException e) {Log.w(myTag,"分片请求发送后的状态回调失败"+e.toString());}});}}.start();}
6.同理, 分片完成,进行合并 myMerge(task_id1)
/*** 文件合并** @param taskId 任务id* @throws SignatureException*/public void myMerge(final String taskId) {final String mergeUrl = LFASR_HOST + MERGE;/*if (response == null) {throw new RuntimeException("文件合并接口请求失败!");}if (JSON.parseObject(response).getInteger("ok") == 0) {System.out.println("文件合并成功, taskId: " + taskId);return;}throw new RuntimeException("文件合并失败!" + response);*/new Thread() {@Overridepublic void run() {String ts = String.valueOf(System.currentTimeMillis() / 1000L);String signa = "";try {signa = EncryptUtil.HmacSHA1Encrypt(EncryptUtil.MD5(APPID + ts), SECRET_KEY);} catch (SignatureException e) {e.printStackTrace();}OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象。FormBody formBody = new FormBody.Builder().add("app_id", APPID).add("ts", ts).add("signa", signa).add("task_id", taskId).build();Request request = new Request.Builder().post(formBody).url(mergeUrl).build();client.newCall(request).enqueue(new Callback() {@Overridepublic void onResponse(Call call, Response response) throws IOException {if(response.isSuccessful()){//回调的方法执行在子线程。String result = response.body().string();Log.w(myTag,"文件合并接口请求成功"+result);ApiResultDto apiResultDto = JSONObject.parseObject(result,ApiResultDto.class);if(apiResultDto.getOk()==0){Log.w(myTag,"文件合并成功");stringBuffer.append("\n文件合并成功");Message message = new Message();message.what = 2;message.obj = taskId;mHandler.sendMessage(message);}else{Log.w(myTag,"文件合并失败!taskId:"+taskId+"/状态码:"+apiResultDto.getErr_no()+"/原因:"+apiResultDto.getFailed());return;}}}@Overridepublic void onFailure(Call call, IOException e) {Log.w(myTag,"文件合并接口请求失败"+e.toString());}});}}.start();}
7.合并后,设置轮询定时器。不断查询服务器是否完成,查询到返回结果,状态为9,即可对该任务查询最后的翻译结果。
if(myTimer == null){myTimer = new Timer();myTimer.schedule(new TimerTask() {@Overridepublic void run() {myProgress(task_id2);}},10*1000,20*1000);}
/*** 获取任务进度* @param taskId 任务id* @throws SignatureException*/public void myProgress(final String taskId) {final String progressUrl = LFASR_HOST + GET_PROGRESS;new Thread(){@Overridepublic void run() {String ts = String.valueOf(System.currentTimeMillis() / 1000L);String signa = "";try {signa = EncryptUtil.HmacSHA1Encrypt(EncryptUtil.MD5(APPID + ts), SECRET_KEY);} catch (SignatureException e) {e.printStackTrace();}OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象。FormBody formBody = new FormBody.Builder().add("app_id", APPID).add("ts", ts).add("signa", signa).add("task_id", taskId).build();Request request = new Request.Builder().post(formBody).url(progressUrl).build();client.newCall(request).enqueue(new Callback() {@Overridepublic void onResponse(Call call, Response response) throws IOException {if(response.isSuccessful()){//回调的方法执行在子线程。String result = response.body().string();Log.w(myTag,"获取任务进度接口请求成功"+result);ApiResultDto apiResultDto = JSONObject.parseObject(result,ApiResultDto.class);if(apiResultDto.getOk()==0){Log.w(myTag,"任务进度接口回调成功!taskId:"+taskId);JSONObject jsonObject = JSONObject.parseObject(apiResultDto.getData());//"data":"{\"desc\":\"任务创建成功\",\"status\":0}"Log.w(myTag,"任务进度回调得到数据getData:"+JSONObject.toJSONString(jsonObject));int status = jsonObject.getInteger("status");Log.w(myTag,"任务进度回调得到数据status:"+status);if(status == 9){stringBuffer.append("\n任务进度回调得到数据成功");Message message = new Message();message.what = 3;message.obj = taskId;mHandler.sendMessage(message);}}else{Log.w(myTag,"任务进度接口回调失败!taskId:"+taskId+"/状态码:"+apiResultDto.getErr_no()+"/原因:"+apiResultDto.getFailed());return;}myTimer.cancel();myTimer = null;//无论音频是否被完成,都取消轮询定时器。比如APPID时长不足,会导致老是报提示。}}@Overridepublic void onFailure(Call call, IOException e) {Log.w(myTag,"获取任务进度接口请求失败!"+e.toString());}});}}.start();
8.获取转写结果:
/*** 获取转写结果** @param taskId* @return* @throws SignatureException*/public void getMyResult(final String taskId){final String myUrl = LFASR_HOST + GET_RESULT;//获取转写结果new Thread() {@Overridepublic void run() {String ts = String.valueOf(System.currentTimeMillis() / 1000L);String signa = "";try {signa = EncryptUtil.HmacSHA1Encrypt(EncryptUtil.MD5(APPID + ts), SECRET_KEY);Log.w(myTag, "signa: "+signa);} catch (SignatureException e) {e.printStackTrace();}OkHttpClient client = new OkHttpClient();//创建OkHttpClient对象。FormBody formBody = new FormBody.Builder().add("app_id", APPID).add("ts", ts).add("signa", signa).add("task_id", taskId).build();Request request = new Request.Builder().post(formBody).url(myUrl).build();client.newCall(request).enqueue(new Callback() {@Overridepublic void onResponse(Call call, Response response) throws IOException {if(response.isSuccessful()){//回调的方法执行在子线程。String result = response.body().string();Log.w(myTag,"获取转写结果接口请求成功:"+result);ApiResultDto apiResultDto = JSONObject.parseObject(result,ApiResultDto.class);if(apiResultDto.getOk()==0){Log.w(myTag,"获取转写结果得到数据!taskId:"+taskId);/* JSONObject jsonObject = JSONObject.parseObject(apiResultDto.getData());//得到字符串的数组JSONArray jsonArray = (JSONArray)jsonObject.get("bills");List<JSONObject> jsonArray = JSONArray.parseArray("apiResultDto.getData()") ;*/JSONArray jsonArray = JSONArray.parseArray(apiResultDto.getData());//"data":"[{\"bg\":\"0\",\"ed\":\"4950\",\"onebest\":\"科大讯飞是中国最大的智能语音技术提供商。\",\"speaker\":\"0\"}]"Log.w(myTag,"获取转写结果得到数据Json getData:"+JSONArray.toJSONString(jsonArray));StringBuffer stringBuffer = new StringBuffer(256);for (int i =0;i<jsonArray.size();i++){JSONObject jsonObject = (JSONObject) jsonArray.get(i);stringBuffer.append(jsonObject.get("onebest"));}String resultString = stringBuffer.toString();stringBuffer.append("\n最后拼接出的结果为:"+resultString);Log.w(myTag,"最后拼接出的结果为:"+resultString);//String onebest = jsonObject.getString("onebest");//Log.w(myTag,"获取转写结果得到文本Text:"+onebest);//主线程得到文本信息和更新UIMessage message = new Message();message.what = 4;message.obj = resultString;mHandler.sendMessage(message);}else{Log.w(myTag,"任务进度接口回调失败!taskId:"+taskId+"/状态码:"+apiResultDto.getErr_no()+"/原因:"+apiResultDto.getFailed());return;}}}@Overridepublic void onFailure(Call call, IOException e) {Log.w(myTag,"获取转写结果接口请求失败"+e.toString());}});}}.start();}
9.其他
别忘了给权限:
<uses-permission android:name="android.permission.RECORD_AUDIO" /><uses-permission android:name="android.permission.INTERNET" /><uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /><uses-permission android:name="android.permission.ACCESS_WIFI_STATE" /><uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" /><uses-permission android:name="android.permission.READ_PHONE_STATE" /><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-permission android:name="android.permission.READ_CONTACTS" /><uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.WRITE_SETTINGS" /><uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /><uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /><uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" /><!-- 允许程序读取或写入系统设置 --><uses-permission android:name="android.permission.WRITE_SETTINGS"/>
<activity android:name=".Audio2TextActivity"android:configChanges="mcc|mnc|locale|touchscreen|keyboard|keyboardHidden|navigation|orientation|screenLayout|fontScale"android:screenOrientation="portrait"android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="/apk/res/android"xmlns:tools="/tools"android:layout_width="match_parent"android:layout_height="match_parent"tools:context=".Audio2TextActivity"><TextViewandroid:id="@+id/myTestInfo"android:layout_width="match_parent"android:layout_height="wrap_content"android:minHeight="375dp"android:text=""android:textColor="@color/content_color"android:textSize="18sp"android:isScrollContainer="true"/></LinearLayout>
10.最后得到的翻译结果成功:
结果为:行,明天在是吧,我看待机时间嗯。
android实现科大讯飞对接就这么完成了。
就这么简单。