Files
ely-eye/backend/tests/test_visual_tokens.py
ZacharyZhang-NY 5289cb4b01 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.
2026-10-07 21:18:28 -04:00

100 lines
4.3 KiB
Python

"""What a picture costs the model that reads it.
The first version divided pixels by 224x224, which priced a 1280x720 frame at 18 tokens —
less than its caption. A vision model spends one token per patch, and Qwen3-VL's patch is
32x32, so the same frame is 920. A context budget built on the old number holds six times
less picture than it claims.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from PIL import Image
import ely_eye.ingestion_media as media_module
from ely_eye.config import Settings
from ely_eye.db import Database
from ely_eye.ingestion import IngestionService
from ely_eye.migrations import SCHEMA_VERSION, apply
from ely_eye.ocr import OcrResult
from ely_eye.storage import token_equivalent, visual_token_equivalent
FRAME = (1280, 720)
def _ingest(tmp_path: Path, settings: Settings) -> Database:
"""Run the real ingest path; each caller decides what its OCR says."""
database = Database(settings)
Image.new("RGB", FRAME, (10, 20, 30)).save(tmp_path / "frame.png")
IngestionService(settings=settings, database=database).ingest_path(tmp_path / "frame.png")
return database
def test_a_frame_is_priced_by_the_patches_a_vision_model_sees() -> None:
assert visual_token_equivalent(1280, 720) == 920
def test_a_small_tile_still_costs_more_than_a_single_token() -> None:
assert visual_token_equivalent(224, 224) == 49
def test_a_region_without_a_size_is_refused_rather_than_guessed() -> None:
for width, height in ((0, 720), (1280, -1)):
with pytest.raises(ValueError, match="needs a positive size"):
visual_token_equivalent(width, height)
def test_an_ingested_image_carries_its_text_and_its_patches(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
settings = Settings(home=tmp_path)
monkeypatch.setattr(media_module, "read_text_from_image", lambda path: OcrResult("购物车", 0.9, []))
database = _ingest(tmp_path, settings)
atom = database.list_atoms()[0]
assert atom.token_equivalent == token_equivalent("购物车") + 920
def test_an_existing_store_is_repriced_by_the_migration(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
settings = Settings(home=tmp_path)
monkeypatch.setattr(media_module, "read_text_from_image", lambda path: OcrResult("", 0.0, []))
database = _ingest(tmp_path, settings)
note = tmp_path / "notes.md"
note.write_text("# 钥匙放在玄关抽屉里\n", encoding="utf-8")
IngestionService(settings=settings, database=database).ingest_path(note)
with database.connect() as connection:
connection.execute("UPDATE atoms SET token_equivalent = 18 WHERE modality = 'image'")
connection.execute("PRAGMA user_version = 6")
with database.connect() as connection:
apply(connection)
atoms = {atom.modality.value: atom for atom in database.list_atoms()}
assert atoms["image"].token_equivalent == 920, "the old 224x224 price must not survive the upgrade"
assert atoms["text"].token_equivalent == token_equivalent(note.read_text(encoding="utf-8"))
with database.connect() as connection:
assert int(connection.execute("PRAGMA user_version").fetchone()[0]) == SCHEMA_VERSION
def test_an_atom_without_a_layout_size_keeps_the_number_it_has(tmp_path: Path) -> None:
"""Nothing can be priced from a missing region, so the stored figure is left alone."""
settings = Settings(home=tmp_path)
database = Database(settings)
with database.connect() as connection:
apply(connection)
connection.execute("PRAGMA user_version = 6")
connection.execute(
"INSERT INTO sources (source_id, path, kind, mime, sha256, size_bytes, created_at, metadata_json)"
" VALUES ('s', '/tmp/x.png', 'image', 'image/png', 'h', 1, '2026-01-01T00:00:00+00:00', '{}')"
)
connection.execute(
"INSERT INTO atoms (atom_id, source_id, modality, source, ingested_at, text, embedding_refs_json,"
" layout_json, relations_json, trust_json, metadata_json, token_equivalent, origin)"
" VALUES ('a1', 's', 'image', '/tmp/x.png', '2026-01-01T00:00:00+00:00', 'x', '[]', '{}', '[]', '{}', '{}', 18, 'artifact')"
)
with database.connect() as connection:
apply(connection)
assert int(connection.execute("SELECT token_equivalent FROM atoms WHERE atom_id = 'a1'").fetchone()[0]) == 18