关于移动应用开发:300行代码实现语音搜索购物的技术分享

38次阅读

共计 10015 个字符,预计需要花费 26 分钟才能阅读完成。

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

最近,程序员阿强的那位敢于尝试新事物的外婆,又迷上了网购。在不太费劲儿地把购物软件摸得门儿清之后,没想到,本认为顺畅的网购之路,卡在了搜寻物品上。
在手写输入环节,要么误操作,无心中更换到不相熟的输入法;要么误按了界面上形象的指令字符……于是阿强也常常收到外婆发来的求助。

其实,不止是购物利用,时下智能手机里装载的大部 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 配置。
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/'}
    }
}

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

  1. 增加编译 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
  1. 在 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
    }
}
  1. 在 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>

2、接入实时语音辨认能力

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;
    }
}
  1. 创立 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");
  1. 覆写“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;
        }
    }
}

3. 接入音频文件转写能力

  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);
    }
}
  1. 新建音频文件转写引擎并初始化;新建音频文件转写配置器。
// 设置 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);
  1. 新建侦听器回调,用于解决音频文件转写后果:
    短语音转写:实用于时长小于 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);
}
  1. 获取音频,上传音频文件到转写引擎中:
// 获取音频文件的 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;
}

拜访华为开发者联盟官网,理解更多相干内容
获取开发领导文档
华为挪动服务开源仓库地址:GitHub、Gitee

正文完
 0