前言

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

语音识别引擎返回的原始文本,往往不能直接用于业务场景。缺少标点、格式混乱、偶尔有错别字——这些问题都需要通过后处理来解决。

flutter_speech本身不做后处理,它只负责把引擎返回的原始文本透传给Dart层。后处理的逻辑应该在Dart层实现,这样可以跨平台复用。

今天我们来看看语音识别结果的常见后处理需求和实现方案。

一、识别文本的标点符号处理

1.1 Core Speech Kit的标点能力

好消息是,Core Speech Kit的中文识别结果通常自带标点

用户说:"今天天气怎么样明天会下雨吗"
识别结果:"今天天气怎么样?明天会下雨吗?"

但标点的准确性不是100%,有时候会缺失或位置不对。

1.2 标点补全策略

如果识别结果缺少标点,可以用简单的规则补全:

String addPunctuation(String text) {
  if (text.isEmpty) return text;

  // 规则1:以疑问词结尾加问号
  final questionWords = ['吗', '呢', '啊', '么', '什么', '怎么', '哪里', '谁', '几'];
  for (final word in questionWords) {
    if (text.endsWith(word) && !text.endsWith('?')) {
      return '$text?';
    }
  }

  // 规则2:以感叹词结尾加感叹号
  final exclamationWords = ['啊', '呀', '哇', '太好了', '真棒'];
  for (final word in exclamationWords) {
    if (text.endsWith(word) && !text.endsWith('!')) {
      return '$text!';
    }
  }

  // 规则3:其他情况加句号
  final lastChar = text[text.length - 1];
  if (!['。', '?', '!', ',', '、', ';', ':'].contains(lastChar)) {
    return '$text。';
  }

  return text;
}

1.3 标点处理的时机

时机 优点 缺点
实时处理(每次onSpeech) 用户立即看到标点 部分结果的标点可能不准
最终处理(onRecognitionComplete) 标点更准确 实时显示时没有标点
混合处理 平衡 实现稍复杂

推荐混合处理:实时显示时不加标点(保持原始结果),最终结果时补全标点。

_speech.setRecognitionResultHandler((String text) {
  // 实时结果:原样显示
  setState(() => transcription = text);
});

_speech.setRecognitionCompleteHandler((String text) {
  // 最终结果:补全标点
  setState(() => transcription = addPunctuation(text));
});

二、中英混合文本的格式化

2.1 常见的格式问题

Core Speech Kit识别中英混合文本时,可能出现格式问题:

输入 识别结果 问题
“打开WiFi” “打开wifi” 大小写丢失
“发送Email给他” “发送email给他” 大小写丢失
“iPhone 15很好” “iphone15很好” 大小写+空格丢失

2.2 英文单词大小写修正

// 常见品牌名和缩写的正确大小写
final Map<String, String> _wordCaseMap = {
  'wifi': 'WiFi',
  'bluetooth': 'Bluetooth',
  'iphone': 'iPhone',
  'ipad': 'iPad',
  'email': 'Email',
  'app': 'App',
  'ok': 'OK',
  'ai': 'AI',
  'api': 'API',
  'url': 'URL',
  'html': 'HTML',
  'css': 'CSS',
  'pdf': 'PDF',
};

String fixWordCase(String text) {
  String result = text;
  _wordCaseMap.forEach((lower, correct) {
    // 用正则匹配完整单词(避免部分匹配)
    result = result.replaceAllMapped(
      RegExp('(?<![a-zA-Z])$lower(?![a-zA-Z])', caseSensitive: false),
      (match) => correct,
    );
  });
  return result;
}

2.3 中英文之间加空格

中英文之间加空格是一种常见的排版规范(称为"盘古之白"):

String addSpaceBetweenCnEn(String text) {
  // 中文后面跟英文,加空格
  text = text.replaceAllMapped(
    RegExp(r'([\u4e00-\u9fa5])([a-zA-Z0-9])'),
    (m) => '${m[1]} ${m[2]}',
  );
  // 英文后面跟中文,加空格
  text = text.replaceAllMapped(
    RegExp(r'([a-zA-Z0-9])([\u4e00-\u9fa5])'),
    (m) => '${m[1]} ${m[2]}',
  );
  return text;
}

处理前后对比:

处理前:"打开wifi设置然后连接bluetooth"
处理后:"打开 WiFi 设置然后连接 Bluetooth"

三、识别置信度与结果筛选

3.1 Core Speech Kit的置信度

当前Core Speech Kit的SpeechRecognitionResult不包含置信度字段

interface SpeechRecognitionResult {
  result: string;    // 识别文本
  isLast: boolean;   // 是否最终结果
  // 没有confidence字段
}

这意味着我们无法直接判断识别结果的可靠程度。

3.2 间接评估置信度

虽然没有直接的置信度,可以通过一些启发式规则间接评估:

class RecognitionConfidence {
  /// 评估识别结果的可信度(0.0-1.0)
  static double estimate(String text, Duration duration) {
    double confidence = 1.0;

    // 规则1:文本太短可能是误识别
    if (text.length < 2) confidence *= 0.5;

    // 规则2:文本长度与时长不匹配
    final charsPerSecond = text.length / duration.inSeconds;
    if (charsPerSecond > 10) confidence *= 0.7;  // 说话太快,可能有误
    if (charsPerSecond < 1) confidence *= 0.7;   // 说话太慢,可能有误

    // 规则3:包含大量重复字符
    if (_hasExcessiveRepetition(text)) confidence *= 0.5;

    return confidence.clamp(0.0, 1.0);
  }

  static bool _hasExcessiveRepetition(String text) {
    if (text.length < 4) return false;
    for (int i = 0; i < text.length - 3; i++) {
      if (text[i] == text[i + 1] && text[i + 1] == text[i + 2] && text[i + 2] == text[i + 3]) {
        return true;  // 4个连续相同字符
      }
    }
    return false;
  }
}

3.3 低置信度结果的处理

void onRecognitionComplete(String text) {
  final confidence = RecognitionConfidence.estimate(text, _recognitionDuration);

  if (confidence < 0.3) {
    // 置信度太低,提示用户
    setState(() {
      transcription = text;
      _showLowConfidenceWarning = true;
    });
  } else {
    setState(() {
      transcription = text;
      _showLowConfidenceWarning = false;
    });
  }
}

四、文本纠错与智能补全

4.1 常见的识别错误类型

错误类型 示例 原因
同音字错误 “在→再”、“的→地” 语音相同,语义不同
近音字错误 “是→十”、“四→死” 发音相近
吞字 “我想去北京” → “我想去京” 说话太快
多字 “你好” → “你好好” 回声或重复

4.2 简单的纠错规则

String correctCommonErrors(String text) {
  // 常见的同音字纠错(基于上下文)
  final corrections = {
    '在见': '再见',
    '在说': '再说',
    '在来': '再来',
    '的话': '的话',  // 保持不变
    '在次': '再次',
    '做后': '最后',
  };

  String result = text;
  corrections.forEach((wrong, correct) {
    result = result.replaceAll(wrong, correct);
  });
  return result;
}

4.3 基于词典的纠错

更高级的纠错需要词典支持:

class SpellChecker {
  final Set<String> _dictionary = {};

  SpellChecker() {
    // 加载常用词词典
    _loadDictionary();
  }

  String correct(String text) {
    // 分词
    final words = _segment(text);
    // 对每个词检查是否在词典中
    // 如果不在,查找最相似的词
    return words.map((w) => _dictionary.contains(w) ? w : _findSimilar(w)).join();
  }

  String _findSimilar(String word) {
    // 编辑距离最小的词典词
    // ...
    return word;
  }
}

💡 实际建议:对于flutter_speech这样的轻量级插件,不建议在客户端做复杂的纠错。简单的规则纠错就够了。如果需要高质量的纠错,应该在服务端处理。

4.4 智能补全

在用户说到一半时,可以提供补全建议:

// 基于历史输入的补全
List<String> getSuggestions(String partial) {
  return _history
      .where((h) => h.startsWith(partial))
      .take(3)
      .toList();
}

五、识别结果的本地存储与历史管理

5.1 存储方案选择

方案 适用场景 数据量 复杂度
SharedPreferences 少量历史记录 <100条
文件存储 中等数据量 <1000条
SQLite 大量数据+查询 无限制

5.2 使用SharedPreferences存储

import 'package:shared_preferences/shared_preferences.dart';

class RecognitionHistory {
  static const _key = 'speech_history';
  static const _maxItems = 50;

  static Future<void> save(String text) async {
    final prefs = await SharedPreferences.getInstance();
    final history = prefs.getStringList(_key) ?? [];

    history.insert(0, '${DateTime.now().toIso8601String()}|$text');

    // 限制历史记录数量
    if (history.length > _maxItems) {
      history.removeRange(_maxItems, history.length);
    }

    await prefs.setStringList(_key, history);
  }

  static Future<List<HistoryItem>> load() async {
    final prefs = await SharedPreferences.getInstance();
    final history = prefs.getStringList(_key) ?? [];

    return history.map((item) {
      final parts = item.split('|');
      return HistoryItem(
        time: DateTime.parse(parts[0]),
        text: parts.sublist(1).join('|'),
      );
    }).toList();
  }

  static Future<void> clear() async {
    final prefs = await SharedPreferences.getInstance();
    await prefs.remove(_key);
  }
}

class HistoryItem {
  final DateTime time;
  final String text;
  HistoryItem({required this.time, required this.text});
}

5.3 在识别完成时自动保存

_speech.setRecognitionCompleteHandler((String text) {
  setState(() {
    transcription = text;
    _isListening = false;
  });

  // 自动保存到历史记录
  if (text.isNotEmpty) {
    RecognitionHistory.save(text);
  }
});

5.4 历史记录UI

Widget _buildHistoryList() {
  return FutureBuilder<List<HistoryItem>>(
    future: RecognitionHistory.load(),
    builder: (context, snapshot) {
      if (!snapshot.hasData) return CircularProgressIndicator();
      final items = snapshot.data!;
      return ListView.builder(
        itemCount: items.length,
        itemBuilder: (context, index) {
          final item = items[index];
          return ListTile(
            title: Text(item.text, maxLines: 2, overflow: TextOverflow.ellipsis),
            subtitle: Text(_formatTime(item.time)),
            onTap: () => _copyToClipboard(item.text),
          );
        },
      );
    },
  );
}

六、后处理流水线

6.1 统一的后处理管道

将所有后处理步骤串成一个管道:

class TextPostProcessor {
  static String process(String raw) {
    String result = raw;

    // 1. 去除首尾空白
    result = result.trim();

    // 2. 修正英文大小写
    result = fixWordCase(result);

    // 3. 中英文加空格
    result = addSpaceBetweenCnEn(result);

    // 4. 常见纠错
    result = correctCommonErrors(result);

    // 5. 补全标点
    result = addPunctuation(result);

    return result;
  }
}

6.2 在回调中使用

_speech.setRecognitionCompleteHandler((String text) {
  final processed = TextPostProcessor.process(text);
  setState(() {
    transcription = processed;
    _isListening = false;
  });
  RecognitionHistory.save(processed);
});

总结

本文讲解了语音识别结果的后处理方案:

  1. 标点处理:Core Speech Kit自带标点,必要时用规则补全
  2. 中英混合格式化:修正大小写、添加中英文间空格
  3. 置信度评估:通过启发式规则间接评估结果可靠性
  4. 文本纠错:简单规则纠错,复杂纠错交给服务端
  5. 历史管理:SharedPreferences存储识别历史

下一篇我们讲与其他HarmonyOS Kit的联动——语音识别与TTS、AI Framework等Kit的结合。

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


相关资源:

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐