rtx-csm: AudioSeal apply CLI for arbitrary-rate WAVs
End-to-end watermarker driver that handles any source sample rate by resampling to AudioSeal's 16 kHz native, embedding, then resampling back. Tested on 10s of real CSM 24 kHz speech: mean_presence=0.9988 detection, 12/16 message bits round-trip (4-bit erosion from double resample). - examples/audioseal_apply.rs: --in/--out/--source-rate/--message; loads source via audio_io::load_mono_at_rate, calls AudioSealWatermarker through the public Watermarker trait, verifies via in-process detect. - Fix bit-match counter overflow in audioseal_demo.rs and audioseal_apply.rs. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
@@ -136,3 +136,7 @@ path = "examples/audioseal_convert.rs"
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "audioseal_demo"
|
name = "audioseal_demo"
|
||||||
path = "examples/audioseal_demo.rs"
|
path = "examples/audioseal_demo.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "audioseal_apply"
|
||||||
|
path = "examples/audioseal_apply.rs"
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
//! Apply an AudioSeal watermark to any input WAV.
|
||||||
|
//!
|
||||||
|
//! Handles arbitrary source sample rates by resampling to 16 kHz (AudioSeal
|
||||||
|
//! native), embedding the watermark, then resampling back to the original
|
||||||
|
//! rate and writing the output. The CSM TTS pipeline produces 24 kHz audio,
|
||||||
|
//! so the typical use is:
|
||||||
|
//!
|
||||||
|
//! ```bash
|
||||||
|
//! cargo run -p rtx-csm --release --example generate -- \
|
||||||
|
//! --text "Hello." --out /tmp/hello.wav
|
||||||
|
//!
|
||||||
|
//! cargo run -p rtx-csm --release --example audioseal_apply -- \
|
||||||
|
//! --generator /tmp/audioseal_generator.safetensors \
|
||||||
|
//! --detector /tmp/audioseal_detector.safetensors \
|
||||||
|
//! --message 0xBEEF \
|
||||||
|
//! --in /tmp/hello.wav \
|
||||||
|
//! --out /tmp/hello_watermarked.wav
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! The example also runs the detector on the watermarked output to verify
|
||||||
|
//! the round-trip (mean_presence + decoded message).
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use candle_core::{DType, Device};
|
||||||
|
use clap::Parser;
|
||||||
|
use rtx_csm::{audio_io, audioseal::AudioSealWatermarker, watermark::Watermarker};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
const AUDIOSEAL_RATE: u32 = 16_000;
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(name = "audioseal_apply")]
|
||||||
|
struct Cli {
|
||||||
|
/// Path to converted generator safetensors.
|
||||||
|
#[arg(long)]
|
||||||
|
generator: PathBuf,
|
||||||
|
/// Path to converted detector safetensors.
|
||||||
|
#[arg(long)]
|
||||||
|
detector: PathBuf,
|
||||||
|
/// 16-bit message payload (decimal or 0xHEX).
|
||||||
|
#[arg(long, default_value = "0xBEEF")]
|
||||||
|
message: String,
|
||||||
|
/// Input WAV (any rate, any channels).
|
||||||
|
#[arg(long = "in")]
|
||||||
|
input: PathBuf,
|
||||||
|
/// Output WAV path. The output is written at the SOURCE sample rate
|
||||||
|
/// (resample to 16 kHz happens internally only for the watermarker).
|
||||||
|
#[arg(long)]
|
||||||
|
out: PathBuf,
|
||||||
|
/// Source sample rate of the input WAV (default 24000 = CSM-1B native).
|
||||||
|
#[arg(long, default_value_t = 24_000)]
|
||||||
|
source_rate: u32,
|
||||||
|
/// Force CPU device.
|
||||||
|
#[arg(long)]
|
||||||
|
cpu: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_message(s: &str) -> Result<u16> {
|
||||||
|
let s = s.trim();
|
||||||
|
let v = if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
|
||||||
|
u16::from_str_radix(rest, 16)?
|
||||||
|
} else {
|
||||||
|
s.parse::<u16>()?
|
||||||
|
};
|
||||||
|
Ok(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
tracing_subscriber::fmt().init();
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let device = if cli.cpu {
|
||||||
|
Device::Cpu
|
||||||
|
} else if candle_core::utils::metal_is_available() {
|
||||||
|
Device::new_metal(0)?
|
||||||
|
} else {
|
||||||
|
Device::Cpu
|
||||||
|
};
|
||||||
|
let message = parse_message(&cli.message)?;
|
||||||
|
|
||||||
|
// Load source at native rate, then to 16 kHz for AudioSeal.
|
||||||
|
let src_native = audio_io::load_mono_at_rate(&cli.input, cli.source_rate)
|
||||||
|
.context("loading source at native rate")?;
|
||||||
|
let src_16k = audio_io::resample(&src_native, cli.source_rate, AUDIOSEAL_RATE)
|
||||||
|
.context("resample source -> 16 kHz")?;
|
||||||
|
println!(
|
||||||
|
"loaded {}: {} samples @ {} Hz ({} samples @ 16 kHz)",
|
||||||
|
cli.input.display(),
|
||||||
|
src_native.len(),
|
||||||
|
cli.source_rate,
|
||||||
|
src_16k.len(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load model.
|
||||||
|
let gen_vb = unsafe {
|
||||||
|
candle_nn::VarBuilder::from_mmaped_safetensors(&[&cli.generator], DType::F32, &device)
|
||||||
|
}
|
||||||
|
.context("opening generator safetensors")?;
|
||||||
|
let det_vb = unsafe {
|
||||||
|
candle_nn::VarBuilder::from_mmaped_safetensors(&[&cli.detector], DType::F32, &device)
|
||||||
|
}
|
||||||
|
.context("opening detector safetensors")?;
|
||||||
|
let wm = AudioSealWatermarker::from_var_builders(gen_vb, det_vb, device.clone(), message)?;
|
||||||
|
println!("loaded AudioSeal (message=0x{:04X})", message);
|
||||||
|
|
||||||
|
// Embed watermark at 16 kHz.
|
||||||
|
let wm_16k = wm.embed(&src_16k).context("watermark embed")?;
|
||||||
|
|
||||||
|
// Resample back to source rate and write output.
|
||||||
|
let wm_out = audio_io::resample(&wm_16k, AUDIOSEAL_RATE, cli.source_rate)
|
||||||
|
.context("resample 16 kHz -> source rate")?;
|
||||||
|
audio_io::write_wav_mono(&cli.out, &wm_out, cli.source_rate)
|
||||||
|
.context("write watermarked WAV")?;
|
||||||
|
println!(
|
||||||
|
"wrote {} ({} samples @ {} Hz)",
|
||||||
|
cli.out.display(),
|
||||||
|
wm_out.len(),
|
||||||
|
cli.source_rate
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify round-trip: re-resample to 16 kHz and detect.
|
||||||
|
let probe_16k = audio_io::resample(&wm_out, cli.source_rate, AUDIOSEAL_RATE)
|
||||||
|
.context("resample for detect")?;
|
||||||
|
let result = wm.detect(&probe_16k).context("watermark detect")?;
|
||||||
|
println!(
|
||||||
|
"round-trip detect: mean_presence={:.4}, decoded=0x{:04X} (expected 0x{:04X})",
|
||||||
|
result.mean_presence,
|
||||||
|
result.message.unwrap_or(0),
|
||||||
|
message
|
||||||
|
);
|
||||||
|
let xor = result.message.unwrap_or(0) ^ message;
|
||||||
|
let bits_match = 16 - xor.count_ones() as usize;
|
||||||
|
println!("message bits matching: {bits_match}/16");
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -137,11 +137,8 @@ fn main() -> Result<()> {
|
|||||||
"detector decode: mean_presence={mean_presence:.4} (>0.5 = watermarked)",
|
"detector decode: mean_presence={mean_presence:.4} (>0.5 = watermarked)",
|
||||||
);
|
);
|
||||||
println!("decoded message: 0x{:04X}", decoded);
|
println!("decoded message: 0x{:04X}", decoded);
|
||||||
let bits_correct = (decoded ^ message).count_zeros() as usize - (16 - MESSAGE_BITS);
|
let bits_correct = MESSAGE_BITS - (decoded ^ message).count_ones() as usize;
|
||||||
println!(
|
println!("message bits matching: {bits_correct}/{MESSAGE_BITS}");
|
||||||
"message bits matching: {}/{}",
|
|
||||||
bits_correct, MESSAGE_BITS
|
|
||||||
);
|
|
||||||
|
|
||||||
let presence_len = presence.dim(candle_core::D::Minus1)?;
|
let presence_len = presence.dim(candle_core::D::Minus1)?;
|
||||||
println!("per-sample presence length: {presence_len}");
|
println!("per-sample presence length: {presence_len}");
|
||||||
|
|||||||
Reference in New Issue
Block a user