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/knowledge_item.dart';
|
||||||
import 'package:fabled_app/data/models/note.dart';
|
import 'package:fabled_app/data/models/note.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -66,4 +67,35 @@ void main() {
|
|||||||
expect(item.status, isNull);
|
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