import 'dart:async'; import 'dart:typed_data'; import 'package:audioplayers/audioplayers.dart'; import 'package:cyberhybridhub/config/api_config.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_tts/flutter_tts.dart'; import 'package:http/http.dart' as http; import 'spelling_tts_backend.dart'; SpellingTtsBackend createSpellingTtsBackend() => _FlutterDictionaryFallbackBackend(_FlutterTtsBackend()); class _FlutterDictionaryFallbackBackend implements SpellingTtsBackend { _FlutterDictionaryFallbackBackend(this._fallback); final SpellingTtsBackend _fallback; final AudioPlayer _player = AudioPlayer(); Completer? _completer; StreamSubscription? _onCompleteSubscription; Future _fetchAudioBytes(String word) async { final String query = word.trim().toLowerCase(); if (query.isEmpty) return null; try { final String encoded = Uri.encodeComponent(query); final String url = '$apiBaseUrl/v1/spelling/audio/$encoded.mp3'; final response = await http.get(Uri.parse(url)).timeout(const Duration(seconds: 8)); if (response.statusCode == 200) { return response.bodyBytes; } } catch (e) { debugPrint('Client audio fetch failed: $e'); } return null; } @override Future speak(String text, {String? title}) async { await stop(); final String trimmed = text.trim(); if (trimmed.isEmpty) return; final Uint8List? bytes = await _fetchAudioBytes(trimmed); if (bytes != null) { try { final Completer completer = Completer(); _completer = completer; _onCompleteSubscription = _player.onPlayerComplete.listen( (_) { if (!completer.isCompleted) completer.complete(); }, onError: (Object err) { if (!completer.isCompleted) completer.completeError(err); }, onDone: () { if (!completer.isCompleted) completer.complete(); }, ); await _player.play(BytesSource(bytes)); await completer.future.timeout( const Duration(seconds: 10), onTimeout: () => debugPrint('Native audio playback timed out'), ); } catch (e) { debugPrint('Native audio play failed, falling back to TTS: $e'); await _fallback.speak(trimmed, title: title); } finally { await _onCompleteSubscription?.cancel(); _onCompleteSubscription = null; _completer = null; } } else { await _fallback.speak(trimmed, title: title); } } @override Future stop() async { final Completer? completer = _completer; _completer = null; await _onCompleteSubscription?.cancel(); _onCompleteSubscription = null; try { await _player.stop(); } catch (e) { debugPrint('Failed to stop audio player: $e'); } if (completer != null && !completer.isCompleted) { completer.complete(); } await _fallback.stop(); } } class _FlutterTtsBackend implements SpellingTtsBackend { final FlutterTts _tts = FlutterTts(); bool _configured = false; Future _ensureConfigured() async { if (_configured) { return; } await _tts.setLanguage('en-US'); await _tts.setSpeechRate(0.42); await _tts.setPitch(1.0); await _tts.awaitSpeakCompletion(true); _configured = true; } @override Future speak(String text, {String? title}) async { final String trimmed = text.trim(); if (trimmed.isEmpty) { return; } await _ensureConfigured(); await _tts.stop(); if (title != null && title.isNotEmpty) { debugPrint('Spelling TTS utterance title: $title'); } await _tts.speak(trimmed); } @override Future stop() async { try { await _tts.stop(); } catch (_) {} } }