Files
iiEsaywebUIapp/lib/features/chat/services/text_to_speech_service.dart
cogwheel dc2495dca0 fix(voice-call): Improve async handling and state management
Refactor voice call service to handle asynchronous operations more
precisely. Update method signatures to be async, use unawaited for
non-blocking calls, and ensure proper state reset between sessions.
Improve error handling and resource management for voice input and
text-to-speech services.
2026-02-05 17:53:09 +05:30

248 lines
7.0 KiB
Dart

import 'dart:async';
import 'package:flutter/foundation.dart';
import '../../../core/services/api_service.dart';
import '../../../core/services/settings_service.dart';
import 'tts_manager.dart';
export 'tts_manager.dart' show TtsEvent, TtsPlaybackSession;
/// Wrapper around [TtsManager] that provides a callback-based API.
///
/// This service is used by the [TextToSpeechController] and [VoiceCallService]
/// to interact with TTS. It translates [TtsEvent]s from the manager into
/// callbacks for backward compatibility.
class TextToSpeechService {
TextToSpeechService({ApiService? api}) {
// Set the API service on the manager
TtsManager.instance.setApiService(api);
// Listen to TTS events and route to callbacks
_eventSubscription = TtsManager.instance.events.listen(_handleEvent);
}
StreamSubscription<TtsEvent>? _eventSubscription;
bool _initialized = false;
// Callbacks
VoidCallback? _onStart;
VoidCallback? _onComplete;
VoidCallback? _onCancel;
VoidCallback? _onPause;
VoidCallback? _onContinue;
void Function(String message)? _onError;
void Function(int sentenceIndex)? _onSentenceIndex;
void Function(int start, int end)? _onDeviceWordProgress;
/// Whether the service has been initialized.
bool get isInitialized => _initialized;
/// Whether TTS is available.
bool get isAvailable => TtsManager.instance.isAvailable;
/// Whether device TTS is available.
bool get deviceEngineAvailable => TtsManager.instance.deviceAvailable;
/// Whether server TTS is available.
bool get serverEngineAvailable => TtsManager.instance.serverAvailable;
/// Whether server TTS is preferred and available.
bool get prefersServerEngine {
final config = TtsManager.instance.config;
if (config.preferServer && TtsManager.instance.serverAvailable) {
return true;
}
return !TtsManager.instance.deviceAvailable &&
TtsManager.instance.serverAvailable;
}
/// Registers callbacks for TTS lifecycle events.
void bindHandlers({
VoidCallback? onStart,
VoidCallback? onComplete,
VoidCallback? onCancel,
VoidCallback? onPause,
VoidCallback? onContinue,
void Function(String message)? onError,
void Function(int sentenceIndex)? onSentenceIndex,
void Function(int start, int end)? onDeviceWordProgress,
}) {
_onStart = onStart;
_onComplete = onComplete;
_onCancel = onCancel;
_onPause = onPause;
_onContinue = onContinue;
_onError = onError;
_onSentenceIndex = onSentenceIndex;
_onDeviceWordProgress = onDeviceWordProgress;
}
/// Initializes the TTS engine.
Future<bool> initialize({
String? deviceVoice,
String? serverVoice,
double speechRate = 0.5,
double pitch = 1.0,
double volume = 1.0,
TtsEngine engine = TtsEngine.device,
}) async {
if (_initialized) {
// Update config if already initialized
await TtsManager.instance.updateConfig(
TtsConfig(
voice: deviceVoice,
serverVoice: serverVoice,
speechRate: speechRate,
pitch: pitch,
volume: volume,
preferServer: engine == TtsEngine.server,
),
);
return isAvailable;
}
final available = await TtsManager.instance.initialize(
config: TtsConfig(
voice: deviceVoice,
serverVoice: serverVoice,
speechRate: speechRate,
pitch: pitch,
volume: volume,
preferServer: engine == TtsEngine.server,
),
);
_initialized = true;
return available;
}
/// Speaks the given text.
Future<void> speak(String text) async {
if (text.trim().isEmpty) {
throw ArgumentError('Cannot speak empty text');
}
if (!_initialized) {
await initialize();
}
await TtsManager.instance.speak(text);
}
/// Pauses the current playback.
Future<void> pause() async {
await TtsManager.instance.pause();
}
/// Resumes paused playback.
Future<void> resume() async {
await TtsManager.instance.resume();
}
/// Stops the current playback.
Future<void> stop() async {
await TtsManager.instance.stop();
}
/// Disposes the service.
Future<void> dispose() async {
await _eventSubscription?.cancel();
_eventSubscription = null;
// Reset the singleton state for next session
await TtsManager.instance.reset();
}
/// Updates TTS settings.
Future<void> updateSettings({
Object? voice = const _NotProvided(),
Object? serverVoice = const _NotProvided(),
double? speechRate,
double? pitch,
double? volume,
TtsEngine? engine,
}) async {
final current = TtsManager.instance.config;
await TtsManager.instance.updateConfig(
TtsConfig(
voice: voice is _NotProvided ? current.voice : voice as String?,
serverVoice: serverVoice is _NotProvided
? current.serverVoice
: serverVoice as String?,
speechRate: speechRate ?? current.speechRate,
pitch: pitch ?? current.pitch,
volume: volume ?? current.volume,
preferServer: engine != null
? engine == TtsEngine.server
: current.preferServer,
),
);
}
/// Gets available voices from the device TTS engine.
Future<List<Map<String, dynamic>>> getAvailableVoices() async {
if (!_initialized) {
await initialize();
}
final config = TtsManager.instance.config;
if (config.preferServer && TtsManager.instance.serverAvailable) {
return TtsManager.instance.getServerVoices();
}
return TtsManager.instance.getDeviceVoices();
}
/// Splits text into chunks for TTS playback.
List<String> splitTextForSpeech(String text) {
return TtsManager.instance.splitTextForSpeech(text);
}
/// Preloads server default voice configuration.
Future<void> preloadServerDefaults() async {
await TtsManager.instance.preloadServerDefaults();
}
/// Synthesizes a single chunk of text to audio (server TTS only).
Future<SpeechAudioChunk> synthesizeServerSpeechChunk(String text) async {
final result = await TtsManager.instance.synthesizeChunk(text);
return SpeechAudioChunk(bytes: result.bytes, mimeType: result.mimeType);
}
void _handleEvent(TtsEvent event) {
switch (event) {
case TtsStarted():
_onStart?.call();
case TtsChunkStarted(:final chunkIndex):
_onSentenceIndex?.call(chunkIndex);
case TtsWordProgress(:final start, :final end):
_onDeviceWordProgress?.call(start, end);
case TtsCompleted():
_onComplete?.call();
case TtsCancelled():
_onCancel?.call();
case TtsPaused():
_onPause?.call();
case TtsResumed():
_onContinue?.call();
case TtsError(:final message):
_onError?.call(message);
}
}
}
/// Marker class to distinguish "not provided" from null.
class _NotProvided {
const _NotProvided();
}
/// Audio chunk for server TTS synthesis.
class SpeechAudioChunk {
const SpeechAudioChunk({required this.bytes, required this.mimeType});
final Uint8List bytes;
final String mimeType;
}