mathisescriva commited on
Commit
e6e14b8
·
1 Parent(s): 704669a

Initial commit: STT + Diarization pipeline unifié

Browse files
Files changed (4) hide show
  1. README.md +46 -16
  2. app.py +126 -90
  3. processing.py +360 -0
  4. requirements.txt +8 -0
README.md CHANGED
@@ -1,5 +1,5 @@
1
  ---
2
- title: Gilbert - Diarisation pyannote
3
  emoji: 🎤
4
  colorFrom: blue
5
  colorTo: purple
@@ -10,32 +10,62 @@ pinned: false
10
  license: mit
11
  ---
12
 
13
- # Gilbert - Diarisation pyannote
14
 
15
- Interface interactive pour la diarisation de locuteurs avec pyannote.audio.
16
 
17
  ## Fonctionnalités
18
 
19
- - 🎤 Diarisation de locuteurs sur fichiers audio
20
- - 📊 Statistiques détaillées par locuteur
21
- - 📁 Export RTTM et JSON
22
- - ⚙️ Configuration flexible (nombre de locuteurs, modèles)
23
 
24
- ## Modèles supportés
25
 
26
- - `pyannote/speaker-diarization-3.1` (par défaut)
27
- - `pyannote/speaker-diarization-community-1`
 
 
 
 
 
 
28
 
29
  ## Utilisation
30
 
31
- 1. Uploadez un fichier audio (WAV, MP3, M4A)
32
- 2. Configurez les paramètres (optionnel)
33
- 3. Cliquez sur "Diariser"
34
- 4. Téléchargez les résultats (RTTM et JSON)
 
 
 
 
 
 
 
 
 
35
 
36
  ## Configuration
37
 
38
- Pour utiliser cette Space, vous devez avoir un token Hugging Face avec accès aux modèles pyannote.
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- Configurez-le dans les secrets de la Space ou via `HF_TOKEN`.
41
 
 
 
 
 
1
  ---
2
+ title: Gilbert - STT + Diarization
3
  emoji: 🎤
4
  colorFrom: blue
5
  colorTo: purple
 
10
  license: mit
11
  ---
12
 
13
+ # Gilbert - STT + Diarization
14
 
15
+ Pipeline complet de transcription (STT) et diarisation de locuteurs avec sortie formatée.
16
 
17
  ## Fonctionnalités
18
 
19
+ - 🎤 **Diarisation de locuteurs** avec pyannote.audio
20
+ - 📝 **Transcription** avec Whisper Large V3 French (fine-tuné pour le français)
21
+ - 🔗 **Combinaison automatique** pour une sortie formatée: "Speaker A : texte"
22
+ - 📊 **Statistiques détaillées** par locuteur
23
 
24
+ ## Modèles utilisés
25
 
26
+ ### Diarization
27
+ - `pyannote/speaker-diarization-community-1` (par défaut, meilleures performances)
28
+ - `pyannote/speaker-diarization-3.1` (fallback)
29
+
30
+ ### Speech-to-Text (STT)
31
+ - `bofenghuang/whisper-large-v3-french` (Whisper Large V3 fine-tuné pour le français)
32
+ - Meilleures performances sur le français que Whisper standard
33
+ - Support de la casse, ponctuation et nombres
34
 
35
  ## Utilisation
36
 
37
+ 1. Uploadez un fichier audio (WAV, MP3, M4A, FLAC)
38
+ 2. Configurez les paramètres de diarisation (optionnel)
39
+ 3. Cliquez sur "Traiter"
40
+ 4. Téléchargez la transcription avec identification des locuteurs
41
+
42
+ ## Format de sortie
43
+
44
+ La sortie est au format :
45
+ ```
46
+ Speaker A : texte du locuteur A
47
+
48
+ Speaker B : texte du locuteur B
49
+ ```
50
 
51
  ## Configuration
52
 
53
+ Pour utiliser cette Space, vous devez avoir un token Hugging Face avec accès aux modèles pyannote et Whisper.
54
+
55
+ Configurez-le dans les secrets de la Space avec: `HF_TOKEN="votre_token"`
56
+
57
+ ## Exemple de sortie
58
+
59
+ ```
60
+ Speaker A : Bonjour, comment allez-vous aujourd'hui ?
61
+
62
+ Speaker B : Très bien merci, et vous ?
63
+
64
+ Speaker A : Parfait, je suis ravi de vous rencontrer.
65
+ ```
66
 
67
+ ## Performance
68
 
69
+ - **Temps de traitement**: ~1.5x la durée de l'audio (sur CPU)
70
+ - **Précision**: Optimisée pour le français avec le modèle fine-tuné
71
+ - **Formats supportés**: WAV, MP3, M4A, FLAC, OGG
app.py CHANGED
@@ -4,90 +4,147 @@ import tempfile
4
  from pathlib import Path
5
  import sys
6
 
7
- # Ajouter le répertoire parent au path pour importer le script
8
- sys.path.insert(0, str(Path(__file__).parent.parent))
 
 
 
 
 
9
 
10
- from diarization_pyannote_demo import run_pyannote_diarization, write_rttm, write_json
11
 
12
- def diarize_audio(audio_file, model_name, num_speakers, min_speakers, max_speakers, use_exclusive):
13
- """Interface Gradio pour la diarisation pyannote."""
 
 
 
14
 
15
  if audio_file is None:
16
  return None, "❌ Veuillez uploader un fichier audio"
17
 
18
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  # Créer un répertoire temporaire pour les résultats
20
  with tempfile.TemporaryDirectory() as tmpdir:
21
- # Exécuter la diarisation
22
- result = run_pyannote_diarization(
23
- audio_file.name,
24
- output_dir=tmpdir,
25
- model_name=model_name,
26
- num_speakers=num_speakers if num_speakers > 0 else None,
27
- min_speakers=min_speakers if min_speakers > 0 else None,
28
- max_speakers=max_speakers if max_speakers > 0 else None,
29
- use_exclusive=use_exclusive,
30
- show_progress=False
31
- )
32
 
33
- # Générer les fichiers de sortie
34
- audio_name = Path(audio_file.name).stem
35
- rttm_path = os.path.join(tmpdir, f"{audio_name}.rttm")
36
- json_path = os.path.join(tmpdir, f"{audio_name}.json")
 
 
 
 
37
 
38
- write_rttm(result["segments"], rttm_path, audio_name)
39
- write_json(result["segments"], json_path)
 
 
 
 
 
 
40
 
41
- # Lire les fichiers pour les retourner
42
- with open(rttm_path, 'r') as f:
43
- rttm_content = f.read()
44
 
45
- with open(json_path, 'r') as f:
46
- json_content = f.read()
 
 
47
 
48
  # Créer un résumé
49
- summary = f"""
50
- # Résultats de diarisation
51
-
52
- **Fichier:** {Path(audio_file.name).name}
53
- **Modèle:** {model_name}
54
- **Locuteurs détectés:** {result['num_speakers']}
55
- **Segments:** {len(result['segments'])}
56
- **Durée totale:** {result.get('duration', 0):.2f} secondes
57
-
58
- ## Statistiques par locuteur
59
- """
60
  from collections import defaultdict
61
- speaker_stats = defaultdict(lambda: {"total_duration": 0.0, "num_segments": 0})
62
- for seg in result["segments"]:
63
  speaker = seg["speaker"]
64
  duration = seg["end"] - seg["start"]
65
  speaker_stats[speaker]["total_duration"] += duration
66
  speaker_stats[speaker]["num_segments"] += 1
 
67
 
 
 
 
 
 
 
 
 
 
 
 
68
  for speaker, stats in sorted(speaker_stats.items()):
 
 
69
  avg_duration = stats["total_duration"] / stats["num_segments"] if stats["num_segments"] > 0 else 0
70
- summary += f"\n- **{speaker}**: {stats['num_segments']} segments, {stats['total_duration']:.2f}s total, {avg_duration:.2f}s moyenne/segment"
71
 
72
- return rttm_path, json_path, summary
73
 
74
  except Exception as e:
75
  import traceback
76
- error_msg = f"❌ Erreur: {str(e)}\n\n```\n{traceback.format_exc()}\n```"
77
- return None, None, error_msg
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
  # Interface Gradio
80
- with gr.Blocks(title="Gilbert - Diarisation pyannote") as demo:
81
  gr.Markdown("""
82
- # 🎤 Gilbert - Diarisation pyannote
 
 
83
 
84
- Interface pour la diarisation de locuteurs avec pyannote.audio
 
 
 
85
 
86
  **Instructions:**
87
  1. Uploadez un fichier audio (WAV, MP3, M4A)
88
- 2. Configurez les paramètres (optionnel)
89
- 3. Cliquez sur "Diariser"
90
- 4. Téléchargez les résultats (RTTM et JSON)
91
  """)
92
 
93
  with gr.Row():
@@ -97,60 +154,39 @@ with gr.Blocks(title="Gilbert - Diarisation pyannote") as demo:
97
  type="filepath"
98
  )
99
 
100
- model_name = gr.Dropdown(
101
  choices=[
102
- "pyannote/speaker-diarization-3.1",
103
  "pyannote/speaker-diarization-community-1",
 
104
  ],
105
- value="pyannote/speaker-diarization-3.1",
106
- label="Modèle pyannote"
107
  )
108
 
109
- with gr.Row():
110
- num_speakers = gr.Number(
111
- label="Nombre exact de locuteurs",
112
- value=0,
113
- minimum=0,
114
- info="0 = auto-détection"
115
- )
116
- min_speakers = gr.Number(
117
- label="Min locuteurs",
118
- value=0,
119
- minimum=0,
120
- info="0 = pas de limite"
121
- )
122
- max_speakers = gr.Number(
123
- label="Max locuteurs",
124
- value=0,
125
- minimum=0,
126
- info="0 = pas de limite"
127
- )
128
-
129
- use_exclusive = gr.Checkbox(
130
- label="Exclusive speaker diarization",
131
- value=False,
132
- info="Simplifie la réconciliation avec transcription"
133
- )
134
-
135
- diarize_btn = gr.Button("🎯 Diariser", variant="primary")
136
 
137
  with gr.Column():
138
  summary_output = gr.Markdown(label="Résumé")
139
- rttm_output = gr.File(label="Fichier RTTM", type="filepath")
140
- json_output = gr.File(label="Fichier JSON", type="filepath")
 
 
141
 
142
- diarize_btn.click(
143
- fn=diarize_audio,
144
- inputs=[audio_input, model_name, num_speakers, min_speakers, max_speakers, use_exclusive],
145
- outputs=[rttm_output, json_output, summary_output]
146
  )
147
 
148
  gr.Markdown("""
149
  ---
150
- **Note:** Vous devez avoir un token Hugging Face configuré avec accès aux modèles pyannote.
151
- Configurez-le avec: `export HF_TOKEN="votre_token"`
 
 
 
 
152
  """)
153
 
154
  if __name__ == "__main__":
155
  demo.launch()
156
-
 
4
  from pathlib import Path
5
  import sys
6
 
7
+ # Importer le module de traitement
8
+ from processing import (
9
+ run_diarization,
10
+ run_transcription,
11
+ combine_diarization_transcription,
12
+ format_output
13
+ )
14
 
 
15
 
16
+ def process_audio_stt_diarization(
17
+ audio_file,
18
+ diarization_model
19
+ ):
20
+ """Interface Gradio pour STT + Diarization combinés."""
21
 
22
  if audio_file is None:
23
  return None, "❌ Veuillez uploader un fichier audio"
24
 
25
  try:
26
+ # Gérer le chemin du fichier audio
27
+ if isinstance(audio_file, tuple):
28
+ audio_path = audio_file[1] if len(audio_file) > 1 else audio_file[0]
29
+ elif isinstance(audio_file, str):
30
+ audio_path = audio_file
31
+ elif hasattr(audio_file, 'name'):
32
+ audio_path = audio_file.name
33
+ else:
34
+ audio_path = str(audio_file)
35
+
36
+ if not os.path.exists(audio_path):
37
+ return None, f"❌ Fichier audio introuvable: {audio_path}"
38
+
39
+ # Récupérer le token HF
40
+ hf_token = os.environ.get("HF_TOKEN")
41
+ if not hf_token:
42
+ return None, "❌ Token Hugging Face non configuré (HF_TOKEN)"
43
+
44
  # Créer un répertoire temporaire pour les résultats
45
  with tempfile.TemporaryDirectory() as tmpdir:
46
+ # Étape 1: Diarisation
47
+ try:
48
+ diarization_segments = run_diarization(
49
+ audio_path,
50
+ hf_token,
51
+ model_name=diarization_model
52
+ )
53
+ except Exception as e:
54
+ return None, f"❌ Erreur lors de la diarisation: {str(e)}"
 
 
55
 
56
+ # Étape 2: Transcription
57
+ try:
58
+ transcription_segments = run_transcription(
59
+ audio_path,
60
+ hf_token=hf_token
61
+ )
62
+ except Exception as e:
63
+ return None, f"❌ Erreur lors de la transcription: {str(e)}"
64
 
65
+ # Étape 3: Combinaison
66
+ try:
67
+ combined = combine_diarization_transcription(
68
+ diarization_segments,
69
+ transcription_segments
70
+ )
71
+ except Exception as e:
72
+ return None, f"❌ Erreur lors de la combinaison: {str(e)}"
73
 
74
+ # Étape 4: Formatage
75
+ formatted_text = format_output(combined)
 
76
 
77
+ # Sauvegarder dans un fichier temporaire
78
+ output_file = os.path.join(tmpdir, "transcription.txt")
79
+ with open(output_file, 'w', encoding='utf-8') as f:
80
+ f.write(formatted_text)
81
 
82
  # Créer un résumé
 
 
 
 
 
 
 
 
 
 
 
83
  from collections import defaultdict
84
+ speaker_stats = defaultdict(lambda: {"total_duration": 0.0, "num_segments": 0, "text_length": 0})
85
+ for seg in combined:
86
  speaker = seg["speaker"]
87
  duration = seg["end"] - seg["start"]
88
  speaker_stats[speaker]["total_duration"] += duration
89
  speaker_stats[speaker]["num_segments"] += 1
90
+ speaker_stats[speaker]["text_length"] += len(seg["text"])
91
 
92
+ summary = f"""
93
+ # Résultats STT + Diarization
94
+
95
+ **Fichier:** {Path(audio_path).name}
96
+ **Modèle diarization:** {diarization_model}
97
+ **Modèle STT:** bofenghuang/whisper-large-v3-french
98
+ **Locuteurs détectés:** {len(speaker_stats)}
99
+ **Segments combinés:** {len(combined)}
100
+
101
+ ## Statistiques par locuteur
102
+ """
103
  for speaker, stats in sorted(speaker_stats.items()):
104
+ speaker_num = int(speaker.replace("SPEAKER_", ""))
105
+ speaker_name = f"Speaker {chr(65 + speaker_num)}"
106
  avg_duration = stats["total_duration"] / stats["num_segments"] if stats["num_segments"] > 0 else 0
107
+ summary += f"\n- **{speaker_name}**: {stats['num_segments']} segments, {stats['total_duration']:.2f}s total, {avg_duration:.2f}s moyenne/segment, {stats['text_length']} caractères"
108
 
109
+ return output_file, summary
110
 
111
  except Exception as e:
112
  import traceback
113
+ error_details = traceback.format_exc()
114
+ error_msg = f"""❌ **Erreur lors du traitement**
115
+
116
+ **Message:** {str(e)}
117
+
118
+ **Détails techniques:**
119
+ ```
120
+ {error_details}
121
+ ```
122
+
123
+ **Solutions possibles:**
124
+ - Vérifiez que le fichier audio est valide
125
+ - Assurez-vous que le token HF_TOKEN est configuré dans les secrets de la Space
126
+ - Réessayez avec un fichier audio plus court
127
+ """
128
+ return None, error_msg
129
+
130
 
131
  # Interface Gradio
132
+ with gr.Blocks(title="Gilbert - STT + Diarization") as demo:
133
  gr.Markdown("""
134
+ # 🎤 Gilbert - STT + Diarization
135
+
136
+ Pipeline complet de transcription (STT) et diarisation de locuteurs.
137
 
138
+ **Fonctionnalités:**
139
+ - 🎤 Diarisation de locuteurs avec pyannote.audio
140
+ - 📝 Transcription avec Whisper Large V3 French (fine-tuné pour le français)
141
+ - 🔗 Combinaison automatique pour une sortie formatée: "Speaker A : texte"
142
 
143
  **Instructions:**
144
  1. Uploadez un fichier audio (WAV, MP3, M4A)
145
+ 2. Configurez les paramètres de diarisation (optionnel)
146
+ 3. Cliquez sur "Traiter"
147
+ 4. Téléchargez la transcription avec identification des locuteurs
148
  """)
149
 
150
  with gr.Row():
 
154
  type="filepath"
155
  )
156
 
157
+ diarization_model = gr.Dropdown(
158
  choices=[
 
159
  "pyannote/speaker-diarization-community-1",
160
+ "pyannote/speaker-diarization-3.1",
161
  ],
162
+ value="pyannote/speaker-diarization-community-1",
163
+ label="Modèle de diarisation"
164
  )
165
 
166
+ process_btn = gr.Button("🚀 Traiter", variant="primary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
167
 
168
  with gr.Column():
169
  summary_output = gr.Markdown(label="Résumé")
170
+ transcription_output = gr.File(
171
+ label="Transcription (format: Speaker A : texte)",
172
+ type="filepath"
173
+ )
174
 
175
+ process_btn.click(
176
+ fn=process_audio_stt_diarization,
177
+ inputs=[audio_input, diarization_model],
178
+ outputs=[transcription_output, summary_output]
179
  )
180
 
181
  gr.Markdown("""
182
  ---
183
+ **Note:** Vous devez avoir un token Hugging Face configuré avec accès aux modèles pyannote et Whisper.
184
+ Configurez-le dans les secrets de la Space avec: `HF_TOKEN="votre_token"`
185
+
186
+ **Modèles utilisés:**
187
+ - **Diarization**: pyannote/speaker-diarization-community-1 (ou 3.1)
188
+ - **STT**: bofenghuang/whisper-large-v3-french (Whisper Large V3 fine-tuné pour le français)
189
  """)
190
 
191
  if __name__ == "__main__":
192
  demo.launch()
 
processing.py ADDED
@@ -0,0 +1,360 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Module de traitement unifié pour STT + Diarization.
4
+ Utilisé par le Space Gradio.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import List, Dict, Any
11
+ import json
12
+
13
+ # Imports pour pyannote
14
+ try:
15
+ from pyannote.audio import Pipeline
16
+ HAS_PYANNOTE = True
17
+ except ImportError:
18
+ HAS_PYANNOTE = False
19
+
20
+ # Imports pour Whisper et Transformers
21
+ try:
22
+ import whisper
23
+ import torch
24
+ HAS_WHISPER = True
25
+ except ImportError:
26
+ HAS_WHISPER = False
27
+
28
+ try:
29
+ from transformers import AutoModelForSpeechSeq2Seq, AutoProcessor
30
+ HAS_TRANSFORMERS = True
31
+ except ImportError:
32
+ HAS_TRANSFORMERS = False
33
+
34
+ # Corriger le problème PyTorch 2.6 avec weights_only
35
+ if hasattr(torch.serialization, 'add_safe_globals'):
36
+ try:
37
+ torch.serialization.add_safe_globals([torch.torch_version.TorchVersion])
38
+ except:
39
+ pass
40
+
41
+ import numpy as np
42
+ import librosa
43
+ import soundfile as sf
44
+
45
+
46
+ def convert_audio_if_needed(audio_path: str) -> str:
47
+ """
48
+ Convertit l'audio en WAV si nécessaire.
49
+
50
+ Returns:
51
+ Chemin vers le fichier audio (WAV si conversion nécessaire)
52
+ """
53
+ ext = Path(audio_path).suffix.lower()
54
+ supported_formats = {'.wav', '.flac', '.ogg'}
55
+
56
+ if ext in supported_formats:
57
+ return audio_path
58
+
59
+ if ext in {'.m4a', '.mp3', '.mp4', '.aac'}:
60
+ wav_path = str(Path(audio_path).with_suffix('.wav'))
61
+ if os.path.exists(wav_path):
62
+ return wav_path
63
+
64
+ try:
65
+ y, sr = librosa.load(audio_path, sr=16000, mono=True)
66
+ sf.write(wav_path, y, sr)
67
+ return wav_path
68
+ except Exception as e:
69
+ return audio_path
70
+
71
+ return audio_path
72
+
73
+
74
+ def run_diarization(audio_path: str, hf_token: str, model_name: str = "pyannote/speaker-diarization-community-1") -> List[Dict[str, Any]]:
75
+ """Exécute la diarisation avec pyannote."""
76
+ if not HAS_PYANNOTE:
77
+ raise ImportError("pyannote.audio n'est pas installé")
78
+
79
+ # Convertir l'audio en WAV si nécessaire
80
+ audio_path_converted = convert_audio_if_needed(audio_path)
81
+
82
+ # Configurer le token
83
+ if hf_token:
84
+ try:
85
+ from huggingface_hub import login
86
+ login(token=hf_token, add_to_git_credential=False)
87
+ except Exception:
88
+ pass
89
+
90
+ try:
91
+ pipeline = Pipeline.from_pretrained(model_name, token=hf_token)
92
+ except Exception as e:
93
+ if "plda" in str(e).lower() or "unexpected keyword" in str(e).lower():
94
+ pipeline = Pipeline.from_pretrained("pyannote/speaker-diarization-3.1", token=hf_token)
95
+ else:
96
+ raise
97
+
98
+ if torch.cuda.is_available():
99
+ pipeline = pipeline.to(torch.device("cuda"))
100
+
101
+ diarization = pipeline(audio_path_converted)
102
+
103
+ # Convertir en segments
104
+ segments = []
105
+ speakers = sorted(diarization.labels())
106
+ speaker_mapping = {speaker: f"SPEAKER_{idx:02d}" for idx, speaker in enumerate(speakers)}
107
+
108
+ for segment, track, speaker in diarization.itertracks(yield_label=True):
109
+ normalized_speaker = speaker_mapping.get(speaker, speaker)
110
+ segments.append({
111
+ "speaker": normalized_speaker,
112
+ "start": segment.start,
113
+ "end": segment.end
114
+ })
115
+
116
+ segments.sort(key=lambda x: x["start"])
117
+ return segments
118
+
119
+
120
+ def run_transcription(audio_path: str, device: str = None, hf_token: str = None) -> List[Dict[str, Any]]:
121
+ """Exécute la transcription avec le modèle Whisper Large V3 French."""
122
+ if not HAS_WHISPER:
123
+ raise ImportError("whisper n'est pas installé")
124
+
125
+ if device is None:
126
+ device = "cuda" if torch.cuda.is_available() else "cpu"
127
+
128
+ model_id = "bofenghuang/whisper-large-v3-french"
129
+
130
+ # Utiliser Transformers pour charger le modèle
131
+ try:
132
+ if HAS_TRANSFORMERS:
133
+ processor = AutoProcessor.from_pretrained(model_id, token=hf_token)
134
+ model = AutoModelForSpeechSeq2Seq.from_pretrained(
135
+ model_id,
136
+ torch_dtype=torch.float16 if device == "cuda" else torch.float32,
137
+ low_cpu_mem_usage=True,
138
+ token=hf_token
139
+ )
140
+ model.to(device)
141
+ model.eval()
142
+
143
+ # Charger l'audio
144
+ audio_path_converted = convert_audio_if_needed(audio_path)
145
+ waveform, sample_rate = librosa.load(audio_path_converted, sr=16000, mono=True)
146
+
147
+ # Préparer les inputs
148
+ inputs = processor(
149
+ waveform,
150
+ sampling_rate=sample_rate,
151
+ return_tensors="pt"
152
+ )
153
+ inputs = {k: v.to(device) for k, v in inputs.items()}
154
+
155
+ # Transcription
156
+ with torch.no_grad():
157
+ generated_ids = model.generate(
158
+ inputs["input_features"],
159
+ language="fr",
160
+ task="transcribe",
161
+ return_timestamps=True
162
+ )
163
+
164
+ # Décoder avec timestamps
165
+ result = processor.batch_decode(
166
+ generated_ids,
167
+ skip_special_tokens=False,
168
+ output_word_timestamps=True
169
+ )[0]
170
+
171
+ # Extraire les segments avec timestamps depuis les tokens
172
+ tokens = generated_ids[0].cpu().numpy()
173
+ segments = []
174
+ current_segment = {"start": None, "end": None, "text": []}
175
+
176
+ # Parser les tokens pour extraire les timestamps
177
+ for token_id in tokens:
178
+ token_text = processor.tokenizer.decode([token_id], skip_special_tokens=False)
179
+
180
+ # Chercher les tokens de timestamp <|X.XX|>
181
+ if "<|" in token_text and "|>" in token_text:
182
+ try:
183
+ start_idx = token_text.find("<|") + 2
184
+ end_idx = token_text.find("|>")
185
+ if start_idx < end_idx:
186
+ timestamp_str = token_text[start_idx:end_idx]
187
+ timestamp = float(timestamp_str)
188
+
189
+ if current_segment["start"] is None:
190
+ current_segment["start"] = timestamp
191
+ else:
192
+ current_segment["end"] = timestamp
193
+ text = " ".join(current_segment["text"]).strip()
194
+ if text:
195
+ segments.append({
196
+ "start": current_segment["start"],
197
+ "end": current_segment["end"],
198
+ "text": text
199
+ })
200
+ current_segment = {"start": timestamp, "end": None, "text": []}
201
+ except (ValueError, IndexError):
202
+ pass
203
+ else:
204
+ if token_text.strip() and not any(x in token_text for x in ["<|", "|>", "<|startof", "<|endof", "<|notimestamps"]):
205
+ current_segment["text"].append(token_text)
206
+
207
+ # Ajouter le dernier segment
208
+ if current_segment["text"]:
209
+ text = " ".join(current_segment["text"]).strip()
210
+ if text:
211
+ duration = len(waveform) / sample_rate
212
+ segments.append({
213
+ "start": current_segment["start"] if current_segment["start"] is not None else 0.0,
214
+ "end": current_segment["end"] if current_segment["end"] is not None else duration,
215
+ "text": text
216
+ })
217
+
218
+ # Si on n'a pas réussi à extraire les timestamps, utiliser une approche de fallback
219
+ if not segments or all(seg.get("start") is None for seg in segments):
220
+ # Décoder le texte complet
221
+ result_text = processor.decode(generated_ids[0], skip_special_tokens=True)
222
+
223
+ # Diviser en phrases
224
+ sentences = []
225
+ for sent in result_text.split('. '):
226
+ if sent.strip():
227
+ sentences.append(sent.strip() + ('.' if not sent.strip().endswith('.') else ''))
228
+
229
+ if not sentences:
230
+ sentences = [result_text.strip()]
231
+
232
+ # Créer des segments temporels basés sur la durée
233
+ duration = len(waveform) / sample_rate
234
+ segments = []
235
+ time_per_sentence = duration / len(sentences)
236
+
237
+ for i, sentence in enumerate(sentences):
238
+ start_time = i * time_per_sentence
239
+ end_time = min((i + 1) * time_per_sentence, duration)
240
+ segments.append({
241
+ "start": start_time,
242
+ "end": end_time,
243
+ "text": sentence
244
+ })
245
+
246
+ return segments
247
+ except Exception as e:
248
+ # Fallback sur Whisper natif
249
+ model = whisper.load_model("large-v3", device=device)
250
+
251
+ audio_path_converted = convert_audio_if_needed(audio_path)
252
+ result = model.transcribe(
253
+ audio_path_converted,
254
+ language="fr",
255
+ task="transcribe",
256
+ verbose=False
257
+ )
258
+
259
+ segments = []
260
+ for seg in result["segments"]:
261
+ segments.append({
262
+ "start": seg["start"],
263
+ "end": seg["end"],
264
+ "text": seg["text"].strip()
265
+ })
266
+
267
+ return segments
268
+
269
+
270
+ def combine_diarization_transcription(
271
+ diarization_segments: List[Dict[str, Any]],
272
+ transcription_segments: List[Dict[str, Any]]
273
+ ) -> List[Dict[str, Any]]:
274
+ """Combine diarisation et transcription."""
275
+ combined = []
276
+
277
+ # Créer une timeline de diarisation
278
+ diar_timeline = [
279
+ (seg["start"], seg["end"], seg["speaker"])
280
+ for seg in diarization_segments
281
+ ]
282
+ diar_timeline.sort()
283
+
284
+ def get_speaker_for_segment(seg_start: float, seg_end: float) -> str:
285
+ """Détermine le locuteur pour un segment."""
286
+ speaker_time = {}
287
+
288
+ for diar_start, diar_end, speaker in diar_timeline:
289
+ overlap_start = max(seg_start, diar_start)
290
+ overlap_end = min(seg_end, diar_end)
291
+ overlap_duration = max(0, overlap_end - overlap_start)
292
+
293
+ if overlap_duration > 0:
294
+ speaker_time[speaker] = speaker_time.get(speaker, 0) + overlap_duration
295
+
296
+ if speaker_time:
297
+ return max(speaker_time, key=speaker_time.get)
298
+ else:
299
+ # Trouver le locuteur le plus proche
300
+ center_time = (seg_start + seg_end) / 2.0
301
+ min_dist = float('inf')
302
+ closest_speaker = "SPEAKER_00"
303
+ for diar_start, diar_end, speaker in diar_timeline:
304
+ if center_time < diar_start:
305
+ dist = diar_start - center_time
306
+ elif center_time >= diar_end:
307
+ dist = center_time - diar_end
308
+ else:
309
+ return speaker
310
+ if dist < min_dist:
311
+ min_dist = dist
312
+ closest_speaker = speaker
313
+ return closest_speaker
314
+
315
+ # Combiner les segments
316
+ for trans_seg in transcription_segments:
317
+ speaker = get_speaker_for_segment(trans_seg["start"], trans_seg["end"])
318
+ combined.append({
319
+ "speaker": speaker,
320
+ "start": trans_seg["start"],
321
+ "end": trans_seg["end"],
322
+ "text": trans_seg["text"]
323
+ })
324
+
325
+ return combined
326
+
327
+
328
+ def format_output(combined_segments: List[Dict[str, Any]]) -> str:
329
+ """Formate la sortie en texte lisible: "Speaker A : blabla"."""
330
+ output_lines = []
331
+
332
+ current_speaker = None
333
+ current_texts = []
334
+
335
+ for seg in combined_segments:
336
+ speaker = seg["speaker"]
337
+ text = seg["text"]
338
+
339
+ if speaker != current_speaker:
340
+ # Écrire le groupe précédent
341
+ if current_speaker and current_texts:
342
+ speaker_num = int(current_speaker.replace("SPEAKER_", ""))
343
+ speaker_name = f"Speaker {chr(65 + speaker_num)}"
344
+ output_lines.append(f"{speaker_name} : {' '.join(current_texts)}")
345
+
346
+ # Nouveau locuteur
347
+ current_speaker = speaker
348
+ current_texts = [text]
349
+ else:
350
+ # Même locuteur, ajouter le texte
351
+ current_texts.append(text)
352
+
353
+ # Écrire le dernier groupe
354
+ if current_speaker and current_texts:
355
+ speaker_num = int(current_speaker.replace("SPEAKER_", ""))
356
+ speaker_name = f"Speaker {chr(65 + speaker_num)}"
357
+ output_lines.append(f"{speaker_name} : {' '.join(current_texts)}")
358
+
359
+ return "\n\n".join(output_lines)
360
+
requirements.txt CHANGED
@@ -2,7 +2,15 @@ gradio>=4.0.0
2
  pyannote.audio>=3.0.0
3
  pyannote.core>=5.0.0
4
  torch>=2.0.0
 
5
  librosa>=0.10.0
6
  soundfile>=0.12.0
7
  huggingface-hub>=0.20.0
 
 
 
 
 
 
 
8
 
 
2
  pyannote.audio>=3.0.0
3
  pyannote.core>=5.0.0
4
  torch>=2.0.0
5
+ torchaudio>=2.0.0
6
  librosa>=0.10.0
7
  soundfile>=0.12.0
8
  huggingface-hub>=0.20.0
9
+ transformers>=4.30.0
10
+ openai-whisper>=20231117
11
+ accelerate>=0.20.0
12
+
13
+
14
+
15
+
16