Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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.

105 changes: 62 additions & 43 deletions lib/features/transcription/data/deepgram_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,70 +14,89 @@ class DeepgramService {
if (configuredKey != null && configuredKey.isNotEmpty) {
return configuredKey;
}

try {
return (dotenv.env['DEEPGRAM_API_KEY'] ?? '').trim();
} catch (_) {
return '';
}
}

Future<String> transcribe(String recordingPath) async {
final apiKey = _resolveApiKey();
if (apiKey.isEmpty) {
throw Exception('Missing DEEPGRAM_API_KEY in environment');
Future<http.Response> _retryPost({
required Uri uri,
required Map<String, String> headers,
required List<int> body,
int retries = 3,
}) async {
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;

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

final uri = Uri.parse('https://api.deepgram.com/v1/listen?model=nova-2');
Future<String> transcribe(String recordingPath) async {
final apiKey = _resolveApiKey();
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');
}
return _parseTranscript(response.body);
}

final results = decodedResponse['results'];
if (results is! Map<String, dynamic>) {
return 'No speech detected';
}
String _parseTranscript(String responseBody) {
try {
final decoded = json.decode(responseBody);

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

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';
final results = decoded['results'];
final channels = results?['channels'];

if (channels is List && channels.isNotEmpty) {
final alternatives = channels[0]['alternatives'];
if (alternatives is List && alternatives.isNotEmpty) {
final transcript = 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');
}
}
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,
};
}
}
14 changes: 6 additions & 8 deletions lib/features/transcription/domain/transcription_model.dart
Original file line number Diff line number Diff line change
@@ -1,23 +1,21 @@
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,
);
}
}
Loading
Loading