Files
iiEsaywebUIapp/lib/features/chat/providers/text_to_speech_provider.dart

303 lines
7.2 KiB
Dart
Raw Normal View History

2025-09-20 23:58:18 +05:30
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../../core/services/settings_service.dart';
import '../../../core/utils/markdown_to_text.dart';
2025-09-20 23:58:18 +05:30
import '../services/text_to_speech_service.dart';
enum TtsPlaybackStatus { idle, initializing, loading, speaking, paused, error }
class TextToSpeechState {
final bool initialized;
final bool available;
final TtsPlaybackStatus status;
final String? activeMessageId;
final String? errorMessage;
const TextToSpeechState({
this.initialized = false,
this.available = false,
this.status = TtsPlaybackStatus.idle,
this.activeMessageId,
this.errorMessage,
});
bool get isSpeaking => status == TtsPlaybackStatus.speaking;
bool get isBusy =>
status == TtsPlaybackStatus.loading ||
status == TtsPlaybackStatus.initializing;
TextToSpeechState copyWith({
bool? initialized,
bool? available,
TtsPlaybackStatus? status,
String? activeMessageId,
bool clearActiveMessageId = false,
String? errorMessage,
bool clearErrorMessage = false,
}) {
return TextToSpeechState(
initialized: initialized ?? this.initialized,
available: available ?? this.available,
status: status ?? this.status,
activeMessageId: clearActiveMessageId
? null
: activeMessageId ?? this.activeMessageId,
errorMessage: clearErrorMessage
? null
: errorMessage ?? this.errorMessage,
);
}
}
2025-09-21 22:31:44 +05:30
class TextToSpeechController extends Notifier<TextToSpeechState> {
late final TextToSpeechService _service;
bool _handlersBound = false;
2025-09-20 23:58:18 +05:30
Future<bool>? _initializationFuture;
2025-09-21 22:31:44 +05:30
@override
TextToSpeechState build() {
_service = ref.watch(textToSpeechServiceProvider);
2025-09-21 22:31:44 +05:30
if (!_handlersBound) {
_handlersBound = true;
_service.bindHandlers(
onStart: _handleStart,
onComplete: _handleCompletion,
onCancel: _handleCancellation,
onPause: _handlePause,
onContinue: _handleContinue,
onError: _handleError,
);
ref.onDispose(() {
unawaited(_service.stop());
});
}
// Listen to settings changes and update TTS when initialized
ref.listen<AppSettings>(appSettingsProvider, (previous, next) {
if (_service.isInitialized && _service.isAvailable) {
_service.updateSettings(
voice: next.ttsVoice,
speechRate: next.ttsSpeechRate,
pitch: next.ttsPitch,
volume: next.ttsVolume,
);
}
}, fireImmediately: false);
2025-09-21 22:31:44 +05:30
return const TextToSpeechState();
}
2025-09-20 23:58:18 +05:30
Future<bool> _ensureInitialized() {
final existing = _initializationFuture;
if (existing != null) {
return existing;
}
state = state.copyWith(
status: TtsPlaybackStatus.initializing,
clearErrorMessage: true,
);
final settings = ref.read(appSettingsProvider);
2025-09-20 23:58:18 +05:30
final future = _service
.initialize(
voice: settings.ttsVoice,
speechRate: settings.ttsSpeechRate,
pitch: settings.ttsPitch,
volume: settings.ttsVolume,
)
2025-09-20 23:58:18 +05:30
.then((available) {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return available;
}
state = state.copyWith(
initialized: true,
available: available,
status: TtsPlaybackStatus.idle,
);
return available;
})
.catchError((error, _) {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return false;
}
state = state.copyWith(
initialized: true,
available: false,
status: TtsPlaybackStatus.error,
errorMessage: error.toString(),
clearActiveMessageId: true,
);
return false;
});
_initializationFuture = future;
future.whenComplete(() {
_initializationFuture = null;
});
return future;
}
Future<void> toggleForMessage({
required String messageId,
required String text,
}) async {
if (text.trim().isEmpty) {
return;
}
2025-09-21 20:18:21 +05:30
final isCurrentlyActive =
state.activeMessageId == messageId &&
state.status != TtsPlaybackStatus.idle &&
state.status != TtsPlaybackStatus.error;
if (isCurrentlyActive) {
await stop();
return;
}
2025-09-20 23:58:18 +05:30
final available = await _ensureInitialized();
if (!available) {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
state = state.copyWith(
status: TtsPlaybackStatus.error,
errorMessage: 'Text-to-speech unavailable',
clearActiveMessageId: true,
);
return;
}
state = state.copyWith(
status: TtsPlaybackStatus.loading,
activeMessageId: messageId,
clearErrorMessage: true,
);
try {
// Convert markdown to clean text for TTS
final cleanText = MarkdownToText.convert(text);
if (cleanText.isEmpty) {
// No speakable content
if (!ref.mounted) {
return;
}
state = state.copyWith(
status: TtsPlaybackStatus.idle,
clearActiveMessageId: true,
);
return;
}
await _service.speak(cleanText);
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
if (state.status == TtsPlaybackStatus.loading) {
state = state.copyWith(status: TtsPlaybackStatus.speaking);
}
} catch (e) {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
state = state.copyWith(
status: TtsPlaybackStatus.error,
errorMessage: e.toString(),
clearActiveMessageId: true,
);
}
}
Future<void> pause() async {
if (!state.initialized || !state.available) {
return;
}
await _service.pause();
}
Future<void> stop() async {
await _service.stop();
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
state = state.copyWith(
status: TtsPlaybackStatus.idle,
clearActiveMessageId: true,
clearErrorMessage: true,
);
}
void _handleStart() {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
state = state.copyWith(status: TtsPlaybackStatus.speaking);
}
void _handleCompletion() {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
state = state.copyWith(
status: TtsPlaybackStatus.idle,
clearActiveMessageId: true,
);
}
void _handleCancellation() {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
state = state.copyWith(
status: TtsPlaybackStatus.idle,
clearActiveMessageId: true,
);
}
void _handlePause() {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
state = state.copyWith(status: TtsPlaybackStatus.paused);
}
void _handleContinue() {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
state = state.copyWith(status: TtsPlaybackStatus.speaking);
}
void _handleError(String message) {
2025-09-21 22:31:44 +05:30
if (!ref.mounted) {
2025-09-20 23:58:18 +05:30
return;
}
state = state.copyWith(
status: TtsPlaybackStatus.error,
errorMessage: message,
clearActiveMessageId: true,
);
}
}
final textToSpeechServiceProvider = Provider<TextToSpeechService>((ref) {
final service = TextToSpeechService();
ref.onDispose(() {
unawaited(service.dispose());
});
return service;
});
final textToSpeechControllerProvider =
2025-09-21 22:31:44 +05:30
NotifierProvider<TextToSpeechController, TextToSpeechState>(
TextToSpeechController.new,
);