Commit
·
99f6e51
1
Parent(s):
7f81391
Synthèse filtrée ≥0,5 % et barres horizontales
Browse files
app.py
CHANGED
@@ -2,23 +2,35 @@
|
|
2 |
La Fréquence du Vivant – Bio acoustic découverte
|
3 |
Comédie des Mondes Hybrides · Gaspard Boréal
|
4 |
------------------------------------------------
|
5 |
-
• Upload .wav/.mp3 (≤ 30 s)
|
6 |
-
• Prédit les
|
|
|
7 |
• Boutons : « Nouveau fichier » & « Voir tags (condensé / complet) »
|
8 |
"""
|
9 |
|
10 |
# ---------------------------- Imports ---------------------------------
|
11 |
-
import
|
12 |
import torch, torchaudio, librosa, gradio as gr
|
13 |
from transformers import pipeline
|
14 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
15 |
# ---------------------------- 1. Modèle -------------------------------
|
16 |
-
clf = pipeline(
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
device=0 if torch.cuda.is_available() else -1,
|
21 |
-
)
|
22 |
|
23 |
# ---------------------------- 2. Utils audio --------------------------
|
24 |
def load_audio(path, target_sr=16000, max_sec=30):
|
@@ -37,89 +49,106 @@ def load_audio(path, target_sr=16000, max_sec=30):
|
|
37 |
return wav, sr
|
38 |
|
39 |
def top_k_dict(d, k=5):
|
40 |
-
return {k_: v_ for k_, v_ in
|
|
|
41 |
|
42 |
# ---------------------------- 3. Analyse ------------------------------
|
|
|
|
|
43 |
def analyse(audio_path, expanded):
|
44 |
wav, sr = load_audio(audio_path)
|
45 |
-
res
|
46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
47 |
disp = full if expanded else top_k_dict(full, 5)
|
48 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
49 |
ax.set_axis_off(); plt.tight_layout()
|
50 |
-
|
51 |
-
|
52 |
-
def analyse_link(link, expanded):
|
53 |
-
if not link.startswith(("http://", "https://")):
|
54 |
-
raise gr.Error("Colle un lien commençant par http:// ou https://")
|
55 |
-
try:
|
56 |
-
r = requests.get(link, timeout=10); r.raise_for_status()
|
57 |
-
except Exception as e:
|
58 |
-
raise gr.Error(f"Téléchargement impossible : {e}")
|
59 |
-
with tempfile.NamedTemporaryFile(delete=False, suffix=".tmp") as tmp:
|
60 |
-
tmp.write(r.content); tmp_path = tmp.name
|
61 |
-
try:
|
62 |
-
return analyse(tmp_path, expanded)
|
63 |
-
finally:
|
64 |
-
os.remove(tmp_path)
|
65 |
|
66 |
# ---------------------------- 4. Interface ----------------------------
|
67 |
with gr.Blocks(title="La Fréquence du Vivant – Bio acoustic découverte") as demo:
|
68 |
|
69 |
-
gr.Markdown("# La Fréquence du Vivant
|
70 |
-
gr.Markdown("###
|
71 |
|
72 |
expanded = gr.State(False)
|
73 |
full_tags = gr.State({})
|
|
|
74 |
|
75 |
-
# Champ lien + bouton analysé uniquement si URL http(s)
|
76 |
-
with gr.Row():
|
77 |
-
url_in = gr.Textbox(lines=1,
|
78 |
-
placeholder="�� Colle un lien http(s) (Dictaphone iCloud, Dropbox, etc.)",
|
79 |
-
label="Analyse via lien web")
|
80 |
-
url_btn = gr.Button("Analyser le lien", size="sm", variant="primary", interactive=False)
|
81 |
-
|
82 |
-
# Upload local
|
83 |
audio_in = gr.Audio(sources=["upload"], type="filepath",
|
84 |
-
label="🎙️
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
85 |
|
86 |
-
#
|
87 |
gr.Markdown("**Tags AudioSet**")
|
88 |
with gr.Row():
|
89 |
reset_btn = gr.Button("🔄 Nouveau fichier / Réinitialiser", size="sm")
|
90 |
toggle_btn = gr.Button("Voir tous les tags", size="sm", variant="primary")
|
|
|
91 |
tags_out = gr.Label()
|
92 |
spec_out = gr.Plot(label="Spectrogramme")
|
93 |
|
94 |
-
#
|
95 |
def _txt(exp): return "Uniquement les principaux tags" if exp else "Voir tous les tags"
|
96 |
def flip(b): return not b
|
97 |
-
def refresh(exp, full
|
|
|
|
|
98 |
def reset_ui():
|
99 |
fig = plt.figure(figsize=(6, 2)); plt.axis("off")
|
100 |
-
return None,
|
101 |
-
|
102 |
-
# Activer bouton si URL http(s)
|
103 |
-
def enable_btn(link): return gr.update(interactive=link.startswith(("http://", "https://")))
|
104 |
-
url_in.change(enable_btn, url_in, url_btn)
|
105 |
|
106 |
-
|
107 |
-
|
|
|
108 |
|
109 |
-
# Upload local
|
110 |
-
audio_in.upload(analyse, [audio_in, expanded], [tags_out, spec_out, full_tags])
|
111 |
-
|
112 |
-
# Toggle
|
113 |
toggle_btn.click(flip, expanded, expanded)\
|
114 |
-
.then(refresh,
|
|
|
|
|
115 |
|
116 |
-
# Reset
|
117 |
reset_btn.click(
|
118 |
reset_ui, None,
|
119 |
-
[audio_in,
|
|
|
|
|
|
|
|
|
|
|
|
|
120 |
)
|
121 |
|
122 |
-
gr.Markdown("<center>2025 : Gaspard Boréal / La comédie des mondes
|
123 |
|
124 |
# ---------------------------- 5. Run ----------------------------------
|
125 |
if __name__ == "__main__":
|
|
|
2 |
La Fréquence du Vivant – Bio acoustic découverte
|
3 |
Comédie des Mondes Hybrides · Gaspard Boréal
|
4 |
------------------------------------------------
|
5 |
+
• Upload .wav/.mp3 (≤ 30 s)
|
6 |
+
• Prédit les tags AudioSet (AST 527) + spectrogramme
|
7 |
+
• Synthèse bio-acoustique : barres horizontales (≥ 0,5 %)
|
8 |
• Boutons : « Nouveau fichier » & « Voir tags (condensé / complet) »
|
9 |
"""
|
10 |
|
11 |
# ---------------------------- Imports ---------------------------------
|
12 |
+
import json, pathlib, matplotlib.pyplot as plt, pandas as pd
|
13 |
import torch, torchaudio, librosa, gradio as gr
|
14 |
from transformers import pipeline
|
15 |
|
16 |
+
# ---------------------------- 0. Ontology -----------------------------
|
17 |
+
ONTO = json.loads(pathlib.Path(__file__).with_name("ontology.json").read_text())
|
18 |
+
by_id = {n["id"]: n for n in ONTO}
|
19 |
+
name2id = {n["name"]: n["id"] for n in ONTO}
|
20 |
+
parents = {}
|
21 |
+
for n in ONTO:
|
22 |
+
for c in n.get("child_ids", []):
|
23 |
+
parents.setdefault(c, []).append(n["id"])
|
24 |
+
def root_parent(mid): # ID racine
|
25 |
+
while parents.get(mid):
|
26 |
+
mid = parents[mid][0]
|
27 |
+
return mid
|
28 |
+
|
29 |
# ---------------------------- 1. Modèle -------------------------------
|
30 |
+
clf = pipeline("audio-classification",
|
31 |
+
model="MIT/ast-finetuned-audioset-10-10-0.4593",
|
32 |
+
top_k=None,
|
33 |
+
device=0 if torch.cuda.is_available() else -1)
|
|
|
|
|
34 |
|
35 |
# ---------------------------- 2. Utils audio --------------------------
|
36 |
def load_audio(path, target_sr=16000, max_sec=30):
|
|
|
49 |
return wav, sr
|
50 |
|
51 |
def top_k_dict(d, k=5):
|
52 |
+
return {k_: v_ for k_, v_ in
|
53 |
+
sorted(d.items(), key=lambda x: x[1], reverse=True)[:k]}
|
54 |
|
55 |
# ---------------------------- 3. Analyse ------------------------------
|
56 |
+
THRESH = 0.005 # 0,5 % (score >= 0.005)
|
57 |
+
|
58 |
def analyse(audio_path, expanded):
|
59 |
wav, sr = load_audio(audio_path)
|
60 |
+
res = clf(wav.numpy().squeeze(), sampling_rate=sr)
|
61 |
+
|
62 |
+
full = {}
|
63 |
+
for d in res:
|
64 |
+
s = float(d["score"]) / 100 if d["score"] > 1 else float(d["score"])
|
65 |
+
if s >= THRESH:
|
66 |
+
full[d["label"]] = round(s, 4)
|
67 |
+
|
68 |
+
# Synthèse racine (max) puis normalisation
|
69 |
+
synth_raw = {}
|
70 |
+
for label, sc in full.items():
|
71 |
+
mid = name2id.get(label)
|
72 |
+
if mid:
|
73 |
+
root = by_id[root_parent(mid)]["name"]
|
74 |
+
synth_raw[root] = max(synth_raw.get(root, 0), sc)
|
75 |
+
tot = sum(synth_raw.values()) or 1
|
76 |
+
synth_norm = {k: sc / tot for k, sc in synth_raw.items()}
|
77 |
+
|
78 |
disp = full if expanded else top_k_dict(full, 5)
|
79 |
+
|
80 |
+
# DataFrame filtré pour BarPlot (> 0,5 %)
|
81 |
+
df = (pd.DataFrame({"Racine": synth_norm.keys(),
|
82 |
+
"Pourcent": [round(v * 100, 1) for v in synth_norm.values()]})
|
83 |
+
.query("Pourcent >= 0.5")
|
84 |
+
.sort_values("Pourcent", ascending=False))
|
85 |
+
|
86 |
+
fig, ax = plt.subplots(figsize=(6, 2))
|
87 |
+
ax.specgram(wav.numpy()[0], Fs=sr, NFFT=1024, noverlap=512)
|
88 |
ax.set_axis_off(); plt.tight_layout()
|
89 |
+
|
90 |
+
return disp, fig, full, df
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
91 |
|
92 |
# ---------------------------- 4. Interface ----------------------------
|
93 |
with gr.Blocks(title="La Fréquence du Vivant – Bio acoustic découverte") as demo:
|
94 |
|
95 |
+
gr.Markdown("# La Fréquence du Vivant")
|
96 |
+
gr.Markdown("### Écoute bio-acoustique : marche techno-sensible entre vivant, humain et machine")
|
97 |
|
98 |
expanded = gr.State(False)
|
99 |
full_tags = gr.State({})
|
100 |
+
synth_df = gr.State(pd.DataFrame())
|
101 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
102 |
audio_in = gr.Audio(sources=["upload"], type="filepath",
|
103 |
+
label="🎙️ Charger un fichier .wav / .mp3 (≤ 30 s)")
|
104 |
+
|
105 |
+
# Synthèse
|
106 |
+
gr.Markdown("**Synthèse bio-acoustique (racines)**")
|
107 |
+
synth_out = gr.BarPlot(x="Racine", y="Pourcent",
|
108 |
+
y_lim=(0, 100),
|
109 |
+
height=260,
|
110 |
+
title="Répartition par racine (%)")
|
111 |
|
112 |
+
# Détails tags
|
113 |
gr.Markdown("**Tags AudioSet**")
|
114 |
with gr.Row():
|
115 |
reset_btn = gr.Button("🔄 Nouveau fichier / Réinitialiser", size="sm")
|
116 |
toggle_btn = gr.Button("Voir tous les tags", size="sm", variant="primary")
|
117 |
+
|
118 |
tags_out = gr.Label()
|
119 |
spec_out = gr.Plot(label="Spectrogramme")
|
120 |
|
121 |
+
# Helpers
|
122 |
def _txt(exp): return "Uniquement les principaux tags" if exp else "Voir tous les tags"
|
123 |
def flip(b): return not b
|
124 |
+
def refresh(exp, full, df):
|
125 |
+
disp = full if exp else top_k_dict(full, 5)
|
126 |
+
return _txt(exp), disp, df
|
127 |
def reset_ui():
|
128 |
fig = plt.figure(figsize=(6, 2)); plt.axis("off")
|
129 |
+
return None, {}, fig, pd.DataFrame(), {}, False, "Voir tous les tags"
|
|
|
|
|
|
|
|
|
130 |
|
131 |
+
audio_in.upload(analyse,
|
132 |
+
[audio_in, expanded],
|
133 |
+
[tags_out, spec_out, full_tags, synth_df])
|
134 |
|
|
|
|
|
|
|
|
|
135 |
toggle_btn.click(flip, expanded, expanded)\
|
136 |
+
.then(refresh,
|
137 |
+
[expanded, full_tags, synth_df],
|
138 |
+
[toggle_btn, tags_out, synth_out])
|
139 |
|
|
|
140 |
reset_btn.click(
|
141 |
reset_ui, None,
|
142 |
+
[audio_in, tags_out, spec_out,
|
143 |
+
synth_out, full_tags, expanded, toggle_btn]
|
144 |
+
)
|
145 |
+
|
146 |
+
synth_df.change(
|
147 |
+
lambda d: d.query("Pourcent >= 0.5").sort_values("Pourcent", ascending=False),
|
148 |
+
synth_df, synth_out
|
149 |
)
|
150 |
|
151 |
+
gr.Markdown("<center>2025 : Gaspard Boréal / La comédie des mondes hybrides</center>")
|
152 |
|
153 |
# ---------------------------- 5. Run ----------------------------------
|
154 |
if __name__ == "__main__":
|