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

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +326 -137
app.py CHANGED
@@ -1,3 +1,4 @@
 
1
  import os
2
  import uuid
3
  import hashlib
@@ -10,97 +11,80 @@ 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
 
@@ -110,21 +94,28 @@ def _embed_mono(y, sr, key, alpha):
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(
@@ -132,24 +123,37 @@ def _embed_mono(y, sr, key, alpha):
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
 
@@ -158,47 +162,80 @@ def _detect_mono(y, sr, key):
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."
@@ -211,50 +248,100 @@ def embed_watermark(audio_path, watermark_key=42, alpha=ALPHA):
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."
@@ -264,87 +351,189 @@ def detect_watermark(audio_path, watermark_key=42):
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()
 
1
+ import spaces
2
  import os
3
  import uuid
4
  import hashlib
 
11
  import matplotlib.pyplot as plt
12
  import gradio as gr
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  TARGET_SR = 16000
15
  N_FFT = 2048
16
  HOP = 512
17
  BLOCK_SECONDS = 2.0
18
  PAYLOAD_BITS = 32
 
19
  ALPHA = 0.018
20
  LOW_HZ = 700.0
21
  HIGH_HZ = 7000.0
22
  DETECT_THRESHOLD = 0.22
23
  MIN_BLOCKS = 3
24
+ EPS = 1e-8
 
25
 
26
 
27
  def _seed_from_key(key):
28
+ h = hashlib.sha256(str(int(key)).encode()).digest()
29
+ return int.from_bytes(h[:8], "little") % (2**32 - 1)
30
 
31
 
32
  def _payload_from_key(key):
33
+ digest = hashlib.sha256(
34
+ f"AudioShield-v3:{int(key)}".encode()
35
+ ).digest()
36
+ return np.unpackbits(
37
+ np.frombuffer(digest[:4], dtype=np.uint8)
38
+ ).astype(np.int8)
 
 
39
 
40
 
41
  def _freq_bins(sr):
42
  freqs = librosa.fft_frequencies(sr=sr, n_fft=N_FFT)
43
+ upper = min(HIGH_HZ, sr / 2 - 300)
44
+ idx = np.where((freqs >= LOW_HZ) & (freqs <= upper))[0]
45
  if len(idx) < 20:
46
+ raise ValueError("Fréquence d'échantillonnage trop faible.")
47
  return freqs, idx
48
 
49
 
 
 
 
 
 
 
 
 
 
50
  def _make_keyed_pattern(n_freq, n_frames, key, bit_index, block_index):
51
  seed = (
52
  _seed_from_key(key)
53
  ^ ((bit_index + 1) * 0x9E3779B1)
54
  ^ ((block_index + 1) * 0x85EBCA77)
55
  ) & 0xFFFFFFFF
56
+
57
  rng = np.random.default_rng(seed)
 
58
  p = rng.choice([-1.0, 1.0], size=(n_freq, n_frames))
59
+
60
  if n_frames >= 5:
61
+ kernel = np.array([1, 2, 3, 2, 1], dtype=np.float32)
62
+ kernel /= kernel.sum()
63
+ p = np.apply_along_axis(
64
+ lambda row: np.convolve(row, kernel, mode="same"),
65
+ 1, p
66
+ )
67
+
68
+ p /= np.sqrt(np.mean(p * p) + EPS)
69
  return p
70
 
71
 
72
  def _embed_mono(y, sr, key, alpha):
73
+ y16 = librosa.resample(
74
+ y.astype(np.float32),
75
+ orig_sr=sr,
76
+ target_sr=TARGET_SR
77
+ )
78
+
79
+ stft = librosa.stft(
80
+ y16, n_fft=N_FFT, hop_length=HOP,
81
+ win_length=N_FFT, window="hann"
82
+ )
83
+ mag = np.abs(stft)
84
+ phase = np.angle(stft)
85
 
86
  _, fidx = _freq_bins(TARGET_SR)
87
+ block_frames = max(1, int(BLOCK_SECONDS * TARGET_SR / HOP))
88
  n_blocks = max(1, int(np.ceil(mag.shape[1] / block_frames)))
89
  payload = _payload_from_key(key)
90
 
 
94
  for b in range(n_blocks):
95
  a = b * block_frames
96
  z = min((b + 1) * block_frames, mag.shape[1])
97
+
98
  if z - a < max(12, block_frames // 3):
99
  continue
100
 
101
  local = mag[fidx, a:z]
 
102
  ref = np.median(local, axis=1, keepdims=True)
103
+ ref = np.maximum(
104
+ ref,
105
+ np.percentile(local, 25, axis=1, keepdims=True)
106
+ )
107
+ strength = np.clip(
108
+ ref / (np.median(ref) + EPS), 0.25, 2.5
109
+ )
110
 
111
  for bit_i, bit in enumerate(payload):
112
+ p = _make_keyed_pattern(
113
+ len(fidx), z - a, key, bit_i, b
114
+ )
115
  symbol = 1.0 if bit else -1.0
116
  delta = alpha * symbol * p * strength
117
  wm_mag[fidx, a:z] *= np.exp(delta)
118
+
119
  used_blocks += 1
120
 
121
  out = librosa.istft(
 
123
  hop_length=HOP,
124
  win_length=N_FFT,
125
  window="hann",
126
+ length=len(y16)
127
  )
128
+
129
  out = np.clip(out, -0.999, 0.999)
130
+
131
  if sr != TARGET_SR:
132
+ out = librosa.resample(
133
+ out, orig_sr=TARGET_SR, target_sr=sr
134
+ )
135
  out = out[:len(y)]
136
  if len(out) < len(y):
137
  out = np.pad(out, (0, len(y) - len(out)))
138
+
139
  return out, used_blocks
140
 
141
 
142
  def _detect_mono(y, sr, key):
143
+ y16 = librosa.resample(
144
+ y.astype(np.float32),
145
+ orig_sr=sr,
146
+ target_sr=TARGET_SR
147
+ )
148
+
149
+ stft = librosa.stft(
150
+ y16, n_fft=N_FFT, hop_length=HOP,
151
+ win_length=N_FFT, window="hann"
152
+ )
153
  mag = np.abs(stft)
154
+
155
  _, fidx = _freq_bins(TARGET_SR)
156
+ block_frames = max(1, int(BLOCK_SECONDS * TARGET_SR / HOP))
157
  n_blocks = max(1, int(np.ceil(mag.shape[1] / block_frames)))
158
  payload = _payload_from_key(key)
159
 
 
162
  for b in range(n_blocks):
163
  a = b * block_frames
164
  z = min((b + 1) * block_frames, mag.shape[1])
165
+
166
  if z - a < max(12, block_frames // 3):
167
  continue
168
 
169
+ x = mag[fidx, a:z]
170
+ med = np.median(x, axis=1, keepdims=True)
171
+ mad = np.median(np.abs(x - med), axis=1, keepdims=True) + EPS
172
+ x = np.clip((x - med) / (4.0 * mad), -3.0, 3.0)
173
+
174
  for bit_i, bit in enumerate(payload):
175
+ p = _make_keyed_pattern(
176
+ len(fidx), z - a, key, bit_i, b
177
+ )
178
+
179
  xx = x - np.mean(x)
180
  pp = p - np.mean(p)
181
+
182
+ denom = (
183
+ np.linalg.norm(xx) *
184
+ np.linalg.norm(pp)
185
+ ) + EPS
186
+
187
  corr = float(np.sum(xx * pp) / denom)
188
  bit_scores[bit_i].append(corr)
189
 
190
  if not all(bit_scores):
191
+ return 0.0, 0, "Pas assez de blocs exploitables."
192
 
 
193
  scores = []
194
+
195
  for vals in bit_scores:
196
  vals = np.asarray(vals, dtype=np.float32)
197
  k = max(1, len(vals) // 2)
198
+ strongest = vals[
199
+ np.argsort(np.abs(vals))[-k:]
200
+ ]
201
  scores.append(float(np.mean(strongest)))
202
 
203
  expected = np.where(payload > 0, 1.0, -1.0)
204
+ aligned = np.asarray(scores) * expected
205
+
206
  confidence = float(np.mean(aligned))
207
+ positive_bits = int(np.sum(aligned > 0))
208
+
209
+ detected = (
210
  len(bit_scores[0]) >= MIN_BLOCKS
211
  and confidence >= DETECT_THRESHOLD
212
  and positive_bits >= int(PAYLOAD_BITS * 0.75)
213
+ )
214
+
215
+ return (
216
+ confidence,
217
+ positive_bits,
218
+ "WATERMARK DÉTECTÉ"
219
+ if detected else
220
+ "WATERMARK NON CONFIRMÉ"
221
+ )
222
 
223
 
224
  def _load_audio(path):
225
+ y, sr = librosa.load(
226
+ path, sr=None, mono=False, duration=300
227
+ )
228
  return y.astype(np.float32), sr
229
 
230
 
231
+ # ------------------------------------------------------------
232
+ # ZeroGPU functions
233
+ # ------------------------------------------------------------
234
+ # The @spaces.GPU decorator is required by Hugging Face
235
+ # ZeroGPU. Keep it on the OUTER processing functions.
236
+ # ------------------------------------------------------------
237
+
238
+ @spaces.GPU(duration=120)
239
  def embed_watermark(audio_path, watermark_key=42, alpha=ALPHA):
240
  if not audio_path:
241
  return None, None, "Veuillez fournir un fichier audio."
 
248
  if y.ndim == 1:
249
  out, blocks = _embed_mono(y, sr, key, alpha)
250
  out_sf = out
251
+ original = y
252
  else:
253
  channels = []
254
+ blocks = 0
255
+
256
  for ch in range(y.shape[0]):
257
+ wm, b = _embed_mono(
258
+ y[ch], sr, key, alpha
259
+ )
260
  channels.append(wm)
261
+ blocks = max(blocks, b)
262
+
263
  out_sf = np.vstack(channels).T
264
+ original = y[0]
265
 
266
  uid = uuid.uuid4().hex[:8]
267
+ output_path = (
268
+ f"audio_watermarked_v3_{uid}.wav"
269
+ )
270
+
271
+ sf.write(
272
+ output_path,
273
+ out_sf,
274
+ sr,
275
+ subtype="PCM_24"
276
+ )
277
+
278
+ wm_plot = (
279
+ out_sf if out_sf.ndim == 1
280
+ else out_sf[:, 0]
281
+ )
282
 
 
 
283
  D0 = librosa.amplitude_to_db(
284
+ np.abs(librosa.stft(
285
+ original,
286
+ n_fft=N_FFT,
287
+ hop_length=HOP
288
+ )),
289
+ ref=np.max
290
  )
291
+
292
  D1 = librosa.amplitude_to_db(
293
+ np.abs(librosa.stft(
294
+ wm_plot,
295
+ n_fft=N_FFT,
296
+ hop_length=HOP
297
+ )),
298
+ ref=np.max
299
+ )
300
+
301
+ fig, ax = plt.subplots(
302
+ 2, 1, figsize=(11, 7), sharex=True
303
+ )
304
+
305
+ librosa.display.specshow(
306
+ D0, sr=sr, hop_length=HOP,
307
+ x_axis="time", y_axis="hz",
308
+ ax=ax[0]
309
+ )
310
+ ax[0].set_title(
311
+ "Original — spectrogramme"
312
+ )
313
+
314
+ librosa.display.specshow(
315
+ D1 - D0, sr=sr, hop_length=HOP,
316
+ x_axis="time", y_axis="hz",
317
+ ax=ax[1]
318
+ )
319
+ ax[1].set_title(
320
+ "Différence spectrale — Watermark v3"
321
  )
 
322
 
 
 
 
 
 
323
  plt.tight_layout()
324
+
325
  plot_path = f"spectrogram_v3_{uid}.png"
326
  plt.savefig(plot_path, dpi=140)
327
  plt.close(fig)
328
 
329
+ return (
330
+ output_path,
331
+ plot_path,
332
+ " Watermark v3 injecté.\n"
333
+ f"Blocs utilisés : {blocks}\n"
334
+ f"Clé : {key}\n"
335
+ f"Alpha : {alpha:.3f}\n\n"
336
+ "Watermark réparti dans le spectre "
337
+ "sans porteuse ultrasonique fixe."
338
  )
339
+
340
  except Exception as e:
341
+ return None, None, f"❌ Erreur : {e}"
342
 
343
 
344
+ @spaces.GPU(duration=120)
345
  def detect_watermark(audio_path, watermark_key=42):
346
  if not audio_path:
347
  return "Veuillez fournir un fichier audio."
 
351
  key = int(watermark_key)
352
 
353
  if y.ndim == 1:
354
+ conf, bits, status = _detect_mono(
355
+ y, sr, key
356
+ )
357
  else:
358
+ results = [
359
+ _detect_mono(y[ch], sr, key)
360
+ for ch in range(y.shape[0])
361
+ ]
362
+
363
+ conf = float(
364
+ np.mean([r[0] for r in results])
365
+ )
366
+ bits = int(
367
+ np.mean([r[1] for r in results])
368
+ )
369
+ status = (
370
+ "WATERMARK DÉTECTÉ"
371
+ if all(
372
+ r[2] == "WATERMARK DÉTECTÉ"
373
+ for r in results
374
+ )
375
+ else
376
+ "WATERMARK NON CONFIRMÉ"
377
+ )
378
+
379
+ icon = (
380
+ "🟢"
381
+ if status == "WATERMARK DÉTECTÉ"
382
+ else "🔴"
383
+ )
384
 
385
  return (
386
+ f"{icon} {status}\n\n"
387
+ f"Confiance : {conf:.3f}\n"
388
+ f"Bits cohérents : {bits}/{PAYLOAD_BITS}\n"
389
  f"Seuil : {DETECT_THRESHOLD:.3f}\n"
390
  f"Clé testée : {key}\n\n"
391
+ "⚠️ Résultat statistique du prototype. "
392
+ "Ce résultat n'est pas une preuve cryptographique "
393
+ "de provenance."
394
  )
395
+
396
  except Exception as e:
397
+ return f"❌ Erreur : {e}"
398
+
399
 
400
+ # ------------------------------------------------------------
401
+ # Interface
402
+ # ------------------------------------------------------------
403
+
404
+ with gr.Blocks(
405
+ title="AudioShield v3 — Robust Watermark"
406
+ ) as demo:
407
 
 
408
  gr.Markdown(
409
  """
410
  # 🛡️ AudioShield v3 — Watermarking audio robuste
411
 
412
+ Watermark invisible **à spectre étalé et clé secrète**,
413
+ sans tonalité ultrasonique fixe.
414
 
415
+ - MP3 / WAV / FLAC / OGG / M4A / AAC / AIFF
416
  - Mono et stéréo
417
  - Payload déterministe de 32 bits
418
+ - Répétition par blocs
419
+ - Détection multi-blocs
420
+ - Score de confiance
421
  - Analyse spectrale Original / Watermark
422
  """
423
  )
424
 
425
+ with gr.Tab("1. Injecter le Watermark"):
426
+
427
  with gr.Row():
428
+
429
  with gr.Column():
430
+
431
+ audio_in = gr.Audio(
432
+ type="filepath",
433
+ label="Audio source"
434
+ )
435
+
436
+ key_in = gr.Number(
437
+ value=42,
438
+ label="Clé secrète",
439
+ precision=0
440
+ )
441
+
442
  alpha_in = gr.Slider(
443
+ minimum=0.006,
444
+ maximum=0.030,
445
+ value=ALPHA,
446
+ step=0.001,
447
  label="Force d'injection"
448
  )
449
+
450
+ btn_embed = gr.Button(
451
+ "Appliquer le Watermark v3",
452
+ variant="primary"
453
+ )
454
+
455
  with gr.Column():
456
+
457
+ audio_out = gr.Audio(
458
+ label="Audio watermarké — WAV PCM 24-bit"
459
+ )
460
+
461
+ plot_out = gr.Image(
462
+ label="Analyse spectrale"
463
+ )
464
+
465
+ text_out = gr.Textbox(
466
+ label="Statut",
467
+ lines=6
468
+ )
469
 
470
  btn_embed.click(
471
  embed_watermark,
472
+ inputs=[
473
+ audio_in,
474
+ key_in,
475
+ alpha_in
476
+ ],
477
+ outputs=[
478
+ audio_out,
479
+ plot_out,
480
+ text_out
481
+ ]
482
  )
483
 
484
+ with gr.Tab("2. Vérifier / Détecter"):
485
+
486
  with gr.Row():
487
+
488
  with gr.Column():
489
+
490
+ audio_verify = gr.Audio(
491
+ type="filepath",
492
+ label="Audio à vérifier"
493
+ )
494
+
495
+ key_verify = gr.Number(
496
+ value=42,
497
+ label="Clé secrète",
498
+ precision=0
499
+ )
500
+
501
+ btn_detect = gr.Button(
502
+ "Vérifier le Watermark",
503
+ variant="secondary"
504
+ )
505
+
506
  with gr.Column():
507
+
508
+ detect_out = gr.Textbox(
509
+ label="Résultat de détection",
510
+ lines=9
511
+ )
512
 
513
  btn_detect.click(
514
  detect_watermark,
515
+ inputs=[
516
+ audio_verify,
517
+ key_verify
518
+ ],
519
+ outputs=[detect_out]
520
  )
521
 
522
  gr.Markdown(
523
  """
524
+ ### ⚠️ Validation
525
+
526
+ Tester séparément :
527
+
528
+ **ORIGINAL doit rester NON CONFIRMÉ**
529
+
530
+ **WATERMARKÉ → doit être DÉTECTÉ**
531
+
532
+ Puis tester MP3, bruit, resampling, variation de volume,
533
+ low-pass/high-pass, time-stretch et pitch-shift.
534
  """
535
  )
536
 
537
+
538
+ # Required for ZeroGPU request handling.
539
  demo.queue().launch()