飞道的博客

程序员用300行代码,让外婆实现语音搜索购物

501人阅读  评论(0)

 “阿强,手写板怎么又不见了?”

最近,程序员阿强的那位勇于尝试新事物的外婆,又迷上了网购。在不太费劲儿地把购物软件摸得门儿清之后,没想到,本以为顺畅的网购之路,卡在了搜索物品上。

在手写输入环节,要么误操作,无意中更换到不熟悉的输入法;要么误按了界面上抽象的指令字符……于是阿强也经常收到外婆发来的求助。

其实,不止是购物应用,时下智能手机里装载的大部APP,都是倾斜于年轻群体的交互设计,老年人想要体验学会使用,很难真香。

在一次次耐心指导外婆完成操作后,阿强,这个成熟coder给自己提了个需求:提升外婆的网购体验。不是一味让她适应输入法,而是让输入法迎合外婆的使用偏好习惯。

手动输入易出错,那就写个语音转文字的输入方法,只要启动录音按钮,实时语音识别输入,简单又快捷,外婆用了说直说好!

效果示例

 

应用场景

实时语音识别和音频转文字有着丰富的应用的场景。

  1. 游戏应用中的运用:当你在联机游戏场组队开黑时,通过实时语音识别跟队友无阻沟通,不占用双手的同时,也避免了开麦露出声音的尴尬。
  2. 办公应用中的运用:职场里,耗时长的会议,手打码字记录即低效,还容易漏掉细节,凭借音频文件转文字功能,转写会议讨论内容,会后对转写的文字进行梳理润色,事半功倍。
  3. 学习应用中的运用:时下越来越多的音频教学材料,一边观看一边暂停做笔记,很容易打断学习节奏,破坏学习过程的完整性,有了音频文件转写,系统的学习完教材后,再对文字进行复习梳理,学习体验更佳。

实现原理

华为机器学习服务提供实时语音识别音频文件转写能力。

实时语音识别

支持将实时输入的短语音(时长不超过60秒)转换为文本,识别准确率可达95%以上。目前支持中文普通话、英语、中英混说、法语、德语、西班牙语、意大利语、阿拉伯语的识别。

  • 支持实时出字。
  • 提供拾音界面、无拾音界面两种方式。
  • 支持端点检测,可准确定位开始和结束点。
  • 支持静音检测,语音中未说话部分不发送语音包。
  • 支持数字格式的智能转换,例如语音输入“二零二一年”时,能够智能识别为“2021年”。

音频文件转写

可将5小时内的音频文件转换成文字,支持输出标点符号,形成断句合理、易于理解的文本信息。同时支持生成带有时间戳的文本信息,便于后续进行更多功能开发。当前版本支持中英文的转写。

开发步骤

开发前准备

1. 配置华为Maven仓地址并将agconnect-services.json文件放到app目录下:
打开Android Studio项目级“build.gradle”文件。


添加HUAWEI agcp插件以及Maven代码库。

  • 在“allprojects > repositories”中配置HMS Core SDK的Maven仓地址。
  • 在“buildscript > repositories”中配置HMS Core SDK的Maven仓地址。
  • 如果App中添加了“agconnect-services.json”文件则需要在“buildscript > dependencies”中增加agcp配置。

  
  1. buildscript {
  2. repositories {
  3. google()
  4. jcenter()
  5. maven { url 'https: //developer.huawei.com/repo/' }
  6. }
  7. dependencies {
  8. classpath 'com.android.tools.build:gradle: 3.5 .4'
  9. classpath 'com.huawei.agconnect:agcp: 1.4 .1 .300'
  10. // NOTE: Do not place your application dependencies here; they belong
  11. // in the individual module build.gradle files
  12. }
  13. }
  14. allprojects {
  15. repositories {
  16. google()
  17. jcenter()
  18. maven { url 'https: //developer.huawei.com/repo/' }
  19. }
  20. }

参见云端鉴权信息使用须知,设置应用的鉴权信息。

2. 添加编译SDK依赖:


  
  1. dependencies {
  2. //音频文件转写能力 SDK
  3. implementation 'com.huawei.hms:ml-computer-voice-aft:2.2.0.300'
  4. // 实时语音转写 SDK.
  5. implementation 'com.huawei.hms:ml-computer-voice-asr:2.2.0.300'
  6. // 实时语音转写 plugin.
  7. implementation 'com.huawei.hms:ml-computer-voice-asr-plugin:2.2.0.300'
  8. ...
  9. }
  10. apply plugin: 'com.huawei.agconnect' // HUAWEI agconnect Gradle plugin

3.在app的build中配置签名文件并将签名文件(xxx.jks)放入app目录下:


  
  1. signingConfigs {
  2. release {
  3. storeFile file("xxx.jks")
  4. keyAlias xxx
  5. keyPassword xxxxxx
  6. storePassword xxxxxx
  7. v1SigningEnabled true
  8. v2SigningEnabled true
  9. }
  10. }
  11. buildTypes {
  12. release {
  13. minifyEnabled false
  14. proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
  15. }
  16. debug {
  17. signingConfig signingConfigs.release
  18. debuggable true
  19. }
  20. }

4.在Manifest.xml中添加权限:


  
  1. <uses-permission android:name="android.permission.INTERNET" />
  2. <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  3. <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
  4. <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  5. <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
  6. <uses-permission android:name="android.permission.RECORD_AUDIO" />
  7. <application
  8. android:requestLegacyExternalStorage= "true"
  9. ...
  10. </ application>

接入实时语音识别能力

1.进行权限动态申请:


  
  1. if (ActivityCompat.checkSelfPermission( this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
  2. requestCameraPermission();
  3. }
  4. private void requestCameraPermission() {
  5. final String[] permissions = new String[]{Manifest.permission.RECORD_AUDIO};
  6. if (!ActivityCompat.shouldShowRequestPermissionRationale( this, Manifest.permission.RECORD_AUDIO)) {
  7. ActivityCompat.requestPermissions( this, permissions, Constants.AUDIO_PERMISSION_CODE);
  8. return;
  9. }
  10. }

2.创建Intent,用于设置实时语音识别参数。


  
  1. //设置您应用的鉴权信息
  2. MLApplication.getInstance().setApiKey(AGConnectServicesConfig.fromContext( this).getString( "client/api_key"));
  3. 通过intent进行识别设置。
  4. Intent intentPlugin = new Intent( this, MLAsrCaptureActivity. class)
  5. // 设置识别语言为英语,若不设置,则默认识别英语。支持设置: "zh-CN":中文; "en-US":英语等。
  6. .putExtra(MLAsrCaptureConstants.LANGUAGE, MLAsrConstants.LAN_ZH_CN)
  7. // 设置拾音界面是否显示识别结果
  8. .putExtra(MLAsrCaptureConstants.FEATURE, MLAsrCaptureConstants.FEATURE_WORDFLUX);
  9. startActivityForResult(intentPlugin, "1");

3.覆写“onActivityResult”方法,用于处理语音识别服务返回结果。


  
  1. @Override
  2. protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
  3. super.onActivityResult(requestCode, resultCode, data);
  4. String text = "";
  5. if ( null == data) {
  6. addTagItem( "Intent data is null.", true);
  7. }
  8. if (requestCode == "1") {
  9. if ( data == null) {
  10. return;
  11. }
  12. Bundle bundle = data.getExtras();
  13. if (bundle == null) {
  14. return;
  15. }
  16. switch (resultCode) {
  17. case MLAsrCaptureConstants.ASR_SUCCESS:
  18. // 获取语音识别得到的文本信息。
  19. if (bundle.containsKey(MLAsrCaptureConstants.ASR_RESULT)) {
  20. text = bundle.getString(MLAsrCaptureConstants.ASR_RESULT);
  21. }
  22. if (text == null || "".equals(text)) {
  23. text = "Result is null.";
  24. Log.e(TAG, text);
  25. } else {
  26. //将语音识别结果设置在搜索框上
  27. searchEdit.setText(text);
  28. goSearch(text, true);
  29. }
  30. break;
  31. // 返回值为MLAsrCaptureConstants.ASR_FAILURE表示识别失败。
  32. case MLAsrCaptureConstants.ASR_FAILURE:
  33. // 判断是否包含错误码。
  34. if (bundle.containsKey(MLAsrCaptureConstants.ASR_ERROR_CODE)) {
  35. text = text + bundle.getInt(MLAsrCaptureConstants.ASR_ERROR_CODE);
  36. // 对错误码进行处理。
  37. }
  38. // 判断是否包含错误信息。
  39. if (bundle.containsKey(MLAsrCaptureConstants.ASR_ERROR_MESSAGE)) {
  40. String errorMsg = bundle.getString(MLAsrCaptureConstants.ASR_ERROR_MESSAGE);
  41. // 对错误信息进行处理。
  42. if (errorMsg != null && ! "".equals(errorMsg)) {
  43. text = "[" + text + "]" + errorMsg;
  44. }
  45. }
  46. //判断是否包含子错误码。
  47. if (bundle.containsKey(MLAsrCaptureConstants.ASR_SUB_ERROR_CODE)) {
  48. int subErrorCode = bundle.getInt(MLAsrCaptureConstants.ASR_SUB_ERROR_CODE);
  49. // 对子错误码进行处理。
  50. text = "[" + text + "]" + subErrorCode;
  51. }
  52. Log.e(TAG, text);
  53. break;
  54. default:
  55. break;
  56. }
  57. }
  58. }

接入音频文件转写能力

1.申请动态权限。


  
  1. private static final int REQUEST_EXTERNAL_STORAGE = 1;
  2. private static final String[] PERMISSIONS_STORAGE = {
  3. Manifest.permission.READ_EXTERNAL_STORAGE,
  4. Manifest.permission.WRITE_EXTERNAL_STORAGE };
  5. public static void verifyStoragePermissions(Activity activity) {
  6. // Check if we have write permission
  7. int permission = ActivityCompat.checkSelfPermission(activity,
  8. Manifest.permission.WRITE_EXTERNAL_STORAGE);
  9. if (permission != PackageManager.PERMISSION_GRANTED) {
  10. // We don't have permission so prompt the user
  11. ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,
  12. REQUEST_EXTERNAL_STORAGE);
  13. }
  14. }

2.新建音频文件转写引擎并初始化;新建音频文件转写配置器。


  
  1. // 设置 ApiKey.
  2. MLApplication.getInstance().setApiKey(AGConnectServicesConfig.fromContext(getApplication()).getString( "client/api_key"));
  3. MLRemoteAftSetting setting = new MLRemoteAftSetting.Factory()
  4. // 设置转写语言编码,使用BCP-47规范,当前支持中文普通话、英文转写。
  5. .setLanguageCode( "zh")
  6. // 设置是否在转写输出的文本中自动增加标点符号,默认为false。
  7. .enablePunctuation( true)
  8. // 设置是否连带输出每段音频的文字转写结果和对应的音频时移,默认为false(此参数仅小于1分钟的音频需要设置)。
  9. .enableWordTimeOffset( true)
  10. // 设置是否输出句子出现在音频文件中的时间偏移值,默认为false。
  11. .enableSentenceTimeOffset( true)
  12. .create();
  13. // 新建音频文件转写引擎。
  14. MLRemoteAftEngine engine = MLRemoteAftEngine.getInstance();
  15. engine. init( this);
  16. // 将侦听器回调传给第一步中定义的音频文件转写引擎中
  17. engine.setAftListener(aftListener);

3.新建侦听器回调,用于处理音频文件转写结果:

短语音转写:适用于时长小于1分钟的音频文件


  
  1. private MLRemoteAftListener aftListener = new MLRemoteAftListener() {
  2. public void onResult( String taskId, MLRemoteAftResult result, Object ext) {
  3. // 获取转写结果通知。
  4. if (result.isComplete()) {
  5. // 转写结果处理。
  6. }
  7. }
  8. @Override
  9. public void onError( String taskId, int errorCode, String message) {
  10. // 转写错误回调函数。
  11. }
  12. @Override
  13. public void onInitComplete( String taskId, Object ext) {
  14. // 预留接口。
  15. }
  16. @Override
  17. public void onUploadProgress( String taskId, double progress, Object ext) {
  18. // 预留接口。
  19. }
  20. @Override
  21. public void onEvent( String taskId, int eventId, Object ext) {
  22. // 预留接口。
  23. }
  24. };

长语音转写:适用于时长大于1分钟的音频文件


  
  1. private MLRemoteAftListener asrListener = new MLRemoteAftListener() {
  2. @Override
  3. public void onInitComplete( String taskId, Object ext) {
  4. Log.e(TAG, "MLAsrCallBack onInitComplete");
  5. // 长语音初始化完成,开始转写
  6. start(taskId);
  7. }
  8. @Override
  9. public void onUploadProgress( String taskId, double progress, Object ext) {
  10. Log.e(TAG, " MLAsrCallBack onUploadProgress");
  11. }
  12. @Override
  13. public void onEvent( String taskId, int eventId, Object ext) {
  14. // 用于长语音
  15. Log.e(TAG, "MLAsrCallBack onEvent" + eventId);
  16. if (MLAftEvents.UPLOADED_EVENT == eventId) { // 文件上传成功
  17. // 获取转写结果
  18. startQueryResult(taskId);
  19. }
  20. }
  21. @Override
  22. public void onResult( String taskId, MLRemoteAftResult result, Object ext) {
  23. Log.e(TAG, "MLAsrCallBack onResult taskId is :" + taskId + " ");
  24. if (result != null) {
  25. Log.e(TAG, "MLAsrCallBack onResult isComplete: " + result.isComplete());
  26. if (result.isComplete()) {
  27. TimerTask timerTask = timerTaskMap.get(taskId);
  28. if ( null != timerTask) {
  29. timerTask.cancel();
  30. timerTaskMap.remove(taskId);
  31. }
  32. if (result.getText() != null) {
  33. Log.e(TAG, taskId + " MLAsrCallBack onResult result is : " + result.getText());
  34. tvText.setText(result.getText());
  35. }
  36. List<MLRemoteAftResult.Segment> words = result.getWords();
  37. if (words != null && words.size() != 0) {
  38. for (MLRemoteAftResult.Segment word : words) {
  39. Log.e(TAG, "MLAsrCallBack word text is : " + word.getText() + ", startTime is : " + word.getStartTime() + ". endTime is : " + word.getEndTime());
  40. }
  41. }
  42. List<MLRemoteAftResult.Segment> sentences = result.getSentences();
  43. if (sentences != null && sentences.size() != 0) {
  44. for (MLRemoteAftResult.Segment sentence : sentences) {
  45. Log.e(TAG, "MLAsrCallBack sentence text is : " + sentence.getText() + ", startTime is : " + sentence.getStartTime() + ". endTime is : " + sentence.getEndTime());
  46. }
  47. }
  48. }
  49. }
  50. }
  51. @Override
  52. public void onError( String taskId, int errorCode, String message) {
  53. Log.i(TAG, "MLAsrCallBack onError : " + message + "errorCode, " + errorCode);
  54. switch (errorCode) {
  55. case MLAftErrors.ERR_AUDIO_FILE_NOTSUPPORTED:
  56. break;
  57. }
  58. }
  59. };
  60. // 上传转写任务
  61. private void start( String taskId) {
  62. Log.e(TAG, "start");
  63. engine.setAftListener(asrListener);
  64. engine.startTask(taskId);
  65. }
  66. // 获取转写结果
  67. private Map< String, TimerTask> timerTaskMap = new HashMap<>();
  68. private void startQueryResult(final String taskId) {
  69. Timer mTimer = new Timer();
  70. TimerTask mTimerTask = new TimerTask() {
  71. @Override
  72. public void run() {
  73. getResult(taskId);
  74. }
  75. };
  76. // 10s轮训获取长语音转写结果
  77. mTimer.schedule(mTimerTask, 5000, 10000);
  78. // 界面销毁前要清除 timerTaskMap
  79. timerTaskMap.put(taskId, mTimerTask);
  80. }

4.获取音频,上传音频文件到转写引擎中:


  
  1. //获取音频文件的uri
  2. Uri uri = getFileUri();
  3. //获取音频时间
  4. Long audioTime = getAudioFileTimeFromUri(uri);
  5. //判断音频时间是否超过60秒
  6. if (audioTime < 60000) {
  7. // uri为从本地存储或者录音机读取到的语音资源,仅支持时长在1分钟之内的本地音频
  8. this.taskId = this.engine.shortRecognize(uri, this.setting);
  9. Log.i(TAG, "Short audio transcription.");
  10. } else {
  11. // longRecognize为长语音转写接口,用于转写时长大于1分钟,小于5小时的语音。
  12. this.taskId = this.engine.longRecognize(uri, this.setting);
  13. Log.i(TAG, "Long audio transcription.");
  14. }
  15. private Long getAudioFileTimeFromUri(Uri uri) {
  16. Long time = null;
  17. Cursor cursor = this.getContentResolver()
  18. .query(uri, null, null, null, null);
  19. if (cursor != null) {
  20. cursor.moveToFirst();
  21. time = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
  22. } else {
  23. MediaPlayer mediaPlayer = new MediaPlayer();
  24. try {
  25. mediaPlayer.setDataSource(String.valueOf(uri));
  26. mediaPlayer.prepare();
  27. } catch (IOException e) {
  28. Log.e(TAG, "Failed to read the file time.");
  29. }
  30. time = Long.valueOf(mediaPlayer.getDuration());
  31. }
  32. return time;
  33. }

 

 

>>访问华为开发者联盟官网,了解更多相关内容

>>获取开发指导文档

>>华为移动服务开源仓库地址:GitHubGitee

关注我们,第一时间了解华为移动服务最新技术资讯~


转载:https://blog.csdn.net/HUAWEI_HMSCore/article/details/115242088
查看评论
* 以上用户言论只代表其个人观点,不代表本网站的观点或立场