Preserve explicit code candidates and expose verified model asset milestones
This commit is contained in:
1 parent
6e05227f53
commit
e28e20af38
9 files changed
+475
-16
No files matched your search
@@ -17,6 +17,31 @@ use std::{
|
||||
/// Exact bytes of the pinned text model plus its multimedia projection.
|
||||
pub const TOTAL_BYTES: u64 = 864_676_480;
|
||||
const ATTEMPTS: usize = 3;
|
||||
const BACKBONE_FILE: &str = "embeddinggemma-2-Q8_0.gguf";
|
||||
const PROJECTOR_FILE: &str = "mmproj-embeddinggemma-2-Q8_0.gguf";
|
||||
|
||||
/// One verified pinned artifact. This does not imply the full bundle or an
|
||||
/// inference session is ready; a later artifact can still fail provisioning.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum VerifiedAsset {
|
||||
TextBackbone,
|
||||
MediaProjector,
|
||||
}
|
||||
|
||||
fn verified_asset(artifact: &Artifact) -> Option<VerifiedAsset> {
|
||||
match artifact.path.as_str() {
|
||||
BACKBONE_FILE => Some(VerifiedAsset::TextBackbone),
|
||||
PROJECTOR_FILE => Some(VerifiedAsset::MediaProjector),
|
||||
_ => None, // Private HTTP fixtures may use arbitrary artifact names.
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_artifact_order(files: &[Artifact]) -> Result<()> {
|
||||
if files.len() != 2 || files[0].path != BACKBONE_FILE || files[1].path != PROJECTOR_FILE {
|
||||
bail!("bundled model manifest must provision the text backbone before the media projector");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Manifest {
|
||||
@@ -66,6 +91,7 @@ fn manifest() -> Result<Manifest> {
|
||||
if manifest.files[0].path == manifest.files[1].path {
|
||||
bail!("duplicate model manifest artifact");
|
||||
}
|
||||
validate_artifact_order(&manifest.files)?;
|
||||
Ok(manifest)
|
||||
}
|
||||
|
||||
@@ -158,28 +184,60 @@ fn verified(path: &Path, artifact: &Artifact) -> Result<bool> {
|
||||
/// is completed verified bytes plus bytes of the current attempt. On failure it
|
||||
/// resets to verified completed bytes; cached files never trigger network access.
|
||||
pub fn provision(directory: &Path, progress: &AtomicU64) -> Result<()> {
|
||||
provision_with_verified_assets(directory, progress, |_| {})
|
||||
}
|
||||
|
||||
/// Provision the complete pinned pair, notifying after each artifact's exact
|
||||
/// cached SHA verification or successful verified atomic installation. The
|
||||
/// callback is synchronous and has no error return: callers must keep their
|
||||
/// inference/setup errors separate from download errors. Byte progress alone
|
||||
/// never triggers a notification. Success still requires both artifacts.
|
||||
pub fn provision_with_verified_assets(
|
||||
directory: &Path,
|
||||
progress: &AtomicU64,
|
||||
on_verified: impl FnMut(VerifiedAsset),
|
||||
) -> Result<()> {
|
||||
let manifest = manifest()?;
|
||||
let client = Client::builder()
|
||||
.connect_timeout(Duration::from_secs(20))
|
||||
.timeout(Duration::from_secs(900))
|
||||
.user_agent(concat!("Searchi/", env!("CARGO_PKG_VERSION")))
|
||||
.build()?;
|
||||
provision_with(directory, progress, &manifest, &client, |artifact| {
|
||||
format!(
|
||||
"https://huggingface.co/{}/resolve/{}/{}",
|
||||
manifest.export_repository, manifest.revision, artifact.path
|
||||
)
|
||||
})
|
||||
provision_with_callback(
|
||||
directory,
|
||||
progress,
|
||||
&manifest,
|
||||
&client,
|
||||
|artifact| {
|
||||
format!(
|
||||
"https://huggingface.co/{}/resolve/{}/{}",
|
||||
manifest.export_repository, manifest.revision, artifact.path
|
||||
)
|
||||
},
|
||||
on_verified,
|
||||
)
|
||||
}
|
||||
|
||||
// URL injection is private so tests can use a local HTTP fixture. Production
|
||||
// exposes no arbitrary download source or unverified executable provisioner.
|
||||
#[cfg(test)]
|
||||
fn provision_with(
|
||||
directory: &Path,
|
||||
progress: &AtomicU64,
|
||||
manifest: &Manifest,
|
||||
client: &Client,
|
||||
url: impl Fn(&Artifact) -> String,
|
||||
) -> Result<()> {
|
||||
provision_with_callback(directory, progress, manifest, client, url, |_| {})
|
||||
}
|
||||
|
||||
fn provision_with_callback(
|
||||
directory: &Path,
|
||||
progress: &AtomicU64,
|
||||
manifest: &Manifest,
|
||||
client: &Client,
|
||||
url: impl Fn(&Artifact) -> String,
|
||||
mut on_verified: impl FnMut(VerifiedAsset),
|
||||
) -> Result<()> {
|
||||
fs::create_dir_all(directory).context("create model cache directory")?;
|
||||
progress.store(0, Ordering::Release);
|
||||
@@ -190,6 +248,9 @@ fn provision_with(
|
||||
if verified(&destination, artifact)? {
|
||||
completed += artifact.bytes;
|
||||
progress.store(completed, Ordering::Release);
|
||||
if let Some(asset) = verified_asset(artifact) {
|
||||
on_verified(asset);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let mut last_error = None;
|
||||
@@ -225,6 +286,9 @@ fn provision_with(
|
||||
}
|
||||
completed += artifact.bytes;
|
||||
progress.store(completed, Ordering::Release);
|
||||
if let Some(asset) = verified_asset(artifact) {
|
||||
on_verified(asset);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -564,4 +628,238 @@ mod tests {
|
||||
.unwrap()
|
||||
.ends_with("models/embeddinggemma2"));
|
||||
}
|
||||
#[test]
|
||||
fn pinned_backbone_must_precede_projector() {
|
||||
let mut files = manifest().unwrap().files;
|
||||
validate_artifact_order(&files).unwrap();
|
||||
files.swap(0, 1);
|
||||
assert!(validate_artifact_order(&files).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cached_pair_emits_verified_callbacks_without_network() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let manifest = test_manifest(vec![
|
||||
artifact(BACKBONE_FILE, b"backbone"),
|
||||
artifact(PROJECTOR_FILE, b"projection"),
|
||||
]);
|
||||
fs::write(directory.path().join(BACKBONE_FILE), b"backbone").unwrap();
|
||||
fs::write(directory.path().join(PROJECTOR_FILE), b"projection").unwrap();
|
||||
let progress = AtomicU64::new(0);
|
||||
let mut observed = Vec::new();
|
||||
provision_with_callback(
|
||||
directory.path(),
|
||||
&progress,
|
||||
&manifest,
|
||||
&client(),
|
||||
|_| panic!("Cached pair attempted network"),
|
||||
|asset| {
|
||||
observed.push(asset);
|
||||
let bytes = match asset {
|
||||
VerifiedAsset::TextBackbone => 8,
|
||||
VerifiedAsset::MediaProjector => 18,
|
||||
};
|
||||
assert_eq!(progress.load(Ordering::Acquire), bytes);
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
observed,
|
||||
[VerifiedAsset::TextBackbone, VerifiedAsset::MediaProjector]
|
||||
);
|
||||
assert_eq!(progress.load(Ordering::Acquire), 18);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_projector_retains_verified_text_callback_and_progress() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
fs::write(directory.path().join(PROJECTOR_FILE), b"oldproject").unwrap();
|
||||
let manifest = test_manifest(vec![
|
||||
artifact(BACKBONE_FILE, b"backbone"),
|
||||
artifact(PROJECTOR_FILE, b"projection"),
|
||||
]);
|
||||
let mut responses = vec![response(b"backbone", Some(8))];
|
||||
responses.extend(vec![response(b"corrupted!", Some(10)); ATTEMPTS]);
|
||||
let (url, server) = server(responses);
|
||||
let progress = AtomicU64::new(0);
|
||||
let mut observed = Vec::new();
|
||||
let result = provision_with_callback(
|
||||
directory.path(),
|
||||
&progress,
|
||||
&manifest,
|
||||
&client(),
|
||||
|_| url.clone(),
|
||||
|asset| {
|
||||
assert_eq!(asset, VerifiedAsset::TextBackbone);
|
||||
assert_eq!(
|
||||
fs::read(directory.path().join(BACKBONE_FILE)).unwrap(),
|
||||
b"backbone"
|
||||
);
|
||||
observed.push(asset);
|
||||
},
|
||||
);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"The incomplete bundle must not be successful"
|
||||
);
|
||||
assert_eq!(server.join().unwrap(), ATTEMPTS + 1);
|
||||
assert_eq!(observed, [VerifiedAsset::TextBackbone]);
|
||||
assert_eq!(progress.load(Ordering::Acquire), 8);
|
||||
assert_eq!(
|
||||
fs::read(directory.path().join(PROJECTOR_FILE)).unwrap(),
|
||||
b"oldproject"
|
||||
);
|
||||
assert!(!verified(&directory.path().join(PROJECTOR_FILE), &manifest.files[1]).unwrap());
|
||||
assert_no_part_files(directory.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corrupt_backbone_never_emits_a_verified_callback() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
fs::write(directory.path().join(BACKBONE_FILE), b"corrupt!").unwrap();
|
||||
let manifest = test_manifest(vec![
|
||||
artifact(BACKBONE_FILE, b"backbone"),
|
||||
artifact(PROJECTOR_FILE, b"projection"),
|
||||
]);
|
||||
let (url, server) = server(vec![response(b"corrupt!", Some(8)); ATTEMPTS]);
|
||||
let progress = AtomicU64::new(0);
|
||||
let mut observed = Vec::new();
|
||||
assert!(provision_with_callback(
|
||||
directory.path(),
|
||||
&progress,
|
||||
&manifest,
|
||||
&client(),
|
||||
|_| url.clone(),
|
||||
|asset| observed.push(asset)
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(server.join().unwrap(), ATTEMPTS);
|
||||
assert!(observed.is_empty());
|
||||
assert_eq!(progress.load(Ordering::Acquire), 0);
|
||||
assert!(!directory.path().join(PROJECTOR_FILE).exists());
|
||||
assert_no_part_files(directory.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn partial_backbone_download_never_emits_a_verified_callback() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let manifest = test_manifest(vec![
|
||||
artifact(BACKBONE_FILE, b"backbone"),
|
||||
artifact(PROJECTOR_FILE, b"projection"),
|
||||
]);
|
||||
let (url, server) = server(vec![response(b"back", Some(8)); ATTEMPTS]);
|
||||
let progress = AtomicU64::new(0);
|
||||
let mut observed = Vec::new();
|
||||
assert!(provision_with_callback(
|
||||
directory.path(),
|
||||
&progress,
|
||||
&manifest,
|
||||
&client(),
|
||||
|_| url.clone(),
|
||||
|asset| observed.push(asset)
|
||||
)
|
||||
.is_err());
|
||||
assert_eq!(server.join().unwrap(), ATTEMPTS);
|
||||
assert!(observed.is_empty());
|
||||
assert_eq!(progress.load(Ordering::Acquire), 0);
|
||||
assert!(!directory.path().join(BACKBONE_FILE).exists());
|
||||
assert_no_part_files(directory.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_callback_precedes_gated_projector_and_full_completion() {
|
||||
use std::sync::mpsc;
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
fs::write(directory.path().join(BACKBONE_FILE), b"backbone").unwrap();
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let url = format!("http://{}/fixture", listener.local_addr().unwrap());
|
||||
let (release, wait) = mpsc::channel();
|
||||
let (headers_sent, headers) = mpsc::channel();
|
||||
listener.set_nonblocking(true).unwrap();
|
||||
let server = thread::spawn(move || {
|
||||
let deadline = Instant::now() + Duration::from_secs(5);
|
||||
let mut stream = loop {
|
||||
match listener.accept() {
|
||||
Ok((stream, _)) => break stream,
|
||||
Err(error)
|
||||
if error.kind() == std::io::ErrorKind::WouldBlock
|
||||
&& Instant::now() < deadline =>
|
||||
{
|
||||
thread::sleep(Duration::from_millis(2))
|
||||
}
|
||||
Err(error) => panic!("Gated local fixture accept failed: {error}"),
|
||||
}
|
||||
};
|
||||
stream.set_nonblocking(false).unwrap();
|
||||
stream
|
||||
.set_read_timeout(Some(Duration::from_secs(2)))
|
||||
.unwrap();
|
||||
let mut request = Vec::new();
|
||||
let mut buffer = [0u8; 1024];
|
||||
while !request.windows(4).any(|w| w == b"\r\n\r\n") {
|
||||
let n = stream.read(&mut buffer).unwrap();
|
||||
assert!(n > 0);
|
||||
request.extend_from_slice(&buffer[..n]);
|
||||
assert!(request.len() <= 8192);
|
||||
}
|
||||
stream
|
||||
.write_all(b"HTTP/1.1 200 OK\r\nContent-Length: 10\r\nConnection: close\r\n\r\n")
|
||||
.unwrap();
|
||||
headers_sent.send(()).unwrap();
|
||||
wait.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
stream.write_all(b"projection").unwrap();
|
||||
});
|
||||
let (events, observed) = mpsc::channel();
|
||||
let (done, completion) = mpsc::channel();
|
||||
let path = directory.path().to_path_buf();
|
||||
let provisioning = thread::spawn(move || {
|
||||
let manifest = test_manifest(vec![
|
||||
artifact(BACKBONE_FILE, b"backbone"),
|
||||
artifact(PROJECTOR_FILE, b"projection"),
|
||||
]);
|
||||
let progress = AtomicU64::new(0);
|
||||
let result = provision_with_callback(
|
||||
&path,
|
||||
&progress,
|
||||
&manifest,
|
||||
&client(),
|
||||
|_| url.clone(),
|
||||
|asset| {
|
||||
let artifact = match asset {
|
||||
VerifiedAsset::TextBackbone => &manifest.files[0],
|
||||
VerifiedAsset::MediaProjector => &manifest.files[1],
|
||||
};
|
||||
assert!(verified(&path.join(&artifact.path), artifact).unwrap());
|
||||
events.send(asset).unwrap();
|
||||
},
|
||||
);
|
||||
done.send(result).unwrap();
|
||||
});
|
||||
assert_eq!(
|
||||
observed.recv_timeout(Duration::from_secs(2)).unwrap(),
|
||||
VerifiedAsset::TextBackbone
|
||||
);
|
||||
headers.recv_timeout(Duration::from_secs(2)).unwrap();
|
||||
assert!(
|
||||
observed.try_recv().is_err(),
|
||||
"Projector is not yet verified"
|
||||
);
|
||||
assert!(
|
||||
completion.try_recv().is_err(),
|
||||
"Bundle must remain incomplete"
|
||||
);
|
||||
assert!(!directory.path().join(PROJECTOR_FILE).exists());
|
||||
release.send(()).unwrap();
|
||||
assert_eq!(
|
||||
observed.recv_timeout(Duration::from_secs(2)).unwrap(),
|
||||
VerifiedAsset::MediaProjector
|
||||
);
|
||||
completion
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
provisioning.join().unwrap();
|
||||
server.join().unwrap();
|
||||
assert_no_part_files(directory.path());
|
||||
}
|
||||
}
|
||||
@@ -22,7 +22,7 @@ impl SemanticOrigin {
|
||||
}
|
||||
|
||||
/// A conservative intent hint, not a language understanding model. Explicit
|
||||
/// document/visual cues veto it; ordinary topic and instrument queries do not
|
||||
/// document/visual/code cues veto it; ordinary topic and instrument queries do not
|
||||
/// silently restrict modality. Word boundaries avoid filename substrings.
|
||||
fn preferred_origin(query: &str) -> Option<SemanticOrigin> {
|
||||
if query.split_whitespace().count() == 1 && Path::new(query).extension().is_some() {
|
||||
@@ -49,6 +49,12 @@ fn preferred_origin(query: &str) -> Option<SemanticOrigin> {
|
||||
"screenshot",
|
||||
"dokument",
|
||||
"textdatei",
|
||||
"code",
|
||||
"function",
|
||||
"implementation",
|
||||
"quellcode",
|
||||
"funktion",
|
||||
"implementierung",
|
||||
]) {
|
||||
return None;
|
||||
}
|
||||
@@ -211,6 +217,12 @@ mod tests {
|
||||
"a document that someone mentioned",
|
||||
"speech.png",
|
||||
"a screenshot of an audio waveform",
|
||||
"code that chooses audio search when a query mentions speech",
|
||||
"an implementation for identifying music",
|
||||
"a function that decodes audio samples",
|
||||
"Quellcode für die Suche nach gesprochenen Themen",
|
||||
"eine Funktion für die Verarbeitung von Musik",
|
||||
"Implementierung für die Erkennung von Geräuschen",
|
||||
] {
|
||||
assert_eq!(preferred_origin(query), None, "{query}");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
use anyhow::Result;
|
||||
use searchi_core::SearchIndex;
|
||||
use searchi_service::{FfmpegTools, HybridSearch, MediaLimits, SearchOptions, SemanticOrigin};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
#[test]
|
||||
#[ignore = "Requires verified Google CPU worker/model and FFmpeg fixtures"]
|
||||
fn explicit_code_intent_preserves_code_among_real_audio_candidates() -> Result<()> {
|
||||
assert_eq!(std::env::var("SEARCHI_FORCE_CPU")?, "1");
|
||||
let model = std::env::var_os("SEARCHI_GEMMA_MODEL_DIR").expect("Set model directory");
|
||||
let worker = std::env::var_os("SEARCHI_GEMMA_WORKER").expect("Set worker path");
|
||||
let tools = FfmpegTools {
|
||||
ffmpeg: std::env::var_os("SEARCHI_TEST_FFMPEG")
|
||||
.expect("Set FFmpeg")
|
||||
.into(),
|
||||
ffprobe: std::env::var_os("SEARCHI_TEST_FFPROBE")
|
||||
.expect("Set FFprobe")
|
||||
.into(),
|
||||
};
|
||||
let fixture = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../fixtures");
|
||||
let temp = tempfile::tempdir()?;
|
||||
let corpus = temp.path().join("corpus");
|
||||
fs::create_dir(&corpus)?;
|
||||
// Preserve the unchanged diagnostic implementation and actual licensed sound.
|
||||
fs::copy(
|
||||
fixture.join("mixed/sources/crates__searchi-service__src__routing.rs"),
|
||||
corpus.join("routing.rs"),
|
||||
)?;
|
||||
fs::copy(
|
||||
fixture.join("media/assets/sound02.mp3"),
|
||||
corpus.join("sound.mp3"),
|
||||
)?;
|
||||
let database = temp.path().join("index.db");
|
||||
let mut lexical = SearchIndex::open(&database)?;
|
||||
lexical.index_folder(&corpus)?;
|
||||
let mut indexer = HybridSearch::open(&database, &model, &worker, 1)?;
|
||||
let report = indexer.index_embeddings_with_media(&tools, &MediaLimits::default())?;
|
||||
assert!(report.failures.is_empty(), "{report:?}");
|
||||
assert_eq!(report.media_files, 1);
|
||||
drop(indexer);
|
||||
let mut search = HybridSearch::open_query(&database, &model, &worker, 1)?;
|
||||
let query = "code that chooses audio search when a query mentions speech";
|
||||
let options = SearchOptions::default();
|
||||
let audio = search.search_semantic_with_origin(query, 20, &options, SemanticOrigin::Audio)?;
|
||||
assert!(
|
||||
!audio.is_empty(),
|
||||
"The old exclusive audio lane must qualify"
|
||||
);
|
||||
assert!(audio.iter().all(|hit| hit.filename == "sound.mp3"));
|
||||
let hits = search.search_semantic(query, 20, &options)?;
|
||||
assert!(
|
||||
hits.iter().any(|hit| hit.filename == "routing.rs"),
|
||||
"Explicit code intent lost its semantic implementation: {hits:?}"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
Reference in new issue
Block a user