mstar.model.audio_watermark#
Opt-in AudioSeal watermark for generated speech.
A deployment turns it on in its YAML model_kwargs:
model_kwargs:
audio_watermark:
enabled: true # default false
message: 0xBEEF # 16-bit payload: int 0..65535 or a 16-char bit string
alpha: 1.0 # watermark strength (AudioSeal's tune-down ratio)
The watermark is Meta’s AudioSeal (MIT code and weights, pip install
'mstar-ai[audioseal]'): a 14.7M-parameter causal conv/LSTM generator whose
output, the same length as its input, is added to the waveform. The matching
detector scores a clip by the fraction of its samples it finds marked
(python -m mstar.model.audio_watermark detect out.wav). Embedding is
streaming-exact chunk by chunk; detection on one chunk is not guaranteed. On
generated speech even 80 ms chunks scored as marked, but on a synthetic
harmonic tone 200 ms slices scored about 0.07 and 1 s slices about 0.8, so
detect on whole utterances, or at least a few seconds of audio. The mark
is found only at the rate it was added at (24 kHz), so the detector
resamples a file at another rate to 24 kHz first.
Streaming. Audio leaves the engine in chunks, and a chunk-by-chunk watermark
must equal the watermark of the whole utterance, or the boundaries click.
AudioSeal 0.2.0’s model.streaming(batch_size) does not deliver that: its
state propagation walks named_children() and stops at the
nn.Sequential holding the layers, so no conv caches anything and every
chunk is watermarked as if it began the stream (upstream issue #105). This
module keeps each request’s stream state itself, as one flat row of tensors,
and runs the generator’s layers as a function of it: the same ops AudioSeal’s
streaming layers run, with the state passed in and out. That lets requests
at the same point share one batched call, and lets the engine capture that
call per (batch, chunk length) bucket.
The generator works on whole 320-sample frames (its total stride). A chunk that is not a whole number of frames leaves its tail pending until the next chunk, or until the final chunk zero-pads it, so streamed output equals the offline watermark of the whole utterance to float rounding (on a GPU, to a few int16 steps: cuDNN runs the convolutions in TF32). AudioSeal’s own whole-utterance call pads inside each layer instead, so for an utterance that ends mid-frame the two differ in that last partial frame only (a few tens of int16 steps). Codecs whose chunks are whole frames (Qwen3-TTS: 1920 samples per codec frame) never hold anything back and never pad.
Functions
Classes
|
AudioSeal's detector: P(watermarked) and the decoded 16-bit message. |
|
AudioSeal generator plus per-request streaming, on one device. |
|
|
|
One request's watermark state: the generator's layer caches as one flat row ( |
- class mstar.model.audio_watermark.AudioSealDetector(detector, no_compile)[source]#
Bases:
objectAudioSeal’s detector: P(watermarked) and the decoded 16-bit message.
- Parameters:
detector (torch.nn.Module)
- class mstar.model.audio_watermark.AudioSealWatermarker(generator, config, no_compile)[source]#
Bases:
objectAudioSeal generator plus per-request streaming, on one device.
The generator runs as a plain function of the audio and every stream’s caches (
_mark): the same convolutions, LSTM and adds AudioSeal’s streaming layers would run, with each layer’s state passed in and out instead of kept on the module. Its shapes depend on the batch and chunk length only, so the engine can capture it per (batch, length) bucket (piecewise_configs) andapply_pcm16replays those captures.- Parameters:
generator (torch.nn.Module)
config (AudioWatermarkConfig)
- apply(chunks, streams, finals=None)[source]#
Watermark the next chunk of each stream (float [-1, 1], 1-D).
Returns one tensor per chunk: the watermarked audio of every whole frame available so far, which for frame-aligned chunks is the whole chunk. A final chunk flushes the stream’s pending partial frame. Streams at the same frame count run as one batch.
- apply_pcm16(chunks, streams, finals=None, runners=None)[source]#
applyfor PCM16 chunks, quantized back the way the codecs do.Chunks of whole frames with nothing pending (every Qwen3-TTS chunk) run as one call per length, the conversion to float and back included; with
runners(the engine’s captures ofpiecewise_configs) a call whose length was captured replays it, and a final chunk is zero-padded to the next captured length (the generator is causal and the stream’s caches are dropped, so its samples change by float rounding only). Anything else goes throughapply.
- classmethod build(config, device)[source]#
- Parameters:
config (AudioWatermarkConfig)
- Return type:
- offline(audio)[source]#
The whole-utterance watermark of 1-D float audio: the reference a stream must equal. Zero-pads to whole frames like a final chunk.
- class mstar.model.audio_watermark.AudioWatermarkConfig(enabled=False, message=0, alpha=1.0, generator='audioseal_wm_streaming')[source]#
Bases:
objectmodel_kwargs.audio_watermark: per deployment, off by default.