feat(voice): add VoiceApi and VoiceStatus model
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
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 only when voice is enabled AND both STT and TTS are ready.
|
||||
bool get fullyAvailable => enabled && stt && tts;
|
||||
|
||||
factory VoiceStatus.fromJson(Map<String, dynamic> 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<VoiceStatus> checkStatus() async {
|
||||
try {
|
||||
final response = await _dio.get('/api/voice/status');
|
||||
return VoiceStatus.fromJson(response.data as Map<String, dynamic>);
|
||||
} on DioException catch (e) {
|
||||
throw dioToApp(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// POST WebM/Opus audio bytes and return the transcript string.
|
||||
/// Returns empty string on empty or error response.
|
||||
Future<String> transcribe(Uint8List audioBytes) async {
|
||||
try {
|
||||
final formData = FormData.fromMap({
|
||||
'audio': MultipartFile.fromBytes(
|
||||
audioBytes,
|
||||
filename: 'audio.webm',
|
||||
contentType: DioMediaType('audio', 'webm'),
|
||||
),
|
||||
});
|
||||
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<String, dynamic>;
|
||||
return (data['transcript'] as String? ?? '').trim();
|
||||
} on DioException catch (e) {
|
||||
throw dioToApp(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// POST text and return raw WAV bytes.
|
||||
Future<Uint8List> 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<int>);
|
||||
} on DioException catch (e) {
|
||||
throw dioToApp(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:fabled_app/data/api/voice_api.dart';
|
||||
import 'package:fabled_app/data/models/knowledge_item.dart';
|
||||
import 'package:fabled_app/data/models/note.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
@@ -66,4 +67,35 @@ void main() {
|
||||
expect(item.status, isNull);
|
||||
});
|
||||
});
|
||||
|
||||
group('VoiceStatus.fromJson', () {
|
||||
test('parses enabled with stt and tts available', () {
|
||||
final status = VoiceStatus.fromJson({
|
||||
'enabled': true,
|
||||
'stt': true,
|
||||
'tts': true,
|
||||
});
|
||||
expect(status.enabled, isTrue);
|
||||
expect(status.stt, isTrue);
|
||||
expect(status.tts, isTrue);
|
||||
});
|
||||
|
||||
test('parses disabled state', () {
|
||||
final status = VoiceStatus.fromJson({
|
||||
'enabled': false,
|
||||
'stt': false,
|
||||
'tts': false,
|
||||
});
|
||||
expect(status.enabled, isFalse);
|
||||
});
|
||||
|
||||
test('fullyAvailable is false when enabled but stt is false', () {
|
||||
final status = VoiceStatus.fromJson({
|
||||
'enabled': true,
|
||||
'stt': false,
|
||||
'tts': true,
|
||||
});
|
||||
expect(status.fullyAvailable, isFalse);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user