From 48f11dea3d648ccc67bc7b9575043444c440db80 Mon Sep 17 00:00:00 2001 From: karl Date: Tue, 13 Oct 2026 20:18:56 +0200 Subject: [PATCH] Add frozen shared-service speech retrieval and negative-control evaluator --- .../speech-eval/native-validation/Cargo.lock | 1328 +++++++++++++++++ .../speech-eval/native-validation/Cargo.toml | 20 + tools/speech-eval/native-validation/README.md | 53 + .../speech-eval/native-validation/src/main.rs | 378 +++++ tools/speech-eval/score_native_validation.py | 78 + .../test_score_native_validation.py | 50 + 6 files changed, 1907 insertions(+) create mode 100644 tools/speech-eval/native-validation/Cargo.lock create mode 100644 tools/speech-eval/native-validation/Cargo.toml create mode 100644 tools/speech-eval/native-validation/README.md create mode 100644 tools/speech-eval/native-validation/src/main.rs create mode 100644 tools/speech-eval/score_native_validation.py create mode 100644 tools/speech-eval/test_score_native_validation.py diff --git a/tools/speech-eval/native-validation/Cargo.lock b/tools/speech-eval/native-validation/Cargo.lock new file mode 100644 index 0000000..3ba43fc --- /dev/null +++ b/tools/speech-eval/native-validation/Cargo.lock @@ -0,0 +1,1328 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32" +dependencies = [ + "cipher", + "cpubits", + "cpufeatures 0.3.1", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e76a019e91224d279006ff972f1e984179a6e9feb050adba6ce8274aef23195" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a32acac15fe1967bc3986b2a6347dffc965602354ea6f450ad07e8bfd253583" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher", +] + +[[package]] +name = "cc" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6651c9ed80effdc7db0ff72512157f901af5e3549e341e24b1dd4887d836d838" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout", +] + +[[package]] +name = "clap" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d" +dependencies = [ + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_lex" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c133bc6a41be0d194c306b5506d15e6feeea7b1d6604bd3f8310dfb2ca96486" + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + +[[package]] +name = "core_detect" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48" + +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a7799fd6b852db0e61728dde9a204c423b44d689dbd432522543614b490e78" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "cxx" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8a02bfeba9074b2bbb39492c9b0908ec854f455a3cccdcde7a1de55e2315e40" +dependencies = [ + "cc", + "cxx-build", + "cxxbridge-cmd", + "cxxbridge-flags", + "cxxbridge-macro", + "foldhash", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbc2ac76997619d2d888b58ad79488f1c835de41ca4dd3dd7082acf4ecb0a50f" +dependencies = [ + "cc", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "scratch", + "syn 3.0.9", +] + +[[package]] +name = "cxxbridge-cmd" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54a486ed16a3f2a053fe7f5b98db4c1a59846a23e8cb9e49f9d1bd48830d51a2" +dependencies = [ + "clap", + "codespan-reporting", + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c72569be94e2283764d334cd285980b747dbe221e27e315484eb6bcd93fa808a" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.205" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "423e795284a7f7b3f843c956a5322c4bc497b60c2dd1a6339da10313cb18d575" +dependencies = [ + "indexmap", + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid", + "crypto-common 0.2.2", +] + +[[package]] +name = "ecb" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac" +dependencies = [ + "cipher", +] + +[[package]] +name = "encoding_rs" +version = "0.8.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e985e0451871ad22fb8d2b6b076e2028a502a0d3950998c2c5c0a4f9b5d9679" +dependencies = [ + "cfg-if", + "core_detect", + "multiversion_no_op", + "rustversion", + "scopeguard", + "simdutf8", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + +[[package]] +name = "fastrand" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" + +[[package]] +name = "flate2" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e634e2e0ebac1ee034020da1ca582e17ffe4e0f5e985823721e168928136dcb" +dependencies = [ + "crc32fast", + "miniz_oxide 0.9.1", + "zlib-rs", +] + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "rand_core", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "hashlink" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hybrid-array" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17" +dependencies = [ + "typenum", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "image-webp", + "moxcms", + "num-traits", + "png", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "image-webp" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3" +dependencies = [ + "byteorder-lite", + "quick-error", +] + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jpeg-decoder" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00810f1d8b74be64b13dbf3db89ac67740615d6c891f0e7b6179326533011a07" + +[[package]] +name = "libc" +version = "0.2.190" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce5d3ddc6d3fa000eb1536d85e147bfe31aacaba692ed6a876f95cb7c855be78" + +[[package]] +name = "libsqlite3-sys" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6" + +[[package]] +name = "lopdf" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfffda0fe1ab0157e1a13c14bebd3f28671f2fccb7922f0722ec53926e6922d3" +dependencies = [ + "aes", + "bitflags", + "brotli-decompressor", + "cbc", + "ecb", + "encoding_rs", + "flate2", + "getrandom", + "indexmap", + "itoa", + "log", + "md-5", + "nom", + "rand", + "rangemap", + "sha2 0.11.1", + "stringprep", + "thiserror", + "weezl", +] + +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "miniz_oxide" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "multiversion_no_op" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743fb55ba31b18fb1ecef6bdc9aa2743314978ac084044301a7eee33fb99a20d" + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "numkong" +version = "7.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bcc7469814b82ddffa194dbf3418fc28fa49015392261926f85c5f1cae0625" +dependencies = [ + "cc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide 0.8.9", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b1177fdf999d2321d3fb46ff47159d9c1fb9ad66a4879f8c50a0b504615e9b" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af" +dependencies = [ + "chacha20", + "getrandom", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rangemap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d" + +[[package]] +name = "rusqlite" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7753b721174eb8ff87a9a0e799e2d7bc3749323e773db92e0984debb00019d6e" +dependencies = [ + "bitflags", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", +] + +[[package]] +name = "rustix" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891efababe418670775f199f0d233d84843c227a0949a883ce15b37c78d6629d" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scratch" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2" + +[[package]] +name = "searchi-core" +version = "0.1.0" +dependencies = [ + "anyhow", + "libc", + "lopdf", + "quick-xml", + "rusqlite", + "searchi-media", + "serde", + "serde_json", + "sha2 0.10.9", + "walkdir", + "windows-sys", + "zip", +] + +[[package]] +name = "searchi-media" +version = "0.1.0" +dependencies = [ + "anyhow", + "image", + "serde", + "serde_json", + "sha2 0.10.9", + "tempfile", +] + +[[package]] +name = "searchi-semantic" +version = "0.1.0" +dependencies = [ + "anyhow", + "base64", + "image", + "jpeg-decoder", + "serde", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "searchi-service" +version = "0.1.0" +dependencies = [ + "anyhow", + "fs2", + "rusqlite", + "searchi-core", + "searchi-media", + "searchi-semantic", + "searchi-speech", + "serde", + "sha2 0.10.9", + "usearch", +] + +[[package]] +name = "searchi-speech" +version = "0.1.0" +dependencies = [ + "anyhow", + "serde", + "serde_json", + "sha2 0.10.9", +] + +[[package]] +name = "searchi-speech-validation" +version = "0.1.0" +dependencies = [ + "anyhow", + "rusqlite", + "searchi-core", + "searchi-media", + "searchi-service", + "searchi-speech", + "serde_json", + "sha2 0.10.9", + "tempfile", +] + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "serde_json" +version = "1.0.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7e9cc8b1b85264074fbcc02a88680c4096b1e47df8f739dceb03bf482f04bd6" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "digest 0.11.3", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "smallvec" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b3dc8af474f516a851ff4bd12db780f948b9250ad37211e4eec0bccea54e01b" + +[[package]] +name = "stringprep" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", + "unicode-properties", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe5197923287db20a58125f0bc85c062f7f2c892de97b18c356f9efb14b28524" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "tinyvec" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" + +[[package]] +name = "typed-path" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "unicode-bidi" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5" + +[[package]] +name = "unicode-ident" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-properties" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "usearch" +version = "2.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1086fc5e6b4ee87a5bac4fd80fcf66670526ca21b77d5f95b68de01b15329c67" +dependencies = [ + "cxx", + "cxx-build", + "numkong", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "weezl" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "zerocopy" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86502bf56ac7c77571a32e2647bb2a15894565e981fb2a48d7bde2d91c965a9d" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.62" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5457206954b06561e2608c7e19cf58b1926586d999c246eebe4502f7e2039d1a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zip" +version = "8.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b" +dependencies = [ + "crc32fast", + "flate2", + "indexmap", + "memchr", + "typed-path", + "zopfli", +] + +[[package]] +name = "zlib-rs" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b268e58e7c693d7c271f93ffc4ba3b380412554231c85bf61ca7af91042a4112" + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" + +[[package]] +name = "zopfli" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aaf7fc5d30c28483d93805c4a5e12b05bbb52407fa67c5f8bd552374cd01fb11" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zune-core" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56377fd46368984a170bc5aac5567e52ca5da874caa60bea39fcbca78fb658b" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] diff --git a/tools/speech-eval/native-validation/Cargo.toml b/tools/speech-eval/native-validation/Cargo.toml new file mode 100644 index 0000000..172cbcd --- /dev/null +++ b/tools/speech-eval/native-validation/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "searchi-speech-validation" +version = "0.1.0" +edition = "2021" +publish = false + +[workspace] + +[dependencies] +anyhow = "1" +serde_json = "1" +sha2 = "0.10" +rusqlite = { version = "0.32", features = ["bundled"] } +searchi-core = { path = "../../../crates/searchi-core" } +searchi-media = { path = "../../../crates/searchi-media" } +searchi-service = { path = "../../../crates/searchi-service" } +searchi-speech = { path = "../../../crates/searchi-speech" } + +[dev-dependencies] +tempfile = "3" diff --git a/tools/speech-eval/native-validation/README.md b/tools/speech-eval/native-validation/README.md new file mode 100644 index 0000000..1d5dcf1 --- /dev/null +++ b/tools/speech-eval/native-validation/README.md @@ -0,0 +1,53 @@ +# Actual shared-service speech validation + +This internal evaluator uses the real `SpeechWorker`, shared service publication +and actual Google EmbeddingGemma 2. It consumes the unchanged frozen +`fixtures/speech-validation-v1` sources and queries. No fixture reference text, +keywords or expected results enter recognition or indexed passages. + +Run from the repository root after committing the evaluator/source snapshot and +obtaining the coordinator's exclusive CPU inference slot. The normal native +runtime is forced to CPU here; the shown `cpu-v3` speech runtime is an internal +test artifact and must be verified by its owner before execution. + +```powershell +cargo test --manifest-path tools/speech-eval/native-validation/Cargo.toml --locked +python -m unittest discover -s tools/speech-eval -p test_score_native_validation.py +$env:SEARCHI_FORCE_CPU = '1' +cargo run --manifest-path tools/speech-eval/native-validation/Cargo.toml --locked -- fixtures/speech-validation-v1/fixture.json .searchi/speech-validation-v1-seams/prepared.json .searchi/models/embeddinggemma2 .searchi/runtime/auto/Release/searchi-gemma-worker.exe .searchi/speech-runtime/cpu-v3/runtime/searchi-speech-worker.exe .searchi/speech/models/ggml-base.bin .searchi/speech/models/ggml-silero-v6.2.0.bin .searchi/media-runtime/bin/ffmpeg.exe .searchi/media-runtime/bin/ffprobe.exe .searchi/qa/speech-service-validation-v1 +python tools/speech-eval/score_native_validation.py .searchi/qa/speech-service-validation-v1/report.json fixtures/speech-validation-v1/fixture.json .searchi/qa/speech-service-validation-v1/scored.json +``` + +The output directory must not already exist. Individual native-call and query +files, preflight, indexed state and case reports are immutable, with each file +flushed before continuing. Failure writes an explicit incomplete artifact while +preserving completed calls. Local databases/owned copied sources remain there +for inspection. Model/runtime/source hashes, policy fingerprints and executing +binary hash are recorded before inference. Freeze and rebuild shared sources +before a scored run; on-disk source hashes alone cannot prove dependency compile +provenance while another agent is editing. + +The core case retains all four prepared files and all ten frozen queries. +Speech calls are matched against independently decoded source-window PCM hashes. +Accepted native `SpeechTranscription` values or returned errors are stored exactly +through the public client API. **Native wire bytes and stderr are not exposed by +that API**, so these artifacts are not claimed to capture raw transport output. +Published speech text/source hashes/times and byte-verified service excerpts are +retained, and foreground search uses the actual query role with measured traces. +The second case indexes both separate artificial seam sources using the same +service window policy. It checks source coverage and publication bounds; it does +not compare against continuously initialized full-source VAD. + +The scorer independently verifies complete native PCM-window coverage, every +frozen query, result-source identity and negative controls. It reports positive +file recall/MRR, absent queries returning candidates, recognized negative-control +passages and literal publisher-concept substring coverage separately. Concept +matching is a diagnostic, not WER or an independent timed transcript. + +There are no publisher phrase timestamps, so **no strict word/sentence point +quality score is produced**. A timestamp merely inside the complete article is +not precise localization success. Any excluded/failed source invalidates accepted +comparison scoring, and hallucinations on music/silence remain measured failures. +No provider, ranking weight, rejection threshold or production policy is chosen +from this small experiment. The two readings share translated subject matter and +the seam controls repeat real samples artificially. diff --git a/tools/speech-eval/native-validation/src/main.rs b/tools/speech-eval/native-validation/src/main.rs new file mode 100644 index 0000000..1823469 --- /dev/null +++ b/tools/speech-eval/native-validation/src/main.rs @@ -0,0 +1,378 @@ +//! Frozen actual shared-service speech validation; no reference text enters inference. +use anyhow::{ensure, Context, Result}; +use searchi_core::SearchIndex; +use searchi_media::{decode_audio_aligned, probe, AudioWindow, FfmpegTools, MediaLimits}; +use searchi_service::{HybridSearch, SearchOptions, SpeechProvider, SpeechTranscription}; +use searchi_speech::{Device, SpeechWorker}; +use serde_json::{json, Value}; +use sha2::{Digest, Sha256}; +use std::{ + collections::BTreeMap, + fs, + io::{Read, Write}, + path::{Path, PathBuf}, + sync::{Arc, Mutex}, + time::Instant, +}; + +fn hash(bytes: &[u8]) -> String { + format!("{:x}", Sha256::digest(bytes)) +} +fn file_hash(path: &Path) -> Result { + let mut file = fs::File::open(path)?; + let mut state = Sha256::new(); + let mut buffer = [0; 65536]; + loop { + let n = file.read(&mut buffer)?; + if n == 0 { + break; + } + state.update(&buffer[..n]); + } + Ok(format!("{:x}", state.finalize())) +} +fn write_new(path: &Path, value: &Value) -> Result<()> { + let mut file = fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(path)?; + file.write_all(&serde_json::to_vec_pretty(value)?)?; + file.sync_all()?; + Ok(()) +} +fn windows(start: u32, end: u32) -> Result> { + ensure!(start <= end, "Invalid source range"); + let mut values = Vec::new(); + let mut next = start; + while next < end { + ensure!( + values.len() < 4, + "Mini fixture exceeded bounded window count" + ); + values.push(AudioWindow { + start_ms: next, + duration_ms: (end - next).min(30000), + }); + if end - next <= 30000 { + break; + } + next = next.checked_add(28000).context("Source offset overflow")?; + } + ensure!( + values.len() <= 4, + "Mini fixture exceeded bounded window count" + ); + Ok(values) +} + +struct Recorder { + worker: SpeechWorker, + expected: BTreeMap>, + calls: Arc>>, + directory: PathBuf, +} +impl SpeechProvider for Recorder { + fn fingerprint(&self) -> &str { + self.worker.fingerprint() + } + fn transcribe(&mut self, pcm: &[f32], rate: u32) -> Result { + let pcm_bytes: Vec<_> = pcm.iter().flat_map(|value| value.to_le_bytes()).collect(); + let sha = hash(&pcm_bytes); + let expected = self + .expected + .get(&sha) + .context("Service supplied PCM not in independently verified source windows")?; + let started = Instant::now(); + let result = self.worker.transcribe(pcm, rate); + let mut record = json!({"pcm_sha256":sha,"samples":pcm.len(),"sample_rate":rate,"expected_source_windows":expected,"native_fingerprint":self.worker.fingerprint(),"wall_ms":started.elapsed().as_secs_f64()*1000.0,"scope":"Exact accepted SpeechTranscription or returned error; native wire bytes/stderr unavailable through public client API"}); + match &result { + Ok(value) => record["transcription"] = serde_json::to_value(value)?, + Err(error) => record["error"] = json!(format!("{error:#}")), + } + let mut calls = self + .calls + .lock() + .map_err(|_| anyhow::anyhow!("Recording mutex poisoned"))?; + write_new( + &self + .directory + .join(format!("native-call-{:03}.json", calls.len() + 1)), + &record, + )?; + calls.push(record); + result + } +} +struct Assets { + model: PathBuf, + google: PathBuf, + speech: PathBuf, + base: PathBuf, + vad: PathBuf, + tools: FfmpegTools, +} + +fn run_case( + name: &str, + sources: &[Value], + queries: &[Value], + assets: &Assets, + output: &Path, +) -> Result { + let case = output.join(name); + fs::create_dir(&case)?; + let root = case.join("corpus"); + fs::create_dir(&root)?; + let mut expected = BTreeMap::>::new(); + let limits = MediaLimits::default(); + let mut source_rows = Vec::new(); + for source in sources { + let id = source["id"].as_str().context("ID missing")?; + let input = PathBuf::from(source["path"].as_str().context("Source path missing")?); + ensure!( + source["sha256"] == file_hash(&input)?, + "Frozen prepared source changed" + ); + let path = root.join(format!("{id}.wav")); + fs::copy(&input, &path)?; + let metadata = probe(&assets.tools, &path, &limits)?; + let mut source_windows = Vec::new(); + for window in windows(metadata.audio_start_ms, metadata.audio_end_ms)? { + let pcm = decode_audio_aligned(&assets.tools, &path, window, &limits)?; + ensure!( + pcm.len() == window.duration_ms as usize * 16, + "Fixture source clock coverage differs" + ); + let sha = hash( + &pcm.iter() + .flat_map(|value| value.to_le_bytes()) + .collect::>(), + ); + let record = json!({"asset":id,"start_ms":window.start_ms,"duration_ms":window.duration_ms,"samples":pcm.len(),"pcm_sha256":sha}); + expected.entry(sha).or_default().push(record.clone()); + source_windows.push(record); + } + source_rows.push(json!({"id":id,"path":path,"sha256":source["sha256"],"windows":source_windows,"audio_start_ms":metadata.audio_start_ms,"audio_end_ms":metadata.audio_end_ms,"piecewise_map":source.get("piecewise_map")})); + } + let db = case.join("index.sqlite"); + let started = Instant::now(); + let mut lexical = SearchIndex::open(&db)?; + let discovery = lexical.index_folder(&root)?; + let lexical_ms = started.elapsed().as_secs_f64() * 1000.0; + drop(lexical); + let started = Instant::now(); + let native = SpeechWorker::load(&assets.speech, &assets.base, &assets.vad, Device::Cpu)?; + let speech_load_ms = started.elapsed().as_secs_f64() * 1000.0; + let fingerprint = native.fingerprint().to_owned(); + let calls = Arc::new(Mutex::new(Vec::new())); + let recorder = Recorder { + worker: native, + expected: expected.clone(), + calls: calls.clone(), + directory: case.clone(), + }; + let started = Instant::now(); + let mut indexer = HybridSearch::open(&db, &assets.model, &assets.google, 1)? + .with_speech_provider(Box::new(recorder))?; + let google_index_load_ms = started.elapsed().as_secs_f64() * 1000.0; + let started = Instant::now(); + let indexed = indexer.index_embeddings_with_media(&assets.tools, &limits)?; + let indexing_ms = started.elapsed().as_secs_f64() * 1000.0; + let google_index_worker = serde_json::to_value(indexer.worker_info())?; + write_new( + &case.join("indexed.json"), + &json!({"sources":source_rows,"index_report":indexed,"fingerprint":fingerprint,"worker":google_index_worker,"indexing_ms":indexing_ms}), + )?; + drop(indexer); + let recorded = calls + .lock() + .map_err(|_| anyhow::anyhow!("Recording mutex poisoned"))? + .clone(); + let expected_calls = expected.values().map(Vec::len).sum::(); + let complete_calls = + recorded.len() == expected_calls && recorded.iter().all(|row| row.get("error").is_none()); + let connection = + rusqlite::Connection::open_with_flags(&db, rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY)?; + let mut statement=connection.prepare("SELECT path,number,speech_end_ms,snippet,content_hash FROM semantic_chunks WHERE generation=(SELECT active_generation FROM semantic_meta WHERE singleton=1) AND source_type='speech' ORDER BY path,ordinal")?; + let chunks=statement.query_map([],|row|Ok(json!({"path":row.get::<_,String>(0)?,"start_ms":row.get::<_,u32>(1)?,"end_ms":row.get::<_,u32>(2)?,"text":row.get::<_,String>(3)?,"content_hash":row.get::<_,String>(4)?})))?.collect::>>()?; + let started = Instant::now(); + let mut reader = HybridSearch::open_query(&db, &assets.model, &assets.google, 1)?; + let google_query_load_ms = started.elapsed().as_secs_f64() * 1000.0; + let mut citations = Vec::new(); + for chunk in &chunks { + let excerpt = reader.speech_excerpt_matching( + chunk["path"].as_str().unwrap(), + chunk["start_ms"].as_u64().unwrap() as u32, + chunk["text"].as_str().unwrap(), + 4096, + )?; + ensure!( + excerpt + .as_ref() + .is_some_and(|value| value.text == chunk["text"].as_str().unwrap() + && value.provenance == "local_speech_recognition"), + "Published recognized citation missing" + ); + citations.push(serde_json::to_value(excerpt)?); + } + let started = Instant::now(); + reader.warmup()?; + let warmup_ms = started.elapsed().as_secs_f64() * 1000.0; + let mut rows = Vec::new(); + for query in queries { + let response = reader.search_measured( + query["text"].as_str().context("Query text missing")?, + 20, + &SearchOptions::default(), + true, + )?; + let mut hits = Vec::new(); + for hit in response.results { + let id = Path::new(&hit.filename) + .file_stem() + .context("Result ID missing")? + .to_string_lossy() + .into_owned(); + let excerpt = if hit.match_kind == "semantic_speech" { + let location = hit.source.as_ref().context("Speech location missing")?; + reader.speech_excerpt_matching(&hit.path, location.number, &hit.snippet, 4096)? + } else { + None + }; + hits.push(json!({"asset":id,"result":hit,"verified_recognized_excerpt":excerpt})); + } + let row = json!({"query":query,"hits":hits,"metrics":response.metrics,"strict_point_score_eligible":false}); + write_new( + &case.join(format!( + "query-{}.json", + query["id"].as_str().context("Query ID missing")? + )), + &row, + )?; + rows.push(row); + } + for source in &source_rows { + ensure!( + source["sha256"] == file_hash(Path::new(source["path"].as_str().unwrap()))?, + "Source changed during evaluation" + ); + } + let report = json!({"name":name,"sources":source_rows,"lexical_report":discovery,"index_report":indexed,"expected_native_calls":expected_calls,"native_calls":recorded,"published_speech":chunks,"verified_citations":citations,"rows":rows,"native_fingerprint":fingerprint,"google_index_worker":google_index_worker,"google_query_worker":reader.worker_info(),"lexical_ms":lexical_ms,"speech_load_verify_ms":speech_load_ms,"google_index_load_verify_ms":google_index_load_ms,"indexing_ms":indexing_ms,"google_query_load_verify_ms":google_query_load_ms,"query_warmup_ms":warmup_ms,"comparison_eligible":complete_calls&&indexed.speech_failures.is_empty()&&indexed.failures.is_empty(),"strict_point_score_eligible":false}); + write_new(&case.join("report.json"), &report)?; + Ok(report) +} +fn main() -> Result<()> { + let args: Vec<_> = std::env::args_os().skip(1).collect(); + ensure!(args.len()==10,"Supply FIXTURE PREPARED GOOGLE_MODELS GOOGLE_WORKER SPEECH_WORKER BASE VAD FFMPEG FFPROBE NEW_OUTPUT_DIRECTORY"); + ensure!( + std::env::var("SEARCHI_FORCE_CPU").as_deref() == Ok("1"), + "Windows VM diagnostic requires CPU" + ); + let fixture_path = PathBuf::from(&args[0]); + let fixture_raw = fs::read(&fixture_path)?; + let fixture: Value = serde_json::from_slice(&fixture_raw)?; + let input_lock: Value = + serde_json::from_slice(&fs::read(fixture_path.with_file_name("inputs.lock.json"))?)?; + let prepared_raw = fs::read(&args[1])?; + let prepared: Value = serde_json::from_slice(&prepared_raw)?; + ensure!( + input_lock["fixture_sha256"] == hash(&fixture_raw) + && input_lock["prepared_sha256"] == hash(&prepared_raw) + && prepared["fixture_sha256"] == hash(&fixture_raw), + "Frozen fixture/prepared plan differs" + ); + let assets = Assets { + model: PathBuf::from(&args[2]), + google: PathBuf::from(&args[3]), + speech: PathBuf::from(&args[4]), + base: PathBuf::from(&args[5]), + vad: PathBuf::from(&args[6]), + tools: FfmpegTools::new(PathBuf::from(&args[7]), PathBuf::from(&args[8]))?, + }; + ensure!( + prepared["ffmpeg_sha256"] == file_hash(&assets.tools.ffmpeg)? + && prepared["ffprobe_sha256"] == file_hash(&assets.tools.ffprobe)?, + "Frozen decoder differs" + ); + let output = PathBuf::from(&args[9]); + fs::create_dir(&output)?; + let mut provenance = BTreeMap::new(); + for file in [ + "crates/searchi-speech/src/lib.rs", + "crates/searchi-speech/src/process.rs", + "crates/searchi-speech/src/inventory.rs", + "crates/searchi-service/src/speech.rs", + "crates/searchi-service/src/media.rs", + "crates/searchi-service/src/store.rs", + "tools/speech-runtime/worker.cpp", + ] { + provenance.insert(file, file_hash(Path::new(file))?); + } + write_new( + &output.join("preflight.json"), + &json!({"fixture_sha256":hash(&fixture_raw),"prepared_sha256":hash(&prepared_raw),"binary_sha256":file_hash(&std::env::current_exe()?)?,"speech_worker_sha256":file_hash(&assets.speech)?,"speech_inventory_sha256":file_hash(&assets.speech.parent().context("Runtime directory")?.join("runtime-manifest.json"))?,"base_sha256":file_hash(&assets.base)?,"vad_sha256":file_hash(&assets.vad)?,"google_worker_sha256":file_hash(&assets.google)?,"source_hashes_at_start":provenance,"speech_policy":searchi_speech::POLICY,"source_clock_policy":"ActualService speech30s overlap2s; independently decodedPCM window hashes; no strict word labels","force_cpu":true,"threads":1}), + )?; + let core = prepared["clips"].as_array().context("Core clips missing")?; + ensure!(core.len() == 4, "All four controls required"); + let queries = fixture["queries"].as_array().context("Queries missing")?; + ensure!(queries.len() == 10, "All ten queries required"); + let first = match run_case("core", core, queries, &assets, &output) { + Ok(value) => value, + Err(error) => { + write_new( + &output.join("failure.json"), + &json!({"complete":false,"case":"core","error":format!("{error:#}"),"comparison_eligible":false}), + )?; + return Err(error); + } + }; + let seams = prepared["seam_controls"] + .as_array() + .context("Seam controls missing")?; + ensure!(seams.len() == 2, "Both seam sources required"); + let second = match run_case("seams", seams, &[], &assets, &output) { + Ok(value) => value, + Err(error) => { + write_new( + &output.join("failure.json"), + &json!({"complete":false,"case":"seams","error":format!("{error:#}"),"comparison_eligible":false}), + )?; + return Err(error); + } + }; + write_new( + &output.join("report.json"), + &json!({"complete":true,"core":first,"seams":second,"strict_point_score_eligible":false,"production_policy_changed":false,"scope":"Actual shared native SpeechWorker/service/Google path; independent ENDE tinycorpus; recognized outputs immutable acceptedAPIvalues, no rawwire capture; full-sourceVAD comparator not executed; seam source/window bounds only"}), + ) +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn overlap_maps_complete_seam_tail() { + let value = windows(0, 40024).unwrap(); + assert_eq!(value.len(), 2); + assert_eq!(value[1].start_ms, 28000); + assert_eq!(value[1].duration_ms, 12024); + } + #[test] + fn positioned_short_source_keeps_origin() { + let value = windows(2000, 4000).unwrap(); + assert_eq!(value[0].start_ms, 2000); + assert_eq!(value[0].duration_ms, 2000); + } + #[test] + fn invalid_or_large_fixture_ranges_fail() { + assert!(windows(3, 2).is_err()); + assert!(windows(0, 200000).is_err()); + } + #[test] + fn existing_raw_output_is_preserved() -> Result<()> { + let dir = tempfile::tempdir()?; + let path = dir.path().join("raw.json"); + fs::write(&path, b"old")?; + assert!(write_new(&path, &json!({})).is_err()); + ensure!(fs::read(path)? == b"old", "Changed report"); + Ok(()) + } +} diff --git a/tools/speech-eval/score_native_validation.py b/tools/speech-eval/score_native_validation.py new file mode 100644 index 0000000..b4a11cc --- /dev/null +++ b/tools/speech-eval/score_native_validation.py @@ -0,0 +1,78 @@ +"""Recompute frozen service retrieval and negative controls; never invent point scores.""" +import argparse +import hashlib +import json +from collections import Counter +from pathlib import Path + + +def score(report, fixture): + if report.get("complete") is not True or report.get("strict_point_score_eligible") is not False: + raise ValueError("Incomplete report or invented point scoring") + core = report["core"] + if {source["id"] for source in core["sources"]} != {"speech-en", "speech-de", "music-control", "silence-control"}: + raise ValueError("Incomplete competitor pool") + queries = {query["id"]: query for query in fixture["queries"]} + if len(core["rows"]) != len(queries) or {row["query"]["id"] for row in core["rows"]} != set(queries): + raise ValueError("Missing or duplicated frozen query") + rows = [] + for row in core["rows"]: + query = row["query"] + if query != queries[query["id"]] or row.get("strict_point_score_eligible") is not False: + raise ValueError("Frozen query changed") + hits = row["hits"] + ids = [hit["asset"] for hit in hits] + if len(set(ids)) != len(ids) or not set(ids) <= {source["id"] for source in core["sources"]}: + raise ValueError("Unexpected result pool") + rank = next((index + 1 for index, id in enumerate(ids) if id in query["expected"]), None) + rows.append({"id": query["id"], "language": query["language"], "positive": bool(query["expected"]), + "first_relevant_rank": rank, "returned_count": len(hits), + "first_hit": None if not hits else {"asset": hits[0]["asset"], "match_kind": hits[0]["result"]["match_kind"], "snippet": hits[0]["result"]["snippet"], "source": hits[0]["result"]["source"]}}) + positive = [row for row in rows if row["positive"]] + negative = [row for row in rows if not row["positive"]] + native_gate_counts = Counter() + complete_calls = True + for case in (core, report["seams"]): + expected = Counter(window["pcm_sha256"] for source in case["sources"] for window in source["windows"]) + actual = Counter(call["pcm_sha256"] for call in case["native_calls"]) + complete_calls &= expected == actual and all("error" not in call for call in case["native_calls"]) + for call in core["native_calls"]: + ids = {window["asset"] for window in call["expected_source_windows"]} + if len(ids) == 1: + native_gate_counts[next(iter(ids))] += len(call.get("transcription", {}).get("passages", [])) + concepts = {} + for asset in fixture["assets"]: + text = " ".join(chunk["text"] for chunk in core["published_speech"] + if Path(chunk["path"]).stem == asset["id"]).casefold() + concepts[asset["id"]] = {keyword: keyword.casefold() in text for keyword in asset["reference_keywords"]} + no_false_speech = all(native_gate_counts[id] == 0 for id in ("music-control", "silence-control")) + eligible = complete_calls and core["comparison_eligible"] and report["seams"]["comparison_eligible"] + return {"comparison_eligible": eligible, "strict_point_score_eligible": False, + "positive_queries": len(positive), "top1": sum(row["first_relevant_rank"] == 1 for row in positive) if eligible else None, + "recall_at_5": sum(row["first_relevant_rank"] is not None and row["first_relevant_rank"] <= 5 for row in positive) if eligible else None, + "mrr": sum(1 / row["first_relevant_rank"] if row["first_relevant_rank"] else 0 for row in positive) / len(positive) if eligible else None, + "negative_queries": len(negative), "negative_returned_any": sum(row["returned_count"] > 0 for row in negative), + "native_passage_counts": dict(native_gate_counts), "negative_gate_acceptance": no_false_speech, + "reference_concept_substring_coverage": concepts, "rows": rows, + "scope": "Source-level retrieval only, substring concept diagnostic not WER; no timed publisher phrase labels or semantic confidence; retain hallucination failures"} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("report", type=Path) + parser.add_argument("fixture", type=Path) + parser.add_argument("output", type=Path) + args = parser.parse_args() + raw = args.report.read_bytes() + fixture_raw = args.fixture.read_bytes() + lock = json.loads(args.fixture.with_name("labels.lock.json").read_text()) + if hashlib.sha256(fixture_raw).hexdigest() != lock["fixture_sha256"]: + raise ValueError("Frozen labels changed") + result = score(json.loads(raw), json.loads(fixture_raw)) + result.update(raw_report_sha256=hashlib.sha256(raw).hexdigest(), fixture_sha256=lock["fixture_sha256"]) + with args.output.open("x", encoding="utf-8") as output: + json.dump(result, output, ensure_ascii=False, indent=2) + + +if __name__ == "__main__": + main() diff --git a/tools/speech-eval/test_score_native_validation.py b/tools/speech-eval/test_score_native_validation.py new file mode 100644 index 0000000..dcc8ed7 --- /dev/null +++ b/tools/speech-eval/test_score_native_validation.py @@ -0,0 +1,50 @@ +import copy +import unittest + +from score_native_validation import score + + +class ScoringTests(unittest.TestCase): + def inputs(self): + fixture = {"assets": [{"id": "speech-en", "reference_keywords": ["equal"]}, {"id": "speech-de", "reference_keywords": ["gleich"]}], + "queries": [{"id": "positive", "language": "en", "text": "equal dignity", "expected": ["speech-en", "speech-de"]}, {"id": "absent", "language": "en", "text": "salary", "expected": []}]} + sources = [{"id": id, "windows": [{"pcm_sha256": id}]} for id in ("speech-en", "speech-de", "music-control", "silence-control")] + calls = [{"pcm_sha256": source["id"], "expected_source_windows": [{"asset": source["id"]}], "transcription": {"passages": []}} for source in sources] + core = {"sources": sources, "native_calls": calls, "comparison_eligible": True, "published_speech": [], "rows": []} + for query in fixture["queries"]: + core["rows"].append({"query": query, "strict_point_score_eligible": False, + "hits": [{"asset": "speech-de", "result": {"match_kind": "semantic_speech", "snippet": "gleich", "source": {"kind": "timestamp_ms", "number": 5000}}}]}) + return {"complete": True, "strict_point_score_eligible": False, "core": core, "seams": {"sources": [], "native_calls": [], "comparison_eligible": True}}, fixture + + def test_no_answer_return_is_failure_not_positive_recall(self): + report, fixture = self.inputs() + result = score(report, fixture) + self.assertEqual(result["top1"], 1) + self.assertEqual(result["negative_returned_any"], 1) + self.assertFalse(result["strict_point_score_eligible"]) + + def test_dropped_native_window_invalidates_comparison(self): + report, fixture = self.inputs() + report["core"]["native_calls"].pop() + result = score(report, fixture) + self.assertFalse(result["comparison_eligible"]) + self.assertIsNone(result["top1"]) + self.assertIsNone(result["mrr"]) + + def test_silence_hallucination_is_retained_failure(self): + report, fixture = self.inputs() + report["core"]["native_calls"][-1]["transcription"]["passages"] = [{"text": "fake speech"}] + self.assertFalse(score(report, fixture)["negative_gate_acceptance"]) + + def test_frozen_query_mutation_is_rejected(self): + report, fixture = self.inputs() + report = copy.deepcopy(report) + report["core"]["rows"][0]["query"]["text"] = "changed" + with self.assertRaises(ValueError): + score(report, fixture) + + def test_duplicate_query_is_rejected(self): + report, fixture = self.inputs() + report["core"]["rows"][1] = report["core"]["rows"][0] + with self.assertRaises(ValueError): + score(report, fixture)