141 lines
5.2 KiB
Rust
141 lines
5.2 KiB
Rust
//! Explicit identities constrain genuine Google media retrieval, without a cosine cutoff.
|
|
use anyhow::{Context, Result};
|
|
use searchi_core::{IndexOptions, SearchIndex};
|
|
use searchi_service::{FfmpegTools, HybridSearch, MediaLimits, SearchOptions};
|
|
use std::{fs, path::Path, process::Command, sync::atomic::AtomicBool};
|
|
|
|
fn tagged_tone(ffmpeg: &Path, path: &Path, artist: &str, title: &str) -> Result<()> {
|
|
let output = Command::new(ffmpeg)
|
|
.args([
|
|
"-hide_banner",
|
|
"-loglevel",
|
|
"error",
|
|
"-nostdin",
|
|
"-threads",
|
|
"1",
|
|
"-f",
|
|
"lavfi",
|
|
"-i",
|
|
"sine=frequency=440:sample_rate=16000:duration=2",
|
|
"-c:a",
|
|
"pcm_s16le",
|
|
"-metadata",
|
|
])
|
|
.arg(format!("artist={artist}"))
|
|
.arg("-metadata")
|
|
.arg(format!("title={title}"))
|
|
.arg(path)
|
|
.output()?;
|
|
anyhow::ensure!(
|
|
output.status.success(),
|
|
"Owned FFmpeg fixture creation failed: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
#[ignore = "Requires verified Google assets, normal native worker and FFmpeg helpers; CPU only"]
|
|
fn genuine_google_exact_music_constraints_survive_publication_and_hint_refresh() -> Result<()> {
|
|
assert_eq!(
|
|
std::env::var("SEARCHI_FORCE_CPU")?,
|
|
"1",
|
|
"VM inference must use CPU only"
|
|
);
|
|
let model =
|
|
std::env::var_os("SEARCHI_GEMMA_MODEL_DIR").context("Set SEARCHI_GEMMA_MODEL_DIR")?;
|
|
let worker = std::env::var_os("SEARCHI_GEMMA_WORKER").context("Set SEARCHI_GEMMA_WORKER")?;
|
|
let tools = FfmpegTools {
|
|
ffmpeg: std::env::var_os("SEARCHI_TEST_FFMPEG")
|
|
.context("Set SEARCHI_TEST_FFMPEG")?
|
|
.into(),
|
|
ffprobe: std::env::var_os("SEARCHI_TEST_FFPROBE")
|
|
.context("Set SEARCHI_TEST_FFPROBE")?
|
|
.into(),
|
|
};
|
|
let temp = tempfile::tempdir()?;
|
|
let root = temp.path().join("music");
|
|
fs::create_dir(&root)?;
|
|
let target = root.join("z-target.wav");
|
|
tagged_tone(&tools.ffmpeg, &target, "Björk", "Jóga")?;
|
|
// Identical audio is a deliberately strong distractor; identity must come from tags.
|
|
tagged_tone(
|
|
&tools.ffmpeg,
|
|
&root.join("a-distractor.wav"),
|
|
"Other Artist",
|
|
"Björk",
|
|
)?;
|
|
let db = temp.path().join("index.db");
|
|
let mut lexical = SearchIndex::open(&db)?;
|
|
lexical.index_folder(&root)?;
|
|
let mut indexer = HybridSearch::open(&db, &model, &worker, 1)?;
|
|
assert_eq!(
|
|
indexer.worker_info().context("Google worker info")?.device,
|
|
"cpu"
|
|
);
|
|
let report = indexer.index_embeddings_with_media(&tools, &MediaLimits::default())?;
|
|
assert!(report.failures.is_empty());
|
|
assert_eq!(report.media_files, 2);
|
|
assert_eq!(report.audio_inference_calls, 2);
|
|
drop(indexer);
|
|
let mut search = HybridSearch::open_query(&db, &model, &worker, 1)?;
|
|
let options = SearchOptions {
|
|
media_artist: Some("BJÖRK".into()),
|
|
media_title: Some("JÓGA".into()),
|
|
..Default::default()
|
|
};
|
|
let metadata = search.search_measured("", 1, &options, true)?;
|
|
assert_eq!(metadata.results.len(), 1);
|
|
assert_eq!(metadata.results[0].filename, "z-target.wav");
|
|
assert!(!metadata
|
|
.metrics
|
|
.trace
|
|
.context("debug trace")?
|
|
.iter()
|
|
.any(|span| span.name == "semantic.query_embedding"));
|
|
let query = "the sound of a sine wave tone";
|
|
let semantic = search.search_semantic(query, 1, &options)?;
|
|
assert_eq!(semantic.len(), 1);
|
|
assert_eq!(semantic[0].filename, "z-target.wav");
|
|
assert_eq!(
|
|
search.search(query, 1, &options)?[0].filename,
|
|
"z-target.wav"
|
|
);
|
|
let absent = SearchOptions {
|
|
media_artist: Some("Absent Artist".into()),
|
|
..Default::default()
|
|
};
|
|
assert!(search.search(query, 1, &absent)?.is_empty());
|
|
assert!(search.search("", 1, &absent)?.is_empty());
|
|
let collision = SearchOptions {
|
|
media_artist: Some("Jóga".into()),
|
|
..Default::default()
|
|
};
|
|
assert!(search.search(query, 1, &collision)?.is_empty());
|
|
// A native same-stat hint invalidates old tags and warmed ANN eligibility.
|
|
let canonical = target.canonicalize()?;
|
|
lexical.index_paths_cancellable(
|
|
&root,
|
|
std::slice::from_ref(&canonical),
|
|
&IndexOptions::default(),
|
|
&AtomicBool::new(false),
|
|
)?;
|
|
assert!(search.search("", 1, &options)?.is_empty());
|
|
assert!(search.search_semantic(query, 1, &options)?.is_empty());
|
|
let mut indexer = HybridSearch::open(&db, &model, &worker, 1)?;
|
|
let refreshed = indexer.index_embeddings_with_media(&tools, &MediaLimits::default())?;
|
|
assert!(refreshed.failures.is_empty());
|
|
drop(indexer);
|
|
assert_eq!(search.search("", 1, &options)?[0].filename, "z-target.wav");
|
|
assert_eq!(
|
|
search.search_semantic(query, 1, &options)?[0].filename,
|
|
"z-target.wav"
|
|
);
|
|
// Deletion is suppressed without retaining an exact identity match.
|
|
fs::remove_file(&target)?;
|
|
lexical.index_folder(&root)?;
|
|
assert!(search.search("", 1, &options)?.is_empty());
|
|
assert!(search.search_semantic(query, 1, &options)?.is_empty());
|
|
Ok(())
|
|
}
|