You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何实现Android TTS停止后从断点继续朗读图像识别文本?

Android TTS断点续读实现:追踪朗读位置解决停止后无法继续的问题

问题分析

你的代码无法实现断点续读的核心问题有两个:

  • 未监听TextToSpeech的实时朗读进度,无法记录中断时的准确文本位置
  • continueSpeech方法中的位置计算逻辑完全错误(lastPosition = spokenText.length() - lastPosition逻辑颠倒,且无正确初始值)

实现步骤

1. 添加进度监听,实时记录朗读位置

初始化TextToSpeech时,设置OnUtteranceProgressListener,通过监听朗读的字符范围,实时更新当前读到的文本索引。需要:

  • 给每个朗读任务设置唯一的utteranceId,用于区分任务类型
  • 维护全局变量currentSpeechOffset,保存当前朗读到的文本位置

2. 修正停止与续读逻辑

  • 停止朗读时保留currentSpeechOffset,不重置位置
  • 续读时从保存的偏移位置截取剩余文本进行朗读

3. 调整状态变量逻辑

修正isSpeaking、isPaused的状态切换逻辑,确保按钮文本与状态匹配

修改后的完整代码

TTS初始化部分(补充原代码缺失的初始化逻辑)

private TextToSpeech textToSpeech;
private int currentSpeechOffset = 0; // 当前朗读到的文本位置
private String spokenText = ""; // 要朗读的完整文本
private boolean isSpeaking = false;
private boolean isPaused = false;
private static final String TAG = "MainActivity";

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    
    // 初始化TextToSpeech
    textToSpeech = new TextToSpeech(this, status -> {
        if (status == TextToSpeech.SUCCESS) {
            // 设置为中文朗读(可根据需求修改语言)
            int langResult = textToSpeech.setLanguage(Locale.CHINESE);
            if (langResult == TextToSpeech.LANG_MISSING_DATA || langResult == TextToSpeech.LANG_NOT_SUPPORTED) {
                Toast.makeText(this, "不支持当前语言", Toast.LENGTH_SHORT).show();
            }
            
            // 设置朗读进度监听器
            textToSpeech.setOnUtteranceProgressListener(new TextToSpeech.OnUtteranceProgressListener() {
                @Override
                public void onStart(String utteranceId) {
                    // 从头开始朗读时重置偏移量
                    if (utteranceId.equals("START_FROM_BEGINNING")) {
                        currentSpeechOffset = 0;
                    }
                }

                @Override
                public void onDone(String utteranceId) {
                    runOnUiThread(() -> {
                        isSpeaking = false;
                        isPaused = false;
                        startStopContinueSpeechBtn.setText("Start");
                        currentSpeechOffset = 0; // 朗读完成后重置位置
                    });
                }

                @Override
                public void onError(String utteranceId) {}

                // Android 11+ 支持实时监听朗读字符范围
                @Override
                public void onRangeStart(String utteranceId, int start, int end, int frame) {
                    super.onRangeStart(utteranceId, start, end, frame);
                    currentSpeechOffset = start;
                }
            });
        } else {
            Toast.makeText(MainActivity.this, "TTS初始化失败", Toast.LENGTH_SHORT).show();
        }
    });
}

修正后的核心功能方法

private void recognizedTextFromImage() {
    Log.d(TAG, "recognizedTextFromImage: ");
    try {
        InputImage inputImage = InputImage.fromFilePath(this, imageUri);

        textRecognizer.process(inputImage)
                .addOnSuccessListener(text -> {
                    String recognizedText = text.getText();
                    recognizedTextEt.setText(recognizedText);

                    // 保存完整文本,从头开始朗读
                    spokenText = recognizedText;
                    currentSpeechOffset = 0;
                    speakRecognizedText(recognizedText);
                })
                .addOnFailureListener(e -> {
                    Log.e(TAG, "onFailure: ", e);
                    Toast.makeText(MainActivity.this, "识别失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
                });
    } catch (Exception e) {
        Log.e(TAG, "recognizedTextFromImage: ", e);
        Toast.makeText(MainActivity.this, "图片处理失败:" + e.getMessage(), Toast.LENGTH_SHORT).show();
    }
}

private void speakRecognizedText(String text) {
    if (textToSpeech != null && textToSpeech.getEngines().size() > 0) {
        final Handler handler = new Handler(Looper.getMainLooper());
        handler.postDelayed(() -> {
            HashMap<String, String> params = new HashMap<>();
            params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "START_FROM_BEGINNING");
            textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, params);
            isSpeaking = true;
            isPaused = false;
            startStopContinueSpeechBtn.setText("Stop");
        }, 100);
    }
}

private void stopSpeech() {
    if (textToSpeech != null && isSpeaking) {
        textToSpeech.stop();
        isSpeaking = false;
        isPaused = true;
        startStopContinueSpeechBtn.setText("Continue");
    }
}

private void continueSpeech() {
    if (textToSpeech != null && isPaused && !spokenText.isEmpty()) {
        String remainingText = spokenText.substring(currentSpeechOffset);
        if (!remainingText.isEmpty()) {
            HashMap<String, String> params = new HashMap<>();
            params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "CONTINUE_FROM_OFFSET");
            textToSpeech.speak(remainingText, TextToSpeech.QUEUE_FLUSH, params);
            isSpeaking = true;
            isPaused = false;
            startStopContinueSpeechBtn.setText("Stop");
        }
    }
}

private void startSpeech() {
    if (textToSpeech != null) {
        String text = recognizedTextEt.getText().toString();
        if (!text.isEmpty()) {
            spokenText = text;
            currentSpeechOffset = 0;
            HashMap<String, String> params = new HashMap<>();
            params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, "START_FROM_BEGINNING");
            textToSpeech.speak(text, TextToSpeech.QUEUE_FLUSH, params);
            isSpeaking = true;
            isPaused = false;
            startStopContinueSpeechBtn.setText("Stop");
        }
    }
}

@Override
protected void onDestroy() {
    if (textToSpeech != null) {
        textToSpeech.stop();
        textToSpeech.shutdown();
    }
    super.onDestroy();
}

补充说明

  • onRangeStart方法是Android 11(API 30)新增的API,低版本可通过分段朗读+OnUtteranceCompletedListener实现兼容,但实时性和准确性稍差
  • 每个朗读任务必须设置utteranceId,用于在监听器中区分任务类型(从头开始/续读)
  • 停止朗读时不要重置currentSpeechOffset,保留位置用于后续续读

内容的提问来源于stack exchange,提问作者AKD2022

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 13:24:55