“强强,有没有看到爷爷的眼镜啊?”

众所周知,我厂程序员阿强,有个头发花白却对读书看报葆有热忱的爷爷。阿强说每次回家看到爷爷鼻梁上架着老花镜,背部战略性后仰,对着手里的报纸看得吃力又沉迷的样子,就觉得很有必要为老爷子提升下阅读体验。

读报耗费眼力,不妨解放眼睛的工作让耳朵为阅读出份力。于是乎,阿强决定开发一个书本朗读App,用这个App只需要拍摄纸张上的文字内容,就能转化成语音朗读出来,变“看“为”听“。让老爷子能一边悠闲的躺在摇椅上,一边“听书闻报“看天下 。

Demo演示

爷爷八十大寿,程序员为他写了一个书本朗读App-LMLPHP

解决思路:

1、拍摄书本,识别书本照片中的文字信息,并将其中的文本信息提取出来
2、语音合成技术在线将识别出的文本信息转化为语音输出

以上这两个步骤依次用到了华为机器学习服务文本识别语音合成能力,实现起来难度并不大。 以下是具体开发步骤。

开发前准备:

1. 配置华为Maven仓地址并将agconnect-services.json文件放到app目录下:

buildscript {
    repositories {
        google()
        jcenter()
        maven { url "https://developer.huawei.com/repo/" }
    }
    dependencies {
        classpath "com.android.tools.build:gradle:4.1.1"
        classpath 'com.huawei.agconnect:agcp:1.4.2.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-vision-ocr:2.0.5.300'
    // 引入拉丁语文字识别模型包
    implementation 'com.huawei.hms:ml-computer-vision-ocr-latin-model:2.0.5.300'
    // 引入中英文文字识别模型包
    implementation 'com.huawei.hms:ml-computer-vision-ocr-cn-model:2.0.5.300'

    // 引入TTS基础SDK
    implementation 'com.huawei.hms:ml-computer-voice-tts:2.2.0.300'
}

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" />

开发步骤:

1.   进行权限动态申请并配置文本识别、TTS的解析器:

(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) 配置解析器

private void initAnalyers() {
// apikey是一种访问华为服务的简单凭证,云测服务都需要设置,这里是动态获取agconnect-services.json文件中的apikey
    MLApplication.getInstance().setApiKey(AGConnectServicesConfig.fromContext(getApplicationContext()).getString("client/api_key"));

    List<String> languageList = new ArrayList();
    languageList.add("zh");
    languageList.add("en");
    MLRemoteTextSetting ocrSetting =
            new MLRemoteTextSetting.Factory()
                    // MLRemoteTextSetting.OCR_COMPACT_SCENE:文本密集场景的文本识别。
                    .setTextDensityScene(MLRemoteTextSetting.OCR_COMPACT_SCENE)
                    // 设置识别语言列表,使用ISO 639-1标准。
                    .setLanguageList(languageList)
                    // MLRemoteTextSetting.ARC:返回文本排列为弧形的多边形边界的顶点,最多可返回多达72个顶点的坐标。
                    .setBorderType(MLRemoteTextSetting.ARC)
                    .create();
    ocrAnalyzer = MLAnalyzerFactory.getInstance().getRemoteTextAnalyzer(ocrSetting);

    MLTtsConfig mlConfigs;
    mlConfigs = new MLTtsConfig()
            // 设置合成文本为中文。
            .setLanguage(MLTtsConstants.TTS_ZH_HANS)
            // 设置中文发音音色。
            .setPerson(MLTtsConstants.TTS_SPEAKER_FEMALE_ZH)
            // 设置发音速度。范围:(0,5.0],1.0表示正常语速。
            .setSpeed(1.0f)
            // 设置音量。范围:(0,2),1.0表示正常音量。
            .setVolume(1.0f)
            // set the synthesis mode.
            .setSynthesizeMode(MLTtsConstants.TTS_ONLINE_MODE);
    mlTtsEngine = new MLTtsEngine(mlConfigs);
    // 设置内置播放器音量,取值范围:[0,100] dB(分贝)
    mlTtsEngine.setPlayerVolume(20);
    // 设置回调
    mlTtsEngine.setTtsCallback(callback);
}

(3)  配置上面一步中mlTtsEngine需要的回调

//没有特殊需要的话,下面的代码加上就能用
MLTtsCallback callback = new MLTtsCallback() {
    @Override
    public void onError(String taskId, MLTtsError err) {
        // 语音合成失败处理。
    }
    @Override
    public void onWarn(String taskId, MLTtsWarn warn) {
        // 告警处理(不影响业务逻辑)。
    }
    @Override
    // 返回当前播放分片和文本对应关系。start表示音频分片在输入文本中的起始位置,end表示音频分片在输入文本中的结束位置(不包含)。
    public void onRangeStart(String taskId, int start, int end) {
        // 当前播放分片和文本对应关系处理。
    }
    @Override
    // taskId 该音频对应的语音合成任务Id。
    // audioFragment 音频数据。
    // offset 一个语音合成任务会对应一个音频合成数据队列,该字段表示本次传输的音频分片在该队列中的偏移量。
    // range 本次传输的音频分片所在的文本区域,range.first为起始位置(包含的),range.second为结束位置(不包含的)。
    public void onAudioAvailable(String taskId, MLTtsAudioFragment audioFragment, int offset, Pair<Integer, Integer> range,
                                 Bundle bundle){
        // Tts合成音频流回调接口,通过此接口将音频合成数据返回给App。
    }
    @Override
    public void onEvent(String taskId, int eventId, Bundle bundle) {
        // 合成事件回调方法。eventId为事件名称。
        switch (eventId) {
            case MLTtsConstants.EVENT_PLAY_START:
                // 播放开始回调。
                break;
            case MLTtsConstants.EVENT_PLAY_STOP:
                // 播放停止回调。
                boolean isInterrupted = bundle.getBoolean(MLTtsConstants.EVENT_PLAY_STOP_INTERRUPTED);
                break;
            case MLTtsConstants.EVENT_PLAY_RESUME:
                // 播放恢复回调。
                break;
            case MLTtsConstants.EVENT_PLAY_PAUSE:
                // 播放暂停回调。
                break;
            //以下回调事件类型是在不使用内部播放器播放,只关注合成音频数据时,需要关注的回调接口。
            case MLTtsConstants.EVENT_SYNTHESIS_START:
                // 语音合成开始的回调。
                break;
            case MLTtsConstants.EVENT_SYNTHESIS_END:
                // 语音合成结束的回调。
                break;
            case MLTtsConstants.EVENT_SYNTHESIS_COMPLETE:
                // 语音合成完成,同时合成的语音流全部传给App了。
                break;
            default:
                break;
        }
    }
};

2.  为了更好地进行文本识别,需要获得相机拍摄的清晰图片:

//设置照片保存的地址
private void initPhotoPath() {
    StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder();
    StrictMode.setVmPolicy(builder.build());
    mFilePath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/systemCemer";
    File outFilePath = new File(mFilePath);
    if (!outFilePath.exists()) {
        outFilePath.mkdirs();
    }
    String fileName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
    mFilePath = mFilePath + "/" + fileName + ".jpg";
    File outFile = new File(mFilePath);
    uri = Uri.fromFile(outFile);
}

case R.id.bt_takePhoto:
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    //调用相机时传入地址
    intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
    startActivityForResult(intent, 2);
break;

//在onActivityResult中读取保存地址中的图片
public Bitmap loadingImageBitmap(String imagePath) {
    final int width = getWindowManager().getDefaultDisplay().getWidth();
    final int height = getWindowManager().getDefaultDisplay().getHeight();

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    Bitmap bitmap = null;
    try {
        bitmap = BitmapFactory.decodeFile(imagePath, options);

        int widthRaio = (int) Math.ceil(options.outWidth/(float)width);
        int heightRaio = (int) Math.ceil(options.outHeight/(float)height);
        if (widthRaio>1&&heightRaio>1){
            if (widthRaio>heightRaio){
                options.inSampleSize = widthRaio;
            }else {
                options.inSampleSize = heightRaio;
            }
        }
        options.inJustDecodeBounds = false;
        bitmap = BitmapFactory.decodeFile(imagePath, options);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return bitmap;
}

3. 读取完图片后进行文本识别:

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode==RESULT_OK){
        if (requestCode == 2){
            Bitmap bitmap = loadingImageBitmap(mFilePath);
            ((ImageView)findViewById(R.id.im_photo)).setImageBitmap(bitmap);
            mlFrame = new MLFrame.Creator().setBitmap(bitmap).create();
            Task<MLText> task = ocrAnalyzer.asyncAnalyseFrame(mlFrame);
            task.addOnSuccessListener(new OnSuccessListener<MLText>() {
                @Override
                public void onSuccess(MLText text) {
                   //成功识别后,处理结果
                    List<MLText.Block> blocks = text.getBlocks();
                    for (MLText.Block block : blocks) {
                        List<MLText.TextLine> lines = block.getContents();
                        for (MLText.TextLine line : lines) {
                            List<MLText.Word> words = line.getContents();
                            for (MLText.Word word : words) {
                                result += word.getStringValue() + " ";
                            }
                        }
                    }
                    findViewById(R.id.bt_read).setEnabled(true);
                }
            }).addOnFailureListener(new OnFailureListener() {
                @Override
                public void onFailure(Exception e) {
                }
            });
        }
    }
}

4. 开始朗读:

case R.id.bt_read:
    //TTS一次朗读的最大长度为500
    int maxLength = 500;
    if (result.length() > maxLength) {
        int flag = result.length() / 500;
        for (int i = 0; i <= flag; i++) {
            if (500 * (i + 1) > result.length()) {
                // MLTtsEngine.QUEUE_APPEND:排队模式
                // 如果当前正在播放,则将任务放在队列中顺序执行;如果当前处于播放暂停状态,则恢复播放
                // 再将任务放入队列中顺序执行;如果当前无播放任务,则立即执行。
                mlTtsEngine.speak(result.substring(maxLength * (i)), MLTtsEngine.QUEUE_APPEND);
            } else {
                mlTtsEngine.speak(result.substring(maxLength * (i), maxLength * (i + 1)), MLTtsEngine.QUEUE_APPEND);
            }
        }
    }
    break;

5. 界面销毁时,释放资源:

@Override
protected void onDestroy() {
    super.onDestroy();

    if (mlTtsEngine!= null) {
        mlTtsEngine.shutdown();
    }

    if (ocrAnalyzer != null) {
        try {
            ocrAnalyzer.stop();
        } catch (IOException e) {
            // 异常处理。
        }
    }
}

其他使用场景

除了朗读资料文献以外,语音合成功能还有丰富的应用场景,比如,在教育类应用中,为孩子读睡前故事;在导航应用中,合成特定的导航语音;在阅读类应用中,为用户自动读小说等等。

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

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

03-23 10:05