前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

flutter_speech默认配置的最大录音时长是60秒,识别模式是短语音(recognitionMode=0)。对于语音搜索、语音指令这类场景完全够用。但如果你要做语音笔记、会议记录、实时字幕这类需要长时间识别的功能,就需要突破这个限制。

今天我们来探讨如何基于flutter_speech的架构实现持续语音识别。

一、maxAudioDuration 参数扩展

1.1 当前限制

const extraParam: Record<string, Object> = {
  "recognitionMode": 0,        // 短语音模式
  "vadBegin": 2000,
  "vadEnd": 3000,
  "maxAudioDuration": 60000    // 60秒
};

短语音模式下,VAD检测到静音就会自动停止。即使把maxAudioDuration设到很大,用户一停顿就结束了。

1.2 长语音模式

要实现持续识别,需要切换到长语音模式

const extraParam: Record<string, Object> = {
  "recognitionMode": 1,        // 长语音模式
  "vadBegin": 5000,            // 5秒等待开口
  "vadEnd": 5000,              // 5秒静音才停
  "maxAudioDuration": 300000   // 5分钟
};
参数 短语音模式 长语音模式 说明
recognitionMode 0 1 长语音不会因静音自动停止
vadBegin 2000 5000 给用户更多思考时间
vadEnd 3000 5000 允许更长的停顿
maxAudioDuration 60000 300000 5分钟

1.3 通过Dart层传递模式参数

当前flutter_speech的Dart API不支持传递识别模式。可以扩展:

// 扩展后的Dart API(建议改进)
Future listen({bool continuous = false}) =>
    _channel.invokeMethod("speech.listen", {
      'continuous': continuous,
    });
// 原生端接收参数
case "speech.listen":
  const args = call.args as Record<string, Object> | null;
  const continuous = args?.['continuous'] as boolean ?? false;
  this.startListening(result, continuous);
  break;

二、长时间语音识别的会话管理

2.1 单次长会话

最简单的方案——一个会话持续到用户手动停止:

startListening(recognitionMode=1, maxAudioDuration=300000)
    │
    ├── onResult("你好", isLast=false)
    ├── onResult("你好今天", isLast=false)
    ├── (用户停顿5秒)
    ├── onResult("你好今天开会", isLast=false)
    ├── ...持续识别...
    │
    └── 用户点击Stop → finish(sessionId) → onResult(最终结果, isLast=true)

优点:实现简单。
缺点:maxAudioDuration有上限,超长会话可能被系统中断。

2.2 分段续接方案

更健壮的方案——将长会话拆分为多个短会话,自动续接:

第1段:startListening → 识别 → onComplete → 保存结果
    ↓ 自动
第2段:startListening → 识别 → onComplete → 拼接结果
    ↓ 自动
第3段:startListening → 识别 → onComplete → 拼接结果
    ↓
... 直到用户手动停止

2.3 分段续接的实现

// 原生端实现(扩展方案)
private continuousMode: boolean = false;
private accumulatedText: string = '';

private setupContinuousListener(): void {
  if (!this.asrEngine) return;

  const channel = this.channel;
  const plugin = this;

  this.asrEngine.setListener({
    onResult(sessionId, result) {
      const fullText = plugin.accumulatedText + result.result;
      channel?.invokeMethod('speech.onSpeech', fullText);

      if (result.isLast) {
        plugin.accumulatedText = fullText;
        if (plugin.continuousMode) {
          // 自动开始下一段
          plugin.startNextSegment();
        } else {
          plugin.isListening = false;
          channel?.invokeMethod('speech.onRecognitionComplete', fullText);
        }
      }
    },
    onComplete(sessionId, eventMessage) {
      if (plugin.continuousMode && plugin.isListening) {
        plugin.startNextSegment();
      }
    },
    onError(sessionId, errorCode, errorMessage) {
      console.error(TAG, `onError in continuous mode: ${errorCode}`);
      if (plugin.continuousMode && errorCode === 5) {
        // 无语音输入,继续等待
        plugin.startNextSegment();
      } else {
        plugin.isListening = false;
        channel?.invokeMethod('speech.onError', errorCode);
      }
    },
    // ... onStart, onEvent
  });
}

private startNextSegment(): void {
  try {
    const params = this.buildStartParams(true);
    this.asrEngine?.startListening(params);
  } catch (e) {
    console.error(TAG, `startNextSegment error: ${JSON.stringify(e)}`);
  }
}

2.4 分段间的无缝衔接

分段续接的最大挑战是衔接处的文本连贯性。两段之间可能会有重复或遗漏:

第1段结果:"今天的会议主要讨论"
第2段结果:"讨论三个议题"
拼接结果:"今天的会议主要讨论讨论三个议题"  ← "讨论"重复了

解决方案:

  1. 简单拼接:直接拼接,接受少量重复(最简单)
  2. 重叠检测:检测两段结尾和开头的重叠部分,去重
  3. 标点分隔:在每段结尾加标点符号分隔
// 简单的重叠去重
private mergeSegments(prev: string, next: string): string {
  // 检查prev的结尾是否和next的开头重叠
  const maxOverlap = Math.min(prev.length, next.length, 10);
  for (let i = maxOverlap; i > 0; i--) {
    if (prev.endsWith(next.substring(0, i))) {
      return prev + next.substring(i);
    }
  }
  return prev + next;
}

三、自动重连与断点续识策略

3.1 网络中断的处理

在线识别模式下,网络中断会导致识别失败。对于长时间识别,需要自动重连:

onError(sessionId, errorCode, errorMessage) {
  if (plugin.continuousMode) {
    if (errorCode === 1 || errorCode === 2) {
      // 网络错误,延迟重试
      console.warn(TAG, 'network error, retrying in 3 seconds...');
      setTimeout(() => {
        if (plugin.continuousMode && plugin.isListening) {
          plugin.startNextSegment();
        }
      }, 3000);
      return;
    }
  }
  // 其他错误正常处理
  plugin.isListening = false;
  channel?.invokeMethod('speech.onError', errorCode);
}

3.2 重连策略

策略 实现 适用场景
立即重试 错误后立即startListening 临时网络抖动
延迟重试 等待3秒后重试 网络短暂中断
指数退避 1s→2s→4s→8s… 网络持续不稳定
放弃 超过N次重试后停止 网络完全不可用

3.3 断点续识

网络恢复后,已经说过的话不会重新识别。需要在UI上提示用户:

// Dart层提示
void onNetworkRecovery() {
  _showSnackBar('网络已恢复,请继续说话');
  // 之前的识别结果已保存在accumulatedText中
}

四、识别结果拼接与文本累积

4.1 文本累积策略

// Dart层的文本累积
class ContinuousRecognitionState {
  final List<String> segments = [];  // 每段的最终结果
  String currentSegment = '';         // 当前段的实时结果

  String get fullText {
    final completed = segments.join('');
    return completed + currentSegment;
  }

  void onPartialResult(String text) {
    currentSegment = text;
  }

  void onSegmentComplete(String text) {
    segments.add(text);
    currentSegment = '';
  }
}

4.2 UI显示

// 显示累积的完整文本
Widget build(BuildContext context) {
  return SingleChildScrollView(
    child: Text(
      recognitionState.fullText,
      style: TextStyle(fontSize: 16),
    ),
  );
}

4.3 文本格式化

长文本识别需要考虑格式化:

String formatRecognitionText(String raw) {
  // 1. 添加标点(如果识别结果没有标点)
  // 2. 分段落
  // 3. 首字母大写(英文)
  return raw;
}

📌 Core Speech Kit的中文识别通常会自带标点,所以格式化的工作量不大。但分段续接时,段与段之间的标点可能需要手动处理。

五、电量与性能消耗的权衡

5.1 长时间识别的资源消耗

资源 短语音(10秒) 长语音(5分钟) 长语音(1小时)
CPU
内存 ~20MB ~25MB ~30MB+
网络流量 ~320KB ~9.6MB ~115MB
电量 忽略不计 可感知 显著

5.2 优化建议

降低CPU消耗

  • 使用长语音模式而不是反复创建短会话
  • 避免在onResult回调中做复杂计算

降低网络消耗

  • 考虑离线识别模式(准确率会降低)
  • 弱网环境下降级到离线

降低电量消耗

  • 在用户不说话时暂停识别(但这会增加延迟)
  • 提供"省电模式"选项

5.3 用户提示

长时间识别应该在UI上提示用户资源消耗:

// 显示录音时长和预估消耗
Text('已录制 ${duration.inMinutes}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}'),
Text('预估流量:${(duration.inSeconds * 32 / 1024).toStringAsFixed(1)} MB'),

5.4 后台识别的限制

OpenHarmony对后台音频采集有限制。如果App进入后台,麦克风可能被系统回收:


void didChangeAppLifecycleState(AppLifecycleState state) {
  if (state == AppLifecycleState.paused) {
    // App进入后台
    if (_isListening) {
      _speech.stop();  // 保存当前结果
      _showNotification('语音识别已暂停');
    }
  } else if (state == AppLifecycleState.resumed) {
    // App回到前台
    if (_wasContinuousListening) {
      _speech.listen();  // 恢复识别
    }
  }
}

六、实现方案对比

方案 复杂度 最大时长 文本连贯性 推荐场景
单次长会话 5分钟 语音笔记
分段续接 无限制 中等 会议记录
分段+重叠去重 无限制 实时字幕

对于大多数场景,单次长会话(recognitionMode=1 + 较大的maxAudioDuration)就够了。只有需要超过5分钟的场景才需要分段续接。

总结

本文探讨了基于flutter_speech实现持续语音识别的方案:

  1. 长语音模式:recognitionMode=1,不会因静音自动停止
  2. 分段续接:多个短会话自动衔接,突破时长限制
  3. 自动重连:网络中断后延迟重试,保证识别连续性
  4. 文本累积:分段结果拼接,处理重叠和格式化
  5. 资源权衡:长时间识别需要关注CPU、内存、网络、电量消耗

下一篇我们讲语音识别结果的后处理——标点符号、文本格式化、纠错等话题。

如果这篇文章对你有帮助,欢迎点赞、收藏、关注,你的支持是我持续创作的动力!


相关资源:

请添加图片描述

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐