Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
import shutil
|
3 |
+
import zipfile
|
4 |
+
from pathlib import Path
|
5 |
+
|
6 |
+
import gradio as gr
|
7 |
+
import torch
|
8 |
+
from pydub import AudioSegment
|
9 |
+
from transformers import pipeline
|
10 |
+
|
11 |
+
# ------------------------
|
12 |
+
# CONFIG
|
13 |
+
# ------------------------
|
14 |
+
|
15 |
+
MODEL_NAME = "openai/whisper-large-v3"
|
16 |
+
device = 0 if torch.cuda.is_available() else "cpu"
|
17 |
+
|
18 |
+
pipe = pipeline(
|
19 |
+
task="automatic-speech-recognition",
|
20 |
+
model=MODEL_NAME,
|
21 |
+
device=device,
|
22 |
+
model_kwargs={"low_cpu_mem_usage": True},
|
23 |
+
)
|
24 |
+
|
25 |
+
TEMP_DIR = "./temp_audio"
|
26 |
+
os.makedirs(TEMP_DIR, exist_ok=True)
|
27 |
+
|
28 |
+
# On stocke la liste des métadonnées (segments) dans un State
|
29 |
+
# pour la conserver entre les étapes (transcription, découpe, zip).
|
30 |
+
def init_metadata_state():
|
31 |
+
return []
|
32 |
+
|
33 |
+
# ------------------------
|
34 |
+
# FONCTIONS
|
35 |
+
# ------------------------
|
36 |
+
|
37 |
+
def transcribe_audio(audio_path):
|
38 |
+
"""
|
39 |
+
Étape 2 : Transcription du fichier audio via Whisper
|
40 |
+
+ récupération de la transcription brute.
|
41 |
+
"""
|
42 |
+
if not audio_path:
|
43 |
+
return "Aucun fichier audio fourni.", [], None
|
44 |
+
|
45 |
+
# Transcription Whisper
|
46 |
+
result = pipe(audio_path, return_timestamps="word")
|
47 |
+
text = result["text"]
|
48 |
+
chunks = result["chunks"] # liste de { 'timestamp': (start, end), 'text': ... }
|
49 |
+
|
50 |
+
raw_transcription = " ".join([c["text"] for c in chunks])
|
51 |
+
|
52 |
+
# Le 3e retour = chemin du fichier audio, qu'on renverra tel quel pour la découpe
|
53 |
+
return raw_transcription, [], audio_path
|
54 |
+
|
55 |
+
|
56 |
+
def validate_segments(audio_path, table_data, metadata_state):
|
57 |
+
"""
|
58 |
+
Étape 5 : Découpe de l'audio en fonction des segments
|
59 |
+
et mise à jour du State `metadata_state`.
|
60 |
+
- `table_data` doit contenir : [ [Texte, Début(s), Fin(s), ID], ... ]
|
61 |
+
- Retourne :
|
62 |
+
1) Une liste de chemins (extraits audio) pour les players
|
63 |
+
2) La liste des nouvelles métadonnées (mise à jour).
|
64 |
+
"""
|
65 |
+
if not audio_path:
|
66 |
+
return ["Aucun fichier audio..."], metadata_state
|
67 |
+
|
68 |
+
# Nettoyage du dossier temporaire avant recréation des extraits
|
69 |
+
if os.path.exists(TEMP_DIR):
|
70 |
+
shutil.rmtree(TEMP_DIR)
|
71 |
+
os.makedirs(TEMP_DIR, exist_ok=True)
|
72 |
+
|
73 |
+
original_audio = AudioSegment.from_file(audio_path)
|
74 |
+
segment_paths = []
|
75 |
+
updated_metadata = []
|
76 |
+
|
77 |
+
for i, row in enumerate(table_data):
|
78 |
+
# row = [ segment_text, start_time, end_time, seg_id ]
|
79 |
+
if len(row) < 4:
|
80 |
+
continue
|
81 |
+
|
82 |
+
seg_text, start_time, end_time, seg_id = row
|
83 |
+
if not seg_text or start_time is None or end_time is None:
|
84 |
+
continue
|
85 |
+
|
86 |
+
# Si l'utilisateur n'a pas mis d'ID, en créer un
|
87 |
+
if not seg_id:
|
88 |
+
seg_id = f"seg_{i+1:02d}"
|
89 |
+
|
90 |
+
# Découpe
|
91 |
+
start_ms = int(float(start_time) * 1000)
|
92 |
+
end_ms = int(float(end_time) * 1000)
|
93 |
+
extract = original_audio[start_ms:end_ms]
|
94 |
+
|
95 |
+
# Nom de fichier
|
96 |
+
stem_name = Path(audio_path).stem
|
97 |
+
segment_filename = f"{stem_name}_{seg_id}.wav"
|
98 |
+
segment_filepath = os.path.join(TEMP_DIR, segment_filename)
|
99 |
+
extract.export(segment_filepath, format="wav")
|
100 |
+
|
101 |
+
segment_paths.append(segment_filepath)
|
102 |
+
|
103 |
+
# Stocker la métadonnée
|
104 |
+
updated_metadata.append({
|
105 |
+
"audio_file": segment_filename,
|
106 |
+
"text": seg_text,
|
107 |
+
"start_time": start_time,
|
108 |
+
"end_time": end_time,
|
109 |
+
"id": seg_id,
|
110 |
+
})
|
111 |
+
|
112 |
+
return segment_paths, updated_metadata
|
113 |
+
|
114 |
+
|
115 |
+
def generate_zip(metadata_state):
|
116 |
+
"""
|
117 |
+
Étape 8 : Générer un ZIP contenant tous les extraits + un metadata.csv
|
118 |
+
Retourne le chemin vers le ZIP final.
|
119 |
+
"""
|
120 |
+
if not metadata_state:
|
121 |
+
return None
|
122 |
+
|
123 |
+
# Supprimer un ancien zip si présent
|
124 |
+
zip_path = os.path.join(TEMP_DIR, "dataset.zip")
|
125 |
+
if os.path.exists(zip_path):
|
126 |
+
os.remove(zip_path)
|
127 |
+
|
128 |
+
# Créer metadata.csv
|
129 |
+
metadata_csv_path = os.path.join(TEMP_DIR, "metadata.csv")
|
130 |
+
with open(metadata_csv_path, "w", encoding="utf-8") as f:
|
131 |
+
f.write("audio_file|text|speaker_name|API\n")
|
132 |
+
for seg in metadata_state:
|
133 |
+
# Ajuste speaker_name ou API selon ton besoin
|
134 |
+
line = f"{seg['audio_file']}|{seg['text']}|projectname|/API_PHONETIC/\n"
|
135 |
+
f.write(line)
|
136 |
+
|
137 |
+
# Créer le ZIP
|
138 |
+
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
|
139 |
+
# Ajouter chaque extrait
|
140 |
+
for seg in metadata_state:
|
141 |
+
seg_file = os.path.join(TEMP_DIR, seg["audio_file"])
|
142 |
+
if os.path.exists(seg_file):
|
143 |
+
zf.write(seg_file, seg["audio_file"])
|
144 |
+
|
145 |
+
# Ajouter le metadata.csv
|
146 |
+
zf.write(metadata_csv_path, "metadata.csv")
|
147 |
+
|
148 |
+
return zip_path
|
149 |
+
|
150 |
+
|
151 |
+
def distribute_segments_to_players(segments):
|
152 |
+
"""
|
153 |
+
Transforme la liste de segments en un tuple de 20 valeurs max
|
154 |
+
(pour 20 players).
|
155 |
+
Si moins de 20 segments, on complète avec None.
|
156 |
+
"""
|
157 |
+
max_players = 20
|
158 |
+
result = []
|
159 |
+
for i in range(max_players):
|
160 |
+
if i < len(segments):
|
161 |
+
result.append(segments[i])
|
162 |
+
else:
|
163 |
+
result.append(None)
|
164 |
+
return tuple(result)
|
165 |
+
|
166 |
+
|
167 |
+
# ------------------------
|
168 |
+
# CONSTRUCTION UI GRADIO
|
169 |
+
# ------------------------
|
170 |
+
with gr.Blocks(css="style.css") as demo:
|
171 |
+
|
172 |
+
gr.Markdown("# Application de Découpage Audio + Transcription (jusqu'à 20 extraits)")
|
173 |
+
metadata_state = gr.State(init_metadata_state())
|
174 |
+
|
175 |
+
# Étape 1 : Chargement de l'audio
|
176 |
+
with gr.Box():
|
177 |
+
gr.Markdown("### 1. Téléversez votre fichier audio")
|
178 |
+
audio_input = gr.Audio(source="upload", type="filepath", label="Fichier audio")
|
179 |
+
|
180 |
+
# Étape 3 : Transcription brute
|
181 |
+
raw_transcription = gr.Textbox(
|
182 |
+
label="Transcription brute (Whisper)",
|
183 |
+
placeholder="Le texte s'affichera ici après la transcription...",
|
184 |
+
interactive=False
|
185 |
+
)
|
186 |
+
|
187 |
+
# Étape 4 : Tableau pour 20 segments max
|
188 |
+
gr.Markdown("### 2. Définissez jusqu'à 20 segments")
|
189 |
+
gr.Markdown("""**Colonnes :**
|
190 |
+
1) Texte (phrase ou portion copiée depuis la transcription)
|
191 |
+
2) Début (en secondes)
|
192 |
+
3) Fin (en secondes)
|
193 |
+
4) ID segment (optionnel)""")
|
194 |
+
|
195 |
+
table = gr.Dataframe(
|
196 |
+
headers=["Texte", "Début (s)", "Fin (s)", "ID"],
|
197 |
+
datatype=["str", "number", "number", "str"],
|
198 |
+
row_count=20, # <-- 20 lignes
|
199 |
+
col_count=4
|
200 |
+
)
|
201 |
+
|
202 |
+
validate_button = gr.Button("Valider et générer les extraits")
|
203 |
+
|
204 |
+
# Étape 6 : 20 players audio pour l'écoute
|
205 |
+
# On les organise en 5 rangées de 4 players
|
206 |
+
players = []
|
207 |
+
for i in range(20):
|
208 |
+
players.append(gr.Audio(label=f"Extrait {i+1}", interactive=False))
|
209 |
+
|
210 |
+
# Groupons-les en blocs de 4
|
211 |
+
for i in range(0, 20, 4):
|
212 |
+
with gr.Row():
|
213 |
+
for j in range(i, i+4):
|
214 |
+
players[j]
|
215 |
+
|
216 |
+
# Étape 8 : Génération ZIP
|
217 |
+
generate_button = gr.Button("Générer le fichier ZIP")
|
218 |
+
zip_file = gr.File(label="Télécharger le ZIP (audios + metadata.csv)")
|
219 |
+
|
220 |
+
# 1) Callback quand on charge l'audio => Transcription
|
221 |
+
audio_input.change(
|
222 |
+
fn=transcribe_audio,
|
223 |
+
inputs=audio_input,
|
224 |
+
outputs=[raw_transcription, table, audio_input]
|
225 |
+
)
|
226 |
+
|
227 |
+
# 2) Callback quand on valide => Découpe audio + maj metadata
|
228 |
+
validate_button.click(
|
229 |
+
fn=validate_segments,
|
230 |
+
inputs=[audio_input, table, metadata_state],
|
231 |
+
outputs=[ # 1) chemins extraits (list) 2) metadata (list)
|
232 |
+
players, # Les 20 players
|
233 |
+
metadata_state
|
234 |
+
],
|
235 |
+
# On va mapper la liste de segments sur 20 players
|
236 |
+
).then(
|
237 |
+
fn=distribute_segments_to_players,
|
238 |
+
inputs=None, # la sortie "players" (chemins) est déjà captée
|
239 |
+
outputs=players
|
240 |
+
)
|
241 |
+
|
242 |
+
# 3) Génération ZIP
|
243 |
+
generate_button.click(
|
244 |
+
fn=generate_zip,
|
245 |
+
inputs=metadata_state,
|
246 |
+
outputs=zip_file
|
247 |
+
)
|
248 |
+
|
249 |
+
demo.queue().launch()
|