import 'dart:async'; import 'dart:js_interop'; import 'package:cyberhybridhub/config/api_config.dart'; import 'package:flutter/foundation.dart'; import 'spelling_tts_backend.dart'; SpellingTtsBackend createSpellingTtsBackend() => _WebDictionaryFallbackBackend(_WebSpeechBackend()); @JS('Audio') extension type JSAudio._(JSObject _) implements JSObject { external JSAudio([String? src]); external JSPromise play(); external void pause(); external void addEventListener(String type, JSFunction listener); external void removeEventListener(String type, JSFunction listener); } class _WebDictionaryFallbackBackend implements SpellingTtsBackend { _WebDictionaryFallbackBackend(this._fallback); final SpellingTtsBackend _fallback; JSAudio? _currentAudio; Completer? _audioPlayCompleter; @override Future speak(String text, {String? title}) async { await stop(); final String trimmed = text.trim(); if (trimmed.isEmpty) return; final String query = trimmed.toLowerCase(); final String encoded = Uri.encodeComponent(query); final String audioUrl = '$apiBaseUrl/v1/spelling/audio/$encoded.mp3'; try { final JSAudio audio = JSAudio(audioUrl); _currentAudio = audio; final Completer completer = Completer(); _audioPlayCompleter = completer; bool hasError = false; final JSFunction onEnded = (() { if (!completer.isCompleted) completer.complete(); }).toJS; final JSFunction onError = ((JSAny event) { hasError = true; if (!completer.isCompleted) completer.complete(); }).toJS; audio.addEventListener('ended', onEnded); audio.addEventListener('error', onError); try { await audio.play().toDart; await completer.future.timeout( const Duration(seconds: 10), onTimeout: () => debugPrint('Web audio playback timed out'), ); if (hasError) { throw Exception('HTMLAudio element error event fired'); } } catch (e) { debugPrint('Web HTMLAudio play failed, falling back to TTS: $e'); await _fallback.speak(trimmed, title: title); } finally { audio.removeEventListener('ended', onEnded); audio.removeEventListener('error', onError); } } catch (e, st) { debugPrint('Web audio playback setup error: $e\n$st'); await _fallback.speak(trimmed, title: title); } } @override Future stop() async { final Completer? completer = _audioPlayCompleter; _audioPlayCompleter = null; final JSAudio? audio = _currentAudio; _currentAudio = null; if (audio != null) { try { audio.pause(); } catch (e) { debugPrint('Failed to pause web audio: $e'); } } if (completer != null && !completer.isCompleted) { completer.complete(); } await _fallback.stop(); } } @JS('speechSynthesis') external SpeechSynthesis get _speechSynthesis; @JS() extension type SpeechSynthesis._(JSObject _) implements JSObject { external void speak(SpeechSynthesisUtterance utterance); external void cancel(); } @JS() extension type SpeechSynthesisUtterance._(JSObject _) implements JSObject { external SpeechSynthesisUtterance(); external String text; external String lang; external double rate; external double pitch; @JS('onend') external set onEnd(JSFunction? listener); @JS('onerror') external set onError(JSFunction? listener); } class _WebSpeechBackend implements SpellingTtsBackend { Completer? _pending; @override Future speak(String text, {String? title}) async { final String trimmed = text.trim(); if (trimmed.isEmpty) { return; } if (title != null && title.isNotEmpty) { debugPrint('Spelling TTS utterance title: $title'); } await stop(); final Completer done = Completer(); _pending = done; final SpeechSynthesisUtterance utterance = SpeechSynthesisUtterance(); utterance.text = trimmed; utterance.lang = 'en-US'; utterance.rate = 0.85; utterance.pitch = 1.0; utterance.onEnd = (() { if (!done.isCompleted) { done.complete(); } }).toJS; utterance.onError = ((JSAny event) { String details = 'unknown'; if (event.isA()) { try { final errEvent = event as JSObject; details = (errEvent as SpeechSynthesisErrorEvent).error; } catch (_) {} } debugPrint('Spelling web TTS error: $details ($event)'); if (!done.isCompleted) { done.complete(); } }).toJS; try { _speechSynthesis.speak(utterance); await done.future.timeout( const Duration(seconds: 30), onTimeout: () { debugPrint('Spelling web TTS timed out'); }, ); } catch (e, st) { debugPrint('Spelling web TTS failed: $e\n$st'); if (!done.isCompleted) { done.complete(); } } finally { if (identical(_pending, done)) { _pending = null; } } } @override Future stop() async { final Completer? pending = _pending; _pending = null; try { _speechSynthesis.cancel(); } catch (e, st) { debugPrint('Spelling web TTS stop failed: $e\n$st'); } pending?.complete(); } } @JS() extension type SpeechSynthesisErrorEvent._(JSObject _) implements JSObject { external String get error; }