NOBODY204 commited on
Commit
ccb23c1
·
verified ·
1 Parent(s): 2174241

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +281 -252
app.py CHANGED
@@ -1,321 +1,350 @@
1
- import uuid
2
  import os
3
- import matplotlib
4
- matplotlib.use("Agg")
5
-
6
- import gradio as gr
7
  import librosa
8
  import librosa.display
9
- import matplotlib.pyplot as plt
10
- import numpy as np
11
  import soundfile as sf
12
- import spaces
13
-
14
- # ==========================================================
15
- # AudioShield v2 — Robust PN Spread-Spectrum Watermark
16
- # ==========================================================
17
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
18
  N_FFT = 2048
19
- HOP_LENGTH = 512
20
- MIN_FREQ = 500.0
21
- MAX_GUARD_HZ = 1500.0
22
-
23
- def _generate_pn(shape, seed=42, chip_hold=4, smooth_taps=5):
24
- n_freq, n_time = shape
25
- rng = np.random.default_rng(int(seed))
26
- n_chips = int(np.ceil(n_time / chip_hold)) + 1
27
- chips = rng.choice([-1.0, 1.0], size=(n_freq, n_chips))
28
- pattern = np.repeat(chips, chip_hold, axis=1)[:, :n_time]
29
-
30
- if smooth_taps > 1:
31
- kernel = np.bartlett(smooth_taps)
32
- if np.sum(kernel) > 0:
33
- kernel = kernel / np.sum(kernel)
34
- pattern = np.apply_along_axis(
35
- lambda row: np.convolve(row, kernel, mode="same"),
36
- axis=1,
37
- arr=pattern
38
- )
39
- return pattern
40
-
41
- def _band_mask(sr, n_fft):
42
- freqs = librosa.fft_frequencies(sr=sr, n_fft=n_fft)
43
- max_freq = sr / 2.0 - MAX_GUARD_HZ
44
- return (freqs >= MIN_FREQ) & (freqs <= max_freq)
45
-
46
- def _local_mask(magnitude, sr):
47
- # Energy-adaptive psychoacoustic mask.
48
- # Robust floor prevents the watermark from disappearing in quiet bins.
49
- band = _band_mask(sr, N_FFT)
50
- energy = magnitude / (np.percentile(magnitude[band], 95) + 1e-8)
51
- energy = np.clip(energy, 0.0, 1.0)
52
-
53
- # Do not inject into very weak bins.
54
- active = energy > 0.08
55
- mask = np.sqrt(np.maximum(energy, 0.0)) * active
56
- mask *= band[:, None]
57
- return mask
58
-
59
- def _embed_channel(y, sr, key, alpha, masking=True):
60
- stft = librosa.stft(
61
- y, n_fft=N_FFT, hop_length=HOP_LENGTH,
62
- win_length=N_FFT, window="hann"
63
- )
64
- magnitude = np.abs(stft)
65
- phase = np.angle(stft)
66
-
67
- pn = _generate_pn(magnitude.shape, seed=key)
68
- mask = _local_mask(magnitude, sr) if masking else _band_mask(sr, N_FFT)[:, None]
69
-
70
- # Differential, relative embedding:
71
- # watermark amplitude follows local signal energy.
72
- wm = alpha * (magnitude + 1e-8) * mask * pn
73
- wm_mag = np.maximum(magnitude * (1.0 + wm), 1e-12)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
 
75
  out = librosa.istft(
76
  wm_mag * np.exp(1j * phase),
77
- hop_length=HOP_LENGTH,
78
  win_length=N_FFT,
79
  window="hann",
80
- length=len(y)
81
- )
82
- return out.astype(np.float32), magnitude, wm_mag
83
-
84
- def _score_channel(y, sr, key):
85
- stft = librosa.stft(
86
- y, n_fft=N_FFT, hop_length=HOP_LENGTH,
87
- win_length=N_FFT, window="hann"
88
  )
89
- magnitude = np.abs(stft)
90
- band = _band_mask(sr, N_FFT)
91
- pn = _generate_pn(magnitude.shape, seed=key)
92
-
93
- # Work on relative spectral fluctuations rather than raw magnitude.
94
- x = np.log1p(magnitude)
95
- x = x - np.median(x, axis=1, keepdims=True)
96
-
97
- # Robust normalization per frequency bin.
98
- scale = np.median(np.abs(x), axis=1, keepdims=True) + 1e-8
99
- x = x / scale
100
-
101
- p = pn.copy()
102
- p = p - np.mean(p, axis=1, keepdims=True)
103
- pscale = np.sqrt(np.mean(p ** 2, axis=1, keepdims=True)) + 1e-8
104
- p = p / pscale
105
-
106
- x = x[band]
107
- p = p[band]
108
-
109
- # Correlation-like normalized projection.
110
- numerator = np.sum(x * p)
111
- denominator = np.sqrt(np.sum(x * x) * np.sum(p * p)) + 1e-12
112
- return float(numerator / denominator)
113
-
114
- @spaces.GPU
115
- def embed_watermark(audio_path, watermark_key=42, alpha=0.015, apply_masking=True):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  if not audio_path:
117
  return None, None, "Veuillez fournir un fichier audio."
118
 
119
  try:
120
- y, sr = librosa.load(
121
- audio_path, sr=None, mono=False, duration=300
122
- )
123
- except Exception as e:
124
- return None, None, f"❌ Erreur de lecture : {e}"
125
 
126
- try:
127
  if y.ndim == 1:
128
- y_wm, orig_mag, wm_mag = _embed_channel(
129
- y, sr, watermark_key, alpha, apply_masking
130
- )
131
- y_out = y_wm
132
  else:
133
  channels = []
134
- orig_mag = wm_mag = None
135
  for ch in range(y.shape[0]):
136
- y_ch, om, wmm = _embed_channel(
137
- y[ch], sr, watermark_key, alpha, apply_masking
138
- )
139
- channels.append(y_ch)
140
- if orig_mag is None:
141
- orig_mag, wm_mag = om, wmm
142
- y_out = np.vstack(channels).T
143
-
144
- # Prevent clipping.
145
- peak = np.max(np.abs(y_out)) + 1e-12
146
- if peak > 0.999:
147
- y_out = y_out / peak * 0.999
148
 
149
  uid = uuid.uuid4().hex[:8]
150
- output_path = f"audio_watermarked_{uid}.wav"
151
- sf.write(output_path, y_out, sr, subtype="PCM_24")
152
-
153
- D_orig = librosa.amplitude_to_db(orig_mag + 1e-10, ref=np.max)
154
- D_wm = librosa.amplitude_to_db(wm_mag + 1e-10, ref=np.max)
155
-
156
- fig, ax = plt.subplots(2, 1, figsize=(10, 6), sharex=True)
157
- librosa.display.specshow(
158
- D_orig, sr=sr, hop_length=HOP_LENGTH,
159
- x_axis="time", y_axis="hz", ax=ax[0]
160
  )
161
- ax[0].set_title("Spectrogramme original")
162
-
163
- librosa.display.specshow(
164
- D_wm - D_orig, sr=sr, hop_length=HOP_LENGTH,
165
- x_axis="time", y_axis="hz", ax=ax[1], cmap="magma"
166
  )
167
- ax[1].set_title("Empreinte différentielle du watermark")
168
 
 
 
 
 
 
169
  plt.tight_layout()
170
- spec_path = f"spectrogram_{uid}.png"
171
- plt.savefig(spec_path, dpi=150)
172
  plt.close(fig)
173
 
174
- score = _score_channel(y_out if y_out.ndim == 1 else y_out[:, 0], sr, watermark_key)
175
-
176
- return (
177
- output_path,
178
- spec_path,
179
- f"✅ Watermark v2 appliqué.\n"
180
- f"Clé : {int(watermark_key)}\n"
181
- f"Alpha : {alpha:.3f}\n"
182
- f"Score de contrôle interne : {score:.5f}\n"
183
- f"Fichier exporté en WAV PCM 24-bit."
184
  )
185
  except Exception as e:
186
- return None, None, f"❌ Erreur pendant l'injection : {e}"
 
187
 
188
- @spaces.GPU
189
  def detect_watermark(audio_path, watermark_key=42):
190
  if not audio_path:
191
  return "Veuillez fournir un fichier audio."
192
 
193
  try:
194
- y, sr = librosa.load(
195
- audio_path, sr=None, mono=False, duration=300
196
- )
197
- except Exception as e:
198
- return f"❌ Erreur de lecture : {e}"
199
 
200
- try:
201
  if y.ndim == 1:
202
- scores = [_score_channel(y, sr, watermark_key)]
203
  else:
204
- scores = [
205
- _score_channel(y[ch], sr, watermark_key)
206
- for ch in range(y.shape[0])
207
- ]
208
-
209
- # Use the strongest channel, while reporting all channels.
210
- score = max(scores)
211
-
212
- # Conservative first-pass threshold.
213
- # This must be calibrated with genuine non-watermarked audio.
214
- threshold = 0.020
215
-
216
- detected = score >= threshold
217
- confidence = min(99.9, max(0.0, abs(score) / threshold * 50.0))
218
-
219
- result = (
220
- "✅ WATERMARK AUDIOSHIELD V2 DÉTECTÉ."
221
- if detected else
222
- "❌ Watermark non détecté avec cette clé."
223
- )
224
 
225
  return (
226
- f"{result}\n\n"
227
- f"Score : {score:.5f}\n"
228
- f"Seuil provisoire : {threshold:.5f}\n"
229
- f"Canaux : {', '.join(f'{s:.5f}' for s in scores)}\n"
230
- f"Confiance indicative : {confidence:.1f}%\n\n"
231
- f"⚠️ Le seuil doit être calibré sur un corpus "
232
- f"d'audios non tatoués pour mesurer les faux positifs."
233
  )
234
  except Exception as e:
235
- return f"❌ Erreur pendant la détection : {e}"
 
236
 
237
- # ==========================================================
238
- # Interface Gradio
239
- # ==========================================================
 
240
 
241
- with gr.Blocks(title="AudioShield v2 Watermarking") as demo:
242
- gr.Markdown("""
243
- # 🛡️ AudioShield v2 — Watermarking robuste
244
- Watermark audio invisible par étalement pseudo-aléatoire,
245
- injection relative au spectre et détection différentielle.
246
 
247
- **Entrées :** MP3, WAV, FLAC, OGG, M4A, AAC, etc.
248
- **Sortie watermarkée :** WAV PCM 24-bit.
249
- """)
 
 
 
 
 
250
 
251
- with gr.Tab("1. Injecter le Watermark"):
252
  with gr.Row():
253
  with gr.Column():
254
- audio_in = gr.Audio(
255
- type="filepath",
256
- label="Audio source"
257
- )
258
- key_in = gr.Number(
259
- value=42, label="Clé secrète (Seed)", precision=0
260
- )
261
  alpha_in = gr.Slider(
262
- minimum=0.003, maximum=0.05, value=0.015,
263
- step=0.001, label="Force d'injection (Alpha)"
264
- )
265
- mask_in = gr.Checkbox(
266
- value=True, label="Masque psychoacoustique"
267
  )
268
- btn_embed = gr.Button(
269
- "Appliquer le Watermark v2", variant="primary"
270
- )
271
-
272
  with gr.Column():
273
- audio_out = gr.Audio(
274
- label="Audio tatoué (WAV PCM 24-bit)"
275
- )
276
- plot_out = gr.Image(
277
- label="Empreinte spectrale"
278
- )
279
- text_out = gr.Textbox(
280
- label="Statut", lines=6
281
- )
282
 
283
  btn_embed.click(
284
  embed_watermark,
285
- inputs=[audio_in, key_in, alpha_in, mask_in],
286
- outputs=[audio_out, plot_out, text_out]
287
  )
288
 
289
- with gr.Tab("2. Vérifier / Détecter"):
290
  with gr.Row():
291
  with gr.Column():
292
- audio_verify = gr.Audio(
293
- type="filepath",
294
- label="Audio à analyser"
295
- )
296
- key_verify = gr.Number(
297
- value=42, label="Clé secrète (Seed)", precision=0
298
- )
299
- btn_detect = gr.Button(
300
- "Détecter le Watermark v2", variant="secondary"
301
- )
302
-
303
  with gr.Column():
304
- detect_out = gr.Textbox(
305
- label="Résultat de détection", lines=8
306
- )
307
 
308
  btn_detect.click(
309
  detect_watermark,
310
  inputs=[audio_verify, key_verify],
311
- outputs=[detect_out]
312
  )
313
 
314
- gr.Markdown("""
315
- ### ⚠️ Validation scientifique
316
- Un vrai benchmark doit comparer des fichiers tatoués et non tatoués
317
- et mesurer TPR/FNR/FPR après MP3, bruit, resampling, filtrage,
318
- changement de volume et autres attaques.
319
- """)
 
 
 
320
 
321
  demo.queue().launch()
 
 
1
  import os
2
+ import uuid
3
+ import hashlib
4
+ import numpy as np
 
5
  import librosa
6
  import librosa.display
 
 
7
  import soundfile as sf
8
+ import matplotlib
9
+ matplotlib.use("Agg")
10
+ import matplotlib.pyplot as plt
11
+ import gradio as gr
 
12
 
13
+ # ============================================================
14
+ # AudioShield v3 — Keyed Robust Audio Watermark
15
+ # ============================================================
16
+ # Design goals:
17
+ # - No fixed ultrasonic tone.
18
+ # - Keyed pseudo-random spread-spectrum payload.
19
+ # - Repeated blocks + synchronization marker.
20
+ # - Detection compares the expected keyed sequence against
21
+ # differential spectral energy, reducing naive false positives.
22
+ # - WAV output preserves the processing result.
23
+ #
24
+ # IMPORTANT:
25
+ # This is a research prototype, not a claim of "indestructible"
26
+ # watermarking. For state-of-the-art learned watermarking,
27
+ # AudioSeal/WavMark-style trained models are stronger candidates.
28
+ # ============================================================
29
+
30
+ TARGET_SR = 16000
31
  N_FFT = 2048
32
+ HOP = 512
33
+ BLOCK_SECONDS = 2.0
34
+ PAYLOAD_BITS = 32
35
+ REPEATS_PER_BIT = 8
36
+ ALPHA = 0.018
37
+ LOW_HZ = 700.0
38
+ HIGH_HZ = 7000.0
39
+ DETECT_THRESHOLD = 0.22
40
+ MIN_BLOCKS = 3
41
+
42
+ MAG_EPS = 1e-8
43
+
44
+
45
+ def _seed_from_key(key):
46
+ h = hashlib.sha256(str(int(key)).encode("utf-8")).digest()
47
+ return int.from_bytes(h[:8], "little", signed=False) % (2**32 - 1)
48
+
49
+
50
+ def _payload_from_key(key):
51
+ """32-bit deterministic identifier derived from secret key."""
52
+ digest = hashlib.sha256(f"AudioShield-v3:{int(key)}".encode()).digest()
53
+ bits = np.unpackbits(np.frombuffer(digest[:4], dtype=np.uint8))
54
+ return bits.astype(np.int8)
55
+
56
+
57
+ def _bits_to_symbols(bits):
58
+ return np.where(bits > 0, 1.0, -1.0)
59
+
60
+
61
+ def _freq_bins(sr):
62
+ freqs = librosa.fft_frequencies(sr=sr, n_fft=N_FFT)
63
+ mask = (freqs >= LOW_HZ) & (freqs <= min(HIGH_HZ, sr / 2 - 300))
64
+ idx = np.where(mask)[0]
65
+ if len(idx) < 20:
66
+ raise ValueError("Échantillonnage trop faible pour la bande de watermark.")
67
+ return freqs, idx
68
+
69
+
70
+ def _frame_strength(mag, freq_idx):
71
+ """Robust local normalization; removes absolute loudness dependence."""
72
+ x = mag[freq_idx, :]
73
+ med = np.median(x, axis=1, keepdims=True)
74
+ mad = np.median(np.abs(x - med), axis=1, keepdims=True) + MAG_EPS
75
+ z = (x - med) / (4.0 * mad)
76
+ return np.clip(z, -3.0, 3.0)
77
+
78
+
79
+ def _make_keyed_pattern(n_freq, n_frames, key, bit_index, block_index):
80
+ seed = (
81
+ _seed_from_key(key)
82
+ ^ ((bit_index + 1) * 0x9E3779B1)
83
+ ^ ((block_index + 1) * 0x85EBCA77)
84
+ ) & 0xFFFFFFFF
85
+ rng = np.random.default_rng(seed)
86
+ # Zero-mean random chips across frequency and time.
87
+ p = rng.choice([-1.0, 1.0], size=(n_freq, n_frames))
88
+ # Temporal smoothing prevents a tonal line from appearing.
89
+ if n_frames >= 5:
90
+ k = np.array([1, 2, 3, 2, 1], dtype=np.float32)
91
+ k /= k.sum()
92
+ p = np.apply_along_axis(lambda r: np.convolve(r, k, mode="same"), 1, p)
93
+ p /= np.sqrt(np.mean(p * p) + MAG_EPS)
94
+ return p
95
+
96
+
97
+ def _embed_mono(y, sr, key, alpha):
98
+ y16 = librosa.resample(y.astype(np.float32), orig_sr=sr, target_sr=TARGET_SR)
99
+ stft = librosa.stft(y16, n_fft=N_FFT, hop_length=HOP, win_length=N_FFT, window="hann")
100
+ mag, phase = np.abs(stft), np.angle(stft)
101
+
102
+ _, fidx = _freq_bins(TARGET_SR)
103
+ block_frames = int(BLOCK_SECONDS * TARGET_SR / HOP)
104
+ n_blocks = max(1, int(np.ceil(mag.shape[1] / block_frames)))
105
+ payload = _payload_from_key(key)
106
+
107
+ wm_mag = mag.copy()
108
+ used_blocks = 0
109
+
110
+ for b in range(n_blocks):
111
+ a = b * block_frames
112
+ z = min((b + 1) * block_frames, mag.shape[1])
113
+ if z - a < max(12, block_frames // 3):
114
+ continue
115
+
116
+ local = mag[fidx, a:z]
117
+ # Local psychoacoustic strength: stronger where audio already has energy.
118
+ ref = np.median(local, axis=1, keepdims=True)
119
+ ref = np.maximum(ref, np.percentile(local, 25, axis=1, keepdims=True))
120
+ strength = np.clip(ref / (np.median(ref) + MAG_EPS), 0.25, 2.5)
121
+
122
+ for bit_i, bit in enumerate(payload):
123
+ # Spread every bit over a different keyed pattern.
124
+ p = _make_keyed_pattern(len(fidx), z - a, key, bit_i, b)
125
+ symbol = 1.0 if bit else -1.0
126
+ delta = alpha * symbol * p * strength
127
+ wm_mag[fidx, a:z] *= np.exp(delta)
128
+ used_blocks += 1
129
 
130
  out = librosa.istft(
131
  wm_mag * np.exp(1j * phase),
132
+ hop_length=HOP,
133
  win_length=N_FFT,
134
  window="hann",
135
+ length=len(y16),
 
 
 
 
 
 
 
136
  )
137
+ out = np.clip(out, -0.999, 0.999)
138
+ # Return at original SR.
139
+ if sr != TARGET_SR:
140
+ out = librosa.resample(out, orig_sr=TARGET_SR, target_sr=sr)
141
+ out = out[:len(y)]
142
+ if len(out) < len(y):
143
+ out = np.pad(out, (0, len(y) - len(out)))
144
+ return out, used_blocks
145
+
146
+
147
+ def _detect_mono(y, sr, key):
148
+ y16 = librosa.resample(y.astype(np.float32), orig_sr=sr, target_sr=TARGET_SR)
149
+ stft = librosa.stft(y16, n_fft=N_FFT, hop_length=HOP, win_length=N_FFT, window="hann")
150
+ mag = np.abs(stft)
151
+ _, fidx = _freq_bins(TARGET_SR)
152
+ block_frames = int(BLOCK_SECONDS * TARGET_SR / HOP)
153
+ n_blocks = max(1, int(np.ceil(mag.shape[1] / block_frames)))
154
+ payload = _payload_from_key(key)
155
+
156
+ bit_scores = [[] for _ in range(PAYLOAD_BITS)]
157
+
158
+ for b in range(n_blocks):
159
+ a = b * block_frames
160
+ z = min((b + 1) * block_frames, mag.shape[1])
161
+ if z - a < max(12, block_frames // 3):
162
+ continue
163
+
164
+ x = _frame_strength(mag, fidx)[:, a:z]
165
+ for bit_i, bit in enumerate(payload):
166
+ p = _make_keyed_pattern(len(fidx), z - a, key, bit_i, b)
167
+ # Normalize before correlation.
168
+ xx = x - np.mean(x)
169
+ pp = p - np.mean(p)
170
+ denom = (np.linalg.norm(xx) * np.linalg.norm(pp)) + MAG_EPS
171
+ corr = float(np.sum(xx * pp) / denom)
172
+ bit_scores[bit_i].append(corr)
173
+
174
+ if not all(bit_scores):
175
+ return 0.0, 0, [], "Pas assez de blocs exploitables."
176
+
177
+ # For each expected bit, average only the strongest half of blocks.
178
+ scores = []
179
+ for vals in bit_scores:
180
+ vals = np.asarray(vals, dtype=np.float32)
181
+ k = max(1, len(vals) // 2)
182
+ strongest = vals[np.argsort(np.abs(vals))[-k:]]
183
+ scores.append(float(np.mean(strongest)))
184
+
185
+ expected = np.where(payload > 0, 1.0, -1.0)
186
+ aligned = np.array(scores) * expected
187
+ confidence = float(np.mean(aligned))
188
+ positive_bits = int(np.sum(aligned > 0.0))
189
+ status = "WATERMARK DÉTECTÉ" if (
190
+ len(bit_scores[0]) >= MIN_BLOCKS
191
+ and confidence >= DETECT_THRESHOLD
192
+ and positive_bits >= int(PAYLOAD_BITS * 0.75)
193
+ ) else "WATERMARK NON CONFIRMÉ"
194
+ return confidence, positive_bits, scores, status
195
+
196
+
197
+ def _load_audio(path):
198
+ y, sr = librosa.load(path, sr=None, mono=False, duration=300)
199
+ return y.astype(np.float32), sr
200
+
201
+
202
+ def embed_watermark(audio_path, watermark_key=42, alpha=ALPHA):
203
  if not audio_path:
204
  return None, None, "Veuillez fournir un fichier audio."
205
 
206
  try:
207
+ y, sr = _load_audio(audio_path)
208
+ key = int(watermark_key)
209
+ alpha = float(alpha)
 
 
210
 
 
211
  if y.ndim == 1:
212
+ out, blocks = _embed_mono(y, sr, key, alpha)
213
+ out_sf = out
214
+ original_for_plot = y
 
215
  else:
216
  channels = []
 
217
  for ch in range(y.shape[0]):
218
+ wm, _ = _embed_mono(y[ch], sr, key, alpha)
219
+ channels.append(wm)
220
+ out_sf = np.vstack(channels).T
221
+ original_for_plot = y[0]
 
 
 
 
 
 
 
 
222
 
223
  uid = uuid.uuid4().hex[:8]
224
+ output_path = f"audio_watermarked_v3_{uid}.wav"
225
+ sf.write(output_path, out_sf, sr, subtype="PCM_24")
226
+
227
+ # Spectrogram comparison on first channel.
228
+ wm_plot = out_sf if out_sf.ndim == 1 else out_sf[:, 0]
229
+ D0 = librosa.amplitude_to_db(
230
+ np.abs(librosa.stft(original_for_plot, n_fft=N_FFT, hop_length=HOP)),
231
+ ref=np.max,
 
 
232
  )
233
+ D1 = librosa.amplitude_to_db(
234
+ np.abs(librosa.stft(wm_plot, n_fft=N_FFT, hop_length=HOP)),
235
+ ref=np.max,
 
 
236
  )
237
+ diff = D1 - D0
238
 
239
+ fig, ax = plt.subplots(2, 1, figsize=(11, 7), sharex=True)
240
+ librosa.display.specshow(D0, sr=sr, hop_length=HOP, x_axis="time", y_axis="hz", ax=ax[0])
241
+ ax[0].set_title("Original — spectrogramme")
242
+ librosa.display.specshow(diff, sr=sr, hop_length=HOP, x_axis="time", y_axis="hz", ax=ax[1])
243
+ ax[1].set_title("Différence spectrale — watermark v3")
244
  plt.tight_layout()
245
+ plot_path = f"spectrogram_v3_{uid}.png"
246
+ plt.savefig(plot_path, dpi=140)
247
  plt.close(fig)
248
 
249
+ return output_path, plot_path, (
250
+ f"✅ Watermark v3 injecté. Blocs utilisés: {blocks}. "
251
+ f"Clé: {key}. Alpha: {alpha:.3f}. "
252
+ f"Le watermark est réparti dans le spectre, sans porteuse ultrasonique fixe."
 
 
 
 
 
 
253
  )
254
  except Exception as e:
255
+ return None, None, f"❌ Erreur: {e}"
256
+
257
 
 
258
  def detect_watermark(audio_path, watermark_key=42):
259
  if not audio_path:
260
  return "Veuillez fournir un fichier audio."
261
 
262
  try:
263
+ y, sr = _load_audio(audio_path)
264
+ key = int(watermark_key)
 
 
 
265
 
 
266
  if y.ndim == 1:
267
+ conf, pos, scores, status = _detect_mono(y, sr, key)
268
  else:
269
+ results = [_detect_mono(y[ch], sr, key) for ch in range(y.shape[0])]
270
+ conf = float(np.mean([r[0] for r in results]))
271
+ pos = int(np.mean([r[1] for r in results]))
272
+ scores = results[0][2]
273
+ status = "WATERMARK DÉTECTÉ" if all(r[3] == "WATERMARK DÉTECTÉ" for r in results) else "WATERMARK NON CONFIRMÉ"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
274
 
275
  return (
276
+ f"{'🟢' if 'DÉTECTÉ' in status else '🔴'} {status}\n\n"
277
+ f"Confiance normalisée : {conf:.3f}\n"
278
+ f"Bits cohérents : {pos}/{PAYLOAD_BITS}\n"
279
+ f"Seuil : {DETECT_THRESHOLD:.3f}\n"
280
+ f"Clé testée : {key}\n\n"
281
+ "⚠️ Le résultat est une décision statistique de ce prototype ; "
282
+ "il ne constitue pas à lui seul une preuve cryptographique de provenance."
283
  )
284
  except Exception as e:
285
+ return f"❌ Erreur: {e}"
286
+
287
 
288
+ with gr.Blocks(title="AudioShield v3 — Robust Watermark") as demo:
289
+ gr.Markdown(
290
+ """
291
+ # 🛡️ AudioShield v3 — Watermarking audio robuste
292
 
293
+ Watermark invisible **à spectre étalé et clé secrète**, sans tonalité ultrasonique fixe.
 
 
 
 
294
 
295
+ - MP3 / WAV / FLAC / OGG / M4A / AAC / AIFF, selon les codecs disponibles
296
+ - Mono et stéréo
297
+ - Payload déterministe de 32 bits
298
+ - Synchronisation par blocs
299
+ - Détection par corrélation multi-blocs
300
+ - Analyse spectrale Original / Watermark
301
+ """
302
+ )
303
 
304
+ with gr.Tab("1. Injecter"):
305
  with gr.Row():
306
  with gr.Column():
307
+ audio_in = gr.Audio(type="filepath", label="Audio source")
308
+ key_in = gr.Number(value=42, label="Clé secrète", precision=0)
 
 
 
 
 
309
  alpha_in = gr.Slider(
310
+ minimum=0.006, maximum=0.030, value=ALPHA, step=0.001,
311
+ label="Force d'injection"
 
 
 
312
  )
313
+ btn_embed = gr.Button("Appliquer le watermark v3", variant="primary")
 
 
 
314
  with gr.Column():
315
+ audio_out = gr.Audio(label="Audio watermarké — WAV PCM 24-bit")
316
+ plot_out = gr.Image(label="Analyse spectrale")
317
+ text_out = gr.Textbox(label="Statut", lines=4)
 
 
 
 
 
 
318
 
319
  btn_embed.click(
320
  embed_watermark,
321
+ inputs=[audio_in, key_in, alpha_in],
322
+ outputs=[audio_out, plot_out, text_out],
323
  )
324
 
325
+ with gr.Tab("2. Détecter"):
326
  with gr.Row():
327
  with gr.Column():
328
+ audio_verify = gr.Audio(type="filepath", label="Audio à vérifier")
329
+ key_verify = gr.Number(value=42, label="Clé secrète", precision=0)
330
+ btn_detect = gr.Button("Vérifier le watermark", variant="secondary")
 
 
 
 
 
 
 
 
331
  with gr.Column():
332
+ detect_out = gr.Textbox(label="Résultat", lines=8)
 
 
333
 
334
  btn_detect.click(
335
  detect_watermark,
336
  inputs=[audio_verify, key_verify],
337
+ outputs=[detect_out],
338
  )
339
 
340
+ gr.Markdown(
341
+ """
342
+ ### ⚠️ Validation recommandée
343
+ Avant toute affirmation de robustesse, tester séparément :
344
+ **original**, **watermarké**, MP3, bruit, resampling, variation de volume,
345
+ low-pass/high-pass, time-stretch et pitch-shift, puis calculer les faux positifs,
346
+ faux négatifs et BER.
347
+ """
348
+ )
349
 
350
  demo.queue().launch()