Skip to content
This repository was archived by the owner on Dec 9, 2025. It is now read-only.
Merged
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
15 changes: 15 additions & 0 deletions lib/screens/home_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:parachute/models/recording.dart';
import 'package:parachute/services/storage_service.dart';
import 'package:parachute/screens/recording_screen.dart';
import 'package:parachute/screens/recording_detail_screen.dart';
import 'package:parachute/screens/settings_screen.dart';
import 'package:parachute/widgets/recording_tile.dart';
import 'package:parachute/utils/sample_data.dart';

Expand Down Expand Up @@ -106,6 +107,20 @@ class _HomeScreenState extends State<HomeScreen> with WidgetsBindingObserver {
title: const Text('Parachute'),
centerTitle: true,
elevation: 0,
actions: [
IconButton(
icon: const Icon(Icons.settings),
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
);
},
tooltip: 'Settings',
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
Expand Down
120 changes: 115 additions & 5 deletions lib/screens/post_recording_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import 'package:flutter/material.dart';
import 'package:parachute/models/recording.dart';
import 'package:parachute/services/audio_service.dart';
import 'package:parachute/services/storage_service.dart';
import 'package:parachute/services/whisper_service.dart';
import 'package:parachute/screens/settings_screen.dart';

class PostRecordingScreen extends StatefulWidget {
final String recordingPath;
Expand All @@ -24,6 +26,7 @@ class _PostRecordingScreenState extends State<PostRecordingScreen> {
final TextEditingController _transcriptController = TextEditingController();
final StorageService _storageService = StorageService();
final AudioService _audioService = AudioService();
final WhisperService _whisperService = WhisperService();

final List<String> _predefinedTags = [
'Project A',
Expand All @@ -37,6 +40,7 @@ class _PostRecordingScreenState extends State<PostRecordingScreen> {
final Set<String> _selectedTags = {};
bool _isPlaying = false;
bool _isSaving = false;
bool _isTranscribing = false;

@override
void initState() {
Expand Down Expand Up @@ -69,6 +73,90 @@ class _PostRecordingScreenState extends State<PostRecordingScreen> {
}
}

Future<void> _transcribeRecording() async {
if (_isTranscribing) return;

// Check if API key is configured
final isConfigured = await _whisperService.isConfigured();
if (!isConfigured) {
if (!mounted) return;

// Show dialog to navigate to settings
final goToSettings = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
title: const Text('API Key Required'),
content: const Text(
'To use transcription, you need to configure your OpenAI API key in Settings.\n\n'
'Would you like to go to Settings now?',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.pop(context, true),
child: const Text('Go to Settings'),
),
],
),
);

if (goToSettings == true && mounted) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsScreen(),
),
);
}
return;
}

setState(() => _isTranscribing = true);

try {
final transcript = await _whisperService.transcribeAudio(
widget.recordingPath,
);

if (mounted) {
_transcriptController.text = transcript;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Transcription completed!'),
duration: Duration(seconds: 2),
),
);
}
} on WhisperException catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Transcription failed: ${e.message}'),
duration: const Duration(seconds: 4),
backgroundColor: Colors.red,
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Unexpected error: $e'),
duration: const Duration(seconds: 4),
backgroundColor: Colors.red,
),
);
}
} finally {
if (mounted) {
setState(() => _isTranscribing = false);
}
}
}

Future<void> _saveRecording() async {
if (_isSaving) return;

Expand Down Expand Up @@ -188,7 +276,7 @@ class _PostRecordingScreenState extends State<PostRecordingScreen> {
Text(
'${widget.duration.inMinutes}:${(widget.duration.inSeconds % 60).toString().padLeft(2, '0')}',
style: TextStyle(
color: Colors.grey.withValues(alpha: 0.7),
color: Colors.grey.withOpacity(0.7),
),
),
],
Expand Down Expand Up @@ -232,11 +320,33 @@ class _PostRecordingScreenState extends State<PostRecordingScreen> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Transcript',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Transcript',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
),
),
ElevatedButton.icon(
onPressed: _isTranscribing ? null : _transcribeRecording,
icon: _isTranscribing
? const SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(
strokeWidth: 2,
),
)
: const Icon(Icons.auto_awesome, size: 18),
label: Text(_isTranscribing ? 'Transcribing...' : 'Transcribe'),
style: ElevatedButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.primary,
foregroundColor: Colors.white,
),
),
],
),
const SizedBox(height: 8),
SizedBox(
Expand Down
137 changes: 14 additions & 123 deletions lib/screens/recording_screen.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:parachute/services/audio_service.dart';
import 'package:parachute/services/transcription_service.dart';
import 'package:parachute/screens/post_recording_screen.dart';
import 'package:parachute/widgets/recording_visualizer.dart';

Expand All @@ -14,16 +13,13 @@ class RecordingScreen extends StatefulWidget {

class _RecordingScreenState extends State<RecordingScreen> {
final AudioService _audioService = AudioService();
final TranscriptionService _transcriptionService = TranscriptionService();
RecordingState _recordingState = RecordingState.stopped;
Duration _recordingDuration = Duration.zero;
Duration _pausedDuration = Duration.zero;
DateTime? _startTime;
DateTime? _pauseStartTime;
Timer? _timer;
String? _recordingPath;
String _currentTranscription = '';
bool _transcriptionAvailable = false;

@override
void initState() {
Expand All @@ -35,7 +31,6 @@ class _RecordingScreenState extends State<RecordingScreen> {
void dispose() {
_timer?.cancel();
_audioService.dispose();
_transcriptionService.dispose();
super.dispose();
}

Expand Down Expand Up @@ -99,34 +94,19 @@ class _RecordingScreenState extends State<RecordingScreen> {
}

Future<void> _initializeTranscription() async {
try {
print('Attempting to initialize transcription service...');
final initialized = await _transcriptionService.initialize();

if (initialized) {
print('Transcription service initialized successfully');
_transcriptionAvailable = true;

// Start listening for transcription
await _transcriptionService.startListening(
onResult: (text) {
if (mounted) {
setState(() {
_currentTranscription = text;
});
}
},
);
print('Transcription listening started');
} else {
print('Transcription service not available on this device');
_transcriptionAvailable = false;
}
} catch (e) {
print('Transcription error (non-fatal): $e');
_transcriptionAvailable = false;
// Don't show error to user - transcription is optional
}
// NOTE: Real-time transcription is disabled due to microphone conflict.
// On Android, speech_to_text and flutter_sound cannot access the microphone
// simultaneously. iOS supports concurrent access but Android does not.
//
// Alternative solutions:
// 1. Use post-recording transcription with cloud APIs (Whisper, Google STT)
// 2. Use vosk_flutter for offline transcription with audio stream
// 3. Implement transcription after recording completes
//
// For now, transcription is left as a placeholder for future implementation.

print(
'Real-time transcription skipped - microphone conflict with audio recording');
}

void _startTimer() {
Expand Down Expand Up @@ -157,15 +137,6 @@ class _RecordingScreenState extends State<RecordingScreen> {
_pauseStartTime = DateTime.now();
_timer?.cancel();

// Pause transcription if available
if (_transcriptionAvailable) {
try {
await _transcriptionService.pauseListening();
} catch (e) {
print('Error pausing transcription: $e');
}
}

setState(() {
_recordingState = RecordingState.paused;
});
Expand All @@ -179,23 +150,6 @@ class _RecordingScreenState extends State<RecordingScreen> {
_pauseStartTime = null;
}

// Resume transcription if available
if (_transcriptionAvailable) {
try {
await _transcriptionService.resumeListening(
onResult: (text) {
if (mounted) {
setState(() {
_currentTranscription = text;
});
}
},
);
} catch (e) {
print('Error resuming transcription: $e');
}
}

setState(() {
_recordingState = RecordingState.recording;
});
Expand All @@ -210,17 +164,8 @@ class _RecordingScreenState extends State<RecordingScreen> {
_recordingState = RecordingState.stopped;
});

// Stop transcription if available and get final text
// Transcription is currently disabled due to microphone conflicts
String transcription = '';
if (_transcriptionAvailable) {
try {
await _transcriptionService.stopListening();
transcription = _transcriptionService.getFinalTranscription();
print('Final transcription: $transcription');
} catch (e) {
print('Error stopping transcription: $e');
}
}

final path = await _audioService.stopRecording();
if (path != null && mounted) {
Expand Down Expand Up @@ -338,60 +283,6 @@ class _RecordingScreenState extends State<RecordingScreen> {

const SizedBox(height: 20),

// Live transcription preview (only show if transcription is working)
if (_currentTranscription.isNotEmpty)
Container(
margin: const EdgeInsets.symmetric(horizontal: 32),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.surfaceVariant
.withOpacity(0.5),
borderRadius: BorderRadius.circular(12),
border: Border.all(
color:
Theme.of(context).colorScheme.primary.withOpacity(0.3),
),
),
constraints: const BoxConstraints(maxHeight: 100),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
children: [
Icon(
Icons.mic,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
const SizedBox(width: 4),
Text(
'Live Transcription',
style: Theme.of(context)
.textTheme
.labelSmall
?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 4),
Flexible(
child: SingleChildScrollView(
child: Text(
_currentTranscription,
style: Theme.of(context).textTheme.bodySmall,
),
),
),
],
),
),

const Spacer(),

// Control buttons
Expand Down
Loading