Flutter语音转文字应用实现:基于speech_to_text插件的完整方案

7 次阅读

Flutter 做语音转文字功能时,真正需要处理的不只是“点击按钮开始识别”这一件事,还涉及麦克风权限、语音识别初始化、监听识别状态、实时文本更新、识别结束、异常处理以及不同平台的兼容性。speech_to_text 插件提供了一套相对完整的语音识别接口,适合快速构建语音输入、语音搜索、会议记录、语音备忘录等功能。

本文以 Flutter 为基础,完整梳理基于 speech_to_text 的语音转文字应用实现方式,并给出一套可以直接用于项目开发的代码结构。

一、speech_to_text插件适合什么场景

speech_to_text 是 Flutter 生态中常见的语音识别插件之一,它主要调用 Android 和 iOS 系统提供的语音识别能力,将用户通过麦克风输入的语音转换成文本。

比较适合以下场景:

  • 语音搜索

  • 语音输入框

  • 语音备忘录

  • 简单的会议内容记录

  • 客服语音录入

  • 表单语音填写

  • 智能助手的语音输入

  • 无障碍辅助输入

需要注意的是,这类插件主要解决的是“语音识别”,并不是完整的语音识别云服务。如果业务需要长时间连续录音、专业领域词汇识别、批量音频转写或者高度可控的识别模型,通常还需要接入云端语音识别服务。

二、创建Flutter项目并添加依赖

首先创建一个 Flutter 项目:

Bash
flutter create speech_demo
cd speech_demo

然后在 pubspec.yaml 中加入 speech_to_text:

YAML
dependencies:
  flutter:
    sdk: flutter

  speech_to_text: ^7.0.0

实际项目中建议通过 pub.dev 查看当前稳定版本,并根据项目 Flutter/Dart 版本选择兼容的插件版本。

执行:

Bash
flutter pub get

如果项目此前存在旧版本依赖,也可以使用:

Bash
flutter pub outdated

检查依赖升级情况。

三、speech_to_text核心工作流程

整个语音转文字流程可以拆分成几个阶段:

创建 SpeechToText
        ↓
初始化语音识别
        ↓
检查麦克风/语音识别权限
        ↓
调用 listen()
        ↓
持续接收识别结果
        ↓
更新页面文本
        ↓
用户停止或系统结束识别
        ↓
调用 stop()

核心对象是:

dart
final SpeechToText speechToText = SpeechToText();

初始化:

dart
final bool available = await speechToText.initialize();

开始识别:

dart
await speechToText.listen(
  onResult: (result) {
    print(result.recognizedWords);
  },
);

停止:

dart
await speechToText.stop();

判断是否正在识别:

dart
speechToText.isListening

这些接口基本构成了 Flutter 语音转文字功能的核心。

四、完整语音转文字页面实现

下面提供一个较完整的 Flutter 页面示例,包括初始化、开始识别、停止识别、实时文字显示和状态提示。

dart
import 'package:flutter/material.dart';
import 'package:speech_to_text/speech_to_text.dart' as stt;

class SpeechPage extends StatefulWidget {
  const SpeechPage({super.key});

  @override
  State<SpeechPage> createState() => _SpeechPageState();
}

class _SpeechPageState extends State<SpeechPage> {
  final stt.SpeechToText _speech = stt.SpeechToText();

  bool _isAvailable = false;
  bool _isListening = false;

  String _text = '';
  String _status = '等待开始';

  @override
  void initState() {
    super.initState();
    _initSpeech();
  }

  Future<void> _initSpeech() async {
    final available = await _speech.initialize(
      onStatus: (status) {
        if (!mounted) return;

        setState(() {
          _status = status;
          _isListening = status == 'listening';
        });
      },
      onError: (error) {
        if (!mounted) return;

        setState(() {
          _status = '识别错误:${error.errorMsg}';
          _isListening = false;
        });
      },
    );

    if (!mounted) return;

    setState(() {
      _isAvailable = available;
      _status = available ? '语音识别已准备好' : '设备不支持语音识别';
    });
  }

  Future<void> _startListening() async {
    if (!_isAvailable) {
      await _initSpeech();
    }

    if (!_isAvailable) {
      return;
    }

    setState(() {
      _isListening = true;
      _status = '正在聆听...';
    });

    await _speech.listen(
      localeId: 'zh_CN',
      listenFor: const Duration(minutes: 1),
      pauseFor: const Duration(seconds: 5),
      onResult: (result) {
        if (!mounted) return;

        setState(() {
          _text = result.recognizedWords;
        });
      },
    );
  }

  Future<void> _stopListening() async {
    await _speech.stop();

    if (!mounted) return;

    setState(() {
      _isListening = false;
      _status = '识别已停止';
    });
  }

  @override
  void dispose() {
    _speech.stop();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('语音转文字'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(20),
        child: Column(
          children: [
            Container(
              width: double.infinity,
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                borderRadius: BorderRadius.circular(12),
                color: Colors.grey.shade100,
              ),
              child: Text(
                _text.isEmpty ? '请点击下方按钮开始说话' : _text,
                style: const TextStyle(
                  fontSize: 18,
                  height: 1.6,
                ),
              ),
            ),

            const SizedBox(height: 20),

            Text(
              _status,
              style: TextStyle(
                color: _isListening
                    ? Colors.green
                    : Colors.grey.shade700,
              ),
            ),

            const Spacer(),

            FloatingActionButton.large(
              onPressed:
                  _isListening ? _stopListening : _startListening,
              child: Icon(
                _isListening ? Icons.stop : Icons.mic,
                size: 36,
              ),
            ),

            const SizedBox(height: 30),
          ],
        ),
      ),
    );
  }
}

这段代码已经可以构成一个基础的语音转文字页面。

五、为什么必须调用initialize

很多初学者会直接调用:

dart
_speech.listen();

然后发现程序没有反应。

正确流程应该先初始化:

dart
final available = await _speech.initialize();

初始化阶段主要用于确认当前设备是否具备可用的语音识别能力,同时处理相关权限和底层识别服务状态。

比较推荐将初始化放到 initState():

dart
@override
void initState() {
  super.initState();
  _initSpeech();
}

初始化函数:

dart
Future<void> _initSpeech() async {
  final available = await _speech.initialize(
    onStatus: (status) {
      debugPrint('status: $status');
    },
    onError: (error) {
      debugPrint('error: ${error.errorMsg}');
    },
  );

  setState(() {
    _isAvailable = available;
  });
}

这样用户进入页面后,语音识别能力就可以提前准备。

六、Android麦克风权限配置

Android 平台必须声明麦克风权限。

打开:

android/app/src/main/AndroidManifest.xml

加入:

XML
 android:name="android.permission.RECORD_AUDIO" />

如果项目使用的插件版本对 Android 识别服务还有其他要求,应同时按照插件当前版本的官方配置说明进行调整。

需要特别注意,Manifest 中声明权限并不意味着用户已经授权。

Android 运行时仍然可能弹出:

允许应用使用麦克风吗?

speech_to_text 会参与相关权限处理,但实际项目仍然需要做好权限被拒绝后的页面提示。

例如:

dart
final available = await _speech.initialize();

if (!available) {
  if (!mounted) return;

  ScaffoldMessenger.of(context).showSnackBar(
    const SnackBar(
      content: Text('语音识别不可用,请检查麦克风权限'),
    ),
  );
}

七、iOS权限配置

iOS 对麦克风以及语音识别权限的要求更加严格。

打开:

ios/Runner/Info.plist

根据插件当前版本要求添加相关权限说明,例如:

XML
NSMicrophoneUsageDescription
需要使用麦克风进行语音输入

NSSpeechRecognitionUsageDescription
需要使用语音识别将您的语音转换为文字

权限描述不是简单的技术配置。

它会直接展示给用户,因此应该明确说明为什么需要使用麦克风和语音识别功能。

八、实现中文语音识别

如果应用主要面向中文用户,可以指定语言:

dart
await _speech.listen(
  localeId: 'zh_CN',
  onResult: (result) {
    setState(() {
      _text = result.recognizedWords;
    });
  },
);

不同设备支持的语言可能不同,因此实际项目不应该简单假设所有设备都支持固定的 localeId。

可以先获取设备支持的语言:

dart
final locales = await _speech.locales();

然后输出:

dart
for (final locale in locales) {
  debugPrint(
    '${locale.localeId} - ${locale.name}',
  );
}

也可以根据业务需求让用户选择语言。

例如:

dart
String _localeId = 'zh_CN';

启动识别时:

dart
await _speech.listen(
  localeId: _localeId,
  onResult: (result) {
    setState(() {
      _text = result.recognizedWords;
    });
  },
);

这样就可以进一步扩展中文、英文等多语言语音输入。

九、实时显示识别结果

speech_to_text 的一个重要特点是可以通过 onResult 持续获得识别结果。

dart
onResult: (result) {
  setState(() {
    _text = result.recognizedWords;
  });
}

用户说:

今天下午三点开会

识别过程中可能依次得到:

今天

然后:

今天下午

最终:

今天下午三点开会

因此 UI 可以实时更新。

如果想判断当前结果是不是最终结果,可以使用:

dart
result.finalResult

例如:

dart
onResult: (result) {
  debugPrint('文本:${result.recognizedWords}');
  debugPrint('最终结果:${result.finalResult}');

  setState(() {
    _text = result.recognizedWords;
  });
}

这个字段对于需要区分“临时识别结果”和“最终确认文本”的业务非常有用。

十、处理识别状态变化

语音识别并不是简单的开始和结束两个状态。

实际运行时可能经历:

listening
notListening
done

可以通过:

dart
onStatus: (status) {
  debugPrint('语音状态:$status');
}

监听。

例如:

dart
_speech.initialize(
  onStatus: (status) {
    if (!mounted) return;

    setState(() {
      _isListening = status == 'listening';
    });
  },
);

这样即使底层识别服务主动结束,UI 状态也能够及时同步。

这比单纯依赖按钮点击事件更加可靠。

十一、处理语音识别异常

实际项目中必须处理异常情况。

例如:

dart
onError: (error) {
  debugPrint(
    '语音识别异常:${error.errorMsg}',
  );
}

可以进一步将错误展示给用户:

dart
onError: (error) {
  if (!mounted) return;

  setState(() {
    _status = '语音识别失败:${error.errorMsg}';
    _isListening = false;
  });
}

常见原因包括:

  • 没有授予麦克风权限

  • 系统语音识别服务不可用

  • 当前设备不支持目标语言

  • 网络环境异常

  • 系统语音服务暂时不可用

  • 识别时间超过限制

  • 用户主动停止识别

因此不要把所有异常都统一显示成“识别失败”,最好根据业务提供具有操作指引的信息。

十二、listenFor与pauseFor的作用

开始识别时可以设置:

dart
await _speech.listen(
  listenFor: const Duration(minutes: 1),
  pauseFor: const Duration(seconds: 5),
  onResult: (result) {
    setState(() {
      _text = result.recognizedWords;
    });
  },
);

listenFor 用于设置一次监听允许持续的时间范围。

pauseFor 则用于设置用户停顿后,语音识别服务可以等待多久。

例如用户说:

帮我创建一个明天上午九点的提醒

如果 pauseFor 设置得过短,用户稍微停顿一下就可能导致识别提前结束。

因此参数需要结合实际交互场景调整。

十三、实现清空文本功能

实际语音输入页面通常还需要一个清空按钮:

dart
IconButton(
  onPressed: () {
    setState(() {
      _text = '';
    });
  },
  icon: const Icon(Icons.delete_outline),
)

如果文本最终要提交给后端,可以直接使用:

dart
final content = _text.trim();

例如:

dart
void _submitText() {
  final content = _text.trim();

  if (content.isEmpty) {
    return;
  }

  debugPrint('提交内容:$content');
}

这样就可以把语音识别和业务表单结合起来。

十四、语音识别与TextField结合

如果需求不是单纯展示文本,而是让用户把语音直接输入到输入框,可以使用 TextEditingController。

定义:

dart
final TextEditingController _controller =
    TextEditingController();

识别结果:

dart
onResult: (result) {
  setState(() {
    _controller.text = result.recognizedWords;
    _controller.selection = TextSelection.fromPosition(
      TextPosition(
        offset: _controller.text.length,
      ),
    );
  });
}

输入框:

dart
TextField(
  controller: _controller,
  maxLines: 5,
  decoration: const InputDecoration(
    hintText: '请输入内容',
    border: OutlineInputBorder(),
  ),
)

这样就能实现类似聊天应用中的“点击麦克风,说话后自动填充输入框”。

十五、避免重复调用listen

用户快速连续点击按钮时,可能导致多个操作同时发生。

因此按钮状态应该和 _isListening 绑定:

dart
onPressed:
    _isListening ? _stopListening : _startListening,

同时启动方法中增加保护:

dart
if (_isListening) {
  return;
}

停止方法同样可以判断:

dart
if (!_isListening) {
  return;
}

这种状态保护能够减少重复调用底层语音识别服务造成的问题。

十六、Widget销毁时释放资源

页面销毁时不要忘记停止识别:

dart
@override
void dispose() {
  _speech.stop();
  _controller.dispose();
  super.dispose();
}

如果页面中没有 TextEditingController,则只需要:

dart
@override
void dispose() {
  _speech.stop();
  super.dispose();
}

尤其是从语音页面快速返回上一页时,如果没有正确处理生命周期,可能出现识别回调仍然尝试更新已经销毁的 Widget。

因此异步回调中建议增加:

dart
if (!mounted) return;

十七、如何设计更合理的语音输入UI

一个完整的语音转文字页面通常可以分成四个区域:

┌─────────────────────────┐
│      语音转文字          │
├─────────────────────────┤
│                         │
│  识别后的文字内容         │
│                         │
│                         │
├─────────────────────────┤
│ 当前状态:正在聆听        │
│                         │
│                        │
│                         │
│      点击停止识别         │
└─────────────────────────┘

建议至少提供以下状态:

  • 未开始

  • 正在识别

  • 暂停或等待

  • 识别完成

  • 识别失败

  • 权限被拒绝

  • 当前设备不支持

不要只通过麦克风图标颜色判断状态,最好同时提供文字提示,这对实际用户体验更加友好。

十八、常见问题:为什么没有识别结果

首先检查初始化结果:

dart
final available = await _speech.initialize();

debugPrint('available = $available');

如果:

available = false

需要重点检查设备和系统语音服务。

然后检查权限。

Android:

XML
 android:name="android.permission.RECORD_AUDIO" />

iOS:

XML
NSMicrophoneUsageDescription
需要使用麦克风进行语音输入

同时检查 onError:

dart
onError: (error) {
  debugPrint(error.errorMsg);
}

最后确认 localeId 是否适用于当前设备。

十九、常见问题:为什么识别一会儿就停止

这是移动端语音识别应用中比较常见的情况。

系统语音识别通常不是为无限时长连续转写设计的。识别服务可能因为:

  • 长时间没有检测到语音

  • 系统服务限制

  • 监听时长达到限制

  • 网络状态变化

  • 系统资源管理

而主动结束。

如果业务要求长时间录音并连续转换成文字,不建议简单依赖一次 listen()。

可以监听状态:

dart
onStatus: (status) {
  if (status == 'done' && mounted) {
    setState(() {
      _isListening = false;
    });
  }
}

对于真正的长文本场景,则应该重新评估技术方案。

二十、speech_to_text并不等于录音转写

这是开发 Flutter 语音应用时非常容易混淆的概念。

speech_to_text 的主要使用方式是:

用户说话
   ↓
系统语音识别服务
   ↓
实时识别结果
   ↓
Flutter获取文本

而典型的音频文件转写流程是:

用户录音
   ↓
保存音频文件
   ↓
上传服务器
   ↓
云端ASR模型
   ↓
返回完整文本

两者虽然最终都是“语音转文字”,但技术架构完全不同。

如果应用是简单的语音输入,speech_to_text 足够方便。

如果需求是:

  • 上传MP3转文字

  • 上传WAV转文字

  • 会议录音转写

  • 数小时音频批量处理

  • 专业术语识别

  • 多人说话区分

  • 时间戳识别

  • 服务端统一处理

那么更适合使用专门的语音识别服务。

二十一、项目结构建议

如果语音识别功能只是一个页面,可以直接封装在 StatefulWidget 中。

但在大型项目中,更建议将语音识别逻辑抽离。

例如:

lib/
├── main.dart
├── pages/
│   └── speech_page.dart
├── services/
│   └── speech_service.dart
└── widgets/
    └── speech_button.dart

服务层可以封装:

dart
class SpeechService {
  final stt.SpeechToText _speech =
      stt.SpeechToText();

  Future<bool> initialize({
    stt.VoidCallback? onListening,
  }) async {
    return await _speech.initialize();
  }

  Future<void> start({
    required Function(stt.SpeechRecognitionResult) onResult,
  }) async {
    await _speech.listen(
      onResult: onResult,
    );
  }

  Future<void> stop() async {
    await _speech.stop();
  }

  bool get isListening => _speech.isListening;
}

页面只负责展示状态和处理用户交互。

这样后续增加语音搜索、语音表单、语音助手等功能时,可以复用同一个服务。

二十二、生产环境中的几个注意事项

1. 不要假设所有设备都支持语音识别

初始化必须检查:

dart
final available = await _speech.initialize();

2. 不要忽略权限

首次使用时应考虑:

用户点击麦克风
        ↓
请求权限
        ↓
用户允许
        ↓
开始识别

如果用户拒绝,需要提供明确提示。

3. 不要无限依赖单次识别

移动操作系统对语音识别持续时间存在实际限制,长时间转写应该采用其他架构。

4. 正确处理异步回调

尤其是:

dart
onResult
onStatus
onError

中涉及 UI 更新时,应注意:

dart
if (!mounted) return;

5. 不要把识别文本直接当成可信数据

语音识别结果可能存在错别字、同音字、标点错误等问题。

例如:

帮我查一下明天上海天气

可能出现:

帮我查一下明天上嗨天气

如果文本会用于支付、下单、执行命令等关键操作,应增加确认机制。

二十三、可以进一步增加自动标点和业务处理

语音识别拿到文本后,可以继续进行文本处理。

例如:

dart
String normalizeText(String text) {
  return text
      .trim()
      .replaceAll(RegExp(r's+'), ' ');
}

然后:

dart
final content = normalizeText(_text);

如果应用是语音搜索,可以直接:

dart
search(_text);

如果是智能助手,则可以:

dart
sendMessage(_text);

如果是表单填写,则可以将文本交给业务层进一步解析。

例如:

“明天上午十点提醒我给客户打电话”

后续可以通过自然语言处理提取:

日期:明天
时间:10:00
任务:给客户打电话

这样语音识别就不再只是一个输入组件,而可以成为完整业务流程的一部分。

二十四、完整实现思路总结

一个稳定的 Flutter 语音转文字应用,可以按照下面的流程设计:

进入语音页面
   ↓
初始化 SpeechToText
   ↓
检查语音识别能力
   ↓
检查麦克风权限
   ↓
获取可用语言
   ↓
用户点击麦克风
   ↓
调用 listen()
   ↓
onResult持续获取文本
   ↓
实时更新TextField/UI
   ↓
onStatus监听识别状态
   ↓
onError处理异常
   ↓
用户停止或系统结束
   ↓
调用stop()
   ↓
保存/提交最终文本

核心代码可以浓缩为:

dart
final speech = stt.SpeechToText();

final available = await speech.initialize(
  onStatus: (status) {
    debugPrint(status);
  },
  onError: (error) {
    debugPrint(error.errorMsg);
  },
);

if (available) {
  await speech.listen(
    localeId: 'zh_CN',
    onResult: (result) {
      debugPrint(result.recognizedWords);
    },
  );
}

如果需要停止:

dart
await speech.stop();

如果需要判断当前是否正在识别:

dart
speech.isListening

这几个 API 是构建 Flutter speech_to_text 应用最核心的基础。

对于普通语音输入需求,speech_to_text 可以大幅降低开发成本。真正决定应用稳定性的,则是权限管理、状态同步、异常处理、生命周期控制以及平台差异处理。将这些部分封装好之后,就能够进一步扩展成语音搜索、语音表单、语音助手和语音备忘录等更加完整的 Flutter 应用。