cyberhybridhub/lib/services/spelling_tts/spelling_tts_flutter.dart
nathanAndrinoid a521a74214
Some checks failed
Admin portal tests / test (push) Has been cancelled
balance controled working spelling and market questions.
2026-06-17 04:20:45 +00:00

136 lines
3.8 KiB
Dart

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<void>? _completer;
StreamSubscription<void>? _onCompleteSubscription;
Future<Uint8List?> _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<void> 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<void> completer = Completer<void>();
_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<void> stop() async {
final Completer<void>? 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<void> _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<void> 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<void> stop() async {
try {
await _tts.stop();
} catch (_) {}
}
}