Freeze separate music metadata field diagnostic
This commit is contained in:
1 parent
fede6ce3bb
commit
705500425a
2 files changed
+234
No files matched your search
@@ -0,0 +1,216 @@
|
||||
//! Known-pool hypothesis: separate real metadata fields, retaining verified v1 audio scores.
|
||||
use anyhow::{ensure, Context, Result};
|
||||
use searchi_semantic::{Device, GemmaEmbedder};
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::{fs, io::Write, path::PathBuf, time::Instant};
|
||||
|
||||
const BASELINE: &str = "f985ba639a14b8c0288b8fa7812d580f179f31c8cad0c21d67531c8a4fddc5b6";
|
||||
const POLICY: &str = "metadata-fields-v2:artist-title-album-genre-composer:2048bytes-total:neutral-file-title:max-real-evidence:no-cutoff";
|
||||
fn digest(bytes: &[u8]) -> String {
|
||||
format!("{:x}", Sha256::digest(bytes))
|
||||
}
|
||||
fn read_baseline(bytes: &[u8]) -> Result<Value> {
|
||||
ensure!(
|
||||
digest(bytes) == BASELINE,
|
||||
"Expected exact preserved v1 report"
|
||||
);
|
||||
let value: Value = serde_json::from_slice(bytes)?;
|
||||
ensure!(
|
||||
value["worker"]["model"] == "google/embeddinggemma-2" && value["worker"]["device"] == "cpu",
|
||||
"Unexpected baseline runtime"
|
||||
);
|
||||
Ok(value)
|
||||
}
|
||||
fn fields(tags: &Value) -> Vec<Value> {
|
||||
let mut remaining = 2048usize;
|
||||
let mut result = Vec::new();
|
||||
for field in ["artist", "title", "album", "genre", "composer"] {
|
||||
if let Some(value) = tags[field].as_str().filter(|s| !s.trim().is_empty()) {
|
||||
let prefix = format!("{field}: ");
|
||||
let mut end = value.len().min(remaining.saturating_sub(prefix.len()));
|
||||
while !value.is_char_boundary(end) {
|
||||
end -= 1;
|
||||
}
|
||||
if end == 0 {
|
||||
break;
|
||||
}
|
||||
let text = format!("{prefix}{}", &value[..end]);
|
||||
remaining -= text.len();
|
||||
result.push(json!({"field":field,"text":text}));
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
fn write_new(path: &PathBuf, 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 cosine(a: &[f32], b: &[f32]) -> Result<f64> {
|
||||
ensure!(a.len() == 768 && b.len() == 768, "Expected 768 dimensions");
|
||||
ensure!(a.iter().chain(b).all(|v| v.is_finite()), "Nonfinite vector");
|
||||
Ok(a.iter()
|
||||
.zip(b)
|
||||
.map(|(a, b)| f64::from(*a) * f64::from(*b))
|
||||
.sum())
|
||||
}
|
||||
fn main() -> Result<()> {
|
||||
let args: Vec<_> = std::env::args_os().skip(1).collect();
|
||||
ensure!(
|
||||
args.len() == 3 || args.len() == 7,
|
||||
"prepare BASELINE PLAN or run CORPUS BASELINE PLAN MODEL WORKER OUTPUT"
|
||||
);
|
||||
let preparing = args[0] == "prepare";
|
||||
ensure!(
|
||||
(preparing && args.len() == 3) || (args[0] == "run" && args.len() == 7),
|
||||
"Invalid arguments"
|
||||
);
|
||||
let baseline_path = if preparing { &args[1] } else { &args[2] };
|
||||
let baseline = read_baseline(&fs::read(baseline_path)?)?;
|
||||
let assets:Vec<_>=baseline["plan"]["assets"].as_array().context("Assets missing")?.iter().map(|asset|json!({"id":asset["id"],"path":asset["path"],"sha256":asset["sha256"],"fields":fields(&asset["all_actual_tags"])})).collect();
|
||||
let plan = json!({"policy":POLICY,"baseline_sha256":BASELINE,"fixture_sha256":baseline["plan"]["fixture_sha256"],"worker_sha256":baseline["worker_sha256"],"assets":assets,"queries":baseline["plan"]["queries"],"scope":"Known two-file diagnostic only. Audio maxima reused verbatim from hash-verified v1. All queries retained; no cutoff or weights. Other-file positives ineligible. Metadata never receives a timestamp and misleading tags remain unverified."});
|
||||
let plan_path = PathBuf::from(if preparing { &args[2] } else { &args[3] });
|
||||
if preparing {
|
||||
return write_new(&plan_path, &plan);
|
||||
}
|
||||
ensure!(
|
||||
std::env::var("SEARCHI_FORCE_CPU").as_deref() == Ok("1"),
|
||||
"CPU required"
|
||||
);
|
||||
let frozen = fs::read(&plan_path)?;
|
||||
ensure!(
|
||||
serde_json::from_slice::<Value>(&frozen)? == plan,
|
||||
"Frozen plan changed"
|
||||
);
|
||||
let root = PathBuf::from(&args[1]);
|
||||
ensure!(
|
||||
plan["fixture_sha256"] == digest(&fs::read(root.join("fixture.json"))?),
|
||||
"Fixture changed"
|
||||
);
|
||||
for asset in &assets {
|
||||
ensure!(
|
||||
asset["sha256"]
|
||||
== digest(&fs::read(
|
||||
root.join(asset["path"].as_str().context("Path missing")?)
|
||||
)?),
|
||||
"Asset changed"
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
plan["worker_sha256"] == digest(&fs::read(&args[5])?),
|
||||
"Worker differs from retained audio baseline"
|
||||
);
|
||||
let output = PathBuf::from(&args[6]);
|
||||
ensure!(!output.exists(), "Preserve existing output");
|
||||
let mut model = GemmaEmbedder::load_with_device(&args[4], &args[5], 1, Device::Cpu)?;
|
||||
ensure!(
|
||||
serde_json::to_value(&model.info)? == baseline["worker"],
|
||||
"Runtime differs from baseline"
|
||||
);
|
||||
let started = Instant::now();
|
||||
let mut evidence = Vec::new();
|
||||
for asset in &assets {
|
||||
for field in asset["fields"].as_array().context("Fields missing")? {
|
||||
let text = field["text"].as_str().context("Field text missing")?;
|
||||
let mut vectors = model.embed_documents(&[text], "File metadata")?;
|
||||
ensure!(vectors.len() == 1, "Expected one field vector");
|
||||
evidence.push((
|
||||
json!({"asset":asset["id"],"kind":"metadata","field":field["field"],"text":text}),
|
||||
vectors.remove(0),
|
||||
));
|
||||
}
|
||||
}
|
||||
let encoding_ms = started.elapsed().as_secs_f64() * 1000.0;
|
||||
let mut rows = Vec::new();
|
||||
for previous in baseline["rows"].as_array().context("Rows missing")? {
|
||||
if previous.get("unsupported").is_some() {
|
||||
rows.push(previous.clone());
|
||||
continue;
|
||||
}
|
||||
let query = &previous["query"];
|
||||
let vector = model.embed_query(query["text"].as_str().context("Query missing")?)?;
|
||||
let mut ranked = previous["lanes"][0]["file_rankings"]
|
||||
.as_array()
|
||||
.context("Audio maxima missing")?
|
||||
.clone();
|
||||
let mut field_scores = Vec::new();
|
||||
for (source, encoded) in &evidence {
|
||||
let score = cosine(&vector, encoded)?;
|
||||
field_scores.push(json!({"source":source,"score":score}));
|
||||
let hit = ranked
|
||||
.iter_mut()
|
||||
.find(|hit| hit["source"]["asset"] == source["asset"])
|
||||
.context("Missing retained file")?;
|
||||
if score > hit["score"].as_f64().context("Score missing")? {
|
||||
*hit = json!({"source":source,"score":score});
|
||||
}
|
||||
}
|
||||
ranked.sort_by(|a, b| {
|
||||
b["score"]
|
||||
.as_f64()
|
||||
.unwrap()
|
||||
.total_cmp(&a["score"].as_f64().unwrap())
|
||||
});
|
||||
rows.push(json!({"query":query,"pool_quality_eligible":previous["pool_quality_eligible"],"audio_only":previous["lanes"][0]["file_rankings"],"whole_metadata_v1":previous["lanes"][1]["file_rankings"],"field_max_v2":ranked,"field_scores":field_scores}));
|
||||
}
|
||||
for asset in &assets {
|
||||
ensure!(
|
||||
asset["sha256"] == digest(&fs::read(root.join(asset["path"].as_str().unwrap()))?),
|
||||
"Asset changed during inference"
|
||||
);
|
||||
}
|
||||
write_new(
|
||||
&output,
|
||||
&json!({"plan":plan,"plan_sha256":digest(&frozen),"worker":model.info,"encoding_ms":encoding_ms,"metadata_inference_calls":evidence.len(),"audio_inference_calls":0,"rows":rows,"production_policy_changed":false}),
|
||||
)
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn untrusted_boilerplate_and_blank_fields_are_excluded() {
|
||||
assert!(fields(&json!({"encoder":"violin","title":" "})).is_empty());
|
||||
}
|
||||
#[test]
|
||||
fn field_order_and_unicode_budget_are_stable() {
|
||||
let values = fields(&json!({"title":"ä".repeat(2000),"artist":"Actual"}));
|
||||
assert_eq!(values[0]["field"], "artist");
|
||||
assert!(
|
||||
values
|
||||
.iter()
|
||||
.map(|v| v["text"].as_str().unwrap().len())
|
||||
.sum::<usize>()
|
||||
<= 2048
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn misleading_metadata_remains_literal() {
|
||||
assert_eq!(
|
||||
fields(&json!({"genre":"Wrong genre"}))[0]["text"],
|
||||
"genre: Wrong genre"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn baseline_tampering_is_rejected() {
|
||||
assert!(read_baseline(b"{}").is_err());
|
||||
}
|
||||
#[test]
|
||||
fn dimension_mismatch_and_nonfinite_vectors_are_rejected() {
|
||||
assert!(cosine(&[1.0], &[1.0]).is_err());
|
||||
assert!(cosine(&vec![f32::NAN; 768], &vec![0.0; 768]).is_err());
|
||||
}
|
||||
#[test]
|
||||
fn output_is_never_overwritten() -> Result<()> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let path = dir.path().join("report");
|
||||
fs::write(&path, b"old")?;
|
||||
assert!(write_new(&path, &json!({})).is_err());
|
||||
assert_eq!(fs::read(path)?, b"old");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user