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(())
}
}
+27
View File
@@ -62,3 +62,30 @@ English recording do not validate German speech or general transcription quality
Pinned CLI flag/output reference:
[upstream cli.cpp](https://github.com/ggml-org/whisper.cpp/blob/d1be6fde11ac6e0407606b4e42fe72d34add8037/examples/cli/cli.cpp).
## Frozen ordinary token-time comparison
`token-policy.json` declares an internal base-only comparison before inference.
Add `--token-sentences` to the existing command. The worker uses full JSON,
ordinary token timestamps and unchanged recognition flags; DTW is disabled.
The harness retains original raw JSON and segments, then adds exact recognized
sentence substrings and their token intervals in `aligned_segments`. No label
selects a sentence, source, word or start. All five sources and complete original
window coverage remain mandatory; empty speech is valid.
Structural tokens cannot become passage text. Ordinary text-token times must be
present, ordered and inside the source segment/window; reconstructed token text
must match original recognized bytes. Invalid alignment is retained as an error
and invalidates an accepted score. No clipping, invented time or omitted failing
competitor is permitted. Rankers must independently reconstruct these passages
from raw JSON and verify policy/contract provenance before publishing quality.
```powershell
$env:SEARCHI_FORCE_CPU = '1'
python tools/speech-eval/run.py --whisper .searchi/speech/build-cpu/bin/Release/whisper-cli.exe --whisper-sha256 12c681b4bcd9c7679b647b9a041e121a2f9e72cb2a9c6f189f529ec8b79c36f8 --ffmpeg .searchi/media-runtime/bin/ffmpeg.exe --ffprobe .searchi/media-runtime/bin/ffprobe.exe --model .searchi/speech/models/ggml-base.bin --candidate base --token-sentences --scratch-parent .searchi/speech/token-scratch --output .searchi/speech/base-token-sentences-v1.json
```
Run only with the coordinator's available CPU inference slot. The previous
base-only run took about 251 seconds including decode/process startup. Token
alignment has not yet been timed; each ASR process retains the 600-second limit.
No production behavior or quality claim follows from structural tests.
+29 -1
View File
@@ -148,10 +148,19 @@ def main():
parser.add_argument("--ffprobe", type=Path, required=True)
parser.add_argument("--model", type=Path, required=True)
parser.add_argument("--candidate", choices=MODELS, required=True)
parser.add_argument("--token-sentences", action="store_true", help="Frozen ordinary token-time diagnostic; retains original segments")
parser.add_argument("--fixture", type=Path, default=Path("fixtures/media/fixture.json"))
parser.add_argument("--scratch-parent", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args()
if args.output.exists():
raise ValueError("Never overwrite experiment evidence")
if args.token_sentences and args.candidate != "base":
raise ValueError("Frozen token experiment accepts multilingual base only")
if args.token_sentences:
policy = json.loads(Path(__file__).with_name("token-policy.json").read_text(encoding="utf-8"))
if policy["worker_sha256"] != args.whisper_sha256:
raise ValueError("Frozen token experiment worker mismatch")
size, digest = MODELS[args.candidate]
if args.model.stat().st_size != size or sha256(args.model) != digest:
raise ValueError("Model size/SHA verification failed")
@@ -200,20 +209,34 @@ def main():
else:
asr_ms = run_owned([str(args.whisper.resolve()), "--no-gpu", "--threads", "1", "--processors", "1",
"--language", "auto", "--model", str(args.model.resolve()), "--file", str(wav),
"--output-json", "--output-file", str(prefix)], scratch, "asr", [(output, MAX_OUTPUT)], 600)
"--output-json-full" if args.token_sentences else "--output-json",
"--output-file", str(prefix)], scratch, "asr", [(output, MAX_OUTPUT)], 600)
raw = json.loads(output.read_text(encoding="utf-8"))
result["windows"].append(recognized_window(raw, offset, length, decode_ms, asr_ms))
result["windows"][-1]["decoded_frames"] = frames
if args.token_sentences:
from token_alignment import aligned_segments
try:
result["windows"][-1]["aligned_segments"] = aligned_segments(raw, offset, length)
except (ValueError, KeyError, TypeError) as error:
result["windows"][-1]["alignment_validation_error"] = str(error)
args.output.parent.mkdir(parents=True, exist_ok=True)
checkpoint = {"complete": False, "fixture_sha256": sha256(args.fixture),
"model_sha256": digest, "worker_sha256": args.whisper_sha256,
"rows": [*rows, result]}
if args.token_sentences:
checkpoint["alignment_policy"] = policy["policy"]
checkpoint["alignment_policy_sha256"] = sha256(Path(__file__).with_name("token-policy.json"))
args.output.write_text(json.dumps(checkpoint, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
if source.stat().st_size != asset["bytes"] or sha256(source) != asset["sha256"]:
raise ValueError("Source changed during recognition")
rows.append(result)
failures = [{"asset": row["asset"], "offset_ms": window["offset_ms"], "error": window["validation_error"]}
for row in rows for window in row["windows"] if "validation_error" in window]
if args.token_sentences:
failures.extend({"asset": row["asset"], "offset_ms": window["offset_ms"],
"error": window["alignment_validation_error"]}
for row in rows for window in row["windows"] if "alignment_validation_error" in window)
report = {"version": 1, "complete": True, "quality_failed": bool(failures), "validation_failures": failures,
"scope": "Actual ASR diagnostic, not Google retrieval or production integration",
"fixture_sha256": sha256(args.fixture), "upstream_commit": UPSTREAM,
@@ -223,6 +246,11 @@ def main():
"harness_sha256": sha256(Path(__file__)),
"policy": "primary-audio-range-v3;aresample16000-before-atrim;samplecoverage-1ms;nonoverlapping30s;independent-process-per-window;auto-language;segment-times",
"rows": rows}
if args.token_sentences:
from token_alignment import POLICY
report["alignment_policy"] = POLICY
report["alignment_contract_sha256"] = sha256(Path(__file__).with_name("token_alignment.py"))
report["alignment_policy_sha256"] = sha256(Path(__file__).with_name("token-policy.json"))
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(json.dumps(report, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
+60
View File
@@ -0,0 +1,60 @@
"""Structural tests only; actual word-time quality requires genuine inference."""
import unittest
from token_alignment import aligned_segments
def fixture():
tokens = [{"id": 100 + index, "text": text,
"offsets": {"from": start, "to": end}}
for index, (text, start, end) in enumerate([
(" Hello", 100, 400), (" world.", 400, 900),
(" Guten", 1100, 1400), (" Tag!", 1400, 1900)])]
return {"transcription": [{"text": " Hello world. Guten Tag!",
"offsets": {"from": 0, "to": 2000}, "tokens": tokens}]}
class TokenAlignmentTests(unittest.TestCase):
def test_exact_sentences_absolute_nonzero_source_coordinates(self):
result = aligned_segments(fixture(), 30000, 2000)
self.assertEqual([row["text"] for row in result], [" Hello world.", " Guten Tag!"])
self.assertEqual([(row["start_ms"], row["end_ms"]) for row in result],
[(30100, 30900), (31100, 31900)])
self.assertEqual(result[0]["original_start_ms"], 30000)
def test_structural_tokens_never_enter_candidates(self):
raw = fixture()
raw["transcription"][0]["tokens"].insert(0, {"id": 50364, "text": "[_BEG_]"})
self.assertEqual(len(aligned_segments(raw, 0, 2000)), 2)
def test_missing_nonmonotonic_outside_segment_and_invalid_special_rejected(self):
mutations = [lambda token: token.pop("offsets"),
lambda token: token.update(offsets={"from": -1, "to": 100}),
lambda token: token.update(offsets={"from": 300, "to": 100}),
lambda token: token.update(offsets={"from": 0, "to": 2500}),
lambda token: token.update(id=50364, text="ordinary words"),
lambda token: token.update(text=" changed")]
for mutate in mutations:
raw = fixture()
mutate(raw["transcription"][0]["tokens"][1])
with self.assertRaises(ValueError):
aligned_segments(raw, 0, 2000)
def test_no_punctuation_preserves_whole_text(self):
raw = fixture()
for token in raw["transcription"][0]["tokens"]:
token["text"] = token["text"].rstrip(".!")
raw["transcription"][0]["text"] = "".join(t["text"] for t in raw["transcription"][0]["tokens"])
result = aligned_segments(raw, 0, 2000)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["text"], raw["transcription"][0]["text"])
def test_empty_speech_is_valid_and_missing_tokens_are_rejected(self):
self.assertEqual(aligned_segments({"transcription": []}, 0, 1000), [])
raw = fixture()
del raw["transcription"][0]["tokens"]
with self.assertRaises(ValueError):
aligned_segments(raw, 0, 2000)
if __name__ == "__main__":
unittest.main()
+14
View File
@@ -0,0 +1,14 @@
{
"schema": 1,
"policy": "ordinary-token-sentences-v1;punctuation-boundaries;exact-text;no-dtw;no-labels",
"candidate": "base",
"worker_sha256": "12c681b4bcd9c7679b647b9a041e121a2f9e72cb2a9c6f189f529ec8b79c36f8",
"fixture_sha256": "b4e465a0eb9582b826eebd2a20797e1680e4907e336d02d0ef2194e2ba0af393",
"assets": ["clip04", "music01", "music02", "sound01", "sound02"],
"query_count": 8,
"asr_flags": ["--no-gpu", "--threads", "1", "--processors", "1", "--language", "auto", "--output-json-full"],
"boundaries": "Sentence-final punctuation in ordinary token text; remainder retained, exact recognized text unchanged",
"acceptance": "Every original source/window retained; invalid token time or text invalidates accepted score; no label-driven passage selection",
"comparison": "Frozen base segment baseline; file top1, strict timestamp-start hits, interval overlap, recognition omissions and processing cost reported separately",
"production_default": false
}
+54
View File
@@ -0,0 +1,54 @@
"""Strict ordinary Whisper token-time sentence subdivision, never label driven."""
import re
POLICY = "ordinary-token-sentences-v1;punctuation-boundaries;exact-text;no-dtw;no-labels"
# Pinned multilingual Whisper vocabulary: ordinary text ids precede EOT.
TEXT_TOKEN_LIMIT = 50257
BOUNDARY = re.compile(r'[.!?。!?]["\u201d\u2019\')\]]*$')
def aligned_segments(document, offset_ms, duration_ms):
from run import segments
originals = segments(document, offset_ms, duration_ms)
result = []
raw_rows = [row for row in document["transcription"] if row["text"].strip()]
for index, (row, original) in enumerate(zip(raw_rows, originals)):
tokens = row.get("tokens")
if not isinstance(tokens, list) or not 0 < len(tokens) <= 4096:
raise ValueError("Missing or oversized token times")
accepted = []
previous = 0
for token in tokens:
token_id, text = token.get("id"), token.get("text")
if type(token_id) is not int or token_id < 0 or not isinstance(text, str):
raise ValueError("Invalid token identity")
# Structural tokens must never become recognized text candidates.
if token_id >= TEXT_TOKEN_LIMIT:
if not text.startswith("[_") or not text.endswith("]"):
raise ValueError("Unrecognized special token")
continue
times = token.get("offsets", {})
start, end = times.get("from"), times.get("to")
if (type(start) is not int or type(end) is not int
or not 0 <= start <= end <= duration_ms or start < previous
or not row["offsets"]["from"] <= start <= end <= row["offsets"]["to"]):
raise ValueError("Invalid ordinary token timestamps")
previous = start
accepted.append((text, start, end))
if "".join(token[0] for token in accepted) != row["text"]:
raise ValueError("Token text differs from original recognized segment")
phrase = []
for token_index, token in enumerate(accepted):
phrase.append(token)
if BOUNDARY.search(token[0]) or token_index == len(accepted) - 1:
text = "".join(part[0] for part in phrase)
if text.strip():
result.append({"start_ms": offset_ms + phrase[0][1],
"end_ms": offset_ms + phrase[-1][2], "text": text,
"original_segment_index": index,
"original_start_ms": original["start_ms"],
"original_end_ms": original["end_ms"]})
phrase = []
if len(result) > 10000:
raise ValueError("Oversized aligned passage pool")
return result