Freeze actual Google music metadata and precise speech timing diagnostics

This commit is contained in:
karl committed 2026-10-13 18:11:07 +02:00
1 parent 8b4275d9b4
commit 6353d02c04
8 files changed
+440 -1

No files matched your search

+5
View File
@@ -869,11 +869,16 @@ 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",
]
+27
View File
@@ -33,3 +33,30 @@ The dense reference samples video every two seconds and PCM in thirty-second win
- Model agreement with another implementation, synthetic document-seed scores, and this small-media benchmark each answer different questions. None is production quality certification.
Detailed provenance is in [the fixture README](../../fixtures/media/README.md). The final assessment belongs in [MEDIA-QUALITY.md](../../docs/MEDIA-QUALITY.md).
## Music metadata hypothesis diagnostic
`music_metadata` compares raw audio with the per-file maximum over real audio
and one Google-embedded metadata passage. It has no tuned weight or rejection
threshold and does not change production. `prepare` freezes actual FFprobe tags,
source hashes, default production audio windows, and all 65 existing queries
before inference. Existing plans/reports cannot be overwritten.
```powershell
cargo test --manifest-path tools/media-eval/Cargo.toml --bin music_metadata --locked
cargo run --manifest-path tools/media-eval/Cargo.toml --bin music_metadata --locked -- prepare fixtures/media .searchi/media-runtime/bin/ffmpeg.exe .searchi/media-runtime/bin/ffprobe.exe .searchi/qa/music-metadata-v1-plan.json
$env:SEARCHI_FORCE_CPU = '1'
cargo run --manifest-path tools/media-eval/Cargo.toml --bin music_metadata --locked -- run fixtures/media .searchi/media-runtime/bin/ffmpeg.exe .searchi/media-runtime/bin/ffprobe.exe .searchi/qa/music-metadata-v1-plan.json .searchi/models/embeddinggemma2 .searchi/runtime/auto/Release/searchi-gemma-worker.exe .searchi/qa/music-metadata-v1-results.json
```
The whitelist is artist, title, album, genre and composer in that fixed order,
bounded to 2048 UTF-8 bytes. Missing/blank tags produce no metadata vector;
encoder, comment, URLs and copyright boilerplate are excluded. Literal misleading
tags remain misleading evidence: this tool does not verify musical instruments
or generate a description. Metadata evidence has no invented timestamp.
This is the known two-music-file pool, not mixed production retrieval. Other-file
positives are explicitly ineligible, filters remain explicitly unsupported, and
all unfiltered negatives remain visible without abstention. Existing queries are
diagnostics rather than new held-out evidence. Unseen music with missing and
misleading tags needs independently frozen validation before deployment.
+224
View File
@@ -0,0 +1,224 @@
//! Frozen two-file hypothesis diagnostic; not production or abstention evidence.
use anyhow::{ensure, Context, Result};
use searchi_media::{audio_windows, decode_audio, probe, FfmpegTools, MediaLimits};
use searchi_semantic::{Device, GemmaEmbedder};
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::{collections::BTreeMap, fs, io::Write, path::PathBuf, time::Instant};
const POLICY: &str =
"music-metadata-v1:artist-title-album-genre-composer:2048bytes:max-real-evidence:no-cutoff";
fn digest(bytes: &[u8]) -> String {
format!("{:x}", Sha256::digest(bytes))
}
fn metadata_text(tags: &BTreeMap<String, String>) -> String {
let mut text = String::new();
for key in ["artist", "title", "album", "genre", "composer"] {
if let Some(value) = tags.get(key).filter(|v| !v.trim().is_empty()) {
let prefix = format!("{key}: ");
let available = 2048usize.saturating_sub(text.len() + prefix.len() + 1);
let mut end = value.len().min(available);
while !value.is_char_boundary(end) {
end -= 1;
}
if end == 0 {
break;
}
text.push_str(&prefix);
text.push_str(&value[..end]);
text.push('\n');
}
}
text
}
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 main() -> Result<()> {
let args: Vec<_> = std::env::args_os().skip(1).collect();
ensure!(
args.len() == 5 || args.len() == 8,
"prepare CORPUS FFMPEG FFPROBE PLAN or run CORPUS FFMPEG FFPROBE PLAN MODEL WORKER OUTPUT"
);
let mode = args[0].to_string_lossy();
ensure!(
(mode == "prepare" && args.len() == 5) || (mode == "run" && args.len() == 8),
"Invalid mode/arguments"
);
let root = PathBuf::from(&args[1]);
let fixture_bytes = fs::read(root.join("fixture.json"))?;
let fixture: Value = serde_json::from_slice(&fixture_bytes)?;
let lock: Value = serde_json::from_slice(&fs::read(root.join("labels.lock.json"))?)?;
ensure!(
lock["fixture_sha256"] == digest(&fixture_bytes),
"Frozen fixture changed"
);
let tools = FfmpegTools::new(PathBuf::from(&args[2]), PathBuf::from(&args[3]))?;
let limits = MediaLimits::default();
let mut assets = Vec::new();
let mut probes = Vec::new();
for asset in fixture["assets"]
.as_array()
.context("Assets missing")?
.iter()
.filter(|a| a["kind"] == "music")
{
let path = root.join(asset["path"].as_str().context("Path missing")?);
ensure!(
asset["sha256"] == digest(&fs::read(&path)?),
"Music bytes changed"
);
let metadata = probe(&tools, &path, &limits)?;
assets.push(json!({"id":asset["id"],"path":asset["path"],"sha256":asset["sha256"],"metadata_text":metadata_text(&metadata.tags),"all_actual_tags":metadata.tags,"audio_windows":audio_windows(&metadata,&limits)?}));
probes.push((path, metadata));
}
ensure!(
assets.len() == 2,
"This diagnostic requires the known two-file pool"
);
let plan = json!({"policy":POLICY,"fixture_sha256":digest(&fixture_bytes),"assets":assets,"queries":fixture["queries"],"scope":"All frozen queries retained; other-file positives and unsupported filters are not quality eligible. Raw audio versus per-file maximum of raw audio and real metadata; no threshold, no weights. Misleading tags are not verified musical descriptions; empty tags add no evidence."});
let plan_path = PathBuf::from(&args[4]);
if mode == "prepare" {
return write_new(&plan_path, &plan);
}
ensure!(
std::env::var("SEARCHI_FORCE_CPU").as_deref() == Ok("1"),
"CPU required"
);
let frozen_bytes = fs::read(&plan_path)?;
ensure!(
serde_json::from_slice::<Value>(&frozen_bytes)? == plan,
"Prepared inputs or policy changed"
);
let output = PathBuf::from(&args[7]);
ensure!(!output.exists(), "Existing output must be preserved");
let mut model = GemmaEmbedder::load_with_device(&args[5], &args[6], 1, Device::Cpu)?;
ensure!(
model.info.device.to_lowercase().contains("cpu"),
"Worker is not CPU"
);
let started = Instant::now();
let mut evidence = Vec::new();
for (asset, (path, metadata)) in assets.iter().zip(&probes) {
for window in audio_windows(metadata, &limits)? {
let vector = model.embed_audio_pcm(&decode_audio(&tools, path, window, &limits)?)?;
evidence.push((json!({"asset":asset["id"],"kind":"audio","start_ms":window.start_ms,"duration_ms":window.duration_ms}),vector));
}
let text = asset["metadata_text"]
.as_str()
.context("Metadata missing")?;
if !text.is_empty() {
let vectors = model.embed_documents(&[text], "Music file metadata")?;
ensure!(vectors.len() == 1, "Expected one metadata vector");
evidence.push((
json!({"asset":asset["id"],"kind":"metadata","text":text}),
vectors[0].clone(),
));
}
ensure!(
asset["sha256"] == digest(&fs::read(path)?),
"Source changed during inference"
);
println!("Encoded {}", asset["id"]);
}
let encoding_ms = started.elapsed().as_secs_f64() * 1000.0;
let mut rows = Vec::new();
for query in fixture["queries"].as_array().context("Queries missing")? {
if query.get("filters").is_some() {
rows.push(json!({"query":query,"unsupported":"Pool-only diagnostic does not enforce production filters"}));
continue;
}
let vector = model.embed_query(query["text"].as_str().context("Query missing")?)?;
let scores: Vec<_> = evidence
.iter()
.map(|(source, encoded)| {
(
source,
vector
.iter()
.zip(encoded)
.map(|(a, b)| f64::from(*a) * f64::from(*b))
.sum::<f64>(),
)
})
.collect();
let mut lanes = Vec::new();
for metadata in [false, true] {
let mut files = Vec::new();
for asset in &assets {
let best = scores
.iter()
.filter(|(source, _)| {
source["asset"] == asset["id"] && (metadata || source["kind"] == "audio")
})
.max_by(|a, b| a.1.total_cmp(&b.1))
.context("Missing audio evidence")?;
files.push(json!({"source":best.0,"score":best.1}));
}
files.sort_by(|a, b| {
b["score"]
.as_f64()
.unwrap()
.total_cmp(&a["score"].as_f64().unwrap())
});
lanes.push(json!({"metadata_enabled":metadata,"file_rankings":files}));
}
let eligible = query["expected"].as_array().is_some_and(|a| {
a.iter()
.all(|id| assets.iter().any(|asset| asset["id"] == *id))
});
rows.push(json!({"query":query,"pool_quality_eligible":eligible,"lanes":lanes}));
}
write_new(
&output,
&json!({"plan":plan,"plan_sha256":digest(&frozen_bytes),"worker":model.info,"worker_sha256":digest(&fs::read(&args[6])?),"encoding_ms":encoding_ms,"evidence_count":evidence.len(),"rows":rows,"production_policy_changed":false}),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_and_untrusted_boilerplate_add_no_evidence() {
assert!(metadata_text(&BTreeMap::from([
("encoder".into(), "Violin".into()),
("artist".into(), " ".into())
]))
.is_empty());
}
#[test]
fn unicode_is_bounded_without_inventing_descriptions() {
let text = metadata_text(&BTreeMap::from([
("title".into(), "ä".repeat(3000)),
("genre".into(), "Jazz".into()),
]));
assert!(text.len() <= 2048);
assert!(text.starts_with("title: ä"));
assert!(!text.contains("Jazz"));
}
#[test]
fn stable_whitelist_order_and_literal_misleading_values() {
assert_eq!(
metadata_text(&BTreeMap::from([
("artist".into(), "A".into()),
("title".into(), "Not actually a violin".into())
])),
"artist: A\ntitle: Not actually a violin\n"
);
}
#[test]
fn existing_report_is_preserved() -> Result<()> {
let dir = tempfile::tempdir()?;
let path = dir.path().join("report.json");
fs::write(&path, b"original")?;
assert!(write_new(&path, &json!({})).is_err());
assert_eq!(fs::read(path)?, b"original");
Ok(())
}
}