mlr2000 commited on
Commit
66157f2
·
verified ·
1 Parent(s): fd1c30c

Upload folder using huggingface_hub

Browse files
README.md ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ library_name: transformers
3
+ tags:
4
+ - audio
5
+ - watermark
6
+ - watermark-detection
7
+ - provenance
8
+ - vocbulwark
9
+ license: cc-by-4.0
10
+ ---
11
+
12
+ # VocBulwark watermark detector
13
+
14
+ Standalone detector for the fixed **32-bit** provenance watermark embedded by
15
+ the companion VocBulwark vocoder. Given an audio clip, it extracts the watermark
16
+ bits with the Cage extractor and compares them to the known fixed code, reporting
17
+ how many bits match.
18
+
19
+ Self-contained: loads with `trust_remote_code=True`, no training repo required.
20
+
21
+ ## Companion Models
22
+
23
+ This detector is part of a set of 6 repositories:
24
+
25
+ | Repo | Role |
26
+ |------|------|
27
+ | `mlr2000/vocoder-large` | Large vocoder |
28
+ | `mlr2000/vocoder-large-watermark-detector` | Watermark detector for the large model |
29
+ | `mlr2000/vocoder-large-speaker-encoder` | Speaker encoder for the large model |
30
+ | `mlr2000/vocoder-small` | Small vocoder (generates the watermarked audio) |
31
+ | `mlr2000/vocoder-small-watermark-detector` | Watermark detector (this repo) |
32
+ | `mlr2000/vocoder-small-speaker-encoder` | Speaker encoder for the small model |
33
+
34
+ ## Usage
35
+
36
+ ```python
37
+ import torchaudio
38
+ from transformers import AutoModel
39
+
40
+ det = AutoModel.from_pretrained("mlr2000/vocoder-small-watermark-detector", trust_remote_code=True).eval()
41
+
42
+ wav, sr = torchaudio.load("clip.wav") # [C, T]
43
+ res = det.detect(wav, input_sample_rate=sr) # resampled to 24 kHz internally
44
+ print(res)
45
+ # {'matches': 32, 'n_bits': 32, 'p_value': 8.9e-16} # from our model
46
+ ```
47
+
48
+ See **`example_roundtrip.ipynb`** in this repo for an end-to-end example
49
+ (generate with the vocoder → detect here).
50
+
51
+ ## How detection works
52
+
53
+ `detect()` returns a dict with:
54
+
55
+ - `matches`: how many of the 32 extracted bits equal the fixed code.
56
+ - `n_bits`: 32.
57
+ - `p_value`: the probability an *unrelated* clip matches at least this well under
58
+ `Binomial(32, 0.5)`.
59
+
60
+ A clip generated by our vocoder matches **all 32 bits** (`p_value` ~ 0); an
61
+ unrelated clip matches about half of them (`p_value` ~ 1). There is no built-in
62
+ yes/no threshold — read `matches` / `p_value` and pick whatever operating point
63
+ you need. Requiring all 32 bits gives an astronomically small false-positive
64
+ rate; allowing a few mismatches trades that for robustness to lossy channels.
65
+
66
+ ## Notes
67
+
68
+ - Detection runs on **24 kHz mono**. Other inputs are resampled when you pass `input_sample_rate`, and multi-channel audio is downmixed to mono.
69
+ - This detector is paired with `mlr2000/vocoder-small`. It will not correctly verify audio generated by the large vocoder (`mlr2000/vocoder-large`), which uses a different fixed 50-bit watermark.
70
+ - A fixed public watermark is for **attribution** ("did our model make this"), not a secret mark.
71
+ - Not intended for surveillance or any use that violates the privacy of individuals.
72
+
73
+ ## Citation
74
+
75
+ If you use this model, please cite:
76
+
77
+ ```bibtex
78
+ @misc{muletta2026,
79
+ title = {Training a Discriminator-Free Foundation Vocoder
80
+ with Integrated Audio Watermarking},
81
+ author = {Muletta, Romolo and Deriu, Jan},
82
+ year = {2026},
83
+ note = {VT2 Project Report, ZHAW School of Engineering}
84
+ }
85
+ ```
86
+
87
+ ## License
88
+
89
+ `cc-by-4.0`. Trained on MLS (CC-BY-4.0) and Common Voice (CC0). Builds
90
+ on BigVGAN (MIT) and wav2vec 2.0 (Apache-2.0). Please retain attribution
91
+ when redistributing or building on this model.
__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from .configuration_detector import CageDetectorConfig
2
+ from .modeling_detector import CageDetector, DetectionOutput
3
+
4
+ __all__ = ["CageDetectorConfig", "CageDetector", "DetectionOutput"]
cage_config.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from transformers import PretrainedConfig
2
+
3
+
4
+ class CageExtractorConfig(PretrainedConfig):
5
+ model_type = "cage_extractor"
6
+
7
+ def __init__(
8
+ self,
9
+ watermark_bits: int = 100,
10
+ in_channels: int = 1,
11
+ fine_kernel: int = 3,
12
+ mid_kernel: int = 5,
13
+ coarse_kernel: int = 7,
14
+ sample_rate: int = 24000,
15
+ **kwargs,
16
+ ):
17
+ super().__init__(**kwargs)
18
+ self.watermark_bits = watermark_bits
19
+ self.in_channels = in_channels
20
+ self.fine_kernel = fine_kernel
21
+ self.mid_kernel = mid_kernel
22
+ self.coarse_kernel = coarse_kernel
23
+ self.sample_rate = sample_rate
cage_extractor.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Coarse-to-Fine Gated Extractor (Cage) from VocBulwark (Appendix B.2).
3
+
4
+ Architecture from paper:
5
+ - Three branches (fine/mid/coarse), each with 4 GSCMs
6
+ - Channel progression: 1 → 32 → 64 → 128 → 128
7
+ - Fine: k=3, s=2, p=1; Mid: k=5, s=2, p=2; Coarse: k=7, s=2, p=3
8
+ - Each GSCM has InstanceNorm + LeakyReLU
9
+ - Each branch output → 1D adaptive average pooling
10
+ - Fusion: GSCM(384→256, k=1, s=1, p=0) → GSCM(256, k=3, s=1, p=1)
11
+ - Dual-path pooling (AAP + AMP) → arithmetic mean
12
+ - Decoder: Linear(256, 512) → Linear(512, watermark_bits)
13
+ """
14
+
15
+ import torch
16
+ import torch.nn as nn
17
+ from transformers import PreTrainedModel
18
+
19
+ from .cage_config import CageExtractorConfig
20
+
21
+
22
+ class GatedSeparableConvModule(nn.Module):
23
+ """GSCM: Gated Separable Convolution Module (Sec 4.3 + Appendix B.2).
24
+
25
+ Depthwise separable conv with gating mechanism.
26
+ Uses InstanceNorm and LeakyReLU as per paper.
27
+ Supports stride and different input/output channels.
28
+ """
29
+
30
+ def __init__(
31
+ self,
32
+ in_channels: int,
33
+ out_channels: int,
34
+ kernel_size: int = 3,
35
+ stride: int = 1,
36
+ padding: int = 1,
37
+ ):
38
+ super().__init__()
39
+
40
+ # Content branch (DSC): depthwise → pointwise → InstanceNorm → LeakyReLU
41
+ self.content = nn.Sequential(
42
+ nn.Conv1d(in_channels, in_channels, kernel_size,
43
+ stride=stride, padding=padding, groups=in_channels),
44
+ nn.Conv1d(in_channels, out_channels, 1),
45
+ nn.InstanceNorm1d(out_channels),
46
+ nn.LeakyReLU(0.2),
47
+ )
48
+
49
+ # Gating branch: same structure but Sigmoid output
50
+ self.gate = nn.Sequential(
51
+ nn.Conv1d(in_channels, in_channels, kernel_size,
52
+ stride=stride, padding=padding, groups=in_channels),
53
+ nn.Conv1d(in_channels, out_channels, 1),
54
+ nn.InstanceNorm1d(out_channels),
55
+ nn.Sigmoid(),
56
+ )
57
+
58
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
59
+ return self.content(x) * self.gate(x)
60
+
61
+
62
+ def _make_branch(kernel_size: int, stride: int, padding: int):
63
+ """Build a 4-GSCM branch with channel progression 1 → 32 → 64 → 128 → 128."""
64
+ return nn.Sequential(
65
+ GatedSeparableConvModule(1, 32, kernel_size, stride, padding),
66
+ GatedSeparableConvModule(32, 64, kernel_size, stride, padding),
67
+ GatedSeparableConvModule(64, 128, kernel_size, stride, padding),
68
+ GatedSeparableConvModule(128, 128, kernel_size, stride, padding),
69
+ )
70
+
71
+
72
+ class CageExtractor(PreTrainedModel):
73
+ config_class = CageExtractorConfig
74
+
75
+ def __init__(self, config: CageExtractorConfig):
76
+ super().__init__(config)
77
+
78
+ # Three branches processing raw audio directly (paper B.2)
79
+ # Fine-grained: k=3, s=2, p=1
80
+ self.fine_branch = _make_branch(
81
+ kernel_size=config.fine_kernel, stride=2, padding=1)
82
+ # Mid-grained: k=5, s=2, p=2
83
+ self.mid_branch = _make_branch(
84
+ kernel_size=config.mid_kernel, stride=2, padding=2)
85
+ # Coarse-grained: k=7, s=2, p=3
86
+ self.coarse_branch = _make_branch(
87
+ kernel_size=config.coarse_kernel, stride=2, padding=3)
88
+
89
+ # Fusion: concat 3 branches (3*128=384) → reduce to 256 → refine
90
+ # GSCM (k=1, s=1, p=0) to reduce channel dimension to 256
91
+ self.fusion_reduce = GatedSeparableConvModule(
92
+ 384, 256, kernel_size=1, stride=1, padding=0)
93
+ # GSCM (k=3, s=1, p=1) that maintains the dimensions
94
+ self.fusion_refine = GatedSeparableConvModule(
95
+ 256, 256, kernel_size=3, stride=1, padding=1)
96
+
97
+ # Decoder: two FC layers (paper B.2)
98
+ self.decoder = nn.Sequential(
99
+ nn.Linear(256, 512),
100
+ nn.LeakyReLU(0.2),
101
+ nn.Linear(512, config.watermark_bits),
102
+ )
103
+
104
+ def forward(self, audio: torch.Tensor) -> torch.Tensor:
105
+ """
106
+ Args:
107
+ audio: [B, 1, T] waveform (or [B, T] which gets unsqueezed)
108
+ Returns:
109
+ logits: [B, watermark_bits] raw logits
110
+ """
111
+ if audio.dim() == 2:
112
+ audio = audio.unsqueeze(1)
113
+
114
+ # Multi-scale branches (each processes raw audio)
115
+ x_fine = self.fine_branch(audio) # [B, 128, T']
116
+ x_mid = self.mid_branch(audio) # [B, 128, T'']
117
+ x_coarse = self.coarse_branch(audio) # [B, 128, T''']
118
+
119
+ # 1D adaptive average pooling to standardize temporal dimension
120
+ pool_t = min(x_fine.shape[-1], x_mid.shape[-1], x_coarse.shape[-1])
121
+ x_fine = nn.functional.adaptive_avg_pool1d(x_fine, pool_t)
122
+ x_mid = nn.functional.adaptive_avg_pool1d(x_mid, pool_t)
123
+ x_coarse = nn.functional.adaptive_avg_pool1d(x_coarse, pool_t)
124
+
125
+ # Fusion
126
+ x_fuse = torch.cat([x_fine, x_mid, x_coarse], dim=1) # [B, 384, pool_t]
127
+ x_fuse = self.fusion_reduce(x_fuse) # [B, 256, pool_t]
128
+ x_fuse = self.fusion_refine(x_fuse) # [B, 256, pool_t]
129
+
130
+ # Dual-path Pooling: AAP + AMP → arithmetic mean
131
+ x_aap = nn.functional.adaptive_avg_pool1d(x_fuse, 1).squeeze(-1) # [B, 256]
132
+ x_amp = nn.functional.adaptive_max_pool1d(x_fuse, 1).squeeze(-1) # [B, 256]
133
+ x_pool = (x_aap + x_amp) / 2
134
+
135
+ # Decode
136
+ logits = self.decoder(x_pool) # [B, watermark_bits]
137
+
138
+ return logits
config.json ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "model_type": "cage_detector",
3
+ "architectures": [
4
+ "CageDetector"
5
+ ],
6
+ "auto_map": {
7
+ "AutoConfig": "configuration_detector.CageDetectorConfig",
8
+ "AutoModel": "modeling_detector.CageDetector"
9
+ },
10
+ "watermark_bits": 32,
11
+ "in_channels": 1,
12
+ "fine_kernel": 3,
13
+ "mid_kernel": 5,
14
+ "coarse_kernel": 7,
15
+ "sample_rate": 24000,
16
+ "fixed_watermark": [
17
+ 1,
18
+ 0,
19
+ 1,
20
+ 1,
21
+ 0,
22
+ 0,
23
+ 1,
24
+ 0,
25
+ 1,
26
+ 1,
27
+ 0,
28
+ 1,
29
+ 0,
30
+ 0,
31
+ 1,
32
+ 1,
33
+ 0,
34
+ 1,
35
+ 1,
36
+ 0,
37
+ 1,
38
+ 0,
39
+ 0,
40
+ 1,
41
+ 1,
42
+ 1,
43
+ 0,
44
+ 0,
45
+ 1,
46
+ 0,
47
+ 1,
48
+ 1
49
+ ]
50
+ }
configuration_detector.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Config for the standalone Cage watermark detector."""
2
+ from .cage_config import CageExtractorConfig
3
+
4
+
5
+ class CageDetectorConfig(CageExtractorConfig):
6
+ model_type = "cage_detector"
7
+
8
+ def __init__(self, fixed_watermark=None, **kwargs):
9
+ super().__init__(**kwargs)
10
+ # The fixed signature this detector checks extracted bits against.
11
+ self.fixed_watermark = fixed_watermark
example_roundtrip.ipynb ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# VocBulwark watermark round-trip\n",
8
+ "\n",
9
+ "The full pipeline, in three self-contained models loaded from the Hub:\n",
10
+ "\n",
11
+ "1. **Speaker encoder** — a reference clip → a fixed-size speaker embedding (*whose* voice).\n",
12
+ "2. **Vocoder** — a mel spectrogram (*what* is said) + that embedding → a 24 kHz waveform, with the fixed provenance watermark embedded automatically.\n",
13
+ "3. **Detector** — the waveform → how many watermark bits match.\n",
14
+ "\n",
15
+ "In a real TTS system you usually already have the mel (from an acoustic model) and the speaker embedding (precomputed once per speaker), so step 1 is only needed to *make* an embedding from audio. Here we reconstruct one clip (use it as both content and speaker reference) to exercise the watermark.\n",
16
+ "\n",
17
+ "Needs only `torch`, `transformers`, `soundfile`, `torchaudio`. Put one or more `.wav` files in an `example_audio/` folder next to this notebook. If the repos are private, run `huggingface-cli login` first."
18
+ ]
19
+ },
20
+ {
21
+ "cell_type": "code",
22
+ "execution_count": null,
23
+ "metadata": {},
24
+ "outputs": [],
25
+ "source": [
26
+ "from pathlib import Path\n",
27
+ "\n",
28
+ "SPK_REPO = \"mlr2000/vocoder-small-speaker-encoder\" # <- speaker encoder\n",
29
+ "GEN_REPO = \"mlr2000/vocoder-small\" # <- vocoder (paired with SPK_REPO)\n",
30
+ "DET_REPO = \"mlr2000/vocoder-small-watermark-detector\" # <- matching detector\n",
31
+ "TGT_SR = 24000 # vocoder output SR\n",
32
+ "MAX_SAMPLES = 16 * TGT_SR\n",
33
+ "\n",
34
+ "AUDIO_DIR = next(p for p in [Path(\"example_audio\"), Path(\"../example_audio\")] if p.exists())"
35
+ ]
36
+ },
37
+ {
38
+ "cell_type": "code",
39
+ "execution_count": null,
40
+ "metadata": {},
41
+ "outputs": [],
42
+ "source": [
43
+ "import torch\n",
44
+ "from transformers import AutoModel\n",
45
+ "\n",
46
+ "enc = AutoModel.from_pretrained(SPK_REPO, trust_remote_code=True).eval()\n",
47
+ "gen = AutoModel.from_pretrained(GEN_REPO, trust_remote_code=True).eval()\n",
48
+ "det = AutoModel.from_pretrained(DET_REPO, trust_remote_code=True).eval()\n",
49
+ "RAW_SR = enc.config.raw_sample_rate # SR the speaker encoder expects\n",
50
+ "print(f\"embedding dim: {enc.config.embedding_size} | watermark: {len(gen.config.fixed_watermark)} bits\")"
51
+ ]
52
+ },
53
+ {
54
+ "cell_type": "code",
55
+ "execution_count": null,
56
+ "metadata": {},
57
+ "outputs": [],
58
+ "source": [
59
+ "import soundfile as sf\n",
60
+ "import torchaudio.functional as AF\n",
61
+ "from transformers import WhisperFeatureExtractor\n",
62
+ "\n",
63
+ "# Mel front-end — must match the trained vocoder (n_fft=1024, hop=256, mel channels from config).\n",
64
+ "N_MELS = gen.config.hifigan_in_channels\n",
65
+ "mel_fe = WhisperFeatureExtractor(sampling_rate=TGT_SR, n_fft=1024, feature_size=N_MELS, hop_length=256)\n",
66
+ "mel_fe.n_samples = MAX_SAMPLES\n",
67
+ "mel_fe.chunk_length = MAX_SAMPLES / TGT_SR\n",
68
+ "\n",
69
+ "def load_inputs(path):\n",
70
+ " \"\"\"A clip -> (mel = content, raw = speaker reference @ RAW_SR).\"\"\"\n",
71
+ " w, sr = sf.read(str(path))\n",
72
+ " w = torch.tensor(w, dtype=torch.float32)\n",
73
+ " if w.dim() == 2:\n",
74
+ " w = w.mean(1) # mono\n",
75
+ " raw = AF.resample(w, sr, RAW_SR) if sr != RAW_SR else w # speaker ref @ RAW_SR\n",
76
+ " tgt = AF.resample(raw, RAW_SR, TGT_SR) # content @ 24 kHz\n",
77
+ " mel = mel_fe(tgt.numpy(), sampling_rate=TGT_SR, padding=\"longest\",\n",
78
+ " return_tensors=\"pt\")[\"input_features\"]\n",
79
+ " return mel, raw.unsqueeze(0)"
80
+ ]
81
+ },
82
+ {
83
+ "cell_type": "code",
84
+ "execution_count": null,
85
+ "metadata": {},
86
+ "outputs": [],
87
+ "source": [
88
+ "from IPython.display import Audio, display\n",
89
+ "\n",
90
+ "for path in sorted(AUDIO_DIR.glob(\"*.wav\")):\n",
91
+ " mel, raw = load_inputs(path)\n",
92
+ " with torch.no_grad():\n",
93
+ " emb = enc.embed(raw) # [1, embedding_size]\n",
94
+ " # The vocoder embeds its fixed watermark automatically.\n",
95
+ " audio = gen(mel_spectrogram=mel, speaker_embedding=emb).audio.squeeze(1)\n",
96
+ " r = det.detect(audio)\n",
97
+ " print(f\"{path.name}: {r['matches']}/{r['n_bits']} bits matched p={r['p_value']:.1e}\")\n",
98
+ " display(Audio(audio[0].numpy(), rate=TGT_SR))"
99
+ ]
100
+ },
101
+ {
102
+ "cell_type": "markdown",
103
+ "metadata": {},
104
+ "source": [
105
+ "Sanity check: an **unrelated** clip (here, random noise) is not from our model, so it matches only about half the bits with a large `p_value`."
106
+ ]
107
+ },
108
+ {
109
+ "cell_type": "code",
110
+ "execution_count": null,
111
+ "metadata": {},
112
+ "outputs": [],
113
+ "source": [
114
+ "print(\"random noise:\", det.detect(torch.randn(1, TGT_SR)))"
115
+ ]
116
+ }
117
+ ],
118
+ "metadata": {
119
+ "language_info": {
120
+ "name": "python"
121
+ }
122
+ },
123
+ "nbformat": 4,
124
+ "nbformat_minor": 5
125
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a1e9d11e749c65af36893011c5871409ffd5f74e3a42ef72c7387a3daac0bfb9
3
+ size 2614304
modeling_detector.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Standalone VocBulwark Cage watermark detector.
2
+
3
+ Loadable with `AutoModel.from_pretrained(path, trust_remote_code=True)` without
4
+ the training repository. Wraps the Cage extractor and compares the extracted
5
+ bits to a fixed provenance watermark baked into the config.
6
+ """
7
+ from dataclasses import dataclass
8
+ from math import comb
9
+
10
+ import torch
11
+ from transformers import PreTrainedModel
12
+ from transformers.modeling_outputs import ModelOutput
13
+
14
+ from .configuration_detector import CageDetectorConfig
15
+ from .cage_config import CageExtractorConfig
16
+ from .cage_extractor import CageExtractor
17
+
18
+
19
+ @dataclass
20
+ class DetectionOutput(ModelOutput):
21
+ bits: torch.Tensor = None
22
+ logits: torch.Tensor = None
23
+
24
+
25
+ class CageDetector(PreTrainedModel):
26
+ config_class = CageDetectorConfig
27
+
28
+ def __init__(self, config):
29
+ super().__init__(config)
30
+ ext_cfg = CageExtractorConfig(
31
+ watermark_bits=config.watermark_bits,
32
+ in_channels=config.in_channels,
33
+ fine_kernel=config.fine_kernel,
34
+ mid_kernel=config.mid_kernel,
35
+ coarse_kernel=config.coarse_kernel,
36
+ sample_rate=config.sample_rate,
37
+ )
38
+ self.cage_extractor = CageExtractor(ext_cfg)
39
+
40
+ def _prep(self, audio, input_sample_rate):
41
+ """Coerce input to [B, T] mono at the detector sample rate."""
42
+ if not torch.is_tensor(audio):
43
+ audio = torch.as_tensor(audio)
44
+ p = next(self.parameters())
45
+ audio = audio.to(device=p.device, dtype=p.dtype)
46
+ if audio.dim() == 1: # [T] -> [1, T]
47
+ audio = audio.unsqueeze(0)
48
+ elif audio.dim() == 3: # [B, C, T] -> [B, T]
49
+ audio = audio.mean(dim=1) if audio.shape[1] > 1 else audio.squeeze(1)
50
+ if input_sample_rate is not None and input_sample_rate != self.config.sample_rate:
51
+ import torchaudio
52
+ audio = torchaudio.functional.resample(
53
+ audio, input_sample_rate, self.config.sample_rate)
54
+ return audio
55
+
56
+ @staticmethod
57
+ def _p_value(matches, n):
58
+ """P(Binomial(n, 0.5) >= matches): chance an unrelated clip matches this well."""
59
+ return sum(comb(n, i) for i in range(int(matches), n + 1)) / (2 ** n)
60
+
61
+ @torch.no_grad()
62
+ def extract(self, audio, input_sample_rate=None):
63
+ """Return (bits, logits). bits: [B, n] in {0,1}; logits: [B, n]."""
64
+ audio = self._prep(audio, input_sample_rate)
65
+ logits = self.cage_extractor(audio)
66
+ bits = (logits > 0).to(torch.int64)
67
+ return bits, logits
68
+
69
+ @torch.no_grad()
70
+ def detect(self, audio, input_sample_rate=None):
71
+ """Detect the fixed watermark. Returns a dict (or list of dicts for B>1):
72
+ {matches, n_bits, p_value}. `matches` is how many of the model's fixed
73
+ bits the clip carries; `p_value` is the chance an unrelated clip matches
74
+ at least this well under Binomial(n_bits, 0.5). A clip from our model
75
+ matches all n_bits (p_value ~ 0); an unrelated clip matches about half."""
76
+ bits, _ = self.extract(audio, input_sample_rate)
77
+ wstar = torch.tensor(self.config.fixed_watermark,
78
+ device=bits.device, dtype=bits.dtype)
79
+ matches = (bits == wstar).sum(dim=-1).tolist()
80
+ n = int(self.config.watermark_bits)
81
+ results = [{
82
+ "matches": int(m),
83
+ "n_bits": n,
84
+ "p_value": self._p_value(m, n),
85
+ } for m in matches]
86
+ return results[0] if len(results) == 1 else results
87
+
88
+ @torch.no_grad()
89
+ def forward(self, audio=None, input_sample_rate=None, **kwargs):
90
+ bits, logits = self.extract(audio, input_sample_rate)
91
+ return DetectionOutput(bits=bits, logits=logits)