// sae_audio — Python module wrapping the v1 audio signature (audio_signature.*). // /// TRACES: IR-004, IR-005 | SR-003 // // Exists so a study or a test can drive the **shipped** signature code from // Python instead of porting the DSP to numpy. A numpy port would be a third // implementation of a fingerprint that only works if every implementation // agrees byte for byte, and it would be the one nobody checks against the // golden vector — so the offset-recovery validation (VR-014) calls this. // // Bound with nanobind, as `sae_embed` and `sae_kpn` are. Not pybind11: a second // binding framework in one build is a second set of ABI and lifetime rules to // get right, for a module that needs nothing nanobind lacks. // // The module deliberately stops at the producer's edge. Matching — sliding one // signature against another and scoring the overlap — is the *consumer's* // algorithm (server SPEC §3, and the jRay plugin implements it), so it is not // bound here and a caller writing a slide in numpy is not re-implementing // anything this repo owns. #include "audio_signature.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace nb = nanobind; using namespace nb::literals; using namespace sae::audio; namespace { using MonoArray = nb::ndarray, nb::c_contig, nb::device::cpu>; // Hand the vector's buffer to Python without copying 1.3 M samples, and let a // capsule own it: the array outlives this call, so the storage has to as well. nb::object own_as_ndarray(std::vector&& samples) { auto* held = new std::vector(std::move(samples)); nb::capsule owner(held, [](void* p) noexcept { delete static_cast*>(p); }); const std::size_t n = held->size(); return nb::cast(nb::ndarray>(held->data(), {n}, owner)); } std::vector to_vector(const MonoArray& a) { return std::vector(a.data(), a.data() + a.shape(0)); } } // namespace NB_MODULE(sae_audio, m) { m.doc() = "JRay v1 audio signature (JRay-public-server SPEC.md section 3), as the " "extraction pipeline computes it. The constants below are the contract: " "changing any of them is a v1 -> v2 change."; m.attr("sample_rate") = kSampleRate; m.attr("frame_size") = kFrameSize; m.attr("hop_size") = kHopSize; m.attr("num_bands") = kNumBands; m.attr("band_lo_hz") = kBandLoHz; m.attr("band_hi_hz") = kBandHiHz; m.attr("window_sec") = kWindowSec; m.attr("window_samples") = kWindowSamples; m.attr("expected_frames") = kExpectedFrames; m.attr("version_prefix") = std::string(kVersionPrefix); m.def( "compute_signature", [](const std::string& path) { return compute_signature(path); }, "path"_a, "Signature of the 120 s window centred on the media's midpoint, or None " "for media shorter than the window (IR-007), media with no audio " "stream, and any decode failure — degradation, never an exception."); m.def( "decode_centre_window", [](const std::string& path) -> nb::object { std::optional> mono = decode_centre_window(path); if (!mono) { return nb::none(); } return own_as_ndarray(std::move(*mono)); }, "path"_a, "The decoded centre window as float32 mono at 11025 Hz, or None. Exposed " "so a caller can slice or perturb real audio and re-sign it without " "going back through a container."); m.def( "signature_from_mono", [](const MonoArray& mono) { return signature_from_mono(to_vector(mono)); }, "mono"_a, "Signature of mono float32 samples already at 11025 Hz, in [-1, 1). None " "when fewer than one whole frame is given."); m.def( "pack_frames", [](const MonoArray& mono) { std::vector packed = pack_frames(to_vector(mono)); return nb::bytes(reinterpret_cast(packed.data()), packed.size()); }, "mono"_a, "One packed byte per whole STFT frame: (band << 2) | energy_class. This " "is the payload the signature base64-encodes."); m.def( "band_fft_bins", [] { const auto& table = band_fft_bins(); return std::vector>(table.begin(), table.end()); }, "The half-open FFT bin range owned by each of the 32 log-spaced bands."); }