600 lines
19 KiB
Dart
600 lines
19 KiB
Dart
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<AudioCapturePage> {
|
|
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<String> 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<FormState>();
|
|
|
|
String sessionEmail = '';
|
|
final fkSessionStop = GlobalKey<FormState>();
|
|
final tecSessionEmail = TextEditingController();
|
|
|
|
bool sessionSummarizeWithLLM = false;
|
|
|
|
@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) {
|
|
print("Permissions non accordées.");
|
|
return;
|
|
} else {
|
|
print("Permissions accordées. C'est tout bon !");
|
|
}
|
|
}
|
|
|
|
// Start the session on the server
|
|
Future<void> _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<void> _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<void> _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<void> _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<void> _startSegmentRecording() async {
|
|
Directory tempDir = await getTemporaryDirectory();
|
|
_filePathSegment = '${tempDir.path}/segment.wav';
|
|
await _audioRecorderSegment.startRecorder(toFile: _filePathSegment);
|
|
|
|
setState(() {
|
|
_segmentDuration = 0;
|
|
});
|
|
|
|
print('Segment commencé');
|
|
}
|
|
|
|
Future<void> _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<void> _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<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/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<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);
|
|
|
|
// 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()
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|