feat: 新增多实例会话管理支持 v1.0.4
🎉 **重大功能更新** - 支持同一页面多个独立的录音会话 - 每个 RecordingButton 可以有独立的 sessionId - 智能会话切换,同一时间只有一个会话活跃 🆕 **YxAsrService 新增API** - registerSession() - 注册新会话 - unregisterSession() - 注销会话 - startListeningForSession() - 会话级录音控制 - stopListeningForSession() - 会话级停止控制 - getResultStreamForSession() - 获取会话结果流 - getStatusStreamForSession() - 获取会话状态流 - getRegisteredSessions() - 获取所有会话列表 - getActiveSessionId() - 获取当前活跃会话 🔧 **RecordingButton 增强** - 新增 sessionId 参数支持多实例场景 - 自动会话注册和资源清理 - 独立的流订阅机制 - 完全向后兼容 ✅ **解决的问题** - 同一页面多个录音按钮状态同步问题 - 识别结果精确分发到对应按钮 - 会话间状态互相干扰问题 📈 **版本升级** - 版本号: 1.0.2 → 1.0.4 - 无破坏性更改,现有代码无需修改
This commit is contained in:
parent
cd6d31457f
commit
5e61eed30f
|
|
@ -0,0 +1,31 @@
|
||||||
|
Extension Discovery Cache
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This folder is used by `package:extension_discovery` to cache lists of
|
||||||
|
packages that contains extensions for other packages.
|
||||||
|
|
||||||
|
DO NOT USE THIS FOLDER
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
* Do not read (or rely) the contents of this folder.
|
||||||
|
* Do write to this folder.
|
||||||
|
|
||||||
|
If you're interested in the lists of extensions stored in this folder use the
|
||||||
|
API offered by package `extension_discovery` to get this information.
|
||||||
|
|
||||||
|
If this package doesn't work for your use-case, then don't try to read the
|
||||||
|
contents of this folder. It may change, and will not remain stable.
|
||||||
|
|
||||||
|
Use package `extension_discovery`
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
If you want to access information from this folder.
|
||||||
|
|
||||||
|
Feel free to delete this folder
|
||||||
|
-------------------------------
|
||||||
|
|
||||||
|
Files in this folder act as a cache, and the cache is discarded if the files
|
||||||
|
are older than the modification time of `.dart_tool/package_config.json`.
|
||||||
|
|
||||||
|
Hence, it should never be necessary to clear this cache manually, if you find a
|
||||||
|
need to do please file a bug.
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
{"version":2,"entries":[{"package":"yx_asr","rootUri":"../","packageUri":"lib/"}]}
|
||||||
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -5,6 +5,51 @@ All notable changes to this project will be documented in this file.
|
||||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||||
|
|
||||||
|
## [1.0.4] - 2025-01-23
|
||||||
|
|
||||||
|
### 🎉 Added - 多实例会话管理支持
|
||||||
|
- **会话管理系统**:新增完整的会话管理功能,支持同一页面多个独立的录音会话
|
||||||
|
- **YxAsrService 新方法**:
|
||||||
|
- `registerSession()` - 注册新会话
|
||||||
|
- `unregisterSession()` - 注销会话
|
||||||
|
- `startListeningForSession()` - 为特定会话开始录音
|
||||||
|
- `stopListeningForSession()` - 为特定会话停止录音
|
||||||
|
- `getResultStreamForSession()` - 获取特定会话的结果流
|
||||||
|
- `getStatusStreamForSession()` - 获取特定会话的状态流
|
||||||
|
- `getRegisteredSessions()` - 获取所有已注册的会话列表
|
||||||
|
- `getActiveSessionId()` - 获取当前活跃的会话ID
|
||||||
|
- **RecordingButton 新参数**:
|
||||||
|
- `sessionId` - 会话ID参数,用于标识不同的录音实例
|
||||||
|
- **智能会话切换**:自动管理会话切换,同一时间只有一个会话活跃
|
||||||
|
|
||||||
|
### 🐛 Fixed
|
||||||
|
- 修复同一页面多个 RecordingButton 状态同步问题
|
||||||
|
- 修复录音结果分发到错误按钮的问题
|
||||||
|
- 修复会话间状态互相干扰的问题
|
||||||
|
|
||||||
|
### ⚡ Improved
|
||||||
|
- 优化会话切换逻辑,无缝切换不同会话
|
||||||
|
- 改进资源管理,会话注销时自动清理资源
|
||||||
|
- 增强错误处理,会话级别的错误追踪
|
||||||
|
|
||||||
|
### 📝 Changed
|
||||||
|
- 保持完全向后兼容,现有代码无需修改
|
||||||
|
- 识别结果同时发送到全局流和会话流(双重分发机制)
|
||||||
|
|
||||||
|
### 🎯 Use Cases
|
||||||
|
此版本主要解决以下场景:
|
||||||
|
- ✅ 同一页面有多个录音输入框(如:问题反馈 + 本月总结)
|
||||||
|
- ✅ 多个 RecordingButton 需要独立工作,互不干扰
|
||||||
|
- ✅ 需要追踪特定录音按钮的状态和结果
|
||||||
|
|
||||||
|
### 💡 Breaking Changes
|
||||||
|
无破坏性更改,完全向后兼容。
|
||||||
|
|
||||||
|
### 📚 Documentation
|
||||||
|
- 新增会话管理API文档
|
||||||
|
- 更新使用示例,包含多实例场景
|
||||||
|
- 添加会话管理最佳实践指南
|
||||||
|
|
||||||
## [1.0.0] - 2025-08-26
|
## [1.0.0] - 2025-08-26
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../interfaces/speech_recognition_service.dart';
|
import '../interfaces/speech_recognition_service.dart';
|
||||||
|
|
@ -13,6 +15,10 @@ class RecordingButton extends StatefulWidget {
|
||||||
/// 语音识别服务实例(可选,默认创建 YxAsrService)
|
/// 语音识别服务实例(可选,默认创建 YxAsrService)
|
||||||
final SpeechRecognitionService? speechService;
|
final SpeechRecognitionService? speechService;
|
||||||
|
|
||||||
|
/// 🆕 会话ID(用于多实例场景,支持同一页面多个录音按钮独立工作)
|
||||||
|
/// 如果不提供,将自动生成
|
||||||
|
final String? sessionId;
|
||||||
|
|
||||||
/// 模型路径(默认 'assets/models')
|
/// 模型路径(默认 'assets/models')
|
||||||
final String modelPath;
|
final String modelPath;
|
||||||
|
|
||||||
|
|
@ -58,6 +64,7 @@ class RecordingButton extends StatefulWidget {
|
||||||
const RecordingButton({
|
const RecordingButton({
|
||||||
super.key,
|
super.key,
|
||||||
this.speechService,
|
this.speechService,
|
||||||
|
this.sessionId, // 🆕 会话ID
|
||||||
this.modelPath = 'assets/models',
|
this.modelPath = 'assets/models',
|
||||||
this.onResult,
|
this.onResult,
|
||||||
this.onError,
|
this.onError,
|
||||||
|
|
@ -78,18 +85,27 @@ class RecordingButton extends StatefulWidget {
|
||||||
State<RecordingButton> createState() => _RecordingButtonState();
|
State<RecordingButton> createState() => _RecordingButtonState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RecordingButtonState extends State<RecordingButton>
|
class _RecordingButtonState extends State<RecordingButton> with TickerProviderStateMixin {
|
||||||
with TickerProviderStateMixin {
|
|
||||||
late SpeechRecognitionService _speechService;
|
late SpeechRecognitionService _speechService;
|
||||||
|
late String _sessionId; // 🆕 会话ID
|
||||||
bool _isListening = false;
|
bool _isListening = false;
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
bool _isProcessing = false; // 防抖标志
|
bool _isProcessing = false; // 防抖标志
|
||||||
late AnimationController _animationController;
|
late AnimationController _animationController;
|
||||||
late Animation<double> _scaleAnimation;
|
late Animation<double> _scaleAnimation;
|
||||||
|
|
||||||
|
// 🆕 会话流订阅
|
||||||
|
StreamSubscription<SpeechRecognitionResult>? _resultSubscription;
|
||||||
|
StreamSubscription<bool>? _statusSubscription;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
|
||||||
|
// 🆕 生成或使用提供的会话ID
|
||||||
|
_sessionId = widget.sessionId ??
|
||||||
|
'btn_${widget.key?.toString() ?? DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
_setupAnimation();
|
_setupAnimation();
|
||||||
_initializeService();
|
_initializeService();
|
||||||
}
|
}
|
||||||
|
|
@ -102,10 +118,7 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
_scaleAnimation = Tween<double>(
|
_scaleAnimation = Tween<double>(
|
||||||
begin: 1.0,
|
begin: 1.0,
|
||||||
end: 1.1,
|
end: 1.1,
|
||||||
).animate(CurvedAnimation(
|
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeInOut));
|
||||||
parent: _animationController,
|
|
||||||
curve: Curves.easeInOut,
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeService() async {
|
Future<void> _initializeService() async {
|
||||||
|
|
@ -117,46 +130,80 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
if (!await _speechService.hasPermission()) {
|
if (!await _speechService.hasPermission()) {
|
||||||
final granted = await _speechService.requestPermission();
|
final granted = await _speechService.requestPermission();
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
widget.onError?.call(SpeechRecognitionError(
|
widget.onError?.call(
|
||||||
errorType: SpeechRecognitionErrorType.permissionDenied,
|
SpeechRecognitionError(
|
||||||
errorMsg: '麦克风权限被拒绝',
|
errorType: SpeechRecognitionErrorType.permissionDenied,
|
||||||
errorCode: null,
|
errorMsg: '麦克风权限被拒绝',
|
||||||
));
|
errorCode: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化服务
|
// 初始化服务
|
||||||
final success = await _speechService.initialize({
|
final success = await _speechService.initialize({'modelPath': widget.modelPath});
|
||||||
'modelPath': widget.modelPath,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
// 监听事件
|
// 🆕 如果服务支持会话管理,使用会话流
|
||||||
_speechService.onResult.listen(widget.onResult ?? (_) {});
|
if (_speechService is YxAsrService) {
|
||||||
_speechService.onError.listen(widget.onError ?? (_) {});
|
final service = _speechService as YxAsrService;
|
||||||
_speechService.onListeningStatusChanged.listen((isListening) {
|
|
||||||
setState(() {
|
// 注册会话
|
||||||
_isListening = isListening;
|
service.registerSession(sessionId: _sessionId);
|
||||||
|
debugPrint('🎤 [RecordingButton] 注册会话: $_sessionId');
|
||||||
|
|
||||||
|
// 🆕 监听该会话的结果流
|
||||||
|
_resultSubscription = service.getResultStreamForSession(_sessionId).listen((result) {
|
||||||
|
debugPrint('🎤 [RecordingButton $_sessionId] 收到结果: ${result.recognizedWords}');
|
||||||
|
widget.onResult?.call(result);
|
||||||
});
|
});
|
||||||
if (isListening) {
|
|
||||||
_animationController.forward();
|
// 🆕 监听该会话的状态流
|
||||||
} else {
|
_statusSubscription = service.getStatusStreamForSession(_sessionId).listen((isListening) {
|
||||||
_animationController.reverse();
|
debugPrint('🎤 [RecordingButton $_sessionId] 状态变化: $isListening');
|
||||||
}
|
setState(() {
|
||||||
widget.onListeningStatusChanged?.call(isListening);
|
_isListening = isListening;
|
||||||
});
|
});
|
||||||
|
if (isListening) {
|
||||||
|
_animationController.forward();
|
||||||
|
} else {
|
||||||
|
_animationController.reverse();
|
||||||
|
}
|
||||||
|
widget.onListeningStatusChanged?.call(isListening);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 监听错误流(全局)
|
||||||
|
service.onError.listen(widget.onError ?? (_) {});
|
||||||
|
} else {
|
||||||
|
// 向后兼容:使用全局流
|
||||||
|
_resultSubscription = _speechService.onResult.listen(widget.onResult ?? (_) {});
|
||||||
|
_statusSubscription = _speechService.onListeningStatusChanged.listen((isListening) {
|
||||||
|
setState(() {
|
||||||
|
_isListening = isListening;
|
||||||
|
});
|
||||||
|
if (isListening) {
|
||||||
|
_animationController.forward();
|
||||||
|
} else {
|
||||||
|
_animationController.reverse();
|
||||||
|
}
|
||||||
|
widget.onListeningStatusChanged?.call(isListening);
|
||||||
|
});
|
||||||
|
_speechService.onError.listen(widget.onError ?? (_) {});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_isInitialized = success;
|
_isInitialized = success;
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
widget.onError?.call(SpeechRecognitionError(
|
widget.onError?.call(
|
||||||
errorType: SpeechRecognitionErrorType.service,
|
SpeechRecognitionError(
|
||||||
errorMsg: '初始化失败: $e',
|
errorType: SpeechRecognitionErrorType.service,
|
||||||
errorCode: null,
|
errorMsg: '初始化失败: $e',
|
||||||
));
|
errorCode: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -178,19 +225,30 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
_animationController.reverse();
|
_animationController.reverse();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (_isListening) {
|
// 🆕 使用会话方法(如果服务支持)
|
||||||
await _speechService.stopListening();
|
if (_speechService is YxAsrService) {
|
||||||
|
final service = _speechService as YxAsrService;
|
||||||
|
if (_isListening) {
|
||||||
|
await service.stopListeningForSession(_sessionId);
|
||||||
|
} else {
|
||||||
|
await service.startListeningForSession(_sessionId, partialResults: widget.partialResults);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
await _speechService.startListening(
|
// 向后兼容:使用全局方法
|
||||||
partialResults: widget.partialResults,
|
if (_isListening) {
|
||||||
);
|
await _speechService.stopListening();
|
||||||
|
} else {
|
||||||
|
await _speechService.startListening(partialResults: widget.partialResults);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
widget.onError?.call(SpeechRecognitionError(
|
widget.onError?.call(
|
||||||
errorType: SpeechRecognitionErrorType.service,
|
SpeechRecognitionError(
|
||||||
errorMsg: '切换录音状态失败: $e',
|
errorType: SpeechRecognitionErrorType.service,
|
||||||
errorCode: null,
|
errorMsg: '切换录音状态失败: $e',
|
||||||
));
|
errorCode: null,
|
||||||
|
),
|
||||||
|
);
|
||||||
} finally {
|
} finally {
|
||||||
// 延迟重置处理标志,确保有足够的防抖时间
|
// 延迟重置处理标志,确保有足够的防抖时间
|
||||||
Future.delayed(const Duration(milliseconds: 300), () {
|
Future.delayed(const Duration(milliseconds: 300), () {
|
||||||
|
|
@ -255,11 +313,10 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
height: widget.size,
|
height: widget.size,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor:
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
AlwaysStoppedAnimation<Color>(Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
@ -269,10 +326,7 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (widget.tooltip != null) {
|
if (widget.tooltip != null) {
|
||||||
button = Tooltip(
|
button = Tooltip(message: widget.tooltip!, child: button);
|
||||||
message: widget.tooltip!,
|
|
||||||
child: button,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return button;
|
return button;
|
||||||
|
|
@ -280,6 +334,17 @@ class _RecordingButtonState extends State<RecordingButton>
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
// 🆕 取消流订阅
|
||||||
|
_resultSubscription?.cancel();
|
||||||
|
_statusSubscription?.cancel();
|
||||||
|
|
||||||
|
// 🆕 注销会话
|
||||||
|
if (_speechService is YxAsrService) {
|
||||||
|
final service = _speechService as YxAsrService;
|
||||||
|
service.unregisterSession(_sessionId);
|
||||||
|
debugPrint('🎤 [RecordingButton] 注销会话: $_sessionId');
|
||||||
|
}
|
||||||
|
|
||||||
_animationController.dispose();
|
_animationController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -158,8 +158,7 @@ class AdvancedRecognitionConfig {
|
||||||
);
|
);
|
||||||
|
|
||||||
/// 高质量模式预设
|
/// 高质量模式预设
|
||||||
static const AdvancedRecognitionConfig highQuality =
|
static const AdvancedRecognitionConfig highQuality = AdvancedRecognitionConfig(
|
||||||
AdvancedRecognitionConfig(
|
|
||||||
decodingMethod: DecodingMethod.modifiedBeamSearch,
|
decodingMethod: DecodingMethod.modifiedBeamSearch,
|
||||||
maxActivePaths: 8,
|
maxActivePaths: 8,
|
||||||
enableEndpoint: true,
|
enableEndpoint: true,
|
||||||
|
|
@ -172,12 +171,7 @@ class AdvancedRecognitionConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 识别状态枚举
|
/// 识别状态枚举
|
||||||
enum RecognitionState {
|
enum RecognitionState { idle, processing, listening, error }
|
||||||
idle,
|
|
||||||
processing,
|
|
||||||
listening,
|
|
||||||
error,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 识别结果类
|
/// 识别结果类
|
||||||
class RecognitionResult {
|
class RecognitionResult {
|
||||||
|
|
@ -185,17 +179,37 @@ class RecognitionResult {
|
||||||
final double confidence;
|
final double confidence;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
|
|
||||||
RecognitionResult({
|
RecognitionResult({required this.text, required this.confidence, required this.timestamp});
|
||||||
required this.text,
|
|
||||||
required this.confidence,
|
|
||||||
required this.timestamp,
|
|
||||||
});
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() => {
|
Map<String, dynamic> toJson() => {
|
||||||
'text': text,
|
'text': text,
|
||||||
'confidence': confidence,
|
'confidence': confidence,
|
||||||
'timestamp': timestamp.toIso8601String(),
|
'timestamp': timestamp.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 单个会话的状态管理
|
||||||
|
/// 用于支持同一页面多个独立的录音会话
|
||||||
|
class _SessionState {
|
||||||
|
/// 会话唯一标识
|
||||||
|
final String sessionId;
|
||||||
|
|
||||||
|
/// 会话是否处于活跃状态(正在录音)
|
||||||
|
bool isActive;
|
||||||
|
|
||||||
|
/// 该会话最后识别的文本(用于去重)
|
||||||
|
String lastRecognizedText;
|
||||||
|
|
||||||
|
/// 会话创建时间
|
||||||
|
final DateTime createdAt;
|
||||||
|
|
||||||
|
_SessionState(this.sessionId)
|
||||||
|
: isActive = false,
|
||||||
|
lastRecognizedText = '',
|
||||||
|
createdAt = DateTime.now();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => '_SessionState(id: $sessionId, active: $isActive, created: $createdAt)';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 基于 sherpa_onnx 的完整语音识别实现
|
/// 基于 sherpa_onnx 的完整语音识别实现
|
||||||
|
|
@ -230,21 +244,32 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
SampleRate _sampleRate = SampleRate.standard;
|
SampleRate _sampleRate = SampleRate.standard;
|
||||||
|
|
||||||
// 高级识别配置
|
// 高级识别配置
|
||||||
AdvancedRecognitionConfig _advancedConfig =
|
AdvancedRecognitionConfig _advancedConfig = AdvancedRecognitionConfig.balanced;
|
||||||
AdvancedRecognitionConfig.balanced;
|
|
||||||
|
|
||||||
// 事件流控制器
|
// 事件流控制器
|
||||||
final StreamController<SpeechRecognitionResult> _resultController =
|
final StreamController<SpeechRecognitionResult> _resultController =
|
||||||
StreamController<SpeechRecognitionResult>.broadcast();
|
StreamController<SpeechRecognitionResult>.broadcast();
|
||||||
final StreamController<SpeechRecognitionError> _errorController =
|
final StreamController<SpeechRecognitionError> _errorController =
|
||||||
StreamController<SpeechRecognitionError>.broadcast();
|
StreamController<SpeechRecognitionError>.broadcast();
|
||||||
final StreamController<bool> _statusController =
|
final StreamController<bool> _statusController = StreamController<bool>.broadcast();
|
||||||
StreamController<bool>.broadcast();
|
|
||||||
|
|
||||||
// 定时器和订阅
|
// 定时器和订阅
|
||||||
Timer? _recognitionTimer;
|
Timer? _recognitionTimer;
|
||||||
StreamSubscription<Uint8List>? _audioSubscription;
|
StreamSubscription<Uint8List>? _audioSubscription;
|
||||||
|
|
||||||
|
// 🆕 会话管理相关字段
|
||||||
|
/// 所有已注册的会话状态
|
||||||
|
final Map<String, _SessionState> _sessions = {};
|
||||||
|
|
||||||
|
/// 当前活跃的会话ID(同一时间只能有一个会话录音)
|
||||||
|
String? _activeSessionId;
|
||||||
|
|
||||||
|
/// 每个会话的结果流控制器
|
||||||
|
final Map<String, StreamController<SpeechRecognitionResult>> _sessionResultControllers = {};
|
||||||
|
|
||||||
|
/// 每个会话的状态流控制器
|
||||||
|
final Map<String, StreamController<bool>> _sessionStatusControllers = {};
|
||||||
|
|
||||||
/// 检查语音识别是否可用
|
/// 检查语音识别是否可用
|
||||||
Future<bool> isAvailable() async {
|
Future<bool> isAvailable() async {
|
||||||
try {
|
try {
|
||||||
|
|
@ -287,8 +312,7 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
/// [speed] - 识别速度配置
|
/// [speed] - 识别速度配置
|
||||||
void setRecognitionSpeed(RecognitionSpeed speed) {
|
void setRecognitionSpeed(RecognitionSpeed speed) {
|
||||||
_recognitionSpeed = speed;
|
_recognitionSpeed = speed;
|
||||||
debugPrint(
|
debugPrint('🔧 [YxAsr] 识别速度设置为: ${speed.description} (${speed.milliseconds}ms)');
|
||||||
'🔧 [YxAsr] 识别速度设置为: ${speed.description} (${speed.milliseconds}ms)');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前识别速度
|
/// 获取当前识别速度
|
||||||
|
|
@ -300,8 +324,7 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
/// 注意:需要在初始化之前设置,或重新初始化后生效
|
/// 注意:需要在初始化之前设置,或重新初始化后生效
|
||||||
void setSampleRate(SampleRate sampleRate) {
|
void setSampleRate(SampleRate sampleRate) {
|
||||||
_sampleRate = sampleRate;
|
_sampleRate = sampleRate;
|
||||||
debugPrint(
|
debugPrint('🔧 [YxAsr] 采样率设置为: ${sampleRate.description} (${sampleRate.hz}Hz)');
|
||||||
'🔧 [YxAsr] 采样率设置为: ${sampleRate.description} (${sampleRate.hz}Hz)');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 获取当前采样率
|
/// 获取当前采样率
|
||||||
|
|
@ -364,22 +387,31 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
if (!permissionInfo['isGranted']) {
|
if (!permissionInfo['isGranted']) {
|
||||||
if (permissionInfo['isPermanentlyDenied']) {
|
if (permissionInfo['isPermanentlyDenied']) {
|
||||||
print('❌ [YxAsr] 麦克风权限被永久拒绝,需要用户手动在设置中开启');
|
print('❌ [YxAsr] 麦克风权限被永久拒绝,需要用户手动在设置中开启');
|
||||||
_sendError(SpeechRecognitionErrorType.permissionDenied,
|
_sendError(
|
||||||
'麦克风权限被永久拒绝,请在设置中手动开启麦克风权限', 'PERMISSION_PERMANENTLY_DENIED');
|
SpeechRecognitionErrorType.permissionDenied,
|
||||||
|
'麦克风权限被永久拒绝,请在设置中手动开启麦克风权限',
|
||||||
|
'PERMISSION_PERMANENTLY_DENIED',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
} else if (permissionInfo['canRequest']) {
|
} else if (permissionInfo['canRequest']) {
|
||||||
print('⚠️ [YxAsr] 麦克风权限未授予,尝试请求权限...');
|
print('⚠️ [YxAsr] 麦克风权限未授予,尝试请求权限...');
|
||||||
final granted = await requestPermission();
|
final granted = await requestPermission();
|
||||||
if (!granted) {
|
if (!granted) {
|
||||||
print('❌ [YxAsr] 用户拒绝了麦克风权限');
|
print('❌ [YxAsr] 用户拒绝了麦克风权限');
|
||||||
_sendError(SpeechRecognitionErrorType.permissionDenied,
|
_sendError(
|
||||||
'需要麦克风权限才能进行语音识别,请允许应用访问麦克风', 'PERMISSION_DENIED');
|
SpeechRecognitionErrorType.permissionDenied,
|
||||||
|
'需要麦克风权限才能进行语音识别,请允许应用访问麦克风',
|
||||||
|
'PERMISSION_DENIED',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print('❌ [YxAsr] 麦克风权限受限');
|
print('❌ [YxAsr] 麦克风权限受限');
|
||||||
_sendError(SpeechRecognitionErrorType.permissionDenied,
|
_sendError(
|
||||||
'麦克风权限受限,无法进行语音识别', 'PERMISSION_RESTRICTED');
|
SpeechRecognitionErrorType.permissionDenied,
|
||||||
|
'麦克风权限受限,无法进行语音识别',
|
||||||
|
'PERMISSION_RESTRICTED',
|
||||||
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -408,17 +440,12 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
// 构建模型配置
|
// 构建模型配置
|
||||||
print('🔍 [YxAsr] 构建识别器配置...');
|
print('🔍 [YxAsr] 构建识别器配置...');
|
||||||
debugPrint('🔧 [YxAsr] 使用采样率: ${_sampleRate.description}');
|
debugPrint('🔧 [YxAsr] 使用采样率: ${_sampleRate.description}');
|
||||||
debugPrint(
|
debugPrint('🔧 [YxAsr] 解码方法: ${_advancedConfig.decodingMethod.description}');
|
||||||
'🔧 [YxAsr] 解码方法: ${_advancedConfig.decodingMethod.description}');
|
|
||||||
debugPrint('🔧 [YxAsr] 最大活跃路径: ${_advancedConfig.maxActivePaths}');
|
debugPrint('🔧 [YxAsr] 最大活跃路径: ${_advancedConfig.maxActivePaths}');
|
||||||
debugPrint(
|
debugPrint('🔧 [YxAsr] 端点检测: ${_advancedConfig.enableEndpoint ? "启用" : "禁用"}');
|
||||||
'🔧 [YxAsr] 端点检测: ${_advancedConfig.enableEndpoint ? "启用" : "禁用"}');
|
|
||||||
|
|
||||||
final config = OnlineRecognizerConfig(
|
final config = OnlineRecognizerConfig(
|
||||||
feat: FeatureConfig(
|
feat: FeatureConfig(sampleRate: _sampleRate.hz, featureDim: _advancedConfig.featureDim),
|
||||||
sampleRate: _sampleRate.hz,
|
|
||||||
featureDim: _advancedConfig.featureDim,
|
|
||||||
),
|
|
||||||
model: OnlineModelConfig(
|
model: OnlineModelConfig(
|
||||||
transducer: OnlineTransducerModelConfig(
|
transducer: OnlineTransducerModelConfig(
|
||||||
encoder: encoderPath,
|
encoder: encoderPath,
|
||||||
|
|
@ -471,9 +498,7 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
///
|
///
|
||||||
/// [partialResults] - 是否返回部分结果
|
/// [partialResults] - 是否返回部分结果
|
||||||
/// 采样率使用 setSampleRate() 设置的值
|
/// 采样率使用 setSampleRate() 设置的值
|
||||||
Future<void> startListening({
|
Future<void> startListening({bool partialResults = true}) async {
|
||||||
bool partialResults = true,
|
|
||||||
}) async {
|
|
||||||
try {
|
try {
|
||||||
if (!_isInitialized || _recognizer == null) {
|
if (!_isInitialized || _recognizer == null) {
|
||||||
throw Exception('识别器未初始化,请先调用 initializeWithModel()');
|
throw Exception('识别器未初始化,请先调用 initializeWithModel()');
|
||||||
|
|
@ -597,6 +622,219 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
_statusController.add(false);
|
_statusController.add(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 🆕 会话管理方法 ====================
|
||||||
|
|
||||||
|
/// 🆕 注册一个新会话
|
||||||
|
///
|
||||||
|
/// 每个 RecordingButton 应该注册一个独立的会话
|
||||||
|
/// [sessionId] - 会话唯一标识,如果不提供则自动生成
|
||||||
|
/// 返回实际使用的会话ID
|
||||||
|
String registerSession({String? sessionId}) {
|
||||||
|
final id = sessionId ?? 'session_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
|
if (!_sessions.containsKey(id)) {
|
||||||
|
_sessions[id] = _SessionState(id);
|
||||||
|
_sessionResultControllers[id] = StreamController<SpeechRecognitionResult>.broadcast();
|
||||||
|
_sessionStatusControllers[id] = StreamController<bool>.broadcast();
|
||||||
|
debugPrint('📝 [YxAsr] 注册会话: $id (总计: ${_sessions.length} 个会话)');
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ [YxAsr] 会话 $id 已存在,跳过注册');
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 注销会话
|
||||||
|
///
|
||||||
|
/// 释放会话相关的所有资源
|
||||||
|
/// [sessionId] - 要注销的会话ID
|
||||||
|
Future<void> unregisterSession(String sessionId) async {
|
||||||
|
if (!_sessions.containsKey(sessionId)) {
|
||||||
|
debugPrint('⚠️ [YxAsr] 会话 $sessionId 不存在,无需注销');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是活跃会话,先停止录音
|
||||||
|
if (_activeSessionId == sessionId && _isListening) {
|
||||||
|
debugPrint('⚠️ [YxAsr] 注销活跃会话 $sessionId,先停止录音');
|
||||||
|
await stopListeningForSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭该会话的流控制器
|
||||||
|
await _sessionResultControllers[sessionId]?.close();
|
||||||
|
await _sessionStatusControllers[sessionId]?.close();
|
||||||
|
|
||||||
|
// 移除会话记录
|
||||||
|
_sessions.remove(sessionId);
|
||||||
|
_sessionResultControllers.remove(sessionId);
|
||||||
|
_sessionStatusControllers.remove(sessionId);
|
||||||
|
|
||||||
|
debugPrint('🗑️ [YxAsr] 注销会话: $sessionId (剩余: ${_sessions.length} 个会话)');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 为特定会话开始录音
|
||||||
|
///
|
||||||
|
/// [sessionId] - 会话ID
|
||||||
|
/// [partialResults] - 是否返回部分结果
|
||||||
|
Future<void> startListeningForSession(String sessionId, {bool partialResults = true}) async {
|
||||||
|
try {
|
||||||
|
// 检查会话是否存在
|
||||||
|
if (!_sessions.containsKey(sessionId)) {
|
||||||
|
throw Exception('会话 $sessionId 未注册,请先调用 registerSession()');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有其他会话正在录音,先停止
|
||||||
|
if (_activeSessionId != null && _activeSessionId != sessionId && _isListening) {
|
||||||
|
debugPrint('⚠️ [YxAsr] 停止其他会话 $_activeSessionId,切换到 $sessionId');
|
||||||
|
await stopListeningForSession(_activeSessionId!);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前会话已在录音,忽略
|
||||||
|
if (_activeSessionId == sessionId && _isListening) {
|
||||||
|
debugPrint('⚠️ [YxAsr] 会话 $sessionId 已在录音,忽略重复调用');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置当前活跃会话
|
||||||
|
_activeSessionId = sessionId;
|
||||||
|
_sessions[sessionId]!.isActive = true;
|
||||||
|
_sessions[sessionId]!.lastRecognizedText = '';
|
||||||
|
|
||||||
|
debugPrint('🎤 [YxAsr] 会话 $sessionId 开始录音 (partialResults: $partialResults)');
|
||||||
|
|
||||||
|
// 调用原有的 startListening 逻辑
|
||||||
|
await startListening(partialResults: partialResults);
|
||||||
|
|
||||||
|
// 通知该会话的监听者状态变化
|
||||||
|
_sessionStatusControllers[sessionId]?.add(true);
|
||||||
|
|
||||||
|
debugPrint('✅ [YxAsr] 会话 $sessionId 录音启动成功');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ [YxAsr] 会话 $sessionId 开始录音失败: $e');
|
||||||
|
|
||||||
|
// 清理失败状态
|
||||||
|
if (_sessions.containsKey(sessionId)) {
|
||||||
|
_sessions[sessionId]!.isActive = false;
|
||||||
|
}
|
||||||
|
if (_activeSessionId == sessionId) {
|
||||||
|
_activeSessionId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知错误
|
||||||
|
_sendErrorForSession(sessionId, SpeechRecognitionErrorType.service, '开始录音失败: $e', null);
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 为特定会话停止录音
|
||||||
|
///
|
||||||
|
/// [sessionId] - 会话ID
|
||||||
|
Future<void> stopListeningForSession(String sessionId) async {
|
||||||
|
// 检查会话是否存在
|
||||||
|
if (!_sessions.containsKey(sessionId)) {
|
||||||
|
debugPrint('⚠️ [YxAsr] 会话 $sessionId 不存在,忽略停止请求');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否是活跃会话
|
||||||
|
if (_activeSessionId != sessionId) {
|
||||||
|
debugPrint('⚠️ [YxAsr] 会话 $sessionId 不是活跃会话(活跃: $_activeSessionId),忽略停止请求');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('🛑 [YxAsr] 会话 $sessionId 停止录音');
|
||||||
|
|
||||||
|
// 调用原有的 stopListening 逻辑
|
||||||
|
await stopListening();
|
||||||
|
|
||||||
|
// 更新会话状态
|
||||||
|
_sessions[sessionId]!.isActive = false;
|
||||||
|
_activeSessionId = null;
|
||||||
|
|
||||||
|
// 通知该会话的监听者状态变化
|
||||||
|
_sessionStatusControllers[sessionId]?.add(false);
|
||||||
|
|
||||||
|
debugPrint('✅ [YxAsr] 会话 $sessionId 录音已停止');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ [YxAsr] 会话 $sessionId 停止录音失败: $e');
|
||||||
|
_sendErrorForSession(sessionId, SpeechRecognitionErrorType.service, '停止录音失败: $e', null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 获取特定会话的结果流
|
||||||
|
///
|
||||||
|
/// [sessionId] - 会话ID
|
||||||
|
/// 返回该会话的识别结果流
|
||||||
|
Stream<SpeechRecognitionResult> getResultStreamForSession(String sessionId) {
|
||||||
|
if (!_sessionResultControllers.containsKey(sessionId)) {
|
||||||
|
debugPrint('⚠️ [YxAsr] 会话 $sessionId 不存在,自动注册');
|
||||||
|
registerSession(sessionId: sessionId);
|
||||||
|
}
|
||||||
|
return _sessionResultControllers[sessionId]!.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 获取特定会话的状态流
|
||||||
|
///
|
||||||
|
/// [sessionId] - 会话ID
|
||||||
|
/// 返回该会话的状态变化流
|
||||||
|
Stream<bool> getStatusStreamForSession(String sessionId) {
|
||||||
|
if (!_sessionStatusControllers.containsKey(sessionId)) {
|
||||||
|
debugPrint('⚠️ [YxAsr] 会话 $sessionId 不存在,自动注册');
|
||||||
|
registerSession(sessionId: sessionId);
|
||||||
|
}
|
||||||
|
return _sessionStatusControllers[sessionId]!.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 为特定会话发送识别结果
|
||||||
|
void _sendResultForSession(String sessionId, {required String recognizedWords}) {
|
||||||
|
if (_sessionResultControllers.containsKey(sessionId)) {
|
||||||
|
// 检查是否与上次结果相同(去重)
|
||||||
|
if (_sessions[sessionId]?.lastRecognizedText == recognizedWords) {
|
||||||
|
debugPrint('🔄 [YxAsr] 会话 $sessionId 跳过重复结果: "$recognizedWords"');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新最后识别文本
|
||||||
|
if (_sessions.containsKey(sessionId)) {
|
||||||
|
_sessions[sessionId]!.lastRecognizedText = recognizedWords;
|
||||||
|
}
|
||||||
|
|
||||||
|
final result = SpeechRecognitionResult(recognizedWords: recognizedWords);
|
||||||
|
_sessionResultControllers[sessionId]!.add(result);
|
||||||
|
debugPrint('📤 [YxAsr] 会话 $sessionId 发送结果: "$recognizedWords"');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 为特定会话发送错误
|
||||||
|
void _sendErrorForSession(
|
||||||
|
String sessionId,
|
||||||
|
SpeechRecognitionErrorType errorType,
|
||||||
|
String errorMsg,
|
||||||
|
String? errorCode,
|
||||||
|
) {
|
||||||
|
final error = SpeechRecognitionError(
|
||||||
|
errorType: errorType,
|
||||||
|
errorMsg: '[会话 $sessionId] $errorMsg',
|
||||||
|
errorCode: errorCode,
|
||||||
|
);
|
||||||
|
// 发送到全局错误流
|
||||||
|
_errorController.add(error);
|
||||||
|
debugPrint('❌ [YxAsr] 会话 $sessionId 错误: $errorMsg');
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 获取所有已注册的会话列表
|
||||||
|
List<String> getRegisteredSessions() {
|
||||||
|
return _sessions.keys.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🆕 获取当前活跃的会话ID
|
||||||
|
String? getActiveSessionId() {
|
||||||
|
return _activeSessionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 原有方法 ====================
|
||||||
|
|
||||||
/// 是否正在监听
|
/// 是否正在监听
|
||||||
bool get isListening => _isListening;
|
bool get isListening => _isListening;
|
||||||
|
|
||||||
|
|
@ -652,19 +890,16 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
_audioSubscription = stream.listen(
|
_audioSubscription = stream.listen(
|
||||||
(audioData) {
|
(audioData) {
|
||||||
// 多重状态检查,确保所有条件都满足
|
// 多重状态检查,确保所有条件都满足
|
||||||
if (_stream != null &&
|
if (_stream != null && _recognizer != null && _isListening && !_isStartingRecording) {
|
||||||
_recognizer != null &&
|
|
||||||
_isListening &&
|
|
||||||
!_isStartingRecording) {
|
|
||||||
// 将音频数据转换为 Float32List 格式
|
// 将音频数据转换为 Float32List 格式
|
||||||
final samples = _convertToFloat32(audioData);
|
final samples = _convertToFloat32(audioData);
|
||||||
debugPrint(
|
debugPrint('🎵 [YxAsr] 接收音频数据: ${audioData.length} 字节, ${samples.length} 样本');
|
||||||
'🎵 [YxAsr] 接收音频数据: ${audioData.length} 字节, ${samples.length} 样本');
|
|
||||||
// 发送音频数据到识别器进行处理
|
// 发送音频数据到识别器进行处理
|
||||||
_stream!.acceptWaveform(sampleRate: sampleRate, samples: samples);
|
_stream!.acceptWaveform(sampleRate: sampleRate, samples: samples);
|
||||||
} else {
|
} else {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'❌ [YxAsr] 音频数据丢弃: stream=${_stream != null}, recognizer=${_recognizer != null}, listening=$_isListening, starting=$_isStartingRecording');
|
'❌ [YxAsr] 音频数据丢弃: stream=${_stream != null}, recognizer=${_recognizer != null}, listening=$_isListening, starting=$_isStartingRecording',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (error) {
|
onError: (error) {
|
||||||
|
|
@ -721,12 +956,15 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
/// 开始识别循环处理
|
/// 开始识别循环处理
|
||||||
void _startRecognitionLoop(bool partialResults) {
|
void _startRecognitionLoop(bool partialResults) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'🔄 [YxAsr] 开始识别循环, partialResults: $partialResults, 速度: ${_recognitionSpeed.description} (${_recognitionSpeed.milliseconds}ms)');
|
'🔄 [YxAsr] 开始识别循环, partialResults: $partialResults, 速度: ${_recognitionSpeed.description} (${_recognitionSpeed.milliseconds}ms)',
|
||||||
_recognitionTimer = Timer.periodic(
|
);
|
||||||
Duration(milliseconds: _recognitionSpeed.milliseconds), (timer) {
|
_recognitionTimer = Timer.periodic(Duration(milliseconds: _recognitionSpeed.milliseconds), (
|
||||||
|
timer,
|
||||||
|
) {
|
||||||
if (!_isListening || _stream == null || _recognizer == null) {
|
if (!_isListening || _stream == null || _recognizer == null) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'🛑 [YxAsr] 识别循环停止: listening=$_isListening, stream=${_stream != null}, recognizer=${_recognizer != null}');
|
'🛑 [YxAsr] 识别循环停止: listening=$_isListening, stream=${_stream != null}, recognizer=${_recognizer != null}',
|
||||||
|
);
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -743,16 +981,18 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
debugPrint('🔍 [YxAsr] 获取识别结果: "${result.text}"');
|
debugPrint('🔍 [YxAsr] 获取识别结果: "${result.text}"');
|
||||||
|
|
||||||
// 只有当识别结果不为空、启用了部分结果、且与上次结果不同时才发送
|
// 只有当识别结果不为空、启用了部分结果、且与上次结果不同时才发送
|
||||||
if (result.text.isNotEmpty &&
|
if (result.text.isNotEmpty && partialResults && result.text != _lastRecognizedText) {
|
||||||
partialResults &&
|
|
||||||
result.text != _lastRecognizedText) {
|
|
||||||
debugPrint('🎤 [YxAsr] 发送实时识别结果: ${result.text}');
|
debugPrint('🎤 [YxAsr] 发送实时识别结果: ${result.text}');
|
||||||
_lastRecognizedText = result.text; // 更新最后识别的文本
|
_lastRecognizedText = result.text; // 更新最后识别的文本
|
||||||
_sendResult(
|
|
||||||
recognizedWords: result.text,
|
// 发送到全局流(向后兼容)
|
||||||
);
|
_sendResult(recognizedWords: result.text);
|
||||||
} else if (result.text.isNotEmpty &&
|
|
||||||
result.text == _lastRecognizedText) {
|
// 🆕 如果有活跃会话,同时发送到该会话的流
|
||||||
|
if (_activeSessionId != null) {
|
||||||
|
_sendResultForSession(_activeSessionId!, recognizedWords: result.text);
|
||||||
|
}
|
||||||
|
} else if (result.text.isNotEmpty && result.text == _lastRecognizedText) {
|
||||||
debugPrint('🔄 [YxAsr] 跳过重复识别结果: "${result.text}"');
|
debugPrint('🔄 [YxAsr] 跳过重复识别结果: "${result.text}"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -767,19 +1007,14 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发送识别结果到结果流
|
/// 发送识别结果到结果流
|
||||||
void _sendResult({
|
void _sendResult({required String recognizedWords}) {
|
||||||
required String recognizedWords,
|
|
||||||
}) {
|
|
||||||
debugPrint('📤 [YxAsr] 发送识别结果: "$recognizedWords"');
|
debugPrint('📤 [YxAsr] 发送识别结果: "$recognizedWords"');
|
||||||
final result = SpeechRecognitionResult(
|
final result = SpeechRecognitionResult(recognizedWords: recognizedWords);
|
||||||
recognizedWords: recognizedWords,
|
|
||||||
);
|
|
||||||
_resultController.add(result);
|
_resultController.add(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 发送错误信息到错误流
|
/// 发送错误信息到错误流
|
||||||
void _sendError(SpeechRecognitionErrorType errorType, String errorMsg,
|
void _sendError(SpeechRecognitionErrorType errorType, String errorMsg, String? errorCode) {
|
||||||
String? errorCode) {
|
|
||||||
final error = SpeechRecognitionError(
|
final error = SpeechRecognitionError(
|
||||||
errorType: errorType,
|
errorType: errorType,
|
||||||
errorMsg: errorMsg,
|
errorMsg: errorMsg,
|
||||||
|
|
@ -836,10 +1071,12 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [YxAsr] 无法复制模型文件 $fileName: $e');
|
debugPrint('❌ [YxAsr] 无法复制模型文件 $fileName: $e');
|
||||||
// 提供更详细的错误信息
|
// 提供更详细的错误信息
|
||||||
throw Exception('模型文件复制失败: $fileName\n'
|
throw Exception(
|
||||||
'请确保模型文件存在于: $assetFile\n'
|
'模型文件复制失败: $fileName\n'
|
||||||
'当前模型路径: $assetPath\n'
|
'请确保模型文件存在于: $assetFile\n'
|
||||||
'支持的新模型: sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30');
|
'当前模型路径: $assetPath\n'
|
||||||
|
'支持的新模型: sherpa-onnx-streaming-zipformer-zh-int8-2025-06-30',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
debugPrint('⏭️ [YxAsr] 文件已存在,跳过: $fileName');
|
debugPrint('⏭️ [YxAsr] 文件已存在,跳过: $fileName');
|
||||||
|
|
@ -856,9 +1093,18 @@ class YxAsrService implements SpeechRecognitionService {
|
||||||
|
|
||||||
/// 释放所有资源并关闭流
|
/// 释放所有资源并关闭流
|
||||||
Future<void> dispose() async {
|
Future<void> dispose() async {
|
||||||
|
// 🆕 清理所有会话
|
||||||
|
final sessionIds = _sessions.keys.toList();
|
||||||
|
for (final sessionId in sessionIds) {
|
||||||
|
await unregisterSession(sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理原有资源
|
||||||
await _cleanup();
|
await _cleanup();
|
||||||
await _resultController.close();
|
await _resultController.close();
|
||||||
await _errorController.close();
|
await _errorController.close();
|
||||||
await _statusController.close();
|
await _statusController.close();
|
||||||
|
|
||||||
|
debugPrint('🗑️ [YxAsr] 服务已完全清理');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
name: yx_asr
|
name: yx_asr
|
||||||
description: 基于 sherpa_onnx 的 Flutter 语音识别插件,提供完全离线的实时语音转文字功能。
|
description: 基于 sherpa_onnx 的 Flutter 语音识别插件,提供完全离线的实时语音转文字功能。支持多实例会话管理。
|
||||||
version: 1.0.2
|
version: 1.0.4
|
||||||
homepage: https://github.com/yuanxuan/yx_asr
|
homepage: https://github.com/yuanxuan/yx_asr
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.0"
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
@ -22,5 +22,3 @@ dev_dependencies:
|
||||||
integration_test:
|
integration_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_lints: ^3.0.0
|
flutter_lints: ^3.0.0
|
||||||
|
|
||||||
flutter:
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue