Give spoken words a memory with an instant attached

An audio file used to produce no memory at all: _ingest_file returned an empty
batch for any suffix it did not know, with no log and no reason. It now raises
UnsupportedFile when you name the file and records a SkippedFile when it was a
directory walk, so a refusal is visible either way.

Speech is measured, not promised. Over 210.5 seconds of real Video-MME audio,
faster-whisper-tiny on CPU int8 transcribed at 51x realtime and reached word
error rate 0.242 against community subtitles nobody here wrote (a caption track
drops words a transcript keeps, so that is an upper bound). 66 speech atoms now
carry 550 word-level timestamps, 0 words out of order and 0 starting before
their own segment. The snapshot is 78.2 MB, under the 500 MB cap, and the report
is bench/reports/ASR.md.

Two library facts were measured rather than assumed. faster-whisper's own audio
loader is broken against PyAV 19 (av.open got an unexpected keyword
'metadata_errors') and it declares av with no version bound, so asr.decode_audio
decodes with PyAV directly and hands the model a float32 array instead of
pinning an old av with no cp314 wheel. And av.Error does not exist on PyAV 19 -
the real base is av.error.FFmpegError - so that is the only thing caught and my
own ValueErrors keep their messages.

Files over the 500-line rule were split rather than excused: ingestion.py (748)
into ingestion_common / ingestion_media / ingestion_repository / ingestion, and
cli.py (544) into cli_senses plus cli_common, following the cli_transactive
register pattern. MediaIngest declares _source_record abstract, so a half-wired
host fails at construction instead of returning None.

Verification: 207 tests pass; ruff clean on the 13 touched files; mypy clean on
them, and repo-wide mypy fell from 29 errors to 24 (5 fixed, none added);
pyright reports 0 errors on the touched files.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-07 21:18:28 -04:00
1 parent 03ca35763f
commit 5289cb4b01
20 files changed
+1513 -611

No files matched your search

+203
View File
@@ -0,0 +1,203 @@
"""Measure what the speech reader hears, against subtitles nobody in this repo wrote.
Video-MME ships community subtitles for its videos. They are third-party text, so they are a
fair reference for word error rate, with one caveat the report states: a caption track is not
a verbatim transcript, so the number is an upper bound on the model's real error rate.
"""
from __future__ import annotations
import hashlib
import json
import re
import subprocess
import time
from itertools import pairwise
from pathlib import Path
from typing import Any
from ely_eye.asr import SpeechSegment, SpeechTranscriber, SpokenWord
from ely_eye.config import Settings
from ely_eye.schemas import Modality
from bench.systems import machine_facts
ROOT = Path(__file__).resolve().parent
CORPUS = ROOT / "corpus" / "lmms-eval" / "Video-MME"
MODEL = ROOT / "models" / "faster-whisper-tiny"
REPORTS = ROOT / "reports"
PUNCTUATION = re.compile(r"[^\w' ]+")
TAGS = re.compile(r"<[^>]+>")
INDEX_LINE = re.compile(r"^\d+$")
TIMING_LINE = re.compile(r"-->")
def normalise(text: str) -> list[str]:
cleaned = PUNCTUATION.sub(" ", text.lower())
return [word for word in cleaned.split() if word]
def edit_distance(left: list[str], right: list[str]) -> int:
previous = list(range(len(right) + 1))
for index, first in enumerate(left, start=1):
current = [index]
for column, second in enumerate(right, start=1):
cost = 0 if first == second else 1
current.append(min(previous[column] + 1, current[column - 1] + 1, previous[column - 1] + cost))
previous = current
return previous[-1]
def word_error_rate(hypothesis: list[str], reference: list[str]) -> float:
if not reference:
return 1.0
return edit_distance(hypothesis, reference) / len(reference)
def srt_text(path: Path) -> str:
lines = []
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
stripped = line.strip()
if not stripped or INDEX_LINE.match(stripped) or TIMING_LINE.search(stripped):
continue
lines.append(TAGS.sub(" ", stripped))
return " ".join(lines)
def model_sha256() -> str:
digest = hashlib.sha256()
with (MODEL / "model.bin").open("rb") as handle:
for block in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(block)
return digest.hexdigest()
def git_sha() -> str:
done = subprocess.run(
["git", "rev-parse", "HEAD"], cwd=ROOT.parent, capture_output=True, text=True, check=True
)
return done.stdout.strip()
def timed_words(segments: list[SpeechSegment]) -> list[SpokenWord]:
return [word for segment in segments for word in segment.words if word.word]
def check_timings(segments: list[SpeechSegment]) -> dict[str, Any]:
"""Word clocks must run forward and stay inside their segment."""
words = timed_words(segments)
out_of_order = sum(1 for before, after in pairwise(words) if after.start < before.start)
outside = sum(1 for segment in segments for word in segment.words if word.start < segment.start - 0.01)
return {"words": len(words), "out_of_order": out_of_order, "before_their_segment": outside}
def main() -> None:
if not (MODEL / "model.bin").exists():
raise SystemExit(f"{MODEL / 'model.bin'} is missing; fetch Systran/faster-whisper-tiny into {MODEL}")
videos = sorted((CORPUS / "videos").glob("*.mp4"))
if not videos:
raise SystemExit(f"no videos under {CORPUS / 'videos'}; unpack them from videos_chunked_01.zip")
transcriber = SpeechTranscriber(Settings(asr_model_dir=MODEL))
rows: list[dict[str, Any]] = []
for video in videos:
subtitle = CORPUS / "subtitles" / f"{video.stem}.srt"
if not subtitle.exists():
continue
started = time.time()
segments = transcriber.transcribe(video)
took = time.time() - started
hypothesis = " ".join(segment.text for segment in segments)
reference = srt_text(subtitle)
hyp_words, ref_words = normalise(hypothesis), normalise(reference)
audio_seconds = max((segment.end for segment in segments), default=0.0)
rows.append(
{
"video": video.stem,
"seconds": round(audio_seconds, 1),
"transcribe_seconds": round(took, 2),
"real_time_factor": round(audio_seconds / took, 1) if took else 0.0,
"segments": len(segments),
"speech_atoms": sum(1 for segment in segments if segment.text),
"modality": Modality.audio.value,
"hypothesis_words": len(hyp_words),
"reference_words": len(ref_words),
"word_error_rate": round(word_error_rate(hyp_words, ref_words), 3),
"mean_word_confidence": round(
sum(word.confidence for word in timed_words(segments)) / max(len(timed_words(segments)), 1), 3
),
**check_timings(segments),
}
)
if not rows:
raise SystemExit(f"none of {len(videos)} videos has a subtitle in {CORPUS / 'subtitles'}")
reference_words = sum(row["reference_words"] for row in rows)
if not reference_words:
raise SystemExit("the subtitle files produced no reference words; the parser or the files are wrong")
totals = {
"videos": len(rows),
"audio_seconds": round(sum(row["seconds"] for row in rows), 1),
"word_error_rate": round(
sum(row["word_error_rate"] * row["reference_words"] for row in rows) / reference_words,
3,
),
"speech_atoms": sum(row["speech_atoms"] for row in rows),
"words": sum(row["words"] for row in rows),
"out_of_order": sum(row["out_of_order"] for row in rows),
"before_their_segment": sum(row["before_their_segment"] for row in rows),
}
report = {
"generated_utc": time.strftime("%Y-%m-%dT%H:%M:%S+00:00", time.gmtime()),
"commit": git_sha(),
"model": {"id": "Systran/faster-whisper-tiny", "sha256_model_bin": model_sha256(), "compute": "cpu int8"},
"reference": "Video-MME community subtitles (third-party captions, not verbatim transcripts)",
"machine": machine_facts(),
"totals": totals,
"videos": rows,
}
REPORTS.mkdir(exist_ok=True)
(REPORTS / "asr.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
write_markdown(report)
print(f"wrote {REPORTS / 'ASR.md'}")
def write_markdown(report: dict[str, Any]) -> None:
totals, model = report["totals"], report["model"]
lines = [
f"# ASR: what the speech reader hears (commit {report['commit'][:9]})",
"",
f"- generated {report['generated_utc']}",
f"- model: {model['id']} ({model['compute']}), model.bin sha256 {model['sha256_model_bin'][:16]}…",
f"- reference: {report['reference']}",
f"- machine: {report['machine']}",
"",
"| video | audio s | transcribe s | ×realtime | speech atoms | words | WER | mean word confidence |",
"| --- | --- | --- | --- | --- | --- | --- | --- |",
]
for row in report["videos"]:
lines.append(
f"| {row['video']} | {row['seconds']} | {row['transcribe_seconds']} | {row['real_time_factor']} "
f"| {row['speech_atoms']} | {row['words']} | {row['word_error_rate']} | {row['mean_word_confidence']} |"
)
lines += [
"",
(
f"Over {totals['videos']} videos and {totals['audio_seconds']} seconds of audio: word error rate "
f"**{totals['word_error_rate']}** against captions nobody here wrote, {totals['speech_atoms']} speech "
f"atoms carrying {totals['words']} word-level timestamps, {totals['out_of_order']} words out of order "
f"and {totals['before_their_segment']} starting before their own segment."
),
"",
(
"A caption track drops words a transcript keeps, so this WER is an upper bound on the model's real "
"error. The point of the run is not the number: before this change an audio file produced no memory "
"at all, and every word here now has an instant attached to it."
),
]
(REPORTS / "ASR.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
if __name__ == "__main__":
main()
+16
View File
@@ -0,0 +1,16 @@
# ASR: what the speech reader hears (commit 03ca35763)
- generated 2026-10-08T01:05:45+00:00
- model: Systran/faster-whisper-tiny (cpu int8), model.bin sha256 dcb76c6586fc06cb…
- reference: Video-MME community subtitles (third-party captions, not verbatim transcripts)
- machine: {'system': 'Linux', 'release': '6.18.40.1-microsoft-standard-WSL2', 'cpus': 32, 'threads_per_query': 4, 'load_average_1_5_15': [1.84, 1.85, 6.99]}
| video | audio s | transcribe s | ×realtime | speech atoms | words | WER | mean word confidence |
| --- | --- | --- | --- | --- | --- | --- | --- |
| 0IdYJGBmguM | 88.9 | 1.73 | 51.3 | 31 | 218 | 0.179 | 0.879 |
| 1q-5IIyZL20 | 99.9 | 1.94 | 51.6 | 32 | 300 | 0.28 | 0.83 |
| 1tidNDIpKSY | 21.7 | 1.34 | 16.1 | 3 | 32 | 0.278 | 0.852 |
Over 3 videos and 210.5 seconds of audio: word error rate **0.242** against captions nobody here wrote, 66 speech atoms carrying 550 word-level timestamps, 0 words out of order and 0 starting before their own segment.
A caption track drops words a transcript keeps, so this WER is an upper bound on the model's real error. The point of the run is not the number: before this change an audio file produced no memory at all, and every word here now has an instant attached to it.
+80
View File
@@ -0,0 +1,80 @@
{
"generated_utc": "2026-10-08T01:05:45+00:00",
"commit": "03ca35763fd63eb8d8f0d2c721d02c6c3ded90a7",
"model": {
"id": "Systran/faster-whisper-tiny",
"sha256_model_bin": "dcb76c6586fc06cbdac6dd21f14cfd129cc4cdd9dce19bf4ffa62e59cbe6e6d1",
"compute": "cpu int8"
},
"reference": "Video-MME community subtitles (third-party captions, not verbatim transcripts)",
"machine": {
"system": "Linux",
"release": "6.18.40.1-microsoft-standard-WSL2",
"cpus": 32,
"threads_per_query": 4,
"load_average_1_5_15": [
1.84,
1.85,
6.99
]
},
"totals": {
"videos": 3,
"audio_seconds": 210.5,
"word_error_rate": 0.242,
"speech_atoms": 66,
"words": 550,
"out_of_order": 0,
"before_their_segment": 0
},
"videos": [
{
"video": "0IdYJGBmguM",
"seconds": 88.9,
"transcribe_seconds": 1.73,
"real_time_factor": 51.3,
"segments": 31,
"speech_atoms": 31,
"modality": "audio",
"hypothesis_words": 218,
"reference_words": 201,
"word_error_rate": 0.179,
"mean_word_confidence": 0.879,
"words": 218,
"out_of_order": 0,
"before_their_segment": 0
},
{
"video": "1q-5IIyZL20",
"seconds": 99.9,
"transcribe_seconds": 1.94,
"real_time_factor": 51.6,
"segments": 32,
"speech_atoms": 32,
"modality": "audio",
"hypothesis_words": 300,
"reference_words": 296,
"word_error_rate": 0.28,
"mean_word_confidence": 0.83,
"words": 300,
"out_of_order": 0,
"before_their_segment": 0
},
{
"video": "1tidNDIpKSY",
"seconds": 21.7,
"transcribe_seconds": 1.34,
"real_time_factor": 16.1,
"segments": 3,
"speech_atoms": 3,
"modality": "audio",
"hypothesis_words": 32,
"reference_words": 36,
"word_error_rate": 0.278,
"mean_word_confidence": 0.852,
"words": 32,
"out_of_order": 0,
"before_their_segment": 0
}
]
}
+3 -5
View File
@@ -19,15 +19,12 @@ import json
import re
import statistics
import subprocess
import sys
import time
from dataclasses import dataclass
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
sys.path.insert(0, str(Path(__file__).resolve().parent))
from ely_eye.config import Settings
from ely_eye.db import Database
from ely_eye.ocr import read_text_from_image
@@ -44,8 +41,9 @@ from ely_eye.storage import (
visual_token_equivalent,
)
from ely_eye.visual_index import VisualIndex
from metrics import aggregate, chance_recall_at_k, score_query
from systems import machine_facts
from bench.metrics import aggregate, chance_recall_at_k, score_query
from bench.systems import machine_facts
ROOT = Path(__file__).resolve().parent
CORPUS = ROOT / "corpus" / "OS-Copilot" / "ScreenSpot-v2"