114 lines
3.8 KiB
Rust
114 lines
3.8 KiB
Rust
//! Explicit real local speech proof; excluded from ordinary structural checks.
|
|
use anyhow::{ensure, Context, Result};
|
|
use searchi_speech::{Device, SpeechWorker, MAX_SAMPLES};
|
|
use std::{
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
time::{SystemTime, UNIX_EPOCH},
|
|
};
|
|
fn required(name: &str) -> Result<PathBuf> {
|
|
std::env::var_os(name)
|
|
.map(PathBuf::from)
|
|
.with_context(|| format!("Set {name} for actual CPU speech proof"))
|
|
}
|
|
struct OwnedFixture(PathBuf);
|
|
impl Drop for OwnedFixture {
|
|
fn drop(&mut self) {
|
|
let _ = fs::remove_dir_all(&self.0);
|
|
}
|
|
}
|
|
fn link_or_copy(source: &Path, target: &Path) -> Result<()> {
|
|
if fs::hard_link(source, target).is_err() {
|
|
fs::copy(source, target)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "Requires actual pinned native worker/models and a verified speech PCM fixture; CPU only"]
|
|
fn actual_speech_reuses_contexts_and_supports_unicode_runtime_and_model_paths() -> Result<()> {
|
|
let worker = required("SEARCHI_TEST_SPEECH_WORKER")?;
|
|
let base = required("SEARCHI_TEST_SPEECH_BASE")?;
|
|
let vad = required("SEARCHI_TEST_SPEECH_VAD")?;
|
|
let pcm_file = required("SEARCHI_TEST_SPEECH_PCM")?;
|
|
ensure!(
|
|
fs::metadata(&pcm_file)?.len() <= MAX_SAMPLES as u64 * 4,
|
|
"Proof PCM exceeds limit"
|
|
);
|
|
let bytes = fs::read(pcm_file)?;
|
|
ensure!(
|
|
!bytes.is_empty() && bytes.len() % 4 == 0,
|
|
"Invalid proof PCM bytes"
|
|
);
|
|
let pcm: Vec<f32> = bytes
|
|
.as_chunks::<4>()
|
|
.0
|
|
.iter()
|
|
.map(|b| f32::from_le_bytes(*b))
|
|
.collect();
|
|
let unique = SystemTime::now().duration_since(UNIX_EPOCH)?.as_nanos();
|
|
let fixture = OwnedFixture(std::env::temp_dir().join(format!(
|
|
"searchi-sprachtest-München-測試-🚀-{}-{unique}",
|
|
std::process::id()
|
|
)));
|
|
fs::create_dir(&fixture.0)?;
|
|
let runtime = fixture.0.join("Laufzeit-測試-🚀");
|
|
fs::create_dir(&runtime)?;
|
|
let inventory = worker
|
|
.parent()
|
|
.context("Worker directory missing")?
|
|
.join("runtime-manifest.json");
|
|
let manifest: serde_json::Value = serde_json::from_slice(&fs::read(&inventory)?)?;
|
|
for record in manifest["files"]
|
|
.as_array()
|
|
.context("Missing runtime files")?
|
|
{
|
|
let name = record["path"].as_str().context("Invalid inventory name")?;
|
|
ensure!(
|
|
!name.contains(['/', '\\', ':']) && name != "..",
|
|
"Unsafe fixture artifact name"
|
|
);
|
|
fs::copy(worker.parent().unwrap().join(name), runtime.join(name))?;
|
|
}
|
|
fs::copy(inventory, runtime.join("runtime-manifest.json"))?;
|
|
let unicode_base = fixture.0.join("Modell-München-測試-🚀.bin");
|
|
let unicode_vad = fixture.0.join("Sprache-測試-🚀.bin");
|
|
link_or_copy(&base, &unicode_base)?;
|
|
link_or_copy(&vad, &unicode_vad)?;
|
|
ensure!(
|
|
std::env::var("SEARCHI_FORCE_CPU").ok().as_deref() == Some("1"),
|
|
"Actual VM inference proof requires forced CPU"
|
|
);
|
|
let mut client = SpeechWorker::load(
|
|
&runtime.join(worker.file_name().unwrap()),
|
|
&unicode_base,
|
|
&unicode_vad,
|
|
Device::Auto,
|
|
)?;
|
|
let first = client.transcribe(&pcm, 16000)?;
|
|
ensure!(
|
|
!first.passages.is_empty(),
|
|
"Actual speech fixture produced no passages"
|
|
);
|
|
let second = client.transcribe(&pcm, 16000)?;
|
|
ensure!(
|
|
first.passages == second.passages,
|
|
"Warm speech result differs"
|
|
);
|
|
ensure!(
|
|
second.metrics.load_ms == 0.0,
|
|
"Persistent contexts unexpectedly reloaded"
|
|
);
|
|
for passage in first.passages {
|
|
ensure!(
|
|
passage.start_ms <= passage.end_ms && passage.end_ms as usize <= pcm.len() / 16,
|
|
"Native passage outside source window"
|
|
);
|
|
}
|
|
println!(
|
|
"actual CPU speech: first={:?}; warm={:?}",
|
|
first.metrics, second.metrics
|
|
);
|
|
Ok(())
|
|
}
|