1+ import 'dart:developer' as developer;
2+ import 'dart:math' ;
3+ import 'dart:async' ;
4+ import 'package:flutter/foundation.dart' ;
5+ import 'package:path_provider/path_provider.dart' ;
6+ import 'package:record/record.dart' ;
7+ import 'package:permission_handler/permission_handler.dart' ;
8+ import '../data/deepgram_service.dart' ;
9+ import '../data/gemini_service.dart' ;
10+ import '../domain/transcription_model.dart' ;
11+
12+ enum TranscriptionState { idle, recording, transcribing, processing, done, error }
13+
14+ class TranscriptionController extends ChangeNotifier {
15+ final _audioRecorder = AudioRecorder ();
16+ final _deepgramService = DeepgramService ();
17+ final _geminiService = GeminiService ();
18+
19+ TranscriptionState state = TranscriptionState .idle;
20+ TranscriptionModel data = const TranscriptionModel ();
21+ String ? errorMessage;
22+ String _recordingPath = '' ;
23+
24+ // Waveform — kept here since it's driven by recording state
25+ final List <double > waveformValues = List .filled (40 , 0.0 );
26+ Timer ? _waveformTimer;
27+
28+ bool get isRecording => state == TranscriptionState .recording;
29+ bool get isProcessing =>
30+ state == TranscriptionState .transcribing ||
31+ state == TranscriptionState .processing;
32+
33+ String get transcription => data.rawTranscript;
34+ String get summary => data.summary;
35+ String get prescription => data.prescription;
36+
37+ Future <bool > requestPermissions () async {
38+ final status = await Permission .microphone.request ();
39+
40+ if (status.isGranted) {
41+ return true ;
42+ }
43+
44+ if (status.isPermanentlyDenied) {
45+ _setError ('Microphone permission permanently denied. Please enable it in settings.' );
46+ return false ;
47+ }
48+
49+ _setError ('Microphone permission denied' );
50+ return false ;
51+ }
52+
53+ Future <void > toggleRecording () async {
54+ if (isRecording) {
55+ await _stopRecording ();
56+ } else {
57+ await _startRecording ();
58+ }
59+ }
60+
61+ Future <void > _startRecording () async {
62+ try {
63+ if (! await _audioRecorder.hasPermission ()) {
64+ final granted = await requestPermissions ();
65+ if (! granted) {
66+ return ;
67+ }
68+ }
69+
70+ final directory = await getTemporaryDirectory ();
71+ _recordingPath =
72+ '${directory .path }/recording_${DateTime .now ().millisecondsSinceEpoch }.m4a' ;
73+
74+ await _audioRecorder.start (
75+ RecordConfig (
76+ encoder: AudioEncoder .aacLc,
77+ bitRate: 128000 ,
78+ sampleRate: 44100 ,
79+ ),
80+ path: _recordingPath,
81+ );
82+
83+ // Reset previous data
84+ data = const TranscriptionModel ();
85+ state = TranscriptionState .recording;
86+ _startWaveformAnimation ();
87+ notifyListeners ();
88+
89+ developer.log ('Started recording to: $_recordingPath ' );
90+ } catch (e) {
91+ _setError ('Error starting recording: $e ' );
92+ }
93+ }
94+
95+ Future <void > _stopRecording () async {
96+ try {
97+ _waveformTimer? .cancel ();
98+ _resetWaveform ();
99+
100+ await _audioRecorder.stop ();
101+ state = TranscriptionState .transcribing;
102+ notifyListeners ();
103+
104+ developer.log ('Recording stopped, transcribing...' );
105+ await _transcribe ();
106+ } catch (e) {
107+ _setError ('Error stopping recording: $e ' );
108+ }
109+ }
110+
111+ Future <void > _transcribe () async {
112+ try {
113+ final transcript = await _deepgramService.transcribe (_recordingPath);
114+
115+ data = data.copyWith (rawTranscript: transcript);
116+ state = TranscriptionState .processing;
117+ notifyListeners ();
118+
119+ if (transcript.isNotEmpty && transcript != 'No speech detected' ) {
120+ await _processWithGemini (transcript);
121+ } else {
122+ state = TranscriptionState .done;
123+ notifyListeners ();
124+ }
125+ } catch (e) {
126+ _setError ('Transcription error: $e ' );
127+ }
128+ }
129+
130+ Future <void > _processWithGemini (String transcript) async {
131+ try {
132+ final summary = await _geminiService.generateSummary (transcript);
133+ final prescription = await _geminiService.generatePrescription (transcript);
134+
135+ data = data.copyWith (summary: summary, prescription: prescription);
136+ state = TranscriptionState .done;
137+ notifyListeners ();
138+
139+ developer.log ('Gemini processing complete' );
140+ } catch (e) {
141+ _setError ('Gemini error: $e ' );
142+ }
143+ }
144+
145+ void _startWaveformAnimation () {
146+ _waveformTimer = Timer .periodic (const Duration (milliseconds: 100 ), (_) {
147+ for (int i = 0 ; i < waveformValues.length; i++ ) {
148+ waveformValues[i] = Random ().nextDouble ();
149+ }
150+ notifyListeners ();
151+ });
152+ }
153+
154+ void _resetWaveform () {
155+ for (int i = 0 ; i < waveformValues.length; i++ ) {
156+ waveformValues[i] = 0.0 ;
157+ }
158+ }
159+
160+ void _setError (String message) {
161+ errorMessage = message;
162+ state = TranscriptionState .error;
163+ notifyListeners ();
164+ developer.log (message);
165+ }
166+
167+ @override
168+ void dispose () {
169+ _waveformTimer? .cancel ();
170+ _audioRecorder.dispose ();
171+ super .dispose ();
172+ }
173+ }
0 commit comments