Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .env.example

This file was deleted.

2 changes: 1 addition & 1 deletion android/gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip
2 changes: 1 addition & 1 deletion android/settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ pluginManagement {

plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.2.1" apply false
id "com.android.application" version "8.6.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}

Expand Down
119 changes: 65 additions & 54 deletions lib/features/transcription/data/deepgram_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,73 +11,84 @@ class DeepgramService {

String _resolveApiKey() {
final configuredKey = _apiKey;
if (configuredKey != null && configuredKey.isNotEmpty) {
return configuredKey;
}
if (configuredKey != null && configuredKey.isNotEmpty) return configuredKey;
return (dotenv.env['DEEPGRAM_API_KEY'] ?? '').trim();
}

try {
return (dotenv.env['DEEPGRAM_API_KEY'] ?? '').trim();
} catch (_) {
return '';
Future<http.Response> _retryPost({
required Uri uri,
required Map<String, String> headers,
required List<int> body,
int retries = 3,
}) async {
http.Response? lastResponse;

for (int attempt = 0; attempt < retries; attempt++) {
try {
final response = await http
.post(uri, headers: headers, body: body)
.timeout(const Duration(seconds: 30));

if (response.statusCode == 200) return response;

lastResponse = response;

if (response.statusCode >= 500) {
await Future.delayed(Duration(seconds: 2 * (attempt + 1)));
continue;
} else {
throw Exception('Deepgram error (${response.statusCode}): ${response.body}');
}
} on TimeoutException {
if (attempt == retries - 1) throw Exception('Deepgram request timed out');
} on Exception {
rethrow;
} catch (e) {
if (attempt == retries - 1) rethrow;
}

await Future.delayed(Duration(seconds: 2 * (attempt + 1)));
}

throw Exception(
'Deepgram failed after $retries attempts. '
'Last status: ${lastResponse?.statusCode ?? "Unknown"}'
);
}

Future<String> transcribe(String recordingPath) async {
final apiKey = _resolveApiKey();
if (apiKey.isEmpty) {
throw Exception('Missing DEEPGRAM_API_KEY in environment');
}

final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2');
if (apiKey.isEmpty) throw Exception('Missing DEEPGRAM_API_KEY');

final file = File(recordingPath);
if (!await file.exists()) {
throw Exception('Recording file not found');
}
if (!await file.exists()) throw Exception('Recording file not found');

final bytes = await file.readAsBytes();
final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true&smart_format=true');

http.Response response;
try {
response = await http.post(
uri,
headers: {
'Authorization': 'Token $apiKey',
'Content-Type': 'audio/m4a',
},
body: bytes,
).timeout(const Duration(seconds: 30));
} on TimeoutException {
throw Exception('Deepgram request timed out after 30 seconds');
}

if (response.statusCode == 200) {
final decodedResponse = json.decode(response.body);
final response = await _retryPost(
uri: uri,
headers: {
'Authorization': 'Token $apiKey',
'Content-Type': 'application/octet-stream',
},
body: bytes,
);

if (decodedResponse is! Map<String, dynamic>) {
throw Exception('Deepgram returned unexpected response format');
}

final results = decodedResponse['results'];
if (results is! Map<String, dynamic>) {
return 'No speech detected';
}

final channels = results['channels'];
if (channels is! List || channels.isEmpty || channels.first is! Map<String, dynamic>) {
return 'No speech detected';
}
return _parseTranscript(response.body);
}

final alternatives = (channels.first as Map<String, dynamic>)['alternatives'];
if (alternatives is! List || alternatives.isEmpty || alternatives.first is! Map<String, dynamic>) {
return 'No speech detected';
String _parseTranscript(String responseBody) {
try {
final decoded = json.decode(responseBody);
final transcript = decoded['results']?['channels']?[0]?['alternatives']?[0]?['transcript'];

if (transcript is String && transcript.trim().isNotEmpty) {
return transcript.trim();
}

final transcript = (alternatives.first as Map<String, dynamic>)['transcript'];
final result = transcript is String ? transcript.trim() : '';
return result.isNotEmpty ? result : 'No speech detected';
} else {
throw Exception('Deepgram failed: ${response.statusCode}');
return 'No speech detected';
} catch (e) {
throw Exception('Failed to parse Deepgram response: $e');
}
}
}
}
45 changes: 36 additions & 9 deletions lib/features/transcription/data/gemini_service.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,45 @@
import 'dart:convert';
import '../domain/medical_insights.dart';
import 'package:doc_pilot_new_app_gradel_fix/services/chatbot_service.dart';

class GeminiService {
final ChatbotService _chatbotService = ChatbotService();

Future<String> generateSummary(String transcription) async {
return await _chatbotService.getGeminiResponse(
"Generate a summary of the conversation based on this transcription: $transcription",
);
Future<MedicalInsights> generateInsights(String transcription) async {
final prompt = """
Extract structured medical information from the conversation.

Return ONLY valid JSON in this format:
{
"summary": "short summary",
"symptoms": ["symptom1", "symptom2"],
"medicines": ["medicine1", "medicine2"]
}

Conversation:
$transcription
""";

final response = await _chatbotService.getGeminiResponse(prompt);

try {
final cleaned = _extractJson(response);
final jsonData = json.decode(cleaned);
return MedicalInsights.fromJson(jsonData);
} catch (e) {
throw Exception('Failed to parse Gemini JSON response');
}
}

Future<String> generatePrescription(String transcription) async {
await Future.delayed(const Duration(seconds: 3));
return await _chatbotService.getGeminiResponse(
"Generate a prescription based on the conversation in this transcription: $transcription",
);
// Handles messy AI responses
String _extractJson(String response) {
final start = response.indexOf('{');
final end = response.lastIndexOf('}');

if (start != -1 && end != -1) {
return response.substring(start, end + 1);
}

throw Exception('Invalid JSON format from AI');
}
}
29 changes: 29 additions & 0 deletions lib/features/transcription/data/local_storage_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'dart:convert';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import '../domain/transcription_history_model.dart';

class LocalStorageService {
static const String _key = "transcription_history_secure";
final _storage = const FlutterSecureStorage();

// Internal helper to get RAW list without reversing (prevents corruption)
Future<List<TranscriptionHistoryModel>> _getRawList() async {
try {
final jsonStr = await _storage.read(key: _key);
if (jsonStr == null) return [];
final List<dynamic> decoded = jsonDecode(jsonStr);
return decoded.map((item) => TranscriptionHistoryModel.fromJson(item)).toList();
} catch (_) { return []; }
}

Future<void> save(TranscriptionHistoryModel item) async {
final list = await _getRawList(); // Get chronological order
list.add(item);
await _storage.write(key: _key, value: jsonEncode(list.map((e) => e.toJson()).toList()));
}

Future<List<TranscriptionHistoryModel>> getAll() async {
final list = await _getRawList();
return list.reversed.toList(); // Reverse ONLY for UI display
}
}
39 changes: 39 additions & 0 deletions lib/features/transcription/domain/medical_insights.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
class MedicalInsights {
final String summary;
final List<String> symptoms;
final List<String> medicines;

MedicalInsights({
required this.summary,
required this.symptoms,
required this.medicines,
});

factory MedicalInsights.fromJson(Map<String, dynamic> json) {
return MedicalInsights(
// Coerce summary to String regardless of what the AI sends
summary: json['summary']?.toString() ?? '',

// Safely parse lists to avoid 'type is not a subtype' errors
symptoms: _parseList(json['symptoms']),
medicines: _parseList(json['medicines']),
);
}

/// Helper to filter nulls and force elements to strings
static List<String> _parseList(dynamic jsonValue) {
if (jsonValue is! List) return [];
return jsonValue
.where((item) => item != null)
.map((item) => item.toString())
.toList();
}

Map<String, dynamic> toJson() {
return {
'summary': summary,
'symptoms': symptoms,
'medicines': medicines,
};
}
}
29 changes: 29 additions & 0 deletions lib/features/transcription/domain/transcription_history_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import 'medical_insights.dart';

class TranscriptionModel {
final String rawTranscript;
final String summary;
final String prescription;
final MedicalInsights? insights;

const TranscriptionModel({
this.rawTranscript = '',
this.summary = '',
this.prescription = '',
this.insights,
});

TranscriptionModel copyWith({
String? rawTranscript,
String? summary,
String? prescription,
MedicalInsights? insights,
}) {
return TranscriptionModel(
rawTranscript: rawTranscript ?? this.rawTranscript,
summary: summary ?? this.summary,
prescription: prescription ?? this.prescription,
insights: insights ?? this.insights,
);
}
}
51 changes: 34 additions & 17 deletions lib/features/transcription/domain/transcription_model.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,40 @@
class TranscriptionModel {
final String rawTranscript;
import 'medical_insights.dart';

class TranscriptionHistoryModel {
final String transcript;
final String summary;
final String prescription;
final List<String> symptoms;
final List<String> medicines;
final DateTime createdAt;

const TranscriptionModel({
this.rawTranscript = '',
this.summary = '',
this.prescription = '',
const TranscriptionHistoryModel({
required this.transcript,
required this.summary,
required this.symptoms,
required this.medicines,
required this.createdAt,
});

TranscriptionModel copyWith({
String? rawTranscript,
String? summary,
String? prescription,
}) {
return TranscriptionModel(
rawTranscript: rawTranscript ?? this.rawTranscript,
summary: summary ?? this.summary,
prescription: prescription ?? this.prescription,
factory TranscriptionHistoryModel.fromJson(Map<String, dynamic> json) {
return TranscriptionHistoryModel(
transcript: json['transcript'] ?? '',
summary: json['summary'] ?? '',
// FIXED: Use null-coalescing and casting to prevent crashes on missing lists
symptoms: (json['symptoms'] as List?)?.map((e) => e.toString()).toList() ?? [],
medicines: (json['medicines'] as List?)?.map((e) => e.toString()).toList() ?? [],
createdAt: json['createdAt'] != null
? DateTime.parse(json['createdAt'])
: DateTime.now(),
);
}
}

Map<String, dynamic> toJson() {
return {
'transcript': transcript,
'summary': summary,
'symptoms': symptoms,
'medicines': medicines,
'createdAt': createdAt.toIso8601String(),
};
}
}
Loading
Loading