stephane09 commited on
Commit
1e2f309
·
verified ·
1 Parent(s): 7aed35c

Upload 17 files

Browse files
App.tsx ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState, useCallback, useMemo } from 'react';
2
+ import { DiarizationEntry } from './types';
3
+ import { processAudioFile } from './services/geminiService';
4
+ import FileUpload from './components/FileUpload';
5
+ import Loader from './components/Loader';
6
+ import DiarizationResult from './components/DiarizationResult';
7
+ import { ResetIcon } from './components/icons';
8
+ import ApiKeyInput from './components/ApiKeyInput';
9
+
10
+ const App: React.FC = () => {
11
+ const [apiKey, setApiKey] = useState<string>('');
12
+ const [useStarFormat, setUseStarFormat] = useState<boolean>(true);
13
+ const [file, setFile] = useState<File | null>(null);
14
+ const [diarizationResult, setDiarizationResult] = useState<DiarizationEntry[] | null>(null);
15
+ const [isLoading, setIsLoading] = useState<boolean>(false);
16
+ const [loadingMessage, setLoadingMessage] = useState<string>('');
17
+ const [error, setError] = useState<string | null>(null);
18
+
19
+ const isApiKeySet = apiKey.trim() !== '';
20
+
21
+ const handleFileSelect = useCallback(async (selectedFile: File) => {
22
+ if (!isApiKeySet) {
23
+ setError("Veuillez d'abord fournir une clé API Gemini.");
24
+ return;
25
+ }
26
+ setFile(selectedFile);
27
+ setIsLoading(true);
28
+ setError(null);
29
+ setDiarizationResult(null);
30
+ setLoadingMessage('Analyse audio en cours... (cela peut prendre un moment)');
31
+
32
+ try {
33
+ const result = await processAudioFile(selectedFile, apiKey);
34
+
35
+ const formattedResult = result.map(entry => ({
36
+ ...entry,
37
+ speaker: useStarFormat
38
+ ? `*${entry.speaker.replace(/ /g, '_')}`
39
+ : entry.speaker,
40
+ }));
41
+
42
+ setDiarizationResult(formattedResult);
43
+ } catch (err) {
44
+ const errorMessage = err instanceof Error ? err.message : 'An unexpected error occurred.';
45
+ setError(errorMessage);
46
+ } finally {
47
+ setIsLoading(false);
48
+ setLoadingMessage('');
49
+ }
50
+ }, [apiKey, isApiKeySet, useStarFormat]);
51
+
52
+ const speakerCount = useMemo(() => {
53
+ if (!diarizationResult) return 0;
54
+ const speakers = new Set(diarizationResult.map(entry => entry.speaker));
55
+ return speakers.size;
56
+ }, [diarizationResult]);
57
+
58
+ const handleReset = () => {
59
+ setFile(null);
60
+ setDiarizationResult(null);
61
+ setIsLoading(false);
62
+ setError(null);
63
+ setLoadingMessage('');
64
+ };
65
+
66
+ const renderContent = () => {
67
+ if (isLoading) {
68
+ return <Loader message={loadingMessage} />;
69
+ }
70
+ if (error) {
71
+ return (
72
+ <div className="text-center text-red-400 bg-red-900/50 p-4 rounded-lg">
73
+ <p className="font-bold">Erreur</p>
74
+ <p>{error}</p>
75
+ </div>
76
+ );
77
+ }
78
+ if (diarizationResult && file) {
79
+ return <DiarizationResult result={diarizationResult} fileName={file.name} speakerCount={speakerCount} useStarFormat={useStarFormat} />;
80
+ }
81
+ // L'upload est désactivé si la clé n'est pas fournie
82
+ return <FileUpload onFileSelect={handleFileSelect} isLoading={isLoading || !isApiKeySet} />;
83
+ };
84
+
85
+ return (
86
+ <div className="min-h-screen bg-gray-900 text-gray-100 flex flex-col items-center justify-center p-4 sm:p-6 lg:p-8">
87
+ <div className="w-full max-w-4xl text-center mb-8">
88
+ <h1 className="text-4xl sm:text-5xl font-extrabold tracking-tight text-transparent bg-clip-text bg-gradient-to-r from-blue-400 to-purple-500">
89
+ Diarisation de MP3 par IA
90
+ </h1>
91
+ <p className="mt-4 text-lg text-gray-400">
92
+ Fournissez votre clé API Gemini, puis téléchargez un MP3 pour obtenir une analyse des locuteurs.
93
+ </p>
94
+ </div>
95
+
96
+ {!diarizationResult && (
97
+ <div className="w-full max-w-2xl mb-8 p-6 bg-gray-800/50 border border-gray-700 rounded-xl">
98
+ <h2 className="text-xl font-bold mb-4 text-center">Configuration</h2>
99
+ <ApiKeyInput apiKey={apiKey} setApiKey={setApiKey} />
100
+ <div className="mt-4 flex items-center justify-center">
101
+ <input
102
+ type="checkbox"
103
+ id="star-format"
104
+ checked={useStarFormat}
105
+ onChange={(e) => setUseStarFormat(e.target.checked)}
106
+ className="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500 bg-gray-700"
107
+ />
108
+ <label htmlFor="star-format" className="ml-2 block text-sm text-gray-300">
109
+ Formater le nom du locuteur (ex: *Locuteur_A)
110
+ </label>
111
+ </div>
112
+ </div>
113
+ )}
114
+
115
+ <main className="w-full max-w-4xl flex-grow flex items-center justify-center">
116
+ {renderContent()}
117
+ </main>
118
+
119
+ {(diarizationResult || error) && !isLoading && (
120
+ <footer className="mt-8">
121
+ <button
122
+ onClick={handleReset}
123
+ className="flex items-center gap-2 px-6 py-2 bg-gray-700 text-white font-semibold rounded-lg hover:bg-gray-600 transition-colors duration-300"
124
+ >
125
+ <ResetIcon className="w-5 h-5" />
126
+ Analyser un autre fichier
127
+ </button>
128
+ </footer>
129
+ )}
130
+ </div>
131
+ );
132
+ };
133
+
134
+ export default App;
README.md CHANGED
@@ -1,10 +1,20 @@
1
- ---
2
- title: Diarization
3
- emoji: 🐨
4
- colorFrom: blue
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
1
+ <div align="center">
2
+ <img width="1200" height="475" alt="GHBanner" src="https://github.com/user-attachments/assets/0aa67016-6eaf-458a-adb2-6e31a0763ed6" />
3
+ </div>
4
+
5
+ # Run and deploy your AI Studio app
6
+
7
+ This contains everything you need to run your app locally.
8
+
9
+ View your app in AI Studio: https://ai.studio/apps/drive/1IeRaNSyuTNvryQryVlmlLbOVCeMVuzor
10
+
11
+ ## Run Locally
12
+
13
+ **Prerequisites:** Node.js
14
+
15
+
16
+ 1. Install dependencies:
17
+ `npm install`
18
+ 2. Set the `GEMINI_API_KEY` in [.env.local](.env.local) to your Gemini API key
19
+ 3. Run the app:
20
+ `npm run dev`
app.py ADDED
File without changes
components/ApiKeyInput.tsx ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface ApiKeyInputProps {
4
+ apiKey: string;
5
+ setApiKey: (key: string) => void;
6
+ }
7
+
8
+ const ApiKeyInput: React.FC<ApiKeyInputProps> = ({ apiKey, setApiKey }) => {
9
+ return (
10
+ <div className="space-y-3">
11
+ <label htmlFor="api-key" className="block text-sm font-medium text-gray-300">
12
+ Votre Clé API Gemini
13
+ </label>
14
+ <input
15
+ type="password"
16
+ id="api-key"
17
+ value={apiKey}
18
+ onChange={(e) => setApiKey(e.target.value)}
19
+ className="block w-full px-3 py-2 bg-gray-900 border border-gray-600 rounded-md shadow-sm placeholder-gray-500 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm text-gray-200"
20
+ placeholder="Entrez votre clé API ici"
21
+ />
22
+ <p className="text-xs text-gray-400 text-center">
23
+ Votre clé est envoyée de manière sécurisée et n'est pas stockée.
24
+ <a
25
+ href="https://aistudio.google.com/app/apikey"
26
+ target="_blank"
27
+ rel="noopener noreferrer"
28
+ className="text-blue-400 hover:underline ml-1"
29
+ >
30
+ Obtenez votre clé API sur Google AI Studio.
31
+ </a>
32
+ </p>
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export default ApiKeyInput;
components/DiarizationResult.tsx ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+ import { DiarizationEntry } from '../types';
3
+ import { ExportIcon } from './icons';
4
+
5
+ interface DiarizationResultProps {
6
+ result: DiarizationEntry[];
7
+ fileName: string;
8
+ speakerCount: number;
9
+ useStarFormat: boolean;
10
+ }
11
+
12
+ const speakerColors: { [key: string]: string } = {
13
+ 'Locuteur A': 'bg-blue-900/50 border-blue-700',
14
+ '*Locuteur_A': 'bg-blue-900/50 border-blue-700',
15
+ 'Locuteur B': 'bg-purple-900/50 border-purple-700',
16
+ '*Locuteur_B': 'bg-purple-900/50 border-purple-700',
17
+ 'Locuteur C': 'bg-green-900/50 border-green-700',
18
+ '*Locuteur_C': 'bg-green-900/50 border-green-700',
19
+ 'Locuteur D': 'bg-indigo-900/50 border-indigo-700',
20
+ '*Locuteur_D': 'bg-indigo-900/50 border-indigo-700',
21
+ };
22
+
23
+ const defaultColor = 'bg-gray-700/50 border-gray-600';
24
+
25
+ const DiarizationResult: React.FC<DiarizationResultProps> = ({ result, fileName, speakerCount, useStarFormat }) => {
26
+ const getSpeakerColor = (speaker: string) => {
27
+ // Retirer le formatage pour trouver la couleur de base
28
+ const baseSpeaker = speaker.replace(/^\*/, '').replace(/_/g, ' ');
29
+ return speakerColors[baseSpeaker] || defaultColor;
30
+ };
31
+
32
+ const handleExport = () => {
33
+ const header = `Résultat de la Diarisation pour le fichier : ${fileName}\n`;
34
+ const stats = `Nombre de locuteurs détectés : ${speakerCount}\n\n`;
35
+ const content = result
36
+ .map(entry => {
37
+ const prefix = useStarFormat ? '\n' : '';
38
+ return `${prefix}[${entry.timestamp}] ${entry.speaker}: ${entry.text}`;
39
+ })
40
+ .join('\n');
41
+
42
+ const fullText = header + stats + content;
43
+
44
+ const blob = new Blob([fullText], { type: 'text/plain;charset=utf-8' });
45
+ const url = URL.createObjectURL(blob);
46
+ const link = document.createElement('a');
47
+ link.href = url;
48
+ const safeFileName = fileName.replace(/\.[^/.]+$/, "");
49
+ link.download = `${safeFileName}-diarisation.txt`;
50
+
51
+ document.body.appendChild(link);
52
+ link.click();
53
+
54
+ document.body.removeChild(link);
55
+ URL.revokeObjectURL(url);
56
+ };
57
+
58
+ return (
59
+ <div className="w-full max-w-4xl mx-auto bg-gray-900/50 rounded-xl shadow-lg border border-gray-700 overflow-hidden">
60
+ <div className="p-4 bg-gray-800 border-b border-gray-700 flex justify-between items-center">
61
+ <div>
62
+ <h2 className="text-lg font-bold text-white">Résultat de la Diarisation</h2>
63
+ <p className="text-sm text-gray-400 truncate">Fichier : {fileName} | <span className="font-semibold">{speakerCount} locuteur(s) détecté(s)</span></p>
64
+ </div>
65
+ <button
66
+ onClick={handleExport}
67
+ className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white font-semibold rounded-lg hover:bg-blue-500 transition-colors duration-300"
68
+ aria-label="Exporter le résultat au format texte"
69
+ >
70
+ <ExportIcon className="w-5 h-5" />
71
+ Exporter
72
+ </button>
73
+ </div>
74
+ <div className="p-4 md:p-6 space-y-4 h-[60vh] overflow-y-auto">
75
+ {result.map((entry, index) => (
76
+ <div key={index} className={`flex items-start gap-4 ${useStarFormat ? 'mt-3' : ''}`}>
77
+ <div className={`flex-shrink-0 w-24 text-right font-mono text-sm text-gray-400`}>
78
+ [{entry.timestamp}]
79
+ </div>
80
+ <div className={`relative w-full p-3 rounded-lg border ${getSpeakerColor(entry.speaker)}`}>
81
+ <div className="font-semibold text-white mb-1">{entry.speaker}</div>
82
+ <p className="text-gray-300 whitespace-pre-wrap">{entry.text}</p>
83
+ </div>
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </div>
88
+ );
89
+ };
90
+
91
+ export default DiarizationResult;
components/FileUpload.tsx ADDED
@@ -0,0 +1,71 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useRef } from 'react';
2
+ import { MusicIcon } from './icons';
3
+
4
+ interface FileUploadProps {
5
+ onFileSelect: (file: File) => void;
6
+ isLoading: boolean;
7
+ }
8
+
9
+ const FileUpload: React.FC<FileUploadProps> = ({ onFileSelect, isLoading }) => {
10
+ const fileInputRef = useRef<HTMLInputElement>(null);
11
+
12
+ const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
13
+ const file = event.target.files?.[0];
14
+ if (file) {
15
+ onFileSelect(file);
16
+ }
17
+ };
18
+
19
+ const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
20
+ event.preventDefault();
21
+ event.stopPropagation();
22
+ if (isLoading) return;
23
+ const file = event.dataTransfer.files?.[0];
24
+ if (file && file.type === 'audio/mpeg') {
25
+ onFileSelect(file);
26
+ }
27
+ };
28
+
29
+ const handleDragOver = (event: React.DragEvent<HTMLDivElement>) => {
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+ };
33
+
34
+ const handleClick = () => {
35
+ if (isLoading) return;
36
+ fileInputRef.current?.click();
37
+ };
38
+
39
+ const disabledClasses = "opacity-50 cursor-not-allowed";
40
+ const enabledClasses = "hover:border-blue-500 hover:bg-gray-800/50 cursor-pointer";
41
+
42
+ return (
43
+ <div className="w-full max-w-2xl mx-auto">
44
+ <div
45
+ className={`w-full h-64 border-2 border-dashed border-gray-600 rounded-xl flex flex-col items-center justify-center text-gray-400 transition-all duration-300 ${isLoading ? disabledClasses : enabledClasses}`}
46
+ onClick={handleClick}
47
+ onDrop={handleDrop}
48
+ onDragOver={handleDragOver}
49
+ >
50
+ <input
51
+ type="file"
52
+ ref={fileInputRef}
53
+ onChange={handleFileChange}
54
+ accept=".mp3"
55
+ className="hidden"
56
+ disabled={isLoading}
57
+ />
58
+ <MusicIcon className="w-16 h-16 mb-4 text-gray-500" />
59
+ <p className="text-lg font-semibold">
60
+ {isLoading ? "Veuillez fournir une clé API" : "Déposez votre fichier MP3 ici"}
61
+ </p>
62
+ <p>ou</p>
63
+ <p className={`font-bold ${isLoading ? "" : "text-blue-400"}`}>
64
+ {isLoading ? "" : "Cliquez pour parcourir"}
65
+ </p>
66
+ </div>
67
+ </div>
68
+ );
69
+ };
70
+
71
+ export default FileUpload;
components/Loader.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ interface LoaderProps {
4
+ message: string;
5
+ }
6
+
7
+ const Loader: React.FC<LoaderProps> = ({ message }) => {
8
+ return (
9
+ <div className="flex flex-col items-center justify-center space-y-4">
10
+ <div className="w-16 h-16 border-4 border-blue-400 border-dashed rounded-full animate-spin border-t-transparent"></div>
11
+ <p className="text-lg text-blue-300">{message}</p>
12
+ </div>
13
+ );
14
+ };
15
+
16
+ export default Loader;
components/icons.tsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from 'react';
2
+
3
+ export const MusicIcon: React.FC<{ className?: string }> = ({ className }) => (
4
+ <svg
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ className={className}
7
+ width="24"
8
+ height="24"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ >
16
+ <path d="M9 18V5l12-2v13" />
17
+ <circle cx="6" cy="18" r="3" />
18
+ <circle cx="18" cy="16" r="3" />
19
+ </svg>
20
+ );
21
+
22
+ export const ResetIcon: React.FC<{ className?: string }> = ({ className }) => (
23
+ <svg
24
+ xmlns="http://www.w3.org/2000/svg"
25
+ className={className}
26
+ width="24"
27
+ height="24"
28
+ viewBox="0 0 24 24"
29
+ fill="none"
30
+ stroke="currentColor"
31
+ strokeWidth="2"
32
+ strokeLinecap="round"
33
+ strokeLinejoin="round"
34
+ >
35
+ <path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
36
+ <path d="M3 3v5h5" />
37
+ <path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
38
+ <path d="M21 21v-5h-5" />
39
+ </svg>
40
+ );
41
+
42
+ export const ExportIcon: React.FC<{ className?: string }> = ({ className }) => (
43
+ <svg
44
+ xmlns="http://www.w3.org/2000/svg"
45
+ className={className}
46
+ width="24"
47
+ height="24"
48
+ viewBox="0 0 24 24"
49
+ fill="none"
50
+ stroke="currentColor"
51
+ strokeWidth="2"
52
+ strokeLinecap="round"
53
+ strokeLinejoin="round"
54
+ >
55
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
56
+ <polyline points="7 10 12 15 17 10" />
57
+ <line x1="12" y1="15" x2="12" y2="3" />
58
+ </svg>
59
+ );
index.html ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <!DOCTYPE html>
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="UTF-8" />
6
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
+ <title>MP3 Speaker Diarization AI</title>
9
+ <script src="https://cdn.tailwindcss.com"></script>
10
+ <script type="importmap">
11
+ {
12
+ "imports": {
13
+ "react": "https://aistudiocdn.com/react@^19.2.0",
14
+ "react/": "https://aistudiocdn.com/react@^19.2.0/",
15
+ "react-dom/": "https://aistudiocdn.com/react-dom@^19.2.0/",
16
+ "@google/genai": "https://aistudiocdn.com/@google/genai@^1.29.0"
17
+ }
18
+ }
19
+ </script>
20
+ <link rel="stylesheet" href="/index.css">
21
+ </head>
22
+ <body class="bg-gray-900 text-gray-100">
23
+ <div id="root"></div>
24
+ <script type="module" src="/index.tsx"></script>
25
+ </body>
26
+ </html>
index.tsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ import React from 'react';
3
+ import ReactDOM from 'react-dom/client';
4
+ import App from './App';
5
+
6
+ const rootElement = document.getElementById('root');
7
+ if (!rootElement) {
8
+ throw new Error("Could not find root element to mount to");
9
+ }
10
+
11
+ const root = ReactDOM.createRoot(rootElement);
12
+ root.render(
13
+ <React.StrictMode>
14
+ <App />
15
+ </React.StrictMode>
16
+ );
metadata.json ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ {
2
+ "name": "Copy of Copy of MP3 Speaker Diarization AI",
3
+ "description": "An application that uses AI to perform speaker diarization on an audio file's transcript, identifying who spoke and when.",
4
+ "requestFramePermissions": []
5
+ }
package.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "copy-of-copy-of-mp3-speaker-diarization-ai",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "react": "^19.2.0",
13
+ "react-dom": "^19.2.0",
14
+ "@google/genai": "^1.29.0"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^22.14.0",
18
+ "@vitejs/plugin-react": "^5.0.0",
19
+ "typescript": "~5.8.2",
20
+ "vite": "^6.2.0"
21
+ }
22
+ }
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ python-multipart
4
+ google-generativeai
services/geminiService.ts ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { DiarizationEntry } from '../types';
2
+
3
+ /**
4
+ * Cette fonction envoie le fichier audio et la clé API de l'utilisateur au backend.
5
+ * @param file Le fichier MP3 à analyser.
6
+ * @param apiKey La clé API Gemini fournie par l'utilisateur.
7
+ * @returns Une promesse qui se résout avec le résultat de la diarisation.
8
+ */
9
+ export async function processAudioFile(file: File, apiKey: string): Promise<DiarizationEntry[]> {
10
+ const formData = new FormData();
11
+ formData.append('file', file);
12
+
13
+ try {
14
+ const response = await fetch('/api/diarize', {
15
+ method: 'POST',
16
+ headers: {
17
+ // Transmet la clé API de manière sécurisée dans un en-tête
18
+ 'X-API-KEY': apiKey,
19
+ },
20
+ body: formData,
21
+ });
22
+
23
+ if (!response.ok) {
24
+ const errorData = await response.json();
25
+ throw new Error(`Erreur du serveur (${response.status}): ${errorData.detail || 'Erreur inconnue'}`);
26
+ }
27
+
28
+ const result = await response.json();
29
+ return result as DiarizationEntry[];
30
+
31
+ } catch (error) {
32
+ console.error("Erreur lors de l'appel au backend pour la diarisation:", error);
33
+ if (error instanceof Error) {
34
+ throw new Error(`Échec du traitement du fichier : ${error.message}`);
35
+ }
36
+ throw new Error("Une erreur inconnue est survenue lors du traitement du fichier.");
37
+ }
38
+ }
tsconfig.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "experimentalDecorators": true,
5
+ "useDefineForClassFields": false,
6
+ "module": "ESNext",
7
+ "lib": [
8
+ "ES2022",
9
+ "DOM",
10
+ "DOM.Iterable"
11
+ ],
12
+ "skipLibCheck": true,
13
+ "types": [
14
+ "node"
15
+ ],
16
+ "moduleResolution": "bundler",
17
+ "isolatedModules": true,
18
+ "moduleDetection": "force",
19
+ "allowJs": true,
20
+ "jsx": "react-jsx",
21
+ "paths": {
22
+ "@/*": [
23
+ "./*"
24
+ ]
25
+ },
26
+ "allowImportingTsExtensions": true,
27
+ "noEmit": true
28
+ }
29
+ }
types.ts ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+
2
+ export interface DiarizationEntry {
3
+ speaker: string;
4
+ timestamp: string;
5
+ text: string;
6
+ }
vite.config.ts ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import path from 'path';
2
+ import { defineConfig, loadEnv } from 'vite';
3
+ import react from '@vitejs/plugin-react';
4
+
5
+ export default defineConfig(({ mode }) => {
6
+ const env = loadEnv(mode, '.', '');
7
+ return {
8
+ server: {
9
+ port: 3000,
10
+ host: '0.0.0.0',
11
+ },
12
+ plugins: [react()],
13
+ define: {
14
+ 'process.env.API_KEY': JSON.stringify(env.GEMINI_API_KEY),
15
+ 'process.env.GEMINI_API_KEY': JSON.stringify(env.GEMINI_API_KEY)
16
+ },
17
+ resolve: {
18
+ alias: {
19
+ '@': path.resolve(__dirname, '.'),
20
+ }
21
+ }
22
+ };
23
+ });