orbit-watermark 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: orbit-watermark
3
+ Version: 1.0.0
4
+ Summary: High-robustness neural audio watermarking using SilentCipher (Sony AI) & Spread Spectrum fallback
5
+ License: ISC
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: torch<=2.0.0
13
+ Requires-Dist: silentcipher
14
+ Requires-Dist: librosa>=0.10.0
15
+ Requires-Dist: soundfile>=0.12.0
16
+ Requires-Dist: numpy>=1.20.0
17
+
18
+ # orbit_watermark
19
+
20
+ Standalone Python package for ORBIT WATERMARK analysis.
@@ -0,0 +1,3 @@
1
+ # orbit_watermark
2
+
3
+ Standalone Python package for ORBIT WATERMARK analysis.
File without changes
@@ -0,0 +1,426 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ ORBIT SilentCipher Watermarking Script
4
+
5
+ Session 22 - Neural audio watermarking with SilentCipher (Sony AI)
6
+
7
+ This script provides embed and extract functionality for neural watermarking
8
+ using the SilentCipher model from Sony AI.
9
+
10
+ License: MIT (commercially licensable)
11
+ Source: https://github.com/sony/silentcipher
12
+
13
+ Usage:
14
+ # Embed a message into audio
15
+ python scripts/silentcipher_watermark.py embed <audio_path> <output_path> --message "0,1,2,3,4"
16
+
17
+ # Extract message from audio
18
+ python scripts/silentcipher_watermark.py extract <audio_path>
19
+
20
+ Output (JSON):
21
+ Embed: {"success": true, "sdr": 25.5, "message": [0,1,2,3,4]}
22
+ Extract: {"success": true, "message": [0,1,2,3,4], "confidence": 0.98}
23
+
24
+ Requirements:
25
+ pip install silentcipher librosa soundfile numpy
26
+
27
+ Model:
28
+ SilentCipher (Sony AI) - ~100MB, downloaded on first use
29
+ Supports 44.1kHz and 16kHz audio
30
+ Message capacity: 5 x 8-bit integers = 40 bits
31
+
32
+ See: ORBIT_ENHANCEMENTS.md Section 1 (Neural Watermarking)
33
+ """
34
+
35
+ import sys
36
+ import os
37
+ import json
38
+ import argparse
39
+ import warnings
40
+ import logging
41
+
42
+ # Suppress all warnings and verbose logging for cleaner JSON output
43
+ warnings.filterwarnings('ignore')
44
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '3'
45
+ os.environ['TRANSFORMERS_VERBOSITY'] = 'error'
46
+ os.environ['HF_HUB_DISABLE_PROGRESS_BARS'] = '1'
47
+ logging.getLogger('silentcipher').setLevel(logging.ERROR)
48
+ logging.getLogger('huggingface_hub').setLevel(logging.ERROR)
49
+ logging.getLogger('tqdm').setLevel(logging.ERROR)
50
+
51
+ def check_dependencies():
52
+ """Check if required packages are installed."""
53
+ missing = []
54
+
55
+ try:
56
+ import silentcipher
57
+ except ImportError:
58
+ missing.append('silentcipher')
59
+
60
+ try:
61
+ import librosa
62
+ except ImportError:
63
+ missing.append('librosa')
64
+
65
+ try:
66
+ import soundfile
67
+ except ImportError:
68
+ missing.append('soundfile')
69
+
70
+ try:
71
+ import numpy
72
+ except ImportError:
73
+ missing.append('numpy')
74
+
75
+ if missing:
76
+ print(json.dumps({
77
+ 'error': 'missing_dependencies',
78
+ 'message': f'Missing Python packages: {", ".join(missing)}',
79
+ 'install': f'pip install {" ".join(missing)}'
80
+ }))
81
+ sys.exit(1)
82
+
83
+
84
+ class SuppressStderr:
85
+ """Context manager to suppress stderr output during model operations."""
86
+ def __init__(self):
87
+ self._original_stderr = None
88
+ self._devnull = None
89
+
90
+ def __enter__(self):
91
+ self._original_stderr = sys.stderr
92
+ self._devnull = open(os.devnull, 'w')
93
+ sys.stderr = self._devnull
94
+ return self
95
+
96
+ def __exit__(self, *args):
97
+ sys.stderr = self._original_stderr
98
+ if self._devnull:
99
+ self._devnull.close()
100
+
101
+
102
+ class CaptureOutput:
103
+ """Context manager to capture stdout and stderr into buffers instead of /dev/null.
104
+ Prevents model output from corrupting JSON on stdout while preserving
105
+ error messages for diagnostics."""
106
+ def __init__(self):
107
+ self._original_stdout = None
108
+ self._original_stderr = None
109
+ self._stdout_buf = None
110
+ self._stderr_buf = None
111
+
112
+ def __enter__(self):
113
+ import io
114
+ self._original_stdout = sys.stdout
115
+ self._original_stderr = sys.stderr
116
+ self._stdout_buf = io.StringIO()
117
+ self._stderr_buf = io.StringIO()
118
+ sys.stdout = self._stdout_buf
119
+ sys.stderr = self._stderr_buf
120
+ return self
121
+
122
+ def __exit__(self, *args):
123
+ sys.stdout = self._original_stdout
124
+ sys.stderr = self._original_stderr
125
+
126
+ @property
127
+ def stdout(self):
128
+ return self._stdout_buf.getvalue() if self._stdout_buf else ''
129
+
130
+ @property
131
+ def stderr(self):
132
+ return self._stderr_buf.getvalue() if self._stderr_buf else ''
133
+
134
+
135
+ def get_model(sample_rate=44100):
136
+ """Load SilentCipher model for the given sample rate."""
137
+ import silentcipher
138
+ import torch
139
+
140
+ # Determine device
141
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
142
+
143
+ # Select model type based on sample rate
144
+ if sample_rate == 44100:
145
+ model_type = '44.1k'
146
+ elif sample_rate == 16000:
147
+ model_type = '16k'
148
+ else:
149
+ # Resample to 44.1k if other sample rate
150
+ model_type = '44.1k'
151
+
152
+ # Load model (cached after first download) - capture output
153
+ cap = CaptureOutput()
154
+ with cap:
155
+ model = silentcipher.get_model(
156
+ model_type=model_type,
157
+ device=device
158
+ )
159
+ if cap.stderr:
160
+ print(f"[SilentCipher model load stderr]: {cap.stderr}", file=sys.stderr)
161
+
162
+ return model, device, model_type
163
+
164
+
165
+ def cleanup_gpu():
166
+ """Explicitly release GPU memory to prevent memory leaks between calls."""
167
+ import gc
168
+ import torch
169
+ gc.collect()
170
+ if torch.cuda.is_available():
171
+ torch.cuda.empty_cache()
172
+ torch.cuda.synchronize()
173
+
174
+
175
+ def embed_watermark(audio_path, output_path, message, target_sr=44100):
176
+ """
177
+ Embed a watermark message into an audio file.
178
+
179
+ Args:
180
+ audio_path: Path to input audio file
181
+ output_path: Path to save watermarked audio
182
+ message: List of 5 integers (0-255 each)
183
+ target_sr: Target sample rate (44100 or 16000)
184
+
185
+ Returns:
186
+ dict with success status, SDR, and message
187
+ """
188
+ import librosa
189
+ import soundfile as sf
190
+ import numpy as np
191
+ import torch
192
+
193
+ if not os.path.exists(audio_path):
194
+ raise FileNotFoundError(f'Audio file not found: {audio_path}')
195
+
196
+ # Validate message
197
+ if len(message) != 5:
198
+ raise ValueError(f'Message must be exactly 5 integers, got {len(message)}')
199
+ for i, val in enumerate(message):
200
+ if not 0 <= val <= 255:
201
+ raise ValueError(f'Message[{i}] must be 0-255, got {val}')
202
+
203
+ try:
204
+ # Load audio
205
+ audio, sr = librosa.load(audio_path, sr=target_sr, mono=True)
206
+ duration = len(audio) / sr
207
+
208
+ # Minimum duration check (SilentCipher needs at least ~1 second)
209
+ if duration < 1.0:
210
+ raise ValueError(f'Audio too short: {duration:.2f}s (minimum 1.0s)')
211
+
212
+ # Load model
213
+ model, device, model_type = get_model(target_sr)
214
+
215
+ # Embed watermark - capture stdout/stderr from silentcipher library
216
+ # SilentCipher expects message as list of 5 integers [0-255]
217
+ embed_cap = CaptureOutput()
218
+ with embed_cap:
219
+ encoded_audio, sdr = model.encode_wav(audio, sr, message)
220
+ if embed_cap.stderr:
221
+ print(f"[SilentCipher embed stderr]: {embed_cap.stderr}", file=sys.stderr)
222
+ if embed_cap.stdout:
223
+ print(f"[SilentCipher embed stdout]: {embed_cap.stdout}", file=sys.stderr)
224
+
225
+ # Save output
226
+ sf.write(output_path, encoded_audio, sr)
227
+
228
+ # Clean up model and GPU memory
229
+ del model
230
+ del audio
231
+ del encoded_audio
232
+
233
+ return {
234
+ 'success': True,
235
+ 'sdr': float(sdr), # Signal-to-Distortion Ratio (higher = better quality)
236
+ 'message': message,
237
+ 'duration': duration,
238
+ 'sample_rate': sr,
239
+ 'device': device,
240
+ 'model_type': model_type
241
+ }
242
+ finally:
243
+ # Always clean up GPU memory
244
+ cleanup_gpu()
245
+
246
+
247
+ def extract_watermark(audio_path, target_sr=44100, phase_shift_decoding=True):
248
+ """
249
+ Extract watermark message from an audio file.
250
+
251
+ Args:
252
+ audio_path: Path to watermarked audio file
253
+ target_sr: Target sample rate for processing
254
+ phase_shift_decoding: Enable for better robustness to crops (slower)
255
+
256
+ Returns:
257
+ dict with success status, message, and confidence
258
+ """
259
+ import librosa
260
+ import numpy as np
261
+
262
+ if not os.path.exists(audio_path):
263
+ raise FileNotFoundError(f'Audio file not found: {audio_path}')
264
+
265
+ try:
266
+ # Load audio
267
+ audio, sr = librosa.load(audio_path, sr=target_sr, mono=True)
268
+ duration = len(audio) / sr
269
+
270
+ # Load model
271
+ model, device, model_type = get_model(target_sr)
272
+
273
+ # Extract watermark - capture stdout/stderr from silentcipher library
274
+ # phase_shift_decoding=True makes decoder more robust to audio crops
275
+ extract_cap = CaptureOutput()
276
+ with extract_cap:
277
+ result = model.decode_wav(audio, sr, phase_shift_decoding=phase_shift_decoding)
278
+ if extract_cap.stderr:
279
+ print(f"[SilentCipher extract stderr]: {extract_cap.stderr}", file=sys.stderr)
280
+ if extract_cap.stdout:
281
+ print(f"[SilentCipher extract stdout]: {extract_cap.stdout}", file=sys.stderr)
282
+
283
+ # Clean up model
284
+ del model
285
+ del audio
286
+
287
+ # Status can be True, 'success', or truthy value depending on version
288
+ status_ok = result.get('status') in (True, 'success', 'True') or result.get('status') == True
289
+
290
+ if status_ok and result.get('messages'):
291
+ # Get the first (and typically only) message
292
+ message = result['messages'][0] if result['messages'] else None
293
+ confidence = result['confidences'][0] if result['confidences'] else 0.0
294
+
295
+ return {
296
+ 'success': True,
297
+ 'detected': True,
298
+ 'message': list(message) if message is not None else None,
299
+ 'confidence': float(confidence),
300
+ 'duration': duration,
301
+ 'sample_rate': sr,
302
+ 'device': device,
303
+ 'model_type': model_type
304
+ }
305
+ else:
306
+ return {
307
+ 'success': True,
308
+ 'detected': False,
309
+ 'message': None,
310
+ 'confidence': 0.0,
311
+ 'duration': duration,
312
+ 'sample_rate': sr,
313
+ 'device': device,
314
+ 'status_detail': str(result.get('status', 'unknown'))
315
+ }
316
+ finally:
317
+ # Always clean up GPU memory
318
+ cleanup_gpu()
319
+
320
+
321
+ def check_environment():
322
+ """Check if SilentCipher environment is properly set up."""
323
+ check_dependencies()
324
+
325
+ import torch
326
+ import silentcipher
327
+
328
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
329
+
330
+ return {
331
+ 'available': True,
332
+ 'message': 'SilentCipher environment ready',
333
+ 'details': {
334
+ 'device': device,
335
+ 'cuda_available': torch.cuda.is_available(),
336
+ 'silentcipher_version': getattr(silentcipher, '__version__', 'unknown')
337
+ }
338
+ }
339
+
340
+
341
+ def main():
342
+ parser = argparse.ArgumentParser(description='ORBIT SilentCipher Watermarking')
343
+ subparsers = parser.add_subparsers(dest='command', help='Command to run')
344
+
345
+ # Embed command
346
+ embed_parser = subparsers.add_parser('embed', help='Embed watermark into audio')
347
+ embed_parser.add_argument('audio_path', help='Path to input audio file')
348
+ embed_parser.add_argument('output_path', help='Path to save watermarked audio')
349
+ embed_parser.add_argument('--message', required=True,
350
+ help='Message as comma-separated integers (5 values, 0-255 each)')
351
+ embed_parser.add_argument('--sample-rate', type=int, default=44100,
352
+ help='Target sample rate (default: 44100)')
353
+
354
+ # Extract command
355
+ extract_parser = subparsers.add_parser('extract', help='Extract watermark from audio')
356
+ extract_parser.add_argument('audio_path', help='Path to watermarked audio file')
357
+ extract_parser.add_argument('--sample-rate', type=int, default=44100,
358
+ help='Target sample rate (default: 44100)')
359
+ extract_parser.add_argument('--no-phase-shift', action='store_true',
360
+ help='Disable phase shift decoding (faster but less robust)')
361
+
362
+ # Check command
363
+ check_parser = subparsers.add_parser('check', help='Check environment')
364
+
365
+ args = parser.parse_args()
366
+
367
+ if args.command is None:
368
+ parser.print_help()
369
+ sys.exit(1)
370
+
371
+ try:
372
+ if args.command == 'check':
373
+ result = check_environment()
374
+ print(json.dumps(result))
375
+
376
+ elif args.command == 'embed':
377
+ check_dependencies()
378
+
379
+ # Parse message
380
+ message = [int(x.strip()) for x in args.message.split(',')]
381
+
382
+ result = embed_watermark(
383
+ args.audio_path,
384
+ args.output_path,
385
+ message,
386
+ target_sr=args.sample_rate
387
+ )
388
+ print(json.dumps(result))
389
+
390
+ elif args.command == 'extract':
391
+ check_dependencies()
392
+
393
+ result = extract_watermark(
394
+ args.audio_path,
395
+ target_sr=args.sample_rate,
396
+ phase_shift_decoding=not args.no_phase_shift
397
+ )
398
+ print(json.dumps(result))
399
+
400
+ except FileNotFoundError as e:
401
+ print(json.dumps({
402
+ 'error': 'file_not_found',
403
+ 'message': str(e)
404
+ }))
405
+ sys.exit(1)
406
+ except ValueError as e:
407
+ print(json.dumps({
408
+ 'error': 'validation_error',
409
+ 'message': str(e)
410
+ }))
411
+ sys.exit(1)
412
+ except Exception as e:
413
+ print(json.dumps({
414
+ 'error': 'processing_error',
415
+ 'message': str(e),
416
+ 'type': type(e).__name__
417
+ }))
418
+ sys.exit(1)
419
+
420
+
421
+ if __name__ == '__main__':
422
+ main()
423
+
424
+
425
+
426
+
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.4
2
+ Name: orbit-watermark
3
+ Version: 1.0.0
4
+ Summary: High-robustness neural audio watermarking using SilentCipher (Sony AI) & Spread Spectrum fallback
5
+ License: ISC
6
+ Classifier: Programming Language :: Python :: 3
7
+ Classifier: License :: OSI Approved :: ISC License (ISCL)
8
+ Classifier: Operating System :: OS Independent
9
+ Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
10
+ Requires-Python: >=3.8
11
+ Description-Content-Type: text/markdown
12
+ Requires-Dist: torch<=2.0.0
13
+ Requires-Dist: silentcipher
14
+ Requires-Dist: librosa>=0.10.0
15
+ Requires-Dist: soundfile>=0.12.0
16
+ Requires-Dist: numpy>=1.20.0
17
+
18
+ # orbit_watermark
19
+
20
+ Standalone Python package for ORBIT WATERMARK analysis.
@@ -0,0 +1,10 @@
1
+ README.md
2
+ pyproject.toml
3
+ orbit_watermark/__init__.py
4
+ orbit_watermark/silentcipher_watermark.py
5
+ orbit_watermark.egg-info/PKG-INFO
6
+ orbit_watermark.egg-info/SOURCES.txt
7
+ orbit_watermark.egg-info/dependency_links.txt
8
+ orbit_watermark.egg-info/entry_points.txt
9
+ orbit_watermark.egg-info/requires.txt
10
+ orbit_watermark.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ orbit-watermark = orbit_watermark.silentcipher_watermark:main
@@ -0,0 +1,5 @@
1
+ torch<=2.0.0
2
+ silentcipher
3
+ librosa>=0.10.0
4
+ soundfile>=0.12.0
5
+ numpy>=1.20.0
@@ -0,0 +1 @@
1
+ orbit_watermark
@@ -0,0 +1,30 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "orbit-watermark"
7
+ version = "1.0.0"
8
+ description = "High-robustness neural audio watermarking using SilentCipher (Sony AI) & Spread Spectrum fallback"
9
+ readme = "README.md"
10
+ requires-python = ">=3.8"
11
+ license = { text = "ISC" }
12
+ classifiers = [
13
+ "Programming Language :: Python :: 3",
14
+ "License :: OSI Approved :: ISC License (ISCL)",
15
+ "Operating System :: OS Independent",
16
+ "Topic :: Multimedia :: Sound/Audio :: Analysis"
17
+ ]
18
+ dependencies = [
19
+ "torch<=2.0.0",
20
+ "silentcipher",
21
+ "librosa>=0.10.0",
22
+ "soundfile>=0.12.0",
23
+ "numpy>=1.20.0"
24
+ ]
25
+
26
+ [tool.setuptools]
27
+ packages = ["orbit_watermark"]
28
+
29
+ [project.scripts]
30
+ orbit-watermark = "orbit_watermark.silentcipher_watermark:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+