533 lines
16 KiB
Dart
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'),
|
|
),*/
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|