300行代碼讓外婆實現語音搜索購物功能

“阿強,手寫板怎麼又不見瞭?”

最近,程序員阿強的那位勇於嘗試新事物的外婆,又迷上瞭網購。在不太費勁兒地把購物軟件摸得門兒清之後,沒想到,本以為順暢的網購之路,卡在瞭搜索物品上。

在手寫輸入環節,要麼誤操作,無意中更換到不熟悉的輸入法;要麼誤按瞭界面上抽象的指令字符……於是阿強也經常收到外婆發來的求助。

其實,不止是購物應用,時下智能手機裡裝載的大部APP,都是傾斜於年輕群體的交互設計,老年人想要體驗學會使用,很難真香。

在一次次耐心指導外婆完成操作後,阿強,這個成熟coder給自己提瞭個需求:提升外婆的網購體驗。不是一味讓她適應輸入法,而是讓輸入法迎合外婆的使用偏好習慣。

手動輸入易出錯,那就寫個語音轉文字的輸入方法,隻要啟動錄音按鈕,實時語音識別輸入,簡單又快捷,外婆用瞭說直說好!

效果示例

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配置。
buildscript {
 repositories {
 google()
 jcenter()
 maven { url 'https://developer.huawei.com/repo/' }
 }
 dependencies {
 classpath 'com.android.tools.build:gradle:3.5.4'
 classpath 'com.huawei.agconnect:agcp:1.4.1.300'
 // NOTE: Do not place your application dependencies here; they belong
 // in the individual module build.gradle files
 }
}
 
allprojects {
 repositories {
 google()
 jcenter()
 maven { url 'https://developer.huawei.com/repo/' }
 }
}

參見雲端鑒權信息使用須知,設置應用的鑒權信息。

2. 添加編譯SDK依賴:

dependencies {
 //音頻文件轉寫能力 SDK
 implementation 'com.huawei.hms:ml-computer-voice-aft:2.2.0.300'
 // 實時語音轉寫 SDK.
 implementation 'com.huawei.hms:ml-computer-voice-asr:2.2.0.300'
 // 實時語音轉寫 plugin.
 implementation 'com.huawei.hms:ml-computer-voice-asr-plugin:2.2.0.300'
 ...
}
apply plugin: 'com.huawei.agconnect' // HUAWEI agconnect Gradle plugin

3.在app的build中配置簽名文件並將簽名文件(xxx.jks)放入app目錄下:

signingConfigs {
 release {
 storeFile file("xxx.jks")
 keyAlias xxx
 keyPassword xxxxxx
 storePassword xxxxxx
 v1SigningEnabled true
 v2SigningEnabled true
 }
 
}
 
buildTypes {
 release {
 minifyEnabled false
 proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
 }
 
 debug {
 signingConfig signingConfigs.release
 debuggable true
 }
}

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

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
 
<application
 android:requestLegacyExternalStorage="true"
 ...
</application>

接入實時語音識別能力

1.進行權限動態申請:

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
 requestCameraPermission();
}
 
private void requestCameraPermission() {
 final String[] permissions = new String[]{Manifest.permission.RECORD_AUDIO};
 if (!ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
 ActivityCompat.requestPermissions(this, permissions, Constants.AUDIO_PERMISSION_CODE);
 return;
 }
}

2.創建Intent,用於設置實時語音識別參數。

//設置您應用的鑒權信息
MLApplication.getInstance().setApiKey(AGConnectServicesConfig.fromContext(this).getString("client/api_key"));
 通過intent進行識別設置。
Intent intentPlugin = new Intent(this, MLAsrCaptureActivity.class)
 // 設置識別語言為英語,若不設置,則默認識別英語。支持設置:"zh-CN":中文;"en-US":英語等。
 .putExtra(MLAsrCaptureConstants.LANGUAGE, MLAsrConstants.LAN_ZH_CN)
 // 設置拾音界面是否顯示識別結果
 .putExtra(MLAsrCaptureConstants.FEATURE, MLAsrCaptureConstants.FEATURE_WORDFLUX);
startActivityForResult(intentPlugin, "1");

3.覆寫“onActivityResult”方法,用於處理語音識別服務返回結果。

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
 super.onActivityResult(requestCode, resultCode, data);
 String text = "";
 if (null == data) {
 addTagItem("Intent data is null.", true);
 }
 if (requestCode == "1") {
 if (data == null) {
  return;
 }
 Bundle bundle = data.getExtras();
 if (bundle == null) {
  return;
 }
 switch (resultCode) {
  case MLAsrCaptureConstants.ASR_SUCCESS:
  // 獲取語音識別得到的文本信息。
  if (bundle.containsKey(MLAsrCaptureConstants.ASR_RESULT)) {
   text = bundle.getString(MLAsrCaptureConstants.ASR_RESULT);
  }
  if (text == null || "".equals(text)) {
   text = "Result is null.";
   Log.e(TAG, text);
  } else {
   //將語音識別結果設置在搜索框上
   searchEdit.setText(text);
   goSearch(text, true);
  }
  break;
  // 返回值為MLAsrCaptureConstants.ASR_FAILURE表示識別失敗。
  case MLAsrCaptureConstants.ASR_FAILURE:
  // 判斷是否包含錯誤碼。
  if (bundle.containsKey(MLAsrCaptureConstants.ASR_ERROR_CODE)) {
   text = text + bundle.getInt(MLAsrCaptureConstants.ASR_ERROR_CODE);
   // 對錯誤碼進行處理。
  }
  // 判斷是否包含錯誤信息。
  if (bundle.containsKey(MLAsrCaptureConstants.ASR_ERROR_MESSAGE)) {
   String errorMsg = bundle.getString(MLAsrCaptureConstants.ASR_ERROR_MESSAGE);
   // 對錯誤信息進行處理。
   if (errorMsg != null && !"".equals(errorMsg)) {
   text = "[" + text + "]" + errorMsg;
   }
  }
  //判斷是否包含子錯誤碼。
  if (bundle.containsKey(MLAsrCaptureConstants.ASR_SUB_ERROR_CODE)) {
   int subErrorCode = bundle.getInt(MLAsrCaptureConstants.ASR_SUB_ERROR_CODE);
   // 對子錯誤碼進行處理。
   text = "[" + text + "]" + subErrorCode;
  }
  Log.e(TAG, text);
  break;
  default:
  break;
 }
 }
}

接入音頻文件轉寫能力

1.申請動態權限。

private static final int REQUEST_EXTERNAL_STORAGE = 1;
private static final String[] PERMISSIONS_STORAGE = {
 Manifest.permission.READ_EXTERNAL_STORAGE,
 Manifest.permission.WRITE_EXTERNAL_STORAGE };
public static void verifyStoragePermissions(Activity activity) {
 // Check if we have write permission
 int permission = ActivityCompat.checkSelfPermission(activity,
  Manifest.permission.WRITE_EXTERNAL_STORAGE);
 if (permission != PackageManager.PERMISSION_GRANTED) {
 // We don't have permission so prompt the user
 ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,
  REQUEST_EXTERNAL_STORAGE);
 }
}

2.新建音頻文件轉寫引擎並初始化;新建音頻文件轉寫配置器。

// 設置 ApiKey.
MLApplication.getInstance().setApiKey(AGConnectServicesConfig.fromContext(getApplication()).getString("client/api_key"));
MLRemoteAftSetting setting = new MLRemoteAftSetting.Factory()
 // 設置轉寫語言編碼,使用BCP-47規范,當前支持中文普通話、英文轉寫。
 .setLanguageCode("zh")
 // 設置是否在轉寫輸出的文本中自動增加標點符號,默認為false。
 .enablePunctuation(true)
 // 設置是否連帶輸出每段音頻的文字轉寫結果和對應的音頻時移,默認為false(此參數僅小於1分鐘的音頻需要設置)。
 .enableWordTimeOffset(true)
 // 設置是否輸出句子出現在音頻文件中的時間偏移值,默認為false。
 .enableSentenceTimeOffset(true)
 .create();
 
// 新建音頻文件轉寫引擎。
MLRemoteAftEngine engine = MLRemoteAftEngine.getInstance();
engine.init(this);
// 將偵聽器回調傳給第一步中定義的音頻文件轉寫引擎中
engine.setAftListener(aftListener);

3.新建偵聽器回調,用於處理音頻文件轉寫結果:

短語音轉寫:適用於時長小於1分鐘的音頻文件

private MLRemoteAftListener aftListener = new MLRemoteAftListener() {
 public void onResult(String taskId, MLRemoteAftResult result, Object ext) {
 // 獲取轉寫結果通知。
 if (result.isComplete()) {
  // 轉寫結果處理。
 }
 }
 @Override
 public void onError(String taskId, int errorCode, String message) {
 // 轉寫錯誤回調函數。
 }
 @Override
 public void onInitComplete(String taskId, Object ext) {
 // 預留接口。
 }
 @Override
 public void onUploadProgress(String taskId, double progress, Object ext) {
 // 預留接口。
 }
 @Override
 public void onEvent(String taskId, int eventId, Object ext) {
 // 預留接口。
 }
};

長語音轉寫:適用於時長大於1分鐘的音頻文件

private MLRemoteAftListener asrListener = new MLRemoteAftListener() {
 @Override
 public void onInitComplete(String taskId, Object ext) {
 Log.e(TAG, "MLAsrCallBack onInitComplete");
 // 長語音初始化完成,開始轉寫
 start(taskId);
 }
 @Override
 public void onUploadProgress(String taskId, double progress, Object ext) {
 Log.e(TAG, " MLAsrCallBack onUploadProgress");
 }
 @Override
 public void onEvent(String taskId, int eventId, Object ext) {
 // 用於長語音
 Log.e(TAG, "MLAsrCallBack onEvent" + eventId);
 if (MLAftEvents.UPLOADED_EVENT == eventId) { // 文件上傳成功
  // 獲取轉寫結果
  startQueryResult(taskId);
 }
 }
 @Override
 public void onResult(String taskId, MLRemoteAftResult result, Object ext) {
 Log.e(TAG, "MLAsrCallBack onResult taskId is :" + taskId + " ");
 if (result != null) {
  Log.e(TAG, "MLAsrCallBack onResult isComplete: " + result.isComplete());
  if (result.isComplete()) {
  TimerTask timerTask = timerTaskMap.get(taskId);
  if (null != timerTask) {
   timerTask.cancel();
   timerTaskMap.remove(taskId);
  }
  if (result.getText() != null) {
   Log.e(TAG, taskId + " MLAsrCallBack onResult result is : " + result.getText());
   tvText.setText(result.getText());
  }
  List<MLRemoteAftResult.Segment> words = result.getWords();
  if (words != null && words.size() != 0) {
   for (MLRemoteAftResult.Segment word : words) {
   Log.e(TAG, "MLAsrCallBack word text is : " + word.getText() + ", startTime is : " + word.getStartTime() + ". endTime is : " + word.getEndTime());
   }
  }
  List<MLRemoteAftResult.Segment> sentences = result.getSentences();
  if (sentences != null && sentences.size() != 0) {
   for (MLRemoteAftResult.Segment sentence : sentences) {
   Log.e(TAG, "MLAsrCallBack sentence text is : " + sentence.getText() + ", startTime is : " + sentence.getStartTime() + ". endTime is : " + sentence.getEndTime());
   }
  }
  }
 }
 }
 @Override
 public void onError(String taskId, int errorCode, String message) {
 Log.i(TAG, "MLAsrCallBack onError : " + message + "errorCode, " + errorCode);
 switch (errorCode) {
  case MLAftErrors.ERR_AUDIO_FILE_NOTSUPPORTED:
  break;
 }
 }
};
// 上傳轉寫任務
private void start(String taskId) {
 Log.e(TAG, "start");
 engine.setAftListener(asrListener);
 engine.startTask(taskId);
}
// 獲取轉寫結果
private Map<String, TimerTask> timerTaskMap = new HashMap<>();
private void startQueryResult(final String taskId) {
 Timer mTimer = new Timer();
 TimerTask mTimerTask = new TimerTask() {
 @Override
 public void run() {
  getResult(taskId);
 }
 };
 // 10s輪訓獲取長語音轉寫結果
 mTimer.schedule(mTimerTask, 5000, 10000);
 // 界面銷毀前要清除 timerTaskMap
 timerTaskMap.put(taskId, mTimerTask);
}

4.獲取音頻,上傳音頻文件到轉寫引擎中:

//獲取音頻文件的uri
Uri uri = getFileUri();
//獲取音頻時間
Long audioTime = getAudioFileTimeFromUri(uri);
//判斷音頻時間是否超過60秒
if (audioTime < 60000) {
 // uri為從本地存儲或者錄音機讀取到的語音資源,僅支持時長在1分鐘之內的本地音頻
 this.taskId = this.engine.shortRecognize(uri, this.setting);
 Log.i(TAG, "Short audio transcription.");
} else {
 // longRecognize為長語音轉寫接口,用於轉寫時長大於1分鐘,小於5小時的語音。
 this.taskId = this.engine.longRecognize(uri, this.setting);
 Log.i(TAG, "Long audio transcription.");
}
 
private Long getAudioFileTimeFromUri(Uri uri) {
 Long time = null;
 Cursor cursor = this.getContentResolver()
  .query(uri, null, null, null, null);
 if (cursor != null) {
 
 cursor.moveToFirst();
 time = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION));
 } else {
 MediaPlayer mediaPlayer = new MediaPlayer();
 try {
  mediaPlayer.setDataSource(String.valueOf(uri));
  mediaPlayer.prepare();
 } catch (IOException e) {
  Log.e(TAG, "Failed to read the file time.");
 }
 time = Long.valueOf(mediaPlayer.getDuration());
 }
 return time;
}

到此這篇關於300行代碼讓外婆實現語音搜索購物功能的文章就介紹到這瞭,更多相關外婆實現語音搜索購物內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!