scripto/mobile/lib/main.dart
2024-11-29 22:41:27 +01:00

533 lines
16 KiB
Dart

import 'dart:typed_data';
import 'dart:convert';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_document_picker/flutter_document_picker.dart';
import 'package:flutter_sound/flutter_sound.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:http/http.dart' as http;
import 'package:path_provider/path_provider.dart';
import 'dart:async';
import 'package:audio_session/audio_session.dart';
import 'package:uuid/uuid.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Scriptor',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: AudioCapturePage(),
);
}
}
class AudioCapturePage extends StatefulWidget {
@override
_AudioCapturePageState createState() => _AudioCapturePageState();
}
class _AudioCapturePageState extends State<AudioCapturePage> {
FlutterSoundRecorder audioRecorder = FlutterSoundRecorder();
late String filePath;
late Timer timer;
final String apiUrl = 'http://192.168.1.134:3003';
int recordingDuration = 0;
bool isRecording = false;
String sessionName = '';
String sessionEmail = '';
DateTime sessionDate = DateTime.now();
bool bStartSession = false;
bool bStopSession = false;
bool bStopSessionSaveFile = false;
final tecSessionName = TextEditingController();
final tecSessionDateTime = TextEditingController();
final fkSessionCreate = GlobalKey<FormState>();
final fkSessionStop = GlobalKey<FormState>();
final tecSessionEmail = TextEditingController();
bool sessionSummarizeWithLLM = false;
late Timer totalTimer;
@override
void initState() {
super.initState();
initializeAudioSession(); // Initialisation de l'AudioSession
requestPermissions();
}
/// Initialiser et configurer 'AudioSession'
Future<void> initializeAudioSession() async {
final session = await AudioSession.instance;
await session.configure(AudioSessionConfiguration(
avAudioSessionCategory: AVAudioSessionCategory.playAndRecord,
avAudioSessionCategoryOptions:
AVAudioSessionCategoryOptions.allowBluetooth |
AVAudioSessionCategoryOptions.defaultToSpeaker,
avAudioSessionMode: AVAudioSessionMode.spokenAudio,
androidAudioAttributes: const AndroidAudioAttributes(
contentType: AndroidAudioContentType.speech,
flags: AndroidAudioFlags.none,
usage: AndroidAudioUsage.voiceCommunication,
),
androidAudioFocusGainType: AndroidAudioFocusGainType.gain,
androidWillPauseWhenDucked: true,
));
print("AudioSession configurée avec succès !");
}
// Demande des permissions nécessaires
Future<void> requestPermissions() async {
await Permission.microphone.request();
await Permission.storage.request();
var microphoneStatus = await Permission.microphone.status;
var storageStatus = await Permission.storage.status;
if (!microphoneStatus.isGranted || !storageStatus.isGranted) {
if (kDebugMode) {
print("Permissions non accordées.");
}
return;
} else {
if (kDebugMode) {
print("Permissions accordées.");
}
}
}
// Démarre l'enregistrement
Future<void> startRecording() async {
sessionDate = DateTime.now();
sessionName = '';
tecSessionName.text = '';
//Dialog for name and date
bStartSession = false;
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Nommer la session'),
content: Form(
key: fkSessionCreate,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: tecSessionName,
decoration: const InputDecoration(
hintText: 'Nom de la session',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un nom de session';
}
return null;
},
),
TextFormField(
controller: tecSessionDateTime,
decoration: const InputDecoration(
hintText: 'Date et heure',
),
onTap: () async {
DateTime? date = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime(2021),
lastDate: DateTime(2025),
);
if (date != null) {
TimeOfDay? time = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (time != null) {
setState(() {
sessionDate = DateTime(
date.year,
date.month,
date.day,
time.hour,
time.minute,
);
tecSessionDateTime.text =
sessionDate.toString().substring(0, 19);
});
}
}
},
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer une date et une heure';
}
return null;
},
),
],
)),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Annuler'),
),
TextButton(
onPressed: () {
if (fkSessionCreate.currentState!.validate()) {
sessionName = tecSessionName.text;
bStartSession = true;
Navigator.pop(context);
}
},
child: const Text('Valider'),
),
],
);
},
);
if (!bStartSession) {
return;
}
Directory tempDir = await getTemporaryDirectory();
filePath =
'${tempDir.path}/audioScriptorRecord.wav'; // Enregistrement complet
// Ouvre et commence l'enregistrement pour le fichier total
await audioRecorder.openRecorder();
await audioRecorder.startRecorder(toFile: filePath, codec: Codec.pcm16WAV);
setState(() {
isRecording = true;
recordingDuration = 0;
});
// Démarre un timer pour la durée totale de l'enregistrement
totalTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
setState(() {
recordingDuration++;
});
});
print('Enregistrement commencé');
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("L'enregistrement a commencé"),
duration: Duration(seconds: 1),
));
}
// Arrêter l'enregistrement des deux flux
Future<void> stopRecording() async {
String sessionEmail = '';
bStopSession = false;
sessionDate = DateTime.now();
tecSessionDateTime.text = sessionDate.toString().substring(0, 19);
if (kDebugMode) {
tecSessionEmail.text = 'guillaume.david@icloud.com';
}
await showDialog(
context: context,
builder: (context) {
bool summarizeWithLLM = false;
return AlertDialog(
title: const Text('Terminer la session'),
content: Form(
key: fkSessionStop,
child: StatefulBuilder(
builder: (context, setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: tecSessionEmail,
decoration: const InputDecoration(
hintText: 'Email',
),
validator: (value) {
if (value == null || value.isEmpty) {
return 'Veuillez entrer un email';
}
return null;
},
),
const SizedBox(height: 20),
// Checkbox avec mise à jour de l'état
Row(
children: [
Checkbox(
value: summarizeWithLLM,
onChanged: (value) {
setState(() {
summarizeWithLLM = value!;
sessionSummarizeWithLLM = summarizeWithLLM;
});
},
),
const Text('Créer un résumé'),
],
),
],
);
},
),
),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Annuler'),
),
TextButton(
onPressed: () {
if (fkSessionStop.currentState!.validate()) {
print('Résumé activé : $summarizeWithLLM');
bStopSession = true;
Navigator.pop(context);
}
},
child: const Text('Valider'),
),
],
);
},
);
if (!bStopSession) {
return;
}
//Dialog for save file in local storage of phone
bStopSessionSaveFile = false;
await showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text('Sauvegarder le fichier'),
content:
const Text('Voulez-vous sauvegarder le fichier localement ?'),
actions: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text('Non'),
),
TextButton(
onPressed: () {
bStopSessionSaveFile = true;
Navigator.pop(context);
},
child: const Text('Oui'),
),
],
);
},
);
if (bStopSessionSaveFile) {
try {
String fileName =
'$sessionName - ${sessionDate.toString().substring(0, 19)}.wav';
String localPath = await getApplicationDocumentsDirectory().then(
(value) => '${value.path}/$fileName',
);
File sourceFile = File(filePath);
await sourceFile.copy(localPath);
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("Le fichier a été exporté avec succès dans $localPath"),
duration: Duration(seconds: 2),
));
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("Erreur lors de l'exportation du fichier: $e"),
duration: Duration(seconds: 2),
));
}
setState(() {
bStopSessionSaveFile = false;
});
}
await audioRecorder.stopRecorder();
totalTimer.cancel();
setState(() {
isRecording = false;
});
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("L'enregistrement a été arrêté"),
duration: Duration(seconds: 1),
));
//Envoyer le fichier final
await sendFinalToAPI(filePath);
setState(() {
sessionName = '';
sessionEmail = '';
sessionDate = DateTime.now();
sessionSummarizeWithLLM = false;
tecSessionName.text = '';
tecSessionEmail.text = '';
recordingDuration = 0;
});
}
Future<void> sendFinalToAPI(String filePath) async {
try {
File file = File(filePath);
if (!await file.exists()) {
print('Le fichier n\'existe pas à l\'emplacement : $filePath');
return;
}
//Show taille
print('Taille du fichier : ${await file.length()} octets');
var uri = Uri.parse('$apiUrl/work');
var request = http.MultipartRequest('POST', uri);
request.files.add(await http.MultipartFile.fromPath('file', file.path));
// Ajouter le champ sessionId
request.fields['sessionName'] = sessionName;
request.fields['sessionDate'] = sessionDate.toString();
request.fields['sessionEmail'] = tecSessionEmail.text;
request.fields['sessionSummarizeWithLLM'] =
sessionSummarizeWithLLM.toString();
await _sendRequest(request);
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
content: Text("Le fichier a été envoyé. Vous recevrez un email."),
duration: Duration(seconds: 1),
));
} catch (e) {
print('Erreur lors de l\'envoi du fichier : $e');
}
}
// Fonction pour envoyer la requête sans attendre la réponse
Future<void> _sendRequest(http.MultipartRequest request) async {
try {
var response = await request.send();
if (response.statusCode == 200) {
var responseData = await response.stream.bytesToString();
var jsonData = jsonDecode(responseData);
print("Réponse de l'API reçue et traitée.");
} else {
print('Erreur lors de l\'envoi à l\'API : ${response.statusCode}');
}
} catch (e) {
print('Erreur lors de l\'envoi du fichier : $e');
}
}
@override
void dispose() {
audioRecorder.closeRecorder();
totalTimer.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Scriptor',
style: TextStyle(color: Colors.black, fontSize: 32),
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
sessionName != ''
? Text(
sessionName,
style: const TextStyle(fontSize: 20),
)
: const Text(''),
IconButton(
icon: Icon(
isRecording ? Icons.stop : Icons.mic,
size: 100,
color: Colors.red,
),
onPressed: isRecording ? stopRecording : startRecording,
),
const SizedBox(height: 20),
Text(
'${(recordingDuration ~/ 3600).toString().padLeft(2, '0')} H ${((recordingDuration % 3600) ~/ 60).toString().padLeft(2, '0')} M ${(recordingDuration % 60).toString().padLeft(2, '0')} S',
style: const TextStyle(fontSize: 20),
),
const SizedBox(height: 10),
//Check box
Container(
padding: const EdgeInsets.all(10),
child: const Text(
"Pour annoncer un locuteur, dites 'Hey Scripto, je suis %nom% et j'ai le rôle de %rôle%.'",
textAlign: TextAlign.center,
),
)
/*
//Bouton opour enovyer un fichier depuis le téléphone à l'API
ElevatedButton(
onPressed: () async {
FilePickerResult? result =
await FilePicker.platform.pickFiles();
if (result != null) {
String filePath = result.files.single.path!;
sendFinalToAPI(filePath);
}
},
child: const Text('Envoyer un fichier local'),
),*/
],
),
),
);
}
}