mirror of
https://github.com/PR0M3TH3AN/unredact.git
synced 2026-10-27 08:40:29 +00:00
feat: dictionary solver with in-memory word list store
Add solve_dictionary() for linear-scan matching of dictionary entries against a target pixel width, and DictionaryStore for managing named word/name lists. Reuses SolveResult from the solver module. Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
1 parent
11cc29d3f8
commit
b357713e5e
2 files changed
+129
No files matched your search
@@ -0,0 +1,67 @@
|
||||
# tests/test_dictionary.py
|
||||
import pytest
|
||||
from PIL import ImageFont
|
||||
|
||||
from unredact.pipeline.dictionary import solve_dictionary, DictionaryStore
|
||||
|
||||
|
||||
def _get_test_font() -> ImageFont.FreeTypeFont:
|
||||
import subprocess
|
||||
result = subprocess.run(
|
||||
["fc-match", "--format=%{file}", "Liberation Serif"],
|
||||
capture_output=True, text=True,
|
||||
)
|
||||
path = result.stdout.strip()
|
||||
return ImageFont.truetype(path, 40)
|
||||
|
||||
|
||||
class TestSolveDictionary:
|
||||
def test_finds_matching_word(self):
|
||||
font = _get_test_font()
|
||||
entries = ["Smith", "Jones", "Brown"]
|
||||
target = font.getlength("Smith")
|
||||
results = solve_dictionary(font, entries, target, tolerance=0.5)
|
||||
texts = [r.text for r in results]
|
||||
assert "Smith" in texts
|
||||
|
||||
def test_respects_tolerance(self):
|
||||
font = _get_test_font()
|
||||
entries = ["Smith", "Jones", "Brown"]
|
||||
target = font.getlength("Smith")
|
||||
results = solve_dictionary(font, entries, target, tolerance=0.0)
|
||||
assert all(r.error == 0.0 for r in results)
|
||||
|
||||
def test_with_context(self):
|
||||
font = _get_test_font()
|
||||
entries = ["Smith"]
|
||||
target = font.getlength(" Smith ") - font.getlength(" ") - font.getlength(" ")
|
||||
results = solve_dictionary(
|
||||
font, entries, target, tolerance=1.0,
|
||||
left_context=" ", right_context=" ",
|
||||
)
|
||||
assert len(results) >= 0 # may or may not match due to kerning
|
||||
|
||||
|
||||
class TestDictionaryStore:
|
||||
def test_add_and_list(self):
|
||||
store = DictionaryStore()
|
||||
store.add("names", ["Alice", "Bob", "Charlie"])
|
||||
assert "names" in store.list()
|
||||
|
||||
def test_get_entries(self):
|
||||
store = DictionaryStore()
|
||||
store.add("names", ["Alice", "Bob"])
|
||||
assert store.get_entries("names") == ["Alice", "Bob"]
|
||||
|
||||
def test_remove(self):
|
||||
store = DictionaryStore()
|
||||
store.add("names", ["Alice"])
|
||||
store.remove("names")
|
||||
assert "names" not in store.list()
|
||||
|
||||
def test_all_entries(self):
|
||||
store = DictionaryStore()
|
||||
store.add("list1", ["Alice", "Bob"])
|
||||
store.add("list2", ["Charlie"])
|
||||
all_entries = store.all_entries()
|
||||
assert set(all_entries) == {"Alice", "Bob", "Charlie"}
|
||||
@@ -0,0 +1,62 @@
|
||||
# unredact/pipeline/dictionary.py
|
||||
from PIL import ImageFont
|
||||
|
||||
from unredact.pipeline.solver import SolveResult
|
||||
|
||||
|
||||
class DictionaryStore:
|
||||
"""In-memory store for word/name lists."""
|
||||
|
||||
def __init__(self):
|
||||
self._dicts: dict[str, list[str]] = {}
|
||||
|
||||
def add(self, name: str, entries: list[str]):
|
||||
self._dicts[name] = entries
|
||||
|
||||
def remove(self, name: str):
|
||||
self._dicts.pop(name, None)
|
||||
|
||||
def list(self) -> list[str]:
|
||||
return list(self._dicts.keys())
|
||||
|
||||
def get_entries(self, name: str) -> list[str]:
|
||||
return self._dicts.get(name, [])
|
||||
|
||||
def all_entries(self) -> list[str]:
|
||||
seen = set()
|
||||
result = []
|
||||
for entries in self._dicts.values():
|
||||
for e in entries:
|
||||
if e not in seen:
|
||||
seen.add(e)
|
||||
result.append(e)
|
||||
return result
|
||||
|
||||
|
||||
def solve_dictionary(
|
||||
font: ImageFont.FreeTypeFont,
|
||||
entries: list[str],
|
||||
target_width: float,
|
||||
tolerance: float = 0.0,
|
||||
left_context: str = "",
|
||||
right_context: str = "",
|
||||
) -> list[SolveResult]:
|
||||
"""Check each dictionary entry against the target width."""
|
||||
results: list[SolveResult] = []
|
||||
|
||||
for entry in entries:
|
||||
if left_context or right_context:
|
||||
full = left_context + entry + right_context
|
||||
full_len = font.getlength(full)
|
||||
left_len = font.getlength(left_context) if left_context else 0.0
|
||||
right_len = font.getlength(right_context) if right_context else 0.0
|
||||
width = full_len - left_len - right_len
|
||||
else:
|
||||
width = font.getlength(entry)
|
||||
|
||||
error = abs(width - target_width)
|
||||
if error <= tolerance:
|
||||
results.append(SolveResult(text=entry, width=float(width), error=float(error)))
|
||||
|
||||
results.sort(key=lambda r: (r.error, r.text))
|
||||
return results
|
||||
Reference in new issue
Block a user