import 'package:flutter_tts/flutter_tts.dart';
import 'package:just_audio/just_audio.dart';
import 'cloud_tts_service.dart';
enum GuideAudioSource {
prerecorded,
cloudWaveNet,
deviceTts,
}
class GuideAudioService {
GuideAudioService({
CloudTtsService? cloudTts,
FlutterTts? deviceTts,
AudioPlayer? player,
}) : _cloudTts = cloudTts ?? CloudTtsService(),
_deviceTts = deviceTts ?? FlutterTts(),
_player = player ?? AudioPlayer();
final CloudTtsService _cloudTts;
final FlutterTts _deviceTts;
final AudioPlayer _player;
GuideAudioSource? _activeSource;
bool _cloudEnabled = true;
GuideAudioSource? get activeSource => _activeSource;
Future<void> prewarm({
required String text,
required String languageCode,
String? prerecordedUrl,
String cloudVariant = 'normal',
}) async {
if (text.trim().isEmpty ||
prerecordedUrl?.trim().isNotEmpty == true ||
!_cloudEnabled) {
return;
}
try {
await _cloudTts.synthesize(
text: text,
languageCode: languageCode,
variant: cloudVariant,
);
} catch (_) {
// Prewarming is opportunistic; playback still has the normal fallbacks.
}
}
Future<void> speak({
required String text,
required String languageCode,
String? prerecordedUrl,
String cloudVariant = 'normal',
double? deviceSpeechRate,
}) async {
await stop();
final prerecorded = prerecordedUrl?.trim() ?? '';
if (prerecorded.isNotEmpty) {
await _player.setUrl(prerecorded);
await _player.play();
_activeSource = GuideAudioSource.prerecorded;
return;
}
if (_cloudEnabled) {
try {
final result = await _cloudTts.synthesize(
text: text,
languageCode: languageCode,
variant: cloudVariant,
);
await _player.setUrl(result.audioUrl);
await _player.play();
_activeSource = GuideAudioSource.cloudWaveNet;
return;
} catch (_) {
// Cloud disabled, offline, rate-limited or monthly cap reached.
// Fall back to the zero-cost device voice.
}
}
await _deviceTts.setLanguage(_deviceLocale(languageCode));
await _deviceTts.setSpeechRate(
deviceSpeechRate ?? (cloudVariant == 'slow' ? 0.34 : 0.45));
await _deviceTts.setPitch(1.0);
await _deviceTts.speak(text);
_activeSource = GuideAudioSource.deviceTts;
}
Future<void> stop() async {
try {
await _player.stop();
} catch (_) {}
try {
await _deviceTts.stop();
} catch (_) {}
_activeSource = null;
}
void setCloudEnabled(bool enabled) {
_cloudEnabled = enabled;
}
String _deviceLocale(String code) {
final lang = code.toLowerCase().split('-').first;
switch (lang) {
case 'de':
return 'de-DE';
case 'en':
return 'en-GB';
case 'nl':
return 'nl-NL';
case 'fr':
return 'fr-FR';
case 'pl':
return 'pl-PL';
case 'it':
return 'it-IT';
case 'es':
return 'es-ES';
case 'cs':
return 'cs-CZ';
default:
return 'de-DE';
}
}
Future<void> dispose() async {
await stop();
_cloudTts.dispose();
await _player.dispose();
}
}

