import 'dart:typed_data'; import 'dart:convert'; import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.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 { FlutterSoundRecorder _audioRecorderTotal = FlutterSoundRecorder(); FlutterSoundRecorder _audioRecorderSegment = FlutterSoundRecorder(); bool _isRecording = false; late String _filePathTotal; late String _filePathSegment; late Timer _segmentTimer; late Timer _totalTimer; // Timer pour la durée totale de l'enregistrement final String apiUrl = 'http://192.168.1.134:3003'; // Remplacer par votre URL int _recordingDuration = 0; // Durée totale de l'enregistrement int _segmentDuration = 0; // Durée de l'enregistrement du segment List responseApi = []; String sessionid = Uuid().v4(); String sessionName = ''; DateTime sessionDate = DateTime.now(); bool bStartSession = false; bool bStopSession = false; bool directTraitement = false; final tecSessionName = TextEditingController(); final fkSessionCreate = GlobalKey(); String sessionEmail = ''; final fkSessionStop = GlobalKey(); final tecSessionEmail = TextEditingController(); bool sessionSummarizeWithLLM = false; @override void initState() { super.initState(); _initializeAudioSession(); // Initialisation de l'AudioSession _requestPermissions(); } /// Initialiser et configurer `AudioSession` Future _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 _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) { print("Permissions non accordées."); return; } else { print("Permissions accordées. C'est tout bon !"); } } // Start the session on the server Future _startSession() async { try { var uri = Uri.parse(apiUrl + '/start'); var response = await http.post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({'sessionId': sessionid}), ); if (response.statusCode == 200) { print('Session démarrée avec succès'); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Session démarrée avec succès"), duration: Duration(seconds: 1), )); } else { print( 'Erreur lors du démarrage de la session : ${response.statusCode}'); } } catch (e) { print('Erreur lors du démarrage de la session : $e'); } } Future _stopSession(String sessionEmail) async { try { var uri = Uri.parse(apiUrl + '/stop'); var response = await http.post( uri, headers: {'Content-Type': 'application/json'}, body: jsonEncode({'sessionId': sessionid, 'sessionEmail': sessionEmail}), ); if (response.statusCode == 200) { print('Session arrêtée avec succès'); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Session arrêtée avec succès"), duration: Duration(seconds: 1), )); } else { print('Erreur lors de l\'arrêt de la session : ${response.statusCode}'); } } catch (e) { print('Erreur lors de l\'arrêt de la session : $e'); } } // Démarre l'enregistrement des deux flux Future _startRecording() async { //Clear the responseApi list setState(() { responseApi.clear(); }); sessionid = Uuid().v4(); sessionDate = DateTime.now(); sessionName = ''; tecSessionName.text = ''; //Dialog for name and date bStartSession = false; await showDialog( context: context, builder: (context) { return AlertDialog( title: Text('Nommer la session'), content: Form( key: fkSessionCreate, child: Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: tecSessionName, decoration: InputDecoration( hintText: 'Nom de la session', ), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer un nom de session'; } return null; }, ), SizedBox(height: 20), Text( sessionDate.toString().substring(0, 19), textAlign: TextAlign.center, ) ], )), actions: [ TextButton( onPressed: () { Navigator.pop(context); }, child: Text('Annuler'), ), TextButton( onPressed: () { if (fkSessionCreate.currentState!.validate()) { sessionName = tecSessionName.text; bStartSession = true; Navigator.pop(context); } }, child: Text('Valider'), ), ], ); }, ); if (!bStartSession) { return; } await _startSession(); Directory tempDir = await getTemporaryDirectory(); print(tempDir.path); _filePathTotal = '${tempDir.path}/audioTotal.wav'; // Enregistrement complet _filePathSegment = '${tempDir.path}/segment.wav'; // Segment de 10 secondes // Ouvre et commence l'enregistrement pour le segment await _audioRecorderSegment.openRecorder(); await _audioRecorderSegment.startRecorder(toFile: _filePathSegment); // Ouvre et commence l'enregistrement pour le fichier total await _audioRecorderTotal.openRecorder(); await _audioRecorderTotal.startRecorder( toFile: _filePathTotal, codec: Codec.pcm16WAV); setState(() { _isRecording = true; _recordingDuration = 0; _segmentDuration = 0; }); // Démarre un timer pour gérer l'enregistrement des segments _segmentTimer = Timer.periodic(Duration(seconds: 1), (timer) async { setState(() { _segmentDuration++; }); if (_segmentDuration == 30 && directTraitement) { await _stopSegmentRecording(); // Arrêter l'enregistrement du segment await _startSegmentRecording(); // Démarrer un nouvel enregistrement de segment } else if (_segmentDuration == 30 && !directTraitement) { _segmentDuration = 0; } }); // Démarre un timer pour la durée totale de l'enregistrement _totalTimer = Timer.periodic(Duration(seconds: 1), (timer) { setState(() { _recordingDuration++; }); }); print('Enregistrement total et segment commencé'); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Enregistrement démarré"), duration: Duration(seconds: 1), )); } // Arrêter l'enregistrement des deux flux Future _stopRecording() async { await _audioRecorderTotal.stopRecorder(); await _audioRecorderSegment.stopRecorder(); _segmentTimer.cancel(); _totalTimer.cancel(); String sessionEmail = ''; bStopSession = false; if (kDebugMode) { tecSessionEmail.text = 'guillaume.david@icloud.com'; } await showDialog( context: context, builder: (context) { bool summarizeWithLLM = false; return AlertDialog( title: Text('Terminer la session'), content: Form( key: fkSessionStop, child: StatefulBuilder( builder: (context, setState) { return Column( mainAxisSize: MainAxisSize.min, children: [ TextFormField( controller: tecSessionEmail, decoration: InputDecoration( hintText: 'Email', ), validator: (value) { if (value == null || value.isEmpty) { return 'Veuillez entrer un email'; } return null; }, ), SizedBox(height: 20), Text('Voulez-vous vraiment arrêter la session ?'), // Checkbox avec mise à jour de l'état Row( children: [ Checkbox( value: summarizeWithLLM, onChanged: (value) { setState(() { summarizeWithLLM = value!; sessionSummarizeWithLLM = summarizeWithLLM; }); }, ), Text('Créer un résumé'), ], ), ], ); }, ), ), actions: [ TextButton( onPressed: () { Navigator.pop(context); }, child: Text('Annuler'), ), TextButton( onPressed: () { if (fkSessionStop.currentState!.validate()) { print('Résumé activé : $summarizeWithLLM'); bStopSession = true; Navigator.pop(context); } }, child: Text('Valider'), ), ], ); }, ); if (!bStopSession) { return; } await _stopSession(sessionEmail); setState(() { _isRecording = false; }); print("Enregistrement arrêté."); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Enregistrement arrêté"), duration: Duration(seconds: 1), )); if (directTraitement) { await _sendSegmentToAPI(_filePathSegment); } //Envoyer le fichier final _sendFinalToAPI(_filePathTotal); } Future _startSegmentRecording() async { Directory tempDir = await getTemporaryDirectory(); _filePathSegment = '${tempDir.path}/segment.wav'; await _audioRecorderSegment.startRecorder(toFile: _filePathSegment); setState(() { _segmentDuration = 0; }); print('Segment commencé'); } Future _stopSegmentRecording() async { await _audioRecorderSegment.stopRecorder(); await Future.delayed(Duration(seconds: 1)); print('Segment arrêté, fichier envoyé à l\'API'); await _sendSegmentToAPI( _filePathSegment); // Envoi du segment à l'API sans attendre } Future _sendSegmentToAPI(String filePath) async { try { File file = File(filePath); if (!await file.exists()) { print('Le fichier n\'existe pas à l\'emplacement : $filePath'); return; } var uri = Uri.parse('$apiUrl/diarize'); var request = http.MultipartRequest('POST', uri); // Ajouter le fichier //Copie le filePath dans le dossier temporaire avec waitToSend.wav String filePathWaitToSend = '${(await getTemporaryDirectory()).path}/waitToSend.wav'; await file.copy(filePathWaitToSend); File filePathWaitToSendFile = File(filePathWaitToSend); request.files.add(await http.MultipartFile.fromPath( 'file', filePathWaitToSendFile.path)); // Ajouter le champ sessionId request.fields['sessionId'] = sessionid; // Lancer l'envoi de la requête sans attendre la réponse _sendRequest( request); // Appel de la fonction d'envoi sans attendre la réponse //wait 1 second before continuing // Code continue sans attendre la réponse de l'API print("Le fichier est en cours d'envoi, le code continue..."); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Segment envoyé à l'API en arrière-plan"), duration: Duration(seconds: 1), )); } catch (e) { print('Erreur lors de l\'envoi du fichier : $e'); } } Future _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/workfinale'); var request = http.MultipartRequest('POST', uri); request.files.add(await http.MultipartFile.fromPath('file', file.path)); // Ajouter le champ sessionId request.fields['sessionId'] = sessionid; request.fields['sessionName'] = sessionName; request.fields['sessionDate'] = sessionDate.toString(); request.fields['sessionEmail'] = tecSessionEmail.text; request.fields['sessionSummarizeWithLLM'] = sessionSummarizeWithLLM.toString(); // Lancer l'envoi de la requête sans attendre la réponse await _sendRequest( request); // Appel de la fonction d'envoi sans attendre la réponse print("Le fichier est en cours d'envoi, le code continue..."); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text("Le fichier a été envoyé à l'API"), 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 _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); // Manipuler la réponse dans setState sans bloquer l'UI setState(() { var speakerTranscriptions = jsonData['speaker_transcriptions'] ?? []; for (var transcription in speakerTranscriptions) { responseApi.add("${transcription[0]}: ${transcription[1]}"); } }); 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() { _audioRecorderTotal.closeRecorder(); _audioRecorderSegment.closeRecorder(); _segmentTimer.cancel(); _totalTimer.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( 'Scriptor', style: TextStyle(color: Colors.black, fontSize: 32), ), ), body: SingleChildScrollView( child: Center( child: Column( children: [ sessionName != '' ? Text( sessionName, style: TextStyle(fontSize: 20), ) : Text(''), IconButton( icon: Icon( _isRecording ? Icons.stop : Icons.mic, size: 100, color: Colors.red, ), onPressed: _isRecording ? _stopRecording : _startRecording, ), 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: TextStyle(fontSize: 20), ), SizedBox(height: 10), directTraitement ? Text('$_segmentDuration/30', style: TextStyle(fontSize: 20)) : Text(''), //Check box Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Checkbox( value: directTraitement, onChanged: (value) { setState(() { directTraitement = value!; }); }, ), Text('Traitement en direct'), ], ), Container( padding: EdgeInsets.all(10), child: Text( "Pour annoncer un locuteur, dites 'Hey Scripto, je suis %nom% et j'ai le rôle de %rôle%.'"), ), directTraitement ? ListView.builder( shrinkWrap: true, itemCount: responseApi.length, itemBuilder: (context, index) { // Inverser la liste et accéder à l'élément correspondant return ListTile( title: Text(responseApi.reversed .toList()[index]), // Inverser la liste ); }, ) : Container() ], ), ), ), ); } }