import 'dart:typed_data'; import 'package:dio/dio.dart'; import 'api_client.dart'; class VoiceStatus { final bool enabled; final bool stt; final bool tts; const VoiceStatus({ required this.enabled, required this.stt, required this.tts, }); /// True when voice is enabled and at least STT is ready. /// TTS is optional — voice mode works without it (STT-only). bool get fullyAvailable => enabled && stt; factory VoiceStatus.fromJson(Map json) => VoiceStatus( enabled: json['enabled'] as bool? ?? false, stt: json['stt'] as bool? ?? false, tts: json['tts'] as bool? ?? false, ); } class VoiceApi { final Dio _dio; const VoiceApi(this._dio); /// Check whether voice features are available on this server. Future checkStatus() async { try { final response = await _dio.get('/api/voice/status'); return VoiceStatus.fromJson(response.data as Map); } on DioException catch (e) { throw dioToApp(e); } } /// POST audio bytes (WAV) and return the transcript string. /// [context] is optional recent conversation text passed as initial_prompt /// to Whisper, reducing mishearings of domain-specific words. /// Returns empty string on empty or error response. Future transcribe(Uint8List audioBytes, {String? context}) async { try { final fields = { 'audio': MultipartFile.fromBytes( audioBytes, filename: 'audio.wav', contentType: DioMediaType('audio', 'wav'), ), if (context != null && context.isNotEmpty) 'context': context, }; final formData = FormData.fromMap(fields); final response = await _dio.post( '/api/voice/transcribe', data: formData, options: Options( receiveTimeout: const Duration(seconds: 60), contentType: 'multipart/form-data', ), ); final data = response.data as Map; return (data['transcript'] as String? ?? '').trim(); } on DioException catch (e) { throw dioToApp(e); } } /// POST text and return raw WAV bytes. Future synthesise(String text) async { try { final response = await _dio.post( '/api/voice/synthesise', data: {'text': text}, options: Options(responseType: ResponseType.bytes), ); return Uint8List.fromList(response.data as List); } on DioException catch (e) { throw dioToApp(e); } } }