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

107 changes: 78 additions & 29 deletions lib/features/transcription/data/deepgram_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ class DeepgramService {

DeepgramService({String? apiKey}) : _apiKey = apiKey?.trim();

// =============================
// Resolve API Key
// =============================
String _resolveApiKey() {
final configuredKey = _apiKey;
if (configuredKey != null && configuredKey.isNotEmpty) {
Expand All @@ -22,62 +25,108 @@ class DeepgramService {
}
}

// =============================
// Retry Logic (Production Grade)
// =============================
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;
}

// Retry only for server errors
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 after multiple retries');
}
} catch (e) {
if (attempt == retries - 1) {
rethrow;
}
}

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

throw Exception('Failed after $retries retries');
}

// =============================
// Main Transcription Method
// =============================
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');

final file = File(recordingPath);

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

final bytes = await file.readAsBytes();

http.Response response;
final uri = Uri.parse(
'https://api.deepgram.com/v1/listen?model=nova-2&punctuate=true&smart_format=true',
);

try {
response = await http.post(
uri,
final response = await _retryPost(
uri: uri,
headers: {
'Authorization': 'Token $apiKey',
'Content-Type': 'audio/m4a',
'Content-Type': 'application/octet-stream', // 🔥 improved
},
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);
return _parseTranscript(response.body);
} catch (e) {
throw Exception('Transcription error: $e');
}
}

if (decodedResponse is! Map<String, dynamic>) {
throw Exception('Deepgram returned unexpected response format');
}
// =============================
// Response Parser (Robust)
// =============================
String _parseTranscript(String responseBody) {
try {
final decoded = json.decode(responseBody);

final results = decodedResponse['results'];
if (results is! Map<String, dynamic>) {
if (decoded 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';
}
final transcript = decoded['results']?['channels']?[0]?['alternatives']?[0]?['transcript'];

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';
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 (_) {
return 'Failed to parse transcription';
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
}
}
}
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');
}
Comment thread
Shweta-281 marked this conversation as resolved.
}

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');
}
}
27 changes: 27 additions & 0 deletions lib/features/transcription/domain/medical_insights.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
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(
summary: json['summary'] ?? '',
symptoms: List<String>.from(json['symptoms'] ?? []),
medicines: List<String>.from(json['medicines'] ?? []),
);
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}

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,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import 'package:flutter/foundation.dart';
import 'package:path_provider/path_provider.dart';
import 'package:record/record.dart';
import 'package:permission_handler/permission_handler.dart';

import '../data/deepgram_service.dart';
import '../data/gemini_service.dart';
import '../domain/transcription_model.dart';
import '../domain/medical_insights.dart';

enum TranscriptionState { idle, recording, transcribing, processing, done, error }

Expand All @@ -21,25 +23,27 @@ class TranscriptionController extends ChangeNotifier {
String? errorMessage;
String _recordingPath = '';

// Waveform — kept here since it's driven by recording state
// Waveform
final List<double> waveformValues = List.filled(40, 0.0);
Timer? _waveformTimer;

bool get isRecording => state == TranscriptionState.recording;

bool get isProcessing =>
state == TranscriptionState.transcribing ||
state == TranscriptionState.processing;

String get transcription => data.rawTranscript;
String get summary => data.summary;
String get prescription => data.prescription;

// ✅ NEW STRUCTURED GETTERS
String get summary => data.insights?.summary ?? '';
List<String> get symptoms => data.insights?.symptoms ?? [];
List<String> get medicines => data.insights?.medicines ?? [];
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

Future<bool> requestPermissions() async {
final status = await Permission.microphone.request();

if (status.isGranted) {
return true;
}
if (status.isGranted) return true;

if (status.isPermanentlyDenied) {
_setError('Microphone permission permanently denied. Please enable it in settings.');
Expand All @@ -62,9 +66,7 @@ class TranscriptionController extends ChangeNotifier {
try {
if (!await _audioRecorder.hasPermission()) {
final granted = await requestPermissions();
if (!granted) {
return;
}
if (!granted) return;
}

final directory = await getTemporaryDirectory();
Expand All @@ -80,7 +82,6 @@ class TranscriptionController extends ChangeNotifier {
path: _recordingPath,
);

// Reset previous data
data = const TranscriptionModel();
state = TranscriptionState.recording;
_startWaveformAnimation();
Expand Down Expand Up @@ -127,16 +128,17 @@ class TranscriptionController extends ChangeNotifier {
}
}

// ✅ UPDATED: Structured AI Processing
Future<void> _processWithGemini(String transcript) async {
try {
final summary = await _geminiService.generateSummary(transcript);
final prescription = await _geminiService.generatePrescription(transcript);
final MedicalInsights insights =
await _geminiService.generateInsights(transcript);

data = data.copyWith(summary: summary, prescription: prescription);
data = data.copyWith(insights: insights);
state = TranscriptionState.done;
notifyListeners();

developer.log('Gemini processing complete');
developer.log('Gemini structured insights generated');
} catch (e) {
_setError('Gemini error: $e');
}
Expand Down
Loading
Loading