7

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

 3 years ago
source link: https://my.oschina.net/u/4956408/blog/4999735
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

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

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

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

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

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

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

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

1、开发前准备

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>

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;
    }
}

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;
        }
    }
}

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);
    }
}

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;
}

>>访问华为开发者联盟官网,了解更多相关内容
>>获取开发指导文档
>>华为移动服务开源仓库地址:GitHubGitee

点击右上角头像右方的关注,第一时间了解华为移动服务最新技术~


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK