主要变更: 1. 重构多处Provider初始化逻辑,使用Future.microtask避免build阶段修改state 2. 重命名"灵感"模块为"工作流","天气诗词"改为"情景诗词" 3. 新增插件系统页面与路由,添加speech_to_text_windows插件支持 4. 优化玻璃容器性能,添加性能节流与模糊值适配 5. 新增离线横幅、阅读体验控制器、手势控制器等组件 6. 完善头像审核状态展示与缓存管理 7. 修复键盘弹出与页面路由问题 8. 更新依赖与项目配置,优化widget默认数据
247 lines
6.5 KiB
Dart
247 lines
6.5 KiB
Dart
/// ============================================================
|
||
/// 闲言APP — 语音输入服务
|
||
/// 创建时间: 2026-05-25
|
||
/// 更新时间: 2026-05-25
|
||
/// 作用: 基于speech_to_text实现语音转文字
|
||
/// 上次更新: 初始创建
|
||
/// ============================================================
|
||
|
||
import 'dart:async';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:speech_to_text/speech_recognition_error.dart';
|
||
import 'package:speech_to_text/speech_recognition_result.dart';
|
||
import 'package:speech_to_text/speech_to_text.dart';
|
||
|
||
import '../../utils/logger.dart';
|
||
import '../device/haptic_service.dart';
|
||
|
||
/// 语音识别服务状态
|
||
enum SpeechServiceState { unavailable, available, listening, processing, error }
|
||
|
||
/// 语音识别结果
|
||
class SpeechResult {
|
||
const SpeechResult({
|
||
required this.text,
|
||
required this.isFinal,
|
||
this.confidence = 0.0,
|
||
});
|
||
|
||
final String text;
|
||
final bool isFinal;
|
||
final double confidence;
|
||
}
|
||
|
||
/// 语音输入服务 — 全局单例
|
||
///
|
||
/// 基于speech_to_text实现语音转文字,支持中文和英文。
|
||
/// 与HapticService集成,开始/停止时触觉反馈。
|
||
class SpeechService {
|
||
SpeechService._();
|
||
|
||
static final SpeechService instance = SpeechService._();
|
||
|
||
final SpeechToText _speech = SpeechToText();
|
||
|
||
SpeechServiceState _state = SpeechServiceState.unavailable;
|
||
String _lastResult = '';
|
||
String _currentLocale = 'zh_CN';
|
||
|
||
final _stateController = StreamController<SpeechServiceState>.broadcast();
|
||
final _resultController = StreamController<SpeechResult>.broadcast();
|
||
final _errorController = StreamController<String>.broadcast();
|
||
|
||
/// 语音识别状态流
|
||
Stream<SpeechServiceState> get onStateChanged => _stateController.stream;
|
||
|
||
/// 识别结果流
|
||
Stream<SpeechResult> get onResult => _resultController.stream;
|
||
|
||
/// 错误流
|
||
Stream<String> get onError => _errorController.stream;
|
||
|
||
/// 当前状态
|
||
SpeechServiceState get state => _state;
|
||
|
||
/// 语音识别是否可用
|
||
bool get isAvailable => _state != SpeechServiceState.unavailable;
|
||
|
||
/// 是否正在监听
|
||
bool get isListening => _state == SpeechServiceState.listening;
|
||
|
||
/// 最后一次识别结果
|
||
String get lastResult => _lastResult;
|
||
|
||
/// 当前语言
|
||
String get currentLocale => _currentLocale;
|
||
|
||
/// 初始化语音识别
|
||
///
|
||
/// 请求权限并初始化引擎,返回是否可用。
|
||
Future<bool> init() async {
|
||
if (_state != SpeechServiceState.unavailable) return isAvailable;
|
||
|
||
try {
|
||
final available = await _speech.initialize(
|
||
onError: _onError,
|
||
onStatus: _onStatus,
|
||
debugLogging: kDebugMode,
|
||
);
|
||
|
||
if (available) {
|
||
_state = SpeechServiceState.available;
|
||
_emitState();
|
||
Log.i('SpeechService: 初始化成功');
|
||
} else {
|
||
Log.w('SpeechService: 语音识别不可用');
|
||
}
|
||
return available;
|
||
} catch (e) {
|
||
Log.e('SpeechService: 初始化失败', e);
|
||
_state = SpeechServiceState.error;
|
||
_emitState();
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// 开始语音识别
|
||
///
|
||
/// [localeId] 语言标识,默认zh_CN,可传en_US切换英文。
|
||
Future<void> startListening({String? localeId}) async {
|
||
if (!isAvailable || isListening) return;
|
||
|
||
_currentLocale = localeId ?? 'zh_CN';
|
||
|
||
try {
|
||
await _speech.listen(
|
||
onResult: _onSpeechResult,
|
||
listenOptions: SpeechListenOptions(
|
||
localeId: _currentLocale,
|
||
pauseFor: const Duration(seconds: 5),
|
||
listenFor: const Duration(seconds: 30),
|
||
cancelOnError: true,
|
||
listenMode: ListenMode.dictation,
|
||
),
|
||
);
|
||
|
||
HapticService.light();
|
||
Log.i('SpeechService: 开始监听 (locale=$_currentLocale)');
|
||
} catch (e) {
|
||
Log.e('SpeechService: 启动监听失败', e);
|
||
_state = SpeechServiceState.error;
|
||
_emitState();
|
||
_emitError('启动语音识别失败');
|
||
}
|
||
}
|
||
|
||
/// 停止语音识别
|
||
///
|
||
/// 停止后仍会处理已接收的音频,可能产生最终结果。
|
||
Future<void> stopListening() async {
|
||
if (!isListening) return;
|
||
|
||
try {
|
||
await _speech.stop();
|
||
HapticService.light();
|
||
Log.i('SpeechService: 停止监听');
|
||
} catch (e) {
|
||
Log.e('SpeechService: 停止监听失败', e);
|
||
}
|
||
}
|
||
|
||
/// 取消语音识别
|
||
///
|
||
/// 取消后不会产生最终结果。
|
||
Future<void> cancelListening() async {
|
||
if (!isListening) return;
|
||
|
||
try {
|
||
await _speech.cancel();
|
||
HapticService.light();
|
||
Log.i('SpeechService: 取消监听');
|
||
} catch (e) {
|
||
Log.e('SpeechService: 取消监听失败', e);
|
||
}
|
||
}
|
||
|
||
/// 切换语言
|
||
void setLocale(String localeId) {
|
||
_currentLocale = localeId;
|
||
Log.i('SpeechService: 切换语言为 $localeId');
|
||
}
|
||
|
||
/// 获取可用语言列表
|
||
Future<List<LocaleName>> get locales => _speech.locales();
|
||
|
||
/// 获取系统默认语言
|
||
Future<String> get systemLocale async {
|
||
final locale = await _speech.systemLocale();
|
||
return locale?.localeId ?? 'zh_CN';
|
||
}
|
||
|
||
/// 释放资源
|
||
void dispose() {
|
||
_stateController.close();
|
||
_resultController.close();
|
||
_errorController.close();
|
||
}
|
||
|
||
// ============================================================
|
||
// 内部回调
|
||
// ============================================================
|
||
|
||
void _onSpeechResult(SpeechRecognitionResult result) {
|
||
final speechResult = SpeechResult(
|
||
text: result.recognizedWords,
|
||
isFinal: result.finalResult,
|
||
confidence: result.confidence,
|
||
);
|
||
|
||
if (result.finalResult && result.recognizedWords.isNotEmpty) {
|
||
_lastResult = result.recognizedWords;
|
||
}
|
||
|
||
if (!_resultController.isClosed) {
|
||
_resultController.add(speechResult);
|
||
}
|
||
}
|
||
|
||
void _onStatus(String status) {
|
||
Log.d('SpeechService: 状态变化 $status');
|
||
switch (status) {
|
||
case 'listening':
|
||
_state = SpeechServiceState.listening;
|
||
case 'notListening':
|
||
_state = SpeechServiceState.available;
|
||
case 'done':
|
||
_state = SpeechServiceState.available;
|
||
default:
|
||
break;
|
||
}
|
||
_emitState();
|
||
}
|
||
|
||
void _onError(SpeechRecognitionError error) {
|
||
Log.e(
|
||
'SpeechService: 识别错误 ${error.errorMsg} (permanent=${error.permanent})',
|
||
);
|
||
if (error.permanent) {
|
||
_state = SpeechServiceState.error;
|
||
_emitState();
|
||
}
|
||
_emitError(error.errorMsg);
|
||
}
|
||
|
||
void _emitState() {
|
||
if (!_stateController.isClosed) {
|
||
_stateController.add(_state);
|
||
}
|
||
}
|
||
|
||
void _emitError(String message) {
|
||
if (!_errorController.isClosed) {
|
||
_errorController.add(message);
|
||
}
|
||
}
|
||
}
|