feat(F-09): rich parsing — formula + figure extraction #1

Merged
user2595 merged 6 commits from feat/F-09-rich-parsing into main 2026-06-14 01:52:24 +00:00
20 changed files with 3596 additions and 26 deletions

View File

@@ -23,3 +23,7 @@ EMBEDDING_DIM=1024
# E-mail address for the OpenAlex Polite Pool (faster rate limits).
# Required by OpenAlex ToS when making automated requests.
OPENALEX_MAILTO=you@example.com
# Nougat HTTP-Server (Jetson: http://192.168.178.103:8080 | lokal: http://localhost:8080)
# Used by `codex ingest <id> --rich` for full-PDF Mathpix Markdown output.
NOUGAT_URL=http://localhost:8080

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import json
from datetime import UTC, datetime
from typing import Optional
import typer
@@ -10,27 +11,55 @@ import typer
app = typer.Typer(help="codex — personal knowledge base for scientific papers.")
discover_app = typer.Typer(help="Discovery queries over the citation graph.")
prov_app = typer.Typer(help="Provenance: @cite scan, code_links, bib export.")
wiki_app = typer.Typer(help="Wiki-compile: grounded concept pages over the RAG substrate.")
# F-09: search sub-app with paper (semantic) and formula (FTS) subcommands
search_app = typer.Typer(
help="Search: semantic paper search and formula full-text search.",
invoke_without_command=True,
)
app.add_typer(discover_app, name="discover")
app.add_typer(prov_app, name="provenance")
app.add_typer(wiki_app, name="wiki")
app.add_typer(search_app, name="search")
@app.command()
def ingest(
paper_id: str = typer.Argument(..., help="arXiv ID, DOI, or OpenAlex W-ID"),
source: Optional[str] = typer.Option(None, "--source", "-s", help="Path to .tex or .pdf"), # noqa: UP045
rich: bool = typer.Option( # noqa: A002
False,
"--rich",
help="Extract formulas and figures (requires PDF source, F-09).",
),
) -> None:
"""Ingest a paper into the knowledge base."""
from codex.ingest import ingest_paper
result = ingest_paper(paper_id, source_path=source)
result = ingest_paper(paper_id, source_path=source, rich=rich)
typer.echo(
f"Ingested {result.paper_id}: {result.chunks_upserted} chunks, "
f"{result.citations_upserted} citations"
f"{result.citations_upserted} citations, "
f"{result.formulas_upserted} formulas, "
f"{result.figures_upserted} figures"
)
@app.command()
def search(
# ---------------------------------------------------------------------------
# F-09: Search command group
# ---------------------------------------------------------------------------
@search_app.callback(invoke_without_command=True)
def search_callback(ctx: typer.Context) -> None:
"""Search subcommands: ``paper`` for semantic search, ``formula`` for LaTeX FTS."""
if ctx.invoked_subcommand is None:
typer.echo(ctx.get_help())
raise typer.Exit(0)
@search_app.command("paper")
def search_paper(
query: str = typer.Argument(..., help="Natural-language search query"),
limit: int = typer.Option(10, "--limit", "-n", help="Number of results"),
) -> None:
@@ -55,6 +84,37 @@ def search(
typer.echo(f"[{row['distance']:.3f}] {row['id']} ({row['year']}) — {row['title']}")
@search_app.command("formula")
def search_formula(
query: str = typer.Argument(..., help="LaTeX snippet or keyword to search in formulas"),
limit: int = typer.Option(10, "--limit", "-n", help="Number of results"),
) -> None:
"""Full-text search over extracted LaTeX formulas (F-09)."""
from codex.db import get_conn
with get_conn() as conn:
rows = conn.execute(
"""
SELECT paper_id, page, raw_latex, context,
ts_rank(to_tsvector('english', raw_latex || ' ' || coalesce(context, '')),
plainto_tsquery('english', %(query)s)) AS rank
FROM formulas
WHERE to_tsvector('english', raw_latex || ' ' || coalesce(context, ''))
@@ plainto_tsquery('english', %(query)s)
ORDER BY rank DESC
LIMIT %(limit)s
""",
{"query": query, "limit": limit},
).fetchall()
if not rows:
typer.echo("No matching formulas found.")
return
for row in rows:
typer.echo(
f"[{row['rank']:.3f}] {row['paper_id']} p.{row['page']} {row['raw_latex'][:80]}"
)
@discover_app.command("leads")
def discover_leads(
limit: int = typer.Option(20, "--limit", "-n"),
@@ -155,3 +215,121 @@ def ask(question: str = typer.Argument(..., help="Question to answer")) -> None:
"""Ask a question (RAG — not yet implemented)."""
typer.echo("ask: not yet implemented", err=True)
raise typer.Exit(1)
# ---------------------------------------------------------------------------
# F-12: Wiki command group
# ---------------------------------------------------------------------------
@wiki_app.command("compile")
def wiki_compile(
concept: Optional[str] = typer.Option( # noqa: UP045
None, "--concept", help="Compile only this concept slug."
),
all_concepts: bool = typer.Option(
False, "--all", help="Force full recompile (ignore change detection)."
),
top_k: int = typer.Option(0, "--top-k", help="Override config.wiki_top_k (0 = use config)."),
output_dir: Optional[str] = typer.Option( # noqa: UP045
None, "--output-dir", help="Override config.wiki_dir."
),
) -> None:
"""Compile concept pages from retrieved chunks via local LLM.
By default only recompiles concepts whose source chunks have changed
(hash-based incremental). Use --all to force full recompile.
"""
from codex.wiki import compile_all
report = compile_all(
changed_only=not all_concepts,
top_k=top_k if top_k > 0 else None,
concept_filter=concept,
output_dir=output_dir,
)
if report.compiled:
typer.echo(f"Compiled: {', '.join(report.compiled)}")
if report.skipped:
typer.echo(f"Skipped (unchanged): {', '.join(report.skipped)}")
if report.ungrounded:
typer.echo(f"⚠ Ungrounded claims: {len(report.ungrounded)}", err=True)
for slug, text in report.ungrounded:
typer.echo(f" [{slug}] {text[:100]}", err=True)
@wiki_app.command("list")
def wiki_list(
output_dir: Optional[str] = typer.Option( # noqa: UP045
None, "--output-dir", help="Override config.wiki_dir."
),
) -> None:
"""List compiled concept pages with freshness information."""
import json as _json
from pathlib import Path
from codex.config import get_settings
settings = get_settings()
wiki_dir = Path(output_dir or settings.wiki_dir)
state_path = wiki_dir / ".compile-state.json"
state: dict[str, str] = {}
if state_path.exists():
try:
state = _json.loads(state_path.read_text(encoding="utf-8"))
except (_json.JSONDecodeError, OSError):
state = {}
pages = sorted(wiki_dir.glob("*.md"))
if not pages:
typer.echo("No compiled pages found.")
return
for page in pages:
if page.name in ("index.md", "log.md"):
continue
slug = page.stem
hash_val = state.get(slug, "")[:8]
content = page.read_text(encoding="utf-8")
n_claims = content.count("[")
n_ungrounded = content.count("")
mtime = datetime.fromtimestamp(page.stat().st_mtime, tz=UTC).strftime("%Y-%m-%d %H:%M")
typer.echo(
f"{slug:40s} last={mtime} hash={hash_val} claims≈{n_claims} ⚠={n_ungrounded}"
)
@wiki_app.command("check")
def wiki_check(
output_dir: Optional[str] = typer.Option( # noqa: UP045
None, "--output-dir", help="Override config.wiki_dir."
),
) -> None:
"""Check all compiled pages for ungrounded claims.
Exits with code 0 if all claims are grounded, 1 if any are ungrounded.
Suitable for CI pipelines.
"""
from pathlib import Path
from codex.config import get_settings
settings = get_settings()
wiki_dir = Path(output_dir or settings.wiki_dir)
pages = sorted(wiki_dir.glob("*.md"))
found_ungrounded = False
for page in pages:
if page.name in ("index.md", "log.md"):
continue
content = page.read_text(encoding="utf-8")
if "" in content:
typer.echo(f"⚠ Ungrounded claim(s) in: {page.name}", err=True)
found_ungrounded = True
if found_ungrounded:
raise typer.Exit(1)
else:
typer.echo("All claims grounded.")

View File

@@ -84,6 +84,80 @@ class Settings(BaseSettings):
),
)
# ------------------------------------------------------------------
# F-12 Wiki-Compile
# ------------------------------------------------------------------
wiki_dir: str = Field(
default="wiki/",
description=(
"Directory where compiled wiki pages are written. "
"Relative paths are resolved from the current working directory."
),
)
wiki_llm_model: str = Field(
default="qwen2.5:7b",
description=(
"Ollama model name used for wiki synthesis. "
"Must be available at the configured Ollama endpoint (WIKI_LLM_URL or "
"OLLAMA_BASE_URL). Default: qwen2.5:7b (qwen-light profile on Jetson)."
),
)
wiki_llm_url: str | None = Field(
default=None,
description=(
"Ollama base URL for wiki synthesis. "
"When None, falls back to OLLAMA_BASE_URL "
"(http://192.168.178.103:11434 for the Jetson). "
"Set WIKI_LLM_URL to override."
),
)
wiki_top_k: int = Field(
default=12,
gt=0,
description=(
"Number of top chunks retrieved per concept for wiki synthesis. "
"Higher values improve recall at the cost of a larger LLM prompt."
),
)
# ------------------------------------------------------------------
# F-09 Rich Parsing
# ------------------------------------------------------------------
mathpix_app_id: str | None = Field(
default=None,
description=(
"MathPix App ID for cloud formula extraction. "
"When None, pix2tex local OCR is used as fallback."
),
)
mathpix_app_key: str | None = Field(
default=None,
description=(
"MathPix App Key for cloud formula extraction. "
"Must be set together with MATHPIX_APP_ID."
),
)
pix2tex_fallback: bool = Field(
default=True,
description=(
"Enable pix2tex (local LaTeX OCR) as fallback when MathPix credentials "
"are absent. Set to False to disable formula extraction entirely without creds."
),
)
figures_dir: str = Field(
default="figures/",
description=(
"Directory where extracted figure images are written. "
"Relative paths are resolved from the current working directory."
),
)
@lru_cache(maxsize=1)
def get_settings() -> Settings:

View File

@@ -9,6 +9,7 @@ from pathlib import Path
import numpy as np
from codex.config import get_settings
from codex.db import get_conn
from codex.embed import get_embedder
from codex.models import Citation, Paper
@@ -22,11 +23,14 @@ class IngestResult:
paper_id: str
chunks_upserted: int
citations_upserted: int
formulas_upserted: int = 0
figures_upserted: int = 0
def ingest_paper(
paper_id: str,
source_path: str | None = None,
rich: bool = False,
) -> IngestResult:
"""Idempotent ingest of one paper.
@@ -42,11 +46,15 @@ def ingest_paper(
- ``.tex`` → :func:`codex.parsing.tex.latex_to_text` + chunk
- ``.pdf`` → :func:`codex.parsing.nougat.pdf_to_markdown` + chunk
+ :func:`codex.parsing.grobid.extract_references` for refs
rich:
When True and *source_path* is a PDF, also extract formulas via
:func:`codex.parsing.mathpix.extract_formulas` and figures via
:func:`codex.parsing.figures.extract_figures` (F-09).
Returns
-------
IngestResult
Counts of upserted chunks and citations.
Counts of upserted chunks, citations, formulas, and figures.
"""
# ---------------------------------------------------------------
# 1. Fetch metadata (OpenAlex primary, SemanticScholar fallback)
@@ -195,8 +203,63 @@ def ingest_paper(
citations_upserted = len(merged_citations)
conn.commit()
# ---------------------------------------------------------------
# 6. F-09 Rich Parsing: formulas + figures (PDF only)
# ---------------------------------------------------------------
formulas_upserted = 0
figures_upserted = 0
if rich and source_path is not None and Path(source_path).suffix.lower() == ".pdf":
from codex.parsing.figures import extract_figures
from codex.parsing.mathpix import extract_formulas
settings = get_settings()
formulas = extract_formulas(source_path)
figures = extract_figures(source_path, output_dir=settings.figures_dir)
if formulas or figures:
with get_conn() as conn2:
with conn2.cursor() as cur:
# Delete-before-insert keeps re-ingest idempotent (BIGSERIAL has no
# natural UNIQUE key, so ON CONFLICT DO NOTHING never fires).
cur.execute("DELETE FROM formulas WHERE paper_id = %s", (paper.id,))
cur.execute("DELETE FROM figures WHERE paper_id = %s", (paper.id,))
if formulas:
with conn2.cursor() as cur:
cur.executemany(
"""
INSERT INTO formulas (paper_id, page, raw_latex, context, eq_label)
VALUES (%s, %s, %s, %s, %s)
""",
[
(f.paper_id, f.page, f.raw_latex, f.context, f.eq_label)
for f in formulas
],
)
formulas_upserted = len(formulas)
if figures:
with conn2.cursor() as cur:
cur.executemany(
"""
INSERT INTO figures (paper_id, page, caption, image_path)
VALUES (%s, %s, %s, %s)
""",
[
(fig.paper_id, fig.page, fig.caption, fig.image_path)
for fig in figures
],
)
figures_upserted = len(figures)
conn2.commit()
return IngestResult(
paper_id=paper.id,
chunks_upserted=chunks_upserted,
citations_upserted=citations_upserted,
formulas_upserted=formulas_upserted,
figures_upserted=figures_upserted,
)

View File

@@ -78,3 +78,39 @@ class CodeLink:
note: str | None = None
id: int | None = None
added_at: datetime | None = None
@dataclass
class FormulaChunk:
"""Maps to the ``formulas`` table (F-09 Rich Parsing).
Stores a single extracted mathematical formula (LaTeX) from a PDF page.
``id`` is set by the database (BIGSERIAL).
``embedding`` is reserved for future pgvector similarity search.
"""
paper_id: str
page: int
raw_latex: str
context: str
id: int | None = None
eq_label: str | None = None
embedding: list[float] | None = None
@dataclass
class FigureChunk:
"""Maps to the ``figures`` table (F-09 Rich Parsing).
Stores metadata for a single extracted figure from a PDF page.
``image_path`` points to the saved PNG on disk.
``id`` is set by the database (BIGSERIAL).
``embedding`` is reserved for future pgvector similarity search.
"""
paper_id: str
page: int
image_path: str
caption: str
id: int | None = None
embedding: list[float] | None = None

169
codex/parsing/figures.py Normal file
View File

@@ -0,0 +1,169 @@
"""Figure extraction from PDF pages.
Uses PyMuPDF (``fitz``) to iterate over embedded images in each page,
render them to PNG and detect nearby captions using text-block heuristics.
The public entry point is :func:`extract_figures`.
"""
from __future__ import annotations
import logging
from pathlib import Path
from typing import Any
from codex.models import FigureChunk
logger = logging.getLogger(__name__)
# Caption prefixes (case-insensitive check against text block content).
_CAPTION_PREFIXES: tuple[str, ...] = ("figure", "fig.", "abbildung")
# Maximum vertical distance (in points) from the image bottom or top
# to consider a text block as the caption.
_CAPTION_TOLERANCE_Y = 60.0
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _looks_like_caption(text: str) -> bool:
"""Return True if *text* starts with a known caption prefix."""
lowered = text.strip().lower()
return any(lowered.startswith(prefix) for prefix in _CAPTION_PREFIXES)
def _find_caption(
img_rect: Any, # fitz.Rect
text_blocks: list[Any],
) -> str:
"""Find the closest caption text block to *img_rect*.
Searches text blocks that:
1. Start with a caption prefix (e.g. "Figure", "Fig.", "Abbildung").
2. Are within ``_CAPTION_TOLERANCE_Y`` points above or below the image.
Returns the first matching text, stripped; empty string if none found.
"""
best: str = ""
best_dist = float("inf")
for block in text_blocks:
if len(block) < 5:
continue
bx0, by0, bx1, by1 = block[0], block[1], block[2], block[3]
text: str = block[4]
if not _looks_like_caption(text):
continue
# Distance: gap below image or gap above image
gap_below = by0 - img_rect.y1
gap_above = img_rect.y0 - by1
if 0 <= gap_below <= _CAPTION_TOLERANCE_Y:
dist = gap_below
elif 0 <= gap_above <= _CAPTION_TOLERANCE_Y:
dist = gap_above
else:
continue
# Horizontal overlap sanity check (caption should overlap with image)
overlap = min(bx1, img_rect.x1) - max(bx0, img_rect.x0)
if overlap < 0:
continue
if dist < best_dist:
best_dist = dist
best = text.replace("\n", " ").strip()
return best
# ---------------------------------------------------------------------------
# Public entry point
# ---------------------------------------------------------------------------
def extract_figures(
pdf_path: str,
output_dir: str,
) -> list[FigureChunk]:
"""Extract embedded figures from *pdf_path* and save them as PNG files.
For each image on each page:
1. Extract the image via ``page.get_images(full=True)``.
2. Render the image as a PNG via ``fitz.Pixmap``.
3. Search nearby text blocks for a caption (``Figure …`` / ``Fig. …`` /
``Abbildung …``).
4. Save the PNG to ``<output_dir>/<paper_id>_p<page>_fig<n>.png``.
5. Yield a :class:`~codex.models.FigureChunk`.
Parameters
----------
pdf_path:
Path to the source PDF.
output_dir:
Directory where PNG images are written (created if absent).
Returns
-------
list[FigureChunk]
Extracted figure metadata; empty list if no images found.
"""
import fitz
results: list[FigureChunk] = []
paper_id = Path(pdf_path).stem
out_dir = Path(output_dir)
out_dir.mkdir(parents=True, exist_ok=True)
try:
doc: Any = fitz.open(pdf_path)
except Exception:
logger.exception("fitz.open failed for %s", pdf_path)
return results
try:
for page_num in range(len(doc)):
page = doc[page_num]
images: list[Any] = page.get_images(full=True)
text_blocks: list[Any] = page.get_text("blocks")
for fig_idx, img_info in enumerate(images):
xref: int = img_info[0]
# Get image bounding box on the page
img_rects: list[Any] = page.get_image_rects(xref)
if not img_rects:
continue
img_rect = img_rects[0]
# Extract and save the image pixmap
try:
pix: Any = fitz.Pixmap(doc, xref)
if pix.n > 4: # CMYK or similar — convert to RGB
pix = fitz.Pixmap(fitz.csRGB, pix)
filename = f"{paper_id}_p{page_num + 1}_fig{fig_idx + 1}.png"
img_path = out_dir / filename
pix.save(str(img_path))
except Exception:
logger.debug("Could not extract image xref=%d on page %d", xref, page_num + 1)
continue
caption = _find_caption(img_rect, text_blocks)
results.append(
FigureChunk(
paper_id=paper_id,
page=page_num + 1,
image_path=str(img_path),
caption=caption,
)
)
finally:
doc.close()
return results

317
codex/parsing/mathpix.py Normal file
View File

@@ -0,0 +1,317 @@
"""Formula extraction from PDF pages.
Two backends are supported:
1. **MathPix** (cloud): POST the PDF to ``api.mathpix.com/v3/pdf`` and poll
for the result JSON. Requires ``MATHPIX_APP_ID`` and ``MATHPIX_APP_KEY``
environment variables (or ``.env`` file).
2. **pix2tex** (local, MIT licence): crop each detected math bounding-box at
3× resolution and run the ``LatexOCR`` model locally. Used when MathPix
credentials are absent. Model (~116 MB) is downloaded on first call.
The public entry point :func:`extract_formulas` routes to whichever backend
is available.
"""
from __future__ import annotations
import logging
import unicodedata
from pathlib import Path
from typing import Any
from codex.config import get_settings
from codex.models import FormulaChunk
logger = logging.getLogger(__name__)
# Unicode categories that indicate mathematical symbols.
_MATH_CATEGORIES: frozenset[str] = frozenset({"Sm", "So"})
# Bbox size thresholds — avoids cropping tiny index fragments.
_MIN_HEIGHT_PT = 15.0
_MIN_WIDTH_PT = 40.0
# Minimum number of math-category characters in a text block before we
# consider it a formula candidate.
_MIN_MATH_SYMBOLS = 5
# Minimum ratio of math/total chars (as float 01).
_MIN_MATH_RATIO = 0.15
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
def _is_math_char(ch: str) -> bool:
"""Return True if *ch* belongs to a mathematical Unicode category."""
return unicodedata.category(ch) in _MATH_CATEGORIES
def _math_ratio(text: str) -> float:
"""Return the fraction of characters in *text* that are math chars."""
if not text:
return 0.0
math_count = sum(1 for ch in text if _is_math_char(ch))
return math_count / len(text)
def _math_symbol_count(text: str) -> int:
"""Return the absolute count of math-category characters in *text*."""
return sum(1 for ch in text if _is_math_char(ch))
# ---------------------------------------------------------------------------
# pix2tex singleton
# ---------------------------------------------------------------------------
_pix2tex_model: Any = None
def _get_pix2tex_model() -> Any:
"""Lazily load and return the pix2tex LatexOCR singleton."""
global _pix2tex_model # noqa: PLW0603
if _pix2tex_model is None:
from pix2tex.cli import LatexOCR
_pix2tex_model = LatexOCR()
return _pix2tex_model
# ---------------------------------------------------------------------------
# pix2tex backend
# ---------------------------------------------------------------------------
def _extract_formulas_pix2tex(pdf_path: str) -> list[FormulaChunk]:
"""Extract LaTeX formulas from *pdf_path* using pix2tex (local OCR).
Algorithm
---------
1. Open the PDF with PyMuPDF (fitz).
2. For each page, gather text blocks; keep only blocks whose
math-symbol ratio exceeds the threshold and whose bounding box
is larger than the minimum size.
3. Crop the region at 3× resolution (better OCR quality).
4. Run pix2tex ``LatexOCR`` on the cropped image.
5. Wrap the result in a :class:`~codex.models.FormulaChunk`.
"""
import fitz
from PIL import Image
results: list[FormulaChunk] = []
paper_id = Path(pdf_path).stem
model = _get_pix2tex_model()
try:
doc: Any = fitz.open(pdf_path)
except Exception:
logger.exception("fitz.open failed for %s", pdf_path)
return results
scale = fitz.Matrix(3, 3)
try:
for page_num in range(len(doc)):
page = doc[page_num]
blocks: list[Any] = page.get_text("blocks")
for block in blocks:
# block = (x0, y0, x1, y1, text, block_no, block_type)
if len(block) < 5:
continue
x0, y0, x1, y1 = block[0], block[1], block[2], block[3]
text: str = block[4]
# Size filter
width_pt = x1 - x0
height_pt = y1 - y0
if width_pt < _MIN_WIDTH_PT or height_pt < _MIN_HEIGHT_PT:
continue
# Math content filter
if (
_math_symbol_count(text) < _MIN_MATH_SYMBOLS
or _math_ratio(text) < _MIN_MATH_RATIO
):
continue
# Crop at 3× for OCR quality
clip_rect = fitz.Rect(x0, y0, x1, y1)
crop_pix = page.get_pixmap(matrix=scale, clip=clip_rect)
img = Image.frombytes(
"RGB",
(crop_pix.width, crop_pix.height),
crop_pix.samples,
)
try:
latex: str = model(img)
except Exception:
logger.debug("pix2tex failed on page %d block, skipping", page_num + 1)
continue
if not latex or not latex.strip():
continue
context = text[:200].replace("\n", " ")
results.append(
FormulaChunk(
paper_id=paper_id,
page=page_num + 1,
raw_latex=latex.strip(),
context=context,
)
)
finally:
doc.close()
return results
# ---------------------------------------------------------------------------
# MathPix backend
# ---------------------------------------------------------------------------
def _extract_formulas_mathpix(
pdf_path: str,
app_id: str,
app_key: str,
) -> list[FormulaChunk]:
"""Extract LaTeX formulas using the MathPix v3 PDF API.
The function uploads the PDF, polls for results and parses the
returned ``latex_simplified`` list into :class:`~codex.models.FormulaChunk`
objects.
Retries with exponential back-off (via ``tenacity``) on transient HTTP
errors.
"""
import httpx
from tenacity import retry, stop_after_attempt, wait_exponential
paper_id = Path(pdf_path).stem
headers = {
"app_id": app_id,
"app_key": app_key,
}
@retry(
stop=stop_after_attempt(4),
wait=wait_exponential(multiplier=1, min=2, max=30),
reraise=True,
)
def _post_pdf() -> dict[str, Any]:
with open(pdf_path, "rb") as fh:
resp = httpx.post(
"https://api.mathpix.com/v3/pdf",
headers=headers,
files={"file": fh},
timeout=60.0,
)
resp.raise_for_status()
return resp.json() # type: ignore[no-any-return]
submit_response = _post_pdf()
pdf_id: str = submit_response.get("pdf_id", "")
if not pdf_id:
logger.warning("MathPix did not return a pdf_id — aborting")
return []
# Poll until complete
import time
for _ in range(60):
poll_resp = httpx.get(
f"https://api.mathpix.com/v3/pdf/{pdf_id}.mmd",
headers=headers,
timeout=30.0,
)
if poll_resp.status_code == 200:
break
time.sleep(2)
else:
logger.warning("MathPix polling timed out for pdf_id=%s", pdf_id)
return []
# Parse response — MathPix MMD contains \[ ... \] or $...$ blocks
mmd_text: str = poll_resp.text
results: list[FormulaChunk] = []
lines = mmd_text.splitlines()
in_block = False
block_lines: list[str] = []
page = 1
for line in lines:
if line.strip().startswith("\\["):
in_block = True
block_lines = [line]
elif in_block:
block_lines.append(line)
if line.strip().endswith("\\]"):
latex = "\n".join(block_lines).strip()
results.append(
FormulaChunk(
paper_id=paper_id,
page=page,
raw_latex=latex,
context="",
)
)
in_block = False
block_lines = []
# crude page tracking via MMD page markers
if line.strip().startswith("<!-- Page"):
page += 1
return results
# ---------------------------------------------------------------------------
# Public entry point
# ---------------------------------------------------------------------------
def extract_formulas(
pdf_path: str,
mathpix_app_id: str | None = None,
mathpix_app_key: str | None = None,
) -> list[FormulaChunk]:
"""Extract mathematical formulas from *pdf_path*.
Routes to the MathPix cloud backend when ``mathpix_app_id`` and
``mathpix_app_key`` are both non-empty; otherwise falls back to
the local pix2tex model (if ``PIX2TEX_FALLBACK=true``, which is
the default).
Parameters
----------
pdf_path:
Absolute or relative path to the PDF file.
mathpix_app_id:
Override for ``MATHPIX_APP_ID`` setting (optional).
mathpix_app_key:
Override for ``MATHPIX_APP_KEY`` setting (optional).
Returns
-------
list[FormulaChunk]
Extracted formulas; empty list if extraction is disabled or fails.
"""
settings = get_settings()
app_id = mathpix_app_id or settings.mathpix_app_id or ""
app_key = mathpix_app_key or settings.mathpix_app_key or ""
if app_id and app_key:
logger.info("Using MathPix backend for %s", pdf_path)
return _extract_formulas_mathpix(pdf_path, app_id, app_key)
if settings.pix2tex_fallback:
logger.info("Using pix2tex fallback backend for %s", pdf_path)
return _extract_formulas_pix2tex(pdf_path)
logger.info("Formula extraction disabled (no creds, pix2tex_fallback=False)")
return []

674
codex/wiki.py Normal file
View File

@@ -0,0 +1,674 @@
"""Wiki-compile layer — grounded concept pages over the RAG substrate (F-12).
Each concept page is compiled from retrieved chunks via a local LLM (Ollama).
Every claim is grounded against its cited source chunk (Substring-Match MVP).
Cross-references to other concepts are rendered as ``[[slug]]`` links.
Generated pages are written to ``wiki/<slug>.md`` and committed to git.
Compile-state (hash tracking for incremental re-runs) is persisted to
``wiki/.compile-state.json`` on disk — no DB changes (F-12 constraint).
Graceful degradation:
- Missing ``formulas`` table (F-09 not present) → no formula embedding, no crash.
- Missing ``verify_citations`` (F-10 not present) → local substring grounding check.
"""
from __future__ import annotations
import hashlib
import json
import re
import textwrap
from dataclasses import dataclass, field
from datetime import UTC, datetime
from pathlib import Path
from typing import Any, Protocol
import yaml
from codex.config import get_settings
# ---------------------------------------------------------------------------
# Dataclasses
# ---------------------------------------------------------------------------
@dataclass
class Concept:
"""A curated concept seed from ``wiki/concepts.yaml``."""
slug: str
title: str
aliases: list[str]
emphasis: str | None = None
@dataclass
class Claim:
"""A single factual claim extracted from a synthesised concept page."""
text: str
bibkey: str
locator: str # e.g. "page 9" | "eq.(9)" | "chunk 42"
grounded: bool = True # set by Grounding-Guard
@dataclass
class ConceptPage:
"""The compiled wiki page for one concept."""
concept: Concept
markdown: str # final rendered markdown
claims: list[Claim] = field(default_factory=list)
chunk_hash: str = "" # SHA-256 of concatenated source chunks
compiled_at: datetime = field(default_factory=lambda: datetime.now(UTC))
@dataclass
class CompileReport:
"""Summary of a compile run (appended to ``wiki/log.md``)."""
ran_at: datetime = field(default_factory=lambda: datetime.now(UTC))
compiled: list[str] = field(default_factory=list) # slugs written/updated
skipped: list[str] = field(default_factory=list) # slugs skipped (unchanged)
ungrounded: list[tuple[str, str]] = field(default_factory=list) # (slug, claim_text)
conflicts: list[tuple[str, str, str]] = field(default_factory=list) # (slug, bibkey1, bibkey2)
# ---------------------------------------------------------------------------
# LLM protocol (injectable for tests)
# ---------------------------------------------------------------------------
class LLMClient(Protocol):
"""Minimal protocol for an LLM that can generate text."""
def generate(self, prompt: str, model: str) -> str:
"""Return generated text for *prompt* using *model*."""
...
# ---------------------------------------------------------------------------
# Default Ollama LLM client
# ---------------------------------------------------------------------------
class OllamaClient:
"""Thin HTTP wrapper around the Ollama ``/api/generate`` endpoint."""
def __init__(self, base_url: str) -> None:
self._base_url = base_url.rstrip("/")
def generate(self, prompt: str, model: str) -> str: # noqa: D102
import httpx
url = f"{self._base_url}/api/generate"
payload = {"model": model, "prompt": prompt, "stream": False}
response = httpx.post(url, json=payload, timeout=120.0)
response.raise_for_status()
data: dict[str, Any] = response.json()
return str(data.get("response", ""))
# ---------------------------------------------------------------------------
# YAML loader
# ---------------------------------------------------------------------------
def load_concepts(path: str) -> list[Concept]:
"""Parse ``wiki/concepts.yaml`` and return a list of :class:`Concept` objects.
Each entry must have ``slug``, ``title``, and ``aliases`` (list).
``emphasis`` is optional.
"""
raw = Path(path).read_text(encoding="utf-8")
data: dict[str, Any] = yaml.safe_load(raw)
concepts: list[Concept] = []
for entry in data.get("concepts", []):
concepts.append(
Concept(
slug=str(entry["slug"]),
title=str(entry["title"]),
aliases=[str(a) for a in entry.get("aliases", [])],
emphasis=entry.get("emphasis") or None,
)
)
return concepts
# ---------------------------------------------------------------------------
# Retrieval helpers
# ---------------------------------------------------------------------------
def _retrieve_chunks(
queries: list[str],
*,
top_k: int,
) -> list[dict[str, Any]]:
"""Retrieve chunks via hybrid search (dense + FTS) from the DB.
Returns a list of dicts with keys: ``id``, ``paper_id``, ``ord``,
``content``, ``bibkey``. Reference-list chunks are filtered out
(ADR-F12: bibliography fragments pollute top-K).
"""
from codex.db import get_conn
from codex.embed import get_embedder
embedder = get_embedder()
combined_query = " ".join(queries)
dense_vec = embedder.encode_dense([combined_query])[0].tolist()
sql = """
SELECT
c.id,
c.paper_id,
c.ord,
c.content,
p.bibkey,
c.embedding <-> %(emb)s::vector AS dist
FROM chunks c
JOIN papers p ON p.id = c.paper_id
WHERE c.embedding IS NOT NULL
AND p.bibkey IS NOT NULL
ORDER BY c.embedding <-> %(emb)s::vector
LIMIT %(top_k)s
"""
with get_conn() as conn:
rows = conn.execute(
sql,
{"emb": dense_vec, "top_k": top_k * 2}, # over-fetch before filtering
).fetchall()
# Filter reference-list chunks: skip chunks whose content looks like a bibliography
# (heuristic: > 60 % of lines match "^\[\d+\]" or "^[A-Z][a-z]+,?\s+[A-Z]\.").
ref_pattern = re.compile(r"^\s*(\[\d+\]|[A-Z][a-z]+,?\s+[A-Z]\.)", re.MULTILINE)
filtered: list[dict[str, Any]] = []
for row in rows:
content: str = row["content"]
lines = content.splitlines()
if not lines:
continue
ref_hits = len(ref_pattern.findall(content))
if ref_hits / max(len(lines), 1) > 0.6:
continue # skip reference-list chunk
filtered.append(dict(row))
if len(filtered) >= top_k:
break
return filtered
# ---------------------------------------------------------------------------
# Grounding guard
# ---------------------------------------------------------------------------
_CLAIM_RE = re.compile(
r"(?P<text>[^\[]+?)\s*\[(?P<bibkey>[^\],#]+)(?:#(?P<locator>[^\]]+))?\]",
)
def _parse_claims(markdown: str) -> list[Claim]:
"""Extract inline citations from the LLM output.
Expected format per claim::
Some factual statement. [BibKey2008 #page 9]
Returns a :class:`Claim` with ``text``, ``bibkey``, ``locator``.
The ``grounded`` flag defaults to ``True`` and is set by
:func:`_run_grounding_guard`.
"""
claims: list[Claim] = []
for match in _CLAIM_RE.finditer(markdown):
text = match.group("text").strip()
bibkey = match.group("bibkey").strip()
locator = (match.group("locator") or "").strip()
if text and bibkey:
claims.append(Claim(text=text, bibkey=bibkey, locator=locator))
return claims
def _run_grounding_guard(
claims: list[Claim],
chunks: list[dict[str, Any]],
) -> list[Claim]:
"""Check each claim against its cited chunk via substring match (MVP).
A claim is *grounded* if at least one phrase from its text (> 4 words)
appears as a substring in a chunk attributed to the same bibkey,
OR if the claim text shares ≥ 3 consecutive words with any chunk of
that bibkey.
Sets ``claim.grounded = False`` for any claim that fails this check.
"""
# Build a bibkey → [content] index
bib_index: dict[str, list[str]] = {}
for chunk in chunks:
bk = str(chunk.get("bibkey") or "")
if bk:
bib_index.setdefault(bk, []).append(chunk["content"].lower())
for claim in claims:
sources = bib_index.get(claim.bibkey)
if not sources:
claim.grounded = False
continue
# Try to find any n-gram overlap (n ≥ 3 words)
words = claim.text.lower().split()
found = False
for n in range(min(len(words), 6), 2, -1): # try 6-grams down to 3-grams
for i in range(len(words) - n + 1):
phrase = " ".join(words[i : i + n])
if any(phrase in src for src in sources):
found = True
break
if found:
break
claim.grounded = found
return claims
# ---------------------------------------------------------------------------
# Cross-reference injection
# ---------------------------------------------------------------------------
def _inject_cross_refs(
markdown: str,
all_concepts: list[Concept],
current_slug: str,
) -> str:
"""Replace occurrences of other concept titles/aliases with ``[[slug]]`` links.
Only exact case-insensitive whole-word matches outside of existing
``[[…]]`` blocks or inline code are replaced.
"""
for concept in all_concepts:
if concept.slug == current_slug:
continue
terms = [concept.title] + concept.aliases
for term in terms:
# Escape for use in regex; require word boundary
escaped = re.escape(term)
pattern = re.compile(rf"(?<!\[\[)\b{escaped}\b(?!\]\])", re.IGNORECASE)
replacement = f"[[{concept.slug}]]"
markdown = pattern.sub(replacement, markdown)
return markdown
# ---------------------------------------------------------------------------
# Chunk hash (for change detection)
# ---------------------------------------------------------------------------
def _chunk_hash(chunks: list[dict[str, Any]]) -> str:
"""Return a stable SHA-256 hex digest of the concatenated chunk contents."""
combined = "\n".join(c["content"] for c in sorted(chunks, key=lambda x: x["id"]))
return hashlib.sha256(combined.encode("utf-8")).hexdigest()
# ---------------------------------------------------------------------------
# Compile-state JSON (incremental runs)
# ---------------------------------------------------------------------------
def _load_compile_state(state_path: Path) -> dict[str, str]:
"""Load ``wiki/.compile-state.json`` → ``{slug: chunk_hash}`` dict."""
if not state_path.exists():
return {}
try:
raw = state_path.read_text(encoding="utf-8")
data: dict[str, str] = json.loads(raw)
return data
except (json.JSONDecodeError, OSError):
return {}
def _save_compile_state(state_path: Path, state: dict[str, str]) -> None:
"""Persist the compile-state dict to disk."""
state_path.write_text(json.dumps(state, indent=2, sort_keys=True), encoding="utf-8")
# ---------------------------------------------------------------------------
# LLM synthesis prompt
# ---------------------------------------------------------------------------
_SYNTHESIS_PROMPT_TEMPLATE = textwrap.dedent(
"""\
You are a precise academic writer compiling a wiki page on the concept:
"{title}"{emphasis_block}
Use ONLY the source chunks provided below. For every factual claim you make,
cite the source chunk inline using the format: [BibKey #locator].
Example: "The volume formula is V = L(γ₁)+L(γ₂)+L(γ₃). [Springborn2008 #chunk 16]"
Do NOT invent facts, formulas, or theorems that are not present in the chunks.
If a standard result is not in the chunks, do not include it.
Write 3-6 concise paragraphs. Use LaTeX math notation where appropriate ($ … $).
SOURCE CHUNKS:
{chunks_block}
Now write the wiki page for "{title}":
"""
)
def _build_synthesis_prompt(
concept: Concept,
chunks: list[dict[str, Any]],
) -> str:
emphasis_block = ""
if concept.emphasis:
emphasis_block = f"\n\nEmphasis: {concept.emphasis}"
chunks_block_lines = []
for chunk in chunks:
bibkey = chunk.get("bibkey") or chunk["paper_id"]
ord_val = chunk.get("ord", "?")
chunks_block_lines.append(f"[{bibkey} #chunk {ord_val}]\n{chunk['content'].strip()}\n")
chunks_block = "\n---\n".join(chunks_block_lines)
return _SYNTHESIS_PROMPT_TEMPLATE.format(
title=concept.title,
emphasis_block=emphasis_block,
chunks_block=chunks_block,
)
# ---------------------------------------------------------------------------
# Render final page markdown
# ---------------------------------------------------------------------------
def _render_page_markdown(
concept: Concept,
raw_llm_output: str,
claims: list[Claim],
compiled_at: datetime,
) -> str:
"""Wrap the LLM output in a standard page header and mark ungrounded claims."""
ungrounded_texts = {c.text for c in claims if not c.grounded}
body = raw_llm_output.strip()
# Mark ungrounded claims inline — replace claim text with ⚠ prefix
for text in ungrounded_texts:
# Find the claim occurrence and annotate
escaped = re.escape(text)
body = re.sub(
rf"({escaped})",
r"\1",
body,
count=1,
)
ts = compiled_at.strftime("%Y-%m-%d %H:%M UTC")
header = f"# {concept.title}\n\n_Compiled {ts} by `codex wiki compile`_\n\n"
return header + body + "\n"
# ---------------------------------------------------------------------------
# Core compile function
# ---------------------------------------------------------------------------
def compile_concept(
concept: Concept,
*,
top_k: int,
llm: LLMClient,
all_concepts: list[Concept] | None = None,
wiki_dir: Path | None = None,
) -> ConceptPage:
"""Compile a single concept page.
1. Retrieve Top-K chunks (hybrid dense+FTS) for concept title + aliases.
2. Synthesise via LLM (Ollama) with per-claim citation format.
3. Run Grounding-Guard: mark ungrounded claims as ⚠.
4. Inject cross-references to other concepts as [[slug]] links.
5. Embed formula chunks if ``formulas`` table is present (graceful).
Returns a :class:`ConceptPage` with full markdown and claim list.
"""
settings = get_settings()
_wiki_dir = wiki_dir or Path(settings.wiki_dir)
queries = [concept.title] + concept.aliases
chunks = _retrieve_chunks(queries, top_k=top_k)
h = _chunk_hash(chunks)
prompt = _build_synthesis_prompt(concept, chunks)
raw_output = llm.generate(prompt, model=settings.wiki_llm_model)
claims = _parse_claims(raw_output)
claims = _run_grounding_guard(claims, chunks)
_all_concepts = all_concepts or []
raw_output = _inject_cross_refs(raw_output, _all_concepts, concept.slug)
# Graceful: try to embed formula chunks (F-09) — skip if table missing
_try_embed_formulas(concept, chunks)
compiled_at = datetime.now(UTC)
markdown = _render_page_markdown(concept, raw_output, claims, compiled_at)
return ConceptPage(
concept=concept,
markdown=markdown,
claims=claims,
chunk_hash=h,
compiled_at=compiled_at,
)
def _try_embed_formulas(concept: Concept, chunks: list[dict[str, Any]]) -> None:
"""Attempt to look up formula chunks for the concept — graceful no-op if F-09 absent."""
try:
from codex.db import get_conn
with get_conn() as conn:
# Check if formulas table exists
row = conn.execute(
"SELECT 1 FROM information_schema.tables WHERE table_name = 'formulas'"
).fetchone()
if row is None:
return # F-09 not present
# (Future: embed relevant raw_latex into the page)
except Exception: # noqa: BLE001
return # DB not reachable or other error — degrade gracefully
# ---------------------------------------------------------------------------
# compile_all
# ---------------------------------------------------------------------------
def compile_all(
*,
changed_only: bool = True,
top_k: int | None = None,
concept_filter: str | None = None,
output_dir: str | None = None,
llm: LLMClient | None = None,
) -> CompileReport:
"""Compile all (or changed) concept pages.
Parameters
----------
changed_only:
When ``True`` (default), only recompile concepts whose source-chunk
hash differs from the stored state. ``False`` forces full recompile.
top_k:
Override ``config.wiki_top_k``.
concept_filter:
If set, compile only this concept slug.
output_dir:
Override ``config.wiki_dir``.
llm:
Injectable LLM client (defaults to :class:`OllamaClient`).
"""
settings = get_settings()
wiki_dir = Path(output_dir or settings.wiki_dir)
wiki_dir.mkdir(parents=True, exist_ok=True)
k = top_k if top_k is not None else settings.wiki_top_k
concepts_path = wiki_dir / "concepts.yaml"
if not concepts_path.exists():
# Fall back to sibling concepts.yaml next to wiki/ dir
concepts_path = wiki_dir.parent / "wiki" / "concepts.yaml"
concepts = load_concepts(str(concepts_path))
if concept_filter:
concepts = [c for c in concepts if c.slug == concept_filter]
state_path = wiki_dir / ".compile-state.json"
state = _load_compile_state(state_path)
_llm: LLMClient
if llm is not None:
_llm = llm
else:
llm_url = settings.wiki_llm_url or settings.ollama_base_url
_llm = OllamaClient(llm_url)
report = CompileReport()
for concept in concepts:
# Fast check: retrieve chunks and compare hash
queries = [concept.title] + concept.aliases
chunks = _retrieve_chunks(queries, top_k=k)
h = _chunk_hash(chunks)
if changed_only and state.get(concept.slug) == h:
report.skipped.append(concept.slug)
continue
page = compile_concept(
concept,
top_k=k,
llm=_llm,
all_concepts=concepts,
wiki_dir=wiki_dir,
)
# Write page to disk
page_path = wiki_dir / f"{concept.slug}.md"
page_path.write_text(page.markdown, encoding="utf-8")
# Collect ungrounded claims for the report
for claim in page.claims:
if not claim.grounded:
report.ungrounded.append((concept.slug, claim.text))
state[concept.slug] = page.chunk_hash
report.compiled.append(concept.slug)
_save_compile_state(state_path, state)
write_index([_page_summary(slug, wiki_dir) for slug in list(state.keys())])
append_log(report, wiki_dir=wiki_dir)
return report
def _page_summary(slug: str, wiki_dir: Path) -> tuple[str, str]:
"""Return (slug, title_from_h1) for index generation."""
page_path = wiki_dir / f"{slug}.md"
title = slug
if page_path.exists():
first_line = page_path.read_text(encoding="utf-8").splitlines()[0]
if first_line.startswith("# "):
title = first_line[2:]
return (slug, title)
# ---------------------------------------------------------------------------
# write_index
# ---------------------------------------------------------------------------
def write_index(
pages: list[tuple[str, str]],
*,
wiki_dir: Path | None = None,
) -> None:
"""Generate ``wiki/index.md`` with [[links]] to all compiled concept pages.
Parameters
----------
pages:
List of ``(slug, title)`` tuples.
wiki_dir:
Path to the wiki directory (defaults to ``config.wiki_dir``).
"""
settings = get_settings()
_wiki_dir = wiki_dir or Path(settings.wiki_dir)
ts = datetime.now(UTC).strftime("%Y-%m-%d %H:%M UTC")
lines = [
"# Wiki Index",
"",
f"_Generated {ts} by `codex wiki compile`_",
"",
"## Concepts",
"",
]
for slug, title in sorted(pages, key=lambda x: x[0]):
lines.append(f"- [[{slug}]] — {title}")
lines.append("")
content = "\n".join(lines)
(_wiki_dir / "index.md").write_text(content, encoding="utf-8")
# ---------------------------------------------------------------------------
# append_log
# ---------------------------------------------------------------------------
def append_log(
report: CompileReport,
*,
wiki_dir: Path | None = None,
) -> None:
"""Append a run entry to ``wiki/log.md`` (never overwrites).
Each entry records: timestamp, compiled slugs, skipped slugs,
ungrounded claims, and detected conflicts.
"""
settings = get_settings()
_wiki_dir = wiki_dir or Path(settings.wiki_dir)
log_path = _wiki_dir / "log.md"
ts = report.ran_at.strftime("%Y-%m-%d %H:%M:%S UTC")
entry_lines = [
f"\n## Run {ts}",
"",
]
if report.compiled:
entry_lines.append(f"**Compiled:** {', '.join(report.compiled)}")
if report.skipped:
entry_lines.append(f"**Skipped (unchanged):** {', '.join(report.skipped)}")
if report.ungrounded:
entry_lines.append("")
entry_lines.append("**⚠ Ungrounded claims:**")
for slug, text in report.ungrounded:
entry_lines.append(f"- `{slug}`: {text[:120]}")
if report.conflicts:
entry_lines.append("")
entry_lines.append("**⚠ Conflicts detected:**")
for slug, bk1, bk2 in report.conflicts:
entry_lines.append(f"- `{slug}`: conflicting claims in {bk1} vs {bk2}")
entry_lines.append("")
entry = "\n".join(entry_lines)
# Append-only: open in append mode
with log_path.open("a", encoding="utf-8") as fh:
if log_path.stat().st_size == 0:
fh.write("# Wiki Compile Log\n")
fh.write(entry)

View File

@@ -88,3 +88,37 @@ CREATE INDEX code_links_paper_idx ON code_links (paper_id);
-- GROUP BY cited_id
-- ORDER BY pull DESC
-- LIMIT 20;
-- ---------------------------------------------------------------------
-- F-09 Rich Parsing: formulas + figures
-- ---------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS formulas (
id BIGSERIAL PRIMARY KEY,
paper_id TEXT REFERENCES papers(id) ON DELETE CASCADE,
page INT,
raw_latex TEXT NOT NULL,
context TEXT,
eq_label TEXT,
embedding vector(1024)
);
CREATE INDEX IF NOT EXISTS formulas_emb_idx
ON formulas USING hnsw (embedding vector_cosine_ops);
CREATE INDEX IF NOT EXISTS formulas_paper_idx ON formulas (paper_id);
CREATE INDEX IF NOT EXISTS formulas_fts_idx
ON formulas USING gin (
to_tsvector('english', raw_latex || ' ' || coalesce(context, ''))
);
CREATE TABLE IF NOT EXISTS figures (
id BIGSERIAL PRIMARY KEY,
paper_id TEXT REFERENCES papers(id) ON DELETE CASCADE,
page INT,
caption TEXT,
image_path TEXT,
embedding vector(1024)
);
CREATE INDEX IF NOT EXISTS figures_emb_idx
ON figures USING hnsw (embedding vector_cosine_ops);
CREATE INDEX IF NOT EXISTS figures_paper_idx ON figures (paper_id);

View File

@@ -17,6 +17,8 @@ dependencies = [
"typer>=0.12",
"httpx>=0.27",
"tenacity>=8",
"pymupdf>=1.24",
"pix2tex>=0.1.4",
]
[project.scripts]

View File

@@ -39,12 +39,36 @@ class TestIngest:
assert "5 chunks" in result.stdout
assert "3 citations" in result.stdout
def test_ingest_with_rich_flag(self) -> None:
"""Test `ingest --rich` passes rich=True and reports formulas/figures."""
with patch("codex.ingest.ingest_paper") as mock_ingest:
mock_ingest.return_value = IngestResult(
paper_id="2301.07041",
chunks_upserted=5,
citations_upserted=3,
formulas_upserted=12,
figures_upserted=4,
)
result = runner.invoke(app, ["ingest", "2301.07041", "--source", "paper.pdf", "--rich"])
assert result.exit_code == 0
assert "12 formulas" in result.stdout
assert "4 figures" in result.stdout
# Verify rich=True was passed
_, kwargs = mock_ingest.call_args
assert kwargs.get("rich") is True or mock_ingest.call_args[1].get("rich") is True
class TestSearch:
"""Tests for `codex search` command."""
"""Tests for `codex search` subcommands (paper + formula)."""
def test_search_command(self) -> None:
"""Test search command with mocked embedder and DB."""
"""Alias: delegates to test_search_paper_command for backwards compat."""
self.test_search_paper_command()
def test_search_paper_command(self) -> None:
"""Test `search paper` command with mocked embedder and DB."""
with (
patch("codex.embed.get_embedder") as mock_embedder,
patch("codex.db.get_conn") as mock_get_conn,
@@ -74,7 +98,7 @@ class TestSearch:
]
mock_get_conn.return_value = mock_conn
result = runner.invoke(app, ["search", "machine learning", "--limit", "2"])
result = runner.invoke(app, ["search", "paper", "machine learning", "--limit", "2"])
assert result.exit_code == 0
assert "2301.07041" in result.stdout
@@ -82,6 +106,43 @@ class TestSearch:
assert "Test Paper" in result.stdout
assert "0.123" in result.stdout
def test_search_formula_command(self) -> None:
"""Test `search formula` command with mocked DB."""
with patch("codex.db.get_conn") as mock_get_conn:
mock_conn = MagicMock()
mock_conn.__enter__.return_value = mock_conn
mock_conn.__exit__.return_value = None
mock_conn.execute.return_value.fetchall.return_value = [
{
"paper_id": "2301.07041",
"page": 3,
"raw_latex": r"\sum_{i=1}^{n} x_i",
"context": "summation formula",
"rank": 0.856,
},
]
mock_get_conn.return_value = mock_conn
result = runner.invoke(app, ["search", "formula", "summation", "--limit", "5"])
assert result.exit_code == 0
assert "2301.07041" in result.stdout
assert "p.3" in result.stdout
def test_search_formula_no_results(self) -> None:
"""`search formula` with no DB matches prints a 'no results' message."""
with patch("codex.db.get_conn") as mock_get_conn:
mock_conn = MagicMock()
mock_conn.__enter__.return_value = mock_conn
mock_conn.__exit__.return_value = None
mock_conn.execute.return_value.fetchall.return_value = []
mock_get_conn.return_value = mock_conn
result = runner.invoke(app, ["search", "formula", "nonexistent"])
assert result.exit_code == 0
assert "No matching" in result.stdout
class TestDiscoverLeads:
"""Tests for `codex discover leads` command."""

View File

@@ -14,7 +14,7 @@ import numpy as np
import pytest
from codex.ingest import IngestResult, ingest_paper
from codex.models import Citation, Paper
from codex.models import Citation, FigureChunk, FormulaChunk, Paper
# ---------------------------------------------------------------------------
# Helpers
@@ -288,3 +288,134 @@ def test_ingest_paper_no_abstract_uses_zero_vector() -> None:
emb: list[float] = params["abstract_emb"]
assert len(emb) == 1024
assert all(v == 0.0 for v in emb)
# ---------------------------------------------------------------------------
# 8. F-09 Rich Parsing tests
# ---------------------------------------------------------------------------
def test_ingest_paper_rich_calls_formula_and_figure_parsers(tmp_path: Any) -> None:
"""When rich=True and source is PDF, formula + figure parsers are invoked."""
import codex.parsing.figures as _figures_mod
import codex.parsing.mathpix as _mathpix_mod
pdf_file = tmp_path / "paper.pdf"
pdf_file.write_bytes(b"%PDF-1.4 fake content")
paper = _make_paper()
mock_conn = MagicMock()
mock_conn.execute = MagicMock()
mock_conn.executemany = MagicMock()
mock_conn.commit = MagicMock()
fake_formula = FormulaChunk(paper_id=paper.id, page=1, raw_latex=r"\alpha", context="test")
fake_figure = FigureChunk(
paper_id=paper.id, page=1, image_path="/tmp/fig.png", caption="Figure 1."
)
mock_settings = MagicMock()
mock_settings.figures_dir = str(tmp_path / "figures")
with (
patch("codex.ingest.openalex.fetch_paper", return_value=paper),
patch("codex.ingest.openalex.fetch_citations", return_value=[]),
patch("codex.ingest.get_embedder", return_value=_fake_embedder()),
patch("codex.ingest.get_conn", side_effect=_make_conn_cm(mock_conn)),
patch("codex.parsing.nougat.pdf_to_markdown", return_value=""),
patch("codex.parsing.tex.chunk_text", return_value=[]),
patch("codex.parsing.grobid.extract_references", return_value=[]),
patch.object(
_mathpix_mod, "extract_formulas", return_value=[fake_formula]
) as mock_formulas,
patch.object(_figures_mod, "extract_figures", return_value=[fake_figure]) as mock_figures,
patch("codex.ingest.get_settings", return_value=mock_settings),
):
result = ingest_paper(paper.id, source_path=str(pdf_file), rich=True)
mock_formulas.assert_called_once()
mock_figures.assert_called_once()
assert result.formulas_upserted == 1
assert result.figures_upserted == 1
def test_ingest_paper_no_rich_skips_parsers(tmp_path: Any) -> None:
"""When rich=False (default), formula and figure parsers are NOT called."""
import codex.parsing.figures as _figures_mod
import codex.parsing.mathpix as _mathpix_mod
pdf_file = tmp_path / "paper.pdf"
pdf_file.write_bytes(b"%PDF-1.4 fake content")
paper = _make_paper()
mock_conn = MagicMock()
mock_conn.execute = MagicMock()
mock_conn.executemany = MagicMock()
mock_conn.commit = MagicMock()
with (
patch("codex.ingest.openalex.fetch_paper", return_value=paper),
patch("codex.ingest.openalex.fetch_citations", return_value=[]),
patch("codex.ingest.get_embedder", return_value=_fake_embedder()),
patch("codex.ingest.get_conn", side_effect=_make_conn_cm(mock_conn)),
patch("codex.parsing.nougat.pdf_to_markdown", return_value=""),
patch("codex.parsing.tex.chunk_text", return_value=[]),
patch("codex.parsing.grobid.extract_references", return_value=[]),
patch.object(_mathpix_mod, "extract_formulas", return_value=[]) as mock_formulas,
patch.object(_figures_mod, "extract_figures", return_value=[]) as mock_figures,
):
result = ingest_paper(paper.id, source_path=str(pdf_file), rich=False)
mock_formulas.assert_not_called()
mock_figures.assert_not_called()
assert result.formulas_upserted == 0
assert result.figures_upserted == 0
def test_ingest_paper_rich_requires_pdf_source(tmp_path: Any) -> None:
"""When rich=True but source is a .tex file, formula/figure parsers are NOT called."""
import codex.parsing.figures as _figures_mod
import codex.parsing.mathpix as _mathpix_mod
tex_file = tmp_path / "paper.tex"
tex_file.write_text(r"\section{Intro}")
paper = _make_paper()
mock_conn = MagicMock()
mock_conn.execute = MagicMock()
mock_conn.executemany = MagicMock()
mock_conn.commit = MagicMock()
with (
patch("codex.ingest.openalex.fetch_paper", return_value=paper),
patch("codex.ingest.openalex.fetch_citations", return_value=[]),
patch("codex.ingest.get_embedder", return_value=_fake_embedder()),
patch("codex.ingest.get_conn", side_effect=_make_conn_cm(mock_conn)),
patch("codex.parsing.tex.latex_to_text", return_value=""),
patch("codex.parsing.tex.chunk_text", return_value=[]),
patch.object(_mathpix_mod, "extract_formulas", return_value=[]) as mock_formulas,
patch.object(_figures_mod, "extract_figures", return_value=[]) as mock_figures,
):
result = ingest_paper(paper.id, source_path=str(tex_file), rich=True)
mock_formulas.assert_not_called()
mock_figures.assert_not_called()
assert result.formulas_upserted == 0
assert result.figures_upserted == 0
def test_ingest_result_has_formula_figure_counts() -> None:
"""IngestResult has formulas_upserted and figures_upserted fields with default 0."""
result = IngestResult(paper_id="test", chunks_upserted=0, citations_upserted=0)
assert result.formulas_upserted == 0
assert result.figures_upserted == 0
result2 = IngestResult(
paper_id="test",
chunks_upserted=5,
citations_upserted=3,
formulas_upserted=10,
figures_upserted=2,
)
assert result2.formulas_upserted == 10
assert result2.figures_upserted == 2

View File

@@ -0,0 +1,245 @@
"""Tests for codex.parsing.figures — figure extraction module.
All external dependencies (fitz, PIL) are mocked so the suite runs offline.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
def _make_mock_rect(
x0: float = 10.0, y0: float = 10.0, x1: float = 200.0, y1: float = 150.0
) -> MagicMock:
"""Build a mock fitz.Rect-like object."""
rect = MagicMock()
rect.x0 = x0
rect.y0 = y0
rect.x1 = x1
rect.y1 = y1
return rect
def _make_mock_doc(
images: list[tuple[int, ...]], # list of (xref, ...)
text_blocks: list[Any],
img_rects: list[Any],
page_count: int = 1,
) -> tuple[MagicMock, MagicMock]:
"""Build a mock fitz document with one page."""
mock_pix = MagicMock()
mock_pix.n = 3 # RGB — no conversion needed
mock_pix.save = MagicMock()
mock_page = MagicMock()
mock_page.get_images.return_value = images
mock_page.get_text.return_value = text_blocks
mock_page.get_image_rects.return_value = img_rects
mock_doc = MagicMock()
mock_doc.__len__.return_value = page_count
mock_doc.__getitem__ = MagicMock(return_value=mock_page)
return mock_doc, mock_pix
class TestExtractFigures:
"""Tests for extract_figures()."""
def test_no_caption_returns_empty_string(self, tmp_path: Any) -> None:
"""Figure with no nearby caption gets empty caption string."""
img_rect = _make_mock_rect(10, 10, 200, 150)
text_blocks = [
(300.0, 300.0, 400.0, 320.0, "Unrelated text here.", 0, 0),
]
mock_doc, mock_pix = _make_mock_doc(
images=[(5, 0, 0, 0, 0, 0, 0, 0, 8, "png")],
text_blocks=text_blocks,
img_rects=[img_rect],
)
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
results = extract_figures("paper.pdf", output_dir=str(tmp_path))
assert len(results) == 1
assert results[0].caption == ""
def test_figure_png_is_saved(self, tmp_path: Any) -> None:
"""PNG save is called with the correct output path."""
img_rect = _make_mock_rect()
mock_doc, mock_pix = _make_mock_doc(
images=[(5, 0, 0, 0, 0, 0, 0, 0, 8, "png")],
text_blocks=[],
img_rects=[img_rect],
)
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
results = extract_figures("paper.pdf", output_dir=str(tmp_path))
assert len(results) == 1
mock_pix.save.assert_called_once()
saved_path: str = mock_pix.save.call_args[0][0]
assert saved_path.endswith(".png")
def test_caption_detected_below_image(self, tmp_path: Any) -> None:
"""Caption block immediately below the image is detected."""
img_rect = _make_mock_rect(10, 10, 200, 150)
caption_block = (10.0, 155.0, 200.0, 170.0, "Figure 1: Architecture overview.", 0, 0)
mock_doc, mock_pix = _make_mock_doc(
images=[(5, 0, 0, 0, 0, 0, 0, 0, 8, "png")],
text_blocks=[caption_block],
img_rects=[img_rect],
)
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
results = extract_figures("paper.pdf", output_dir=str(tmp_path))
assert results[0].caption == "Figure 1: Architecture overview."
def test_caption_detected_above_image(self, tmp_path: Any) -> None:
"""Caption block immediately above the image is also detected."""
img_rect = _make_mock_rect(10, 80, 200, 200)
caption_block = (10.0, 50.0, 200.0, 75.0, "Fig. 2: Loss curve.", 0, 0)
mock_doc, mock_pix = _make_mock_doc(
images=[(5, 0, 0, 0, 0, 0, 0, 0, 8, "png")],
text_blocks=[caption_block],
img_rects=[img_rect],
)
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
results = extract_figures("paper.pdf", output_dir=str(tmp_path))
assert results[0].caption == "Fig. 2: Loss curve."
def test_german_caption_prefix(self, tmp_path: Any) -> None:
"""'Abbildung' prefix is recognized as a valid German caption."""
img_rect = _make_mock_rect(10, 10, 200, 150)
caption_block = (10.0, 155.0, 200.0, 170.0, "Abbildung 3: Ergebnisse.", 0, 0)
mock_doc, mock_pix = _make_mock_doc(
images=[(5, 0, 0, 0, 0, 0, 0, 0, 8, "png")],
text_blocks=[caption_block],
img_rects=[img_rect],
)
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
results = extract_figures("paper.pdf", output_dir=str(tmp_path))
assert "Abbildung" in results[0].caption
def test_unrelated_text_not_used_as_caption(self, tmp_path: Any) -> None:
"""Text blocks not starting with a caption prefix are ignored."""
img_rect = _make_mock_rect(10, 10, 200, 150)
non_caption = (10.0, 155.0, 200.0, 170.0, "This is a random sentence.", 0, 0)
mock_doc, mock_pix = _make_mock_doc(
images=[(5, 0, 0, 0, 0, 0, 0, 0, 8, "png")],
text_blocks=[non_caption],
img_rects=[img_rect],
)
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
results = extract_figures("paper.pdf", output_dir=str(tmp_path))
assert results[0].caption == ""
def test_no_images_returns_empty_list(self, tmp_path: Any) -> None:
"""PDF with no embedded images returns empty list."""
mock_doc, mock_pix = _make_mock_doc(
images=[],
text_blocks=[],
img_rects=[],
)
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
results = extract_figures("paper.pdf", output_dir=str(tmp_path))
assert results == []
def test_image_without_rect_is_skipped(self, tmp_path: Any) -> None:
"""Images for which get_image_rects returns empty list are skipped."""
mock_doc, mock_pix = _make_mock_doc(
images=[(5, 0, 0, 0, 0, 0, 0, 0, 8, "png")],
text_blocks=[],
img_rects=[], # no rect
)
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
results = extract_figures("paper.pdf", output_dir=str(tmp_path))
assert results == []
def test_output_dir_created_if_absent(self, tmp_path: Any) -> None:
"""output_dir is created if it does not exist."""
new_dir = tmp_path / "new_subdir" / "figures"
assert not new_dir.exists()
mock_doc, mock_pix = _make_mock_doc(images=[], text_blocks=[], img_rects=[])
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
extract_figures("paper.pdf", output_dir=str(new_dir))
assert new_dir.exists()
def test_paper_id_from_pdf_stem(self, tmp_path: Any) -> None:
"""paper_id in FigureChunk is the stem of the PDF filename."""
img_rect = _make_mock_rect()
mock_doc, mock_pix = _make_mock_doc(
images=[(5, 0, 0, 0, 0, 0, 0, 0, 8, "png")],
text_blocks=[],
img_rects=[img_rect],
)
with (
patch("fitz.open", return_value=mock_doc),
patch("fitz.Pixmap", return_value=mock_pix),
):
from codex.parsing.figures import extract_figures
results = extract_figures("/tmp/2301.07041.pdf", output_dir=str(tmp_path))
assert results[0].paper_id == "2301.07041"

View File

@@ -0,0 +1,386 @@
"""Tests for codex.parsing.mathpix — formula extraction module.
All external dependencies (fitz, pix2tex, httpx, get_settings) are mocked
so the suite runs offline without real API calls or model downloads.
"""
from __future__ import annotations
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from codex.models import FormulaChunk
def _mock_open_pdf() -> MagicMock:
"""Return a mock that satisfies ``open(path, 'rb')`` as a context manager."""
mock_file = MagicMock()
mock_file.__enter__ = MagicMock(return_value=mock_file)
mock_file.__exit__ = MagicMock(return_value=False)
mock_open = MagicMock(return_value=mock_file)
return mock_open
class TestMathPixAPIPath:
"""Tests for the MathPix cloud API backend."""
def test_mathpix_200_returns_formulas(self) -> None:
"""extract_formulas routes to MathPix when creds are provided and parses result."""
mmd_content = "\\[\n\\alpha + \\beta\n\\]\n\\[\nE = mc^2\n\\]"
mock_settings = MagicMock()
mock_settings.mathpix_app_id = None
mock_settings.mathpix_app_key = None
mock_settings.pix2tex_fallback = True
with (
patch("codex.parsing.mathpix.get_settings", return_value=mock_settings),
patch("builtins.open", _mock_open_pdf()),
patch("httpx.post") as mock_post,
patch("httpx.get") as mock_get,
patch("time.sleep"),
):
mock_post.return_value = MagicMock(
status_code=200,
json=lambda: {"pdf_id": "abc123"},
)
mock_post.return_value.raise_for_status = MagicMock()
mock_get.return_value = MagicMock(
status_code=200,
text=mmd_content,
)
from codex.parsing.mathpix import _extract_formulas_mathpix
results = _extract_formulas_mathpix("paper.pdf", "app_id", "app_key")
assert len(results) == 2
assert all(isinstance(r, FormulaChunk) for r in results)
assert results[0].raw_latex == "\\[\n\\alpha + \\beta\n\\]"
def test_mathpix_empty_pdf_id_returns_empty(self) -> None:
"""When MathPix returns no pdf_id, result is empty list."""
with (
patch("builtins.open", _mock_open_pdf()),
patch("httpx.post") as mock_post,
patch("time.sleep"),
):
mock_post.return_value = MagicMock(
status_code=200,
json=lambda: {}, # No pdf_id
)
mock_post.return_value.raise_for_status = MagicMock()
from codex.parsing.mathpix import _extract_formulas_mathpix
results = _extract_formulas_mathpix("paper.pdf", "app_id", "app_key")
assert results == []
def test_mathpix_paper_id_from_stem(self) -> None:
"""paper_id in FormulaChunk is derived from the PDF filename stem."""
mmd_content = "\\[\nx^2\n\\]"
with (
patch("builtins.open", _mock_open_pdf()),
patch("httpx.post") as mock_post,
patch("httpx.get") as mock_get,
patch("time.sleep"),
):
mock_post.return_value = MagicMock(
status_code=200,
json=lambda: {"pdf_id": "xyz"},
)
mock_post.return_value.raise_for_status = MagicMock()
mock_get.return_value = MagicMock(status_code=200, text=mmd_content)
from codex.parsing.mathpix import _extract_formulas_mathpix
results = _extract_formulas_mathpix("/tmp/2301.07041.pdf", "id", "key")
assert results[0].paper_id == "2301.07041"
def test_mathpix_page_counter_increments(self) -> None:
"""Page counter increments on MathPix page markers in MMD."""
mmd_content = "\\[\nA\n\\]\n<!-- Page 2 -->\n\\[\nB\n\\]"
with (
patch("builtins.open", _mock_open_pdf()),
patch("httpx.post") as mock_post,
patch("httpx.get") as mock_get,
patch("time.sleep"),
):
mock_post.return_value = MagicMock(
status_code=200,
json=lambda: {"pdf_id": "p1"},
)
mock_post.return_value.raise_for_status = MagicMock()
mock_get.return_value = MagicMock(status_code=200, text=mmd_content)
from codex.parsing.mathpix import _extract_formulas_mathpix
results = _extract_formulas_mathpix("p.pdf", "id", "key")
assert results[0].page == 1
assert results[1].page == 2
def test_mathpix_http_error_propagates(self) -> None:
"""HTTP errors from MathPix are raised (tenacity reraises after retries)."""
import httpx
with (
patch("builtins.open", _mock_open_pdf()),
patch("httpx.post") as mock_post,
):
mock_post.return_value = MagicMock(status_code=401)
mock_post.return_value.raise_for_status.side_effect = httpx.HTTPStatusError(
"401", request=MagicMock(), response=MagicMock()
)
from codex.parsing.mathpix import _extract_formulas_mathpix
with pytest.raises(httpx.HTTPStatusError):
_extract_formulas_mathpix("p.pdf", "id", "key")
class TestPix2TexFallbackPath:
"""Tests for the local pix2tex backend."""
def _make_fitz_page(
self,
blocks: list[Any],
pixmap_samples: bytes = b"\xff" * (30 * 100 * 3),
pixmap_w: int = 100,
pixmap_h: int = 30,
) -> MagicMock:
"""Build a mock fitz page with given text blocks and a fake pixmap."""
mock_pix = MagicMock()
mock_pix.width = pixmap_w
mock_pix.height = pixmap_h
mock_pix.samples = pixmap_samples
mock_page = MagicMock()
mock_page.get_text.return_value = blocks
mock_page.get_pixmap.return_value = mock_pix
return mock_page
def test_pix2tex_fallback_invoked_without_creds(self) -> None:
"""When no MathPix creds, pix2tex fallback is called."""
math_block = (0.0, 0.0, 200.0, 50.0, "∑ α∫β∇γ∂δ∞ε", 0, 0)
mock_settings = MagicMock()
mock_settings.mathpix_app_id = None
mock_settings.mathpix_app_key = None
mock_settings.pix2tex_fallback = True
mock_settings.figures_dir = "/tmp/figs"
mock_model = MagicMock(return_value=r"\sum \alpha")
mock_page = self._make_fitz_page([math_block])
mock_doc = MagicMock()
mock_doc.__len__.return_value = 1
mock_doc.__getitem__ = MagicMock(return_value=mock_page)
with (
patch("codex.parsing.mathpix.get_settings", return_value=mock_settings),
patch("codex.parsing.mathpix._get_pix2tex_model", return_value=mock_model),
patch("fitz.open", return_value=mock_doc),
patch("fitz.Matrix"),
patch("fitz.Rect"),
):
from PIL import Image
with patch.object(Image, "frombytes", return_value=MagicMock()):
from codex.parsing.mathpix import extract_formulas
results = extract_formulas("paper.pdf")
assert len(results) == 1
assert results[0].raw_latex == r"\sum \alpha"
def test_pix2tex_bbox_size_filter(self) -> None:
"""Blocks smaller than MIN_HEIGHT_PT or MIN_WIDTH_PT are skipped."""
tiny_block = (0.0, 0.0, 10.0, 5.0, "∑∫∂∞∇ε", 0, 0) # 10pt wide, 5pt tall
mock_settings = MagicMock()
mock_settings.mathpix_app_id = None
mock_settings.mathpix_app_key = None
mock_settings.pix2tex_fallback = True
mock_model = MagicMock(return_value=r"\sum")
mock_page = self._make_fitz_page([tiny_block])
mock_doc = MagicMock()
mock_doc.__len__.return_value = 1
mock_doc.__getitem__ = MagicMock(return_value=mock_page)
with (
patch("codex.parsing.mathpix.get_settings", return_value=mock_settings),
patch("codex.parsing.mathpix._get_pix2tex_model", return_value=mock_model),
patch("fitz.open", return_value=mock_doc),
patch("fitz.Matrix"),
patch("fitz.Rect"),
):
from codex.parsing.mathpix import extract_formulas
results = extract_formulas("paper.pdf")
assert results == []
def test_pix2tex_math_ratio_filter(self) -> None:
"""Blocks with low math-character ratio are skipped."""
plain_text_block = (0.0, 0.0, 200.0, 50.0, "This is just regular text with no math.", 0, 0)
mock_settings = MagicMock()
mock_settings.mathpix_app_id = None
mock_settings.mathpix_app_key = None
mock_settings.pix2tex_fallback = True
mock_model = MagicMock(return_value=r"\alpha")
mock_page = self._make_fitz_page([plain_text_block])
mock_doc = MagicMock()
mock_doc.__len__.return_value = 1
mock_doc.__getitem__ = MagicMock(return_value=mock_page)
with (
patch("codex.parsing.mathpix.get_settings", return_value=mock_settings),
patch("codex.parsing.mathpix._get_pix2tex_model", return_value=mock_model),
patch("fitz.open", return_value=mock_doc),
patch("fitz.Matrix"),
patch("fitz.Rect"),
):
from codex.parsing.mathpix import extract_formulas
results = extract_formulas("paper.pdf")
assert results == []
def test_pix2tex_exception_swallowed(self) -> None:
"""If pix2tex raises, the block is skipped (no exception propagates)."""
math_block = (0.0, 0.0, 200.0, 50.0, "∑ α∫β∇γ∂δ∞ε", 0, 0)
mock_settings = MagicMock()
mock_settings.mathpix_app_id = None
mock_settings.mathpix_app_key = None
mock_settings.pix2tex_fallback = True
mock_model = MagicMock(side_effect=RuntimeError("GPU OOM"))
mock_page = self._make_fitz_page([math_block])
mock_doc = MagicMock()
mock_doc.__len__.return_value = 1
mock_doc.__getitem__ = MagicMock(return_value=mock_page)
with (
patch("codex.parsing.mathpix.get_settings", return_value=mock_settings),
patch("codex.parsing.mathpix._get_pix2tex_model", return_value=mock_model),
patch("fitz.open", return_value=mock_doc),
patch("fitz.Matrix"),
patch("fitz.Rect"),
):
from PIL import Image
with patch.object(Image, "frombytes", return_value=MagicMock()):
from codex.parsing.mathpix import extract_formulas
results = extract_formulas("paper.pdf") # Must not raise
assert results == []
class TestRouteSelection:
"""Tests for extract_formulas route selection logic."""
def test_mathpix_selected_when_creds_present(self) -> None:
"""extract_formulas calls MathPix backend when both creds are set."""
mock_settings = MagicMock()
mock_settings.mathpix_app_id = "my_id"
mock_settings.mathpix_app_key = "my_key"
mock_settings.pix2tex_fallback = True
with (
patch("codex.parsing.mathpix.get_settings", return_value=mock_settings),
patch(
"codex.parsing.mathpix._extract_formulas_mathpix", return_value=[]
) as mock_mathpix,
patch(
"codex.parsing.mathpix._extract_formulas_pix2tex", return_value=[]
) as mock_pix2tex,
):
from codex.parsing.mathpix import extract_formulas
extract_formulas("paper.pdf")
mock_mathpix.assert_called_once_with("paper.pdf", "my_id", "my_key")
mock_pix2tex.assert_not_called()
def test_pix2tex_selected_without_creds(self) -> None:
"""extract_formulas calls pix2tex when no MathPix creds are set."""
mock_settings = MagicMock()
mock_settings.mathpix_app_id = None
mock_settings.mathpix_app_key = None
mock_settings.pix2tex_fallback = True
with (
patch("codex.parsing.mathpix.get_settings", return_value=mock_settings),
patch(
"codex.parsing.mathpix._extract_formulas_mathpix", return_value=[]
) as mock_mathpix,
patch(
"codex.parsing.mathpix._extract_formulas_pix2tex", return_value=[]
) as mock_pix2tex,
):
from codex.parsing.mathpix import extract_formulas
extract_formulas("paper.pdf")
mock_pix2tex.assert_called_once_with("paper.pdf")
mock_mathpix.assert_not_called()
def test_empty_string_creds_treated_as_absent(self) -> None:
"""Empty string credentials fall through to pix2tex (not MathPix)."""
mock_settings = MagicMock()
mock_settings.mathpix_app_id = ""
mock_settings.mathpix_app_key = ""
mock_settings.pix2tex_fallback = True
with (
patch("codex.parsing.mathpix.get_settings", return_value=mock_settings),
patch(
"codex.parsing.mathpix._extract_formulas_mathpix", return_value=[]
) as mock_mathpix,
patch(
"codex.parsing.mathpix._extract_formulas_pix2tex", return_value=[]
) as mock_pix2tex,
):
from codex.parsing.mathpix import extract_formulas
extract_formulas("paper.pdf")
mock_pix2tex.assert_called_once()
mock_mathpix.assert_not_called()
def test_formula_extraction_disabled_when_fallback_false(self) -> None:
"""No extraction when pix2tex_fallback=False and no creds."""
mock_settings = MagicMock()
mock_settings.mathpix_app_id = None
mock_settings.mathpix_app_key = None
mock_settings.pix2tex_fallback = False
with (
patch("codex.parsing.mathpix.get_settings", return_value=mock_settings),
patch(
"codex.parsing.mathpix._extract_formulas_mathpix", return_value=[]
) as mock_mathpix,
patch(
"codex.parsing.mathpix._extract_formulas_pix2tex", return_value=[]
) as mock_pix2tex,
):
from codex.parsing.mathpix import extract_formulas
results = extract_formulas("paper.pdf")
assert results == []
mock_mathpix.assert_not_called()
mock_pix2tex.assert_not_called()

0
tests/wiki/__init__.py Normal file
View File

163
tests/wiki/test_check.py Normal file
View File

@@ -0,0 +1,163 @@
"""Tests for ``codex wiki check`` CLI command.
Verifies:
- Exit 0 when all pages are grounded (no ⚠ in any page).
- Exit 1 when at least one page contains an ungrounded claim (⚠ marker).
"""
from __future__ import annotations
import textwrap
from pathlib import Path
import pytest
from typer.testing import CliRunner
from codex.cli import app
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def wiki_dir_all_grounded(tmp_path: Path) -> Path:
"""Create a wiki directory with only grounded pages (no ⚠)."""
wiki = tmp_path / "wiki"
wiki.mkdir()
(wiki / "discrete-conformal-map.md").write_text(
textwrap.dedent(
"""\
# Discrete Conformal Map
A discrete conformal map is defined by logarithmic scale factors. [BPS2015 #chunk 0]
The variational principle gives the optimum. [BPS2015 #chunk 1]
"""
),
encoding="utf-8",
)
(wiki / "circle-packing.md").write_text(
textwrap.dedent(
"""\
# Circle Packing
Circle packing is studied via the Koebe-Andreev-Thurston theorem. [Thurston1985 #page 3]
"""
),
encoding="utf-8",
)
return wiki
@pytest.fixture
def wiki_dir_with_ungrounded(tmp_path: Path) -> Path:
"""Create a wiki directory where one page has an ungrounded claim (⚠)."""
wiki = tmp_path / "wiki"
wiki.mkdir()
(wiki / "lobachevsky-function.md").write_text(
textwrap.dedent(
"""\
# Lobachevsky Function
The volume formula is V = L(γ₁)+L(γ₂)+L(γ₃). [Springborn2008 #chunk 16]
⚠ The closed form L(x) = -∫₀ˣ log|2 sin t| dt. [Springborn2008 #chunk 99]
"""
),
encoding="utf-8",
)
return wiki
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_wiki_check_exit_0_when_all_grounded(
wiki_dir_all_grounded: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""codex wiki check exits 0 when no ⚠ in any page."""
from unittest.mock import MagicMock
from codex.config import Settings
mock_settings = MagicMock(spec=Settings)
mock_settings.wiki_dir = str(wiki_dir_all_grounded)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock_settings, raising=False)
runner = CliRunner()
result = runner.invoke(app, ["wiki", "check", "--output-dir", str(wiki_dir_all_grounded)])
assert result.exit_code == 0, (
f"Expected exit 0, got {result.exit_code}. Output: {result.output}"
)
def test_wiki_check_exit_1_when_ungrounded(
wiki_dir_with_ungrounded: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""codex wiki check exits 1 when at least one ⚠ is present."""
from unittest.mock import MagicMock
from codex.config import Settings
mock_settings = MagicMock(spec=Settings)
mock_settings.wiki_dir = str(wiki_dir_with_ungrounded)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock_settings, raising=False)
runner = CliRunner()
result = runner.invoke(app, ["wiki", "check", "--output-dir", str(wiki_dir_with_ungrounded)])
assert result.exit_code == 1, (
f"Expected exit 1, got {result.exit_code}. Output: {result.output}"
)
def test_wiki_check_empty_dir_exits_0(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""codex wiki check exits 0 when wiki dir has no .md pages (nothing to check)."""
wiki = tmp_path / "wiki"
wiki.mkdir()
from unittest.mock import MagicMock
from codex.config import Settings
mock_settings = MagicMock(spec=Settings)
mock_settings.wiki_dir = str(wiki)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock_settings, raising=False)
runner = CliRunner()
result = runner.invoke(app, ["wiki", "check", "--output-dir", str(wiki)])
assert result.exit_code == 0
def test_wiki_check_index_and_log_ignored(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""codex wiki check ignores index.md and log.md even if they contain ⚠."""
wiki = tmp_path / "wiki"
wiki.mkdir()
# index.md and log.md with ⚠ — must NOT trigger exit 1
(wiki / "index.md").write_text("# Wiki Index\n\n⚠ some note\n", encoding="utf-8")
(wiki / "log.md").write_text("# Log\n\n⚠ ungrounded\n", encoding="utf-8")
from unittest.mock import MagicMock
from codex.config import Settings
mock_settings = MagicMock(spec=Settings)
mock_settings.wiki_dir = str(wiki)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock_settings, raising=False)
runner = CliRunner()
result = runner.invoke(app, ["wiki", "check", "--output-dir", str(wiki)])
assert result.exit_code == 0, (
f"index.md / log.md should be excluded. exit={result.exit_code}, out={result.output}"
)

510
tests/wiki/test_compile.py Normal file
View File

@@ -0,0 +1,510 @@
"""Tests for compile_concept, compile_all, write_index, append_log.
DB and LLM are fully mocked — no network or database access required.
"""
from __future__ import annotations
import textwrap
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock
import pytest
from codex.wiki import (
Claim,
CompileReport,
Concept,
ConceptPage,
_chunk_hash,
_inject_cross_refs,
_parse_claims,
_run_grounding_guard,
append_log,
compile_concept,
write_index,
)
# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------
FIXTURE_CHUNKS: list[dict[str, Any]] = [
{
"id": 1,
"paper_id": "springborn-2008",
"ord": 16,
"content": (
"For an ideal tetrahedron with dihedral angles the hyperbolic volume is "
"V = L(gamma1) + L(gamma2) + L(gamma3). "
"The volume function V0 is strictly concave on the angle domain."
),
"bibkey": "Springborn2008",
"dist": 0.31,
},
{
"id": 2,
"paper_id": "springborn-2008",
"ord": 0,
"content": (
"A discrete conformal map is defined by logarithmic scale factors u_i "
"at each vertex such that the edge lengths satisfy a compatibility condition."
),
"bibkey": "Springborn2008",
"dist": 0.35,
},
]
FIXTURE_CONCEPT = Concept(
slug="lobachevsky-function",
title="Lobachevsky Function",
aliases=["Milnor Lobachevsky", "Clausen function"],
emphasis="Verwendung in hyperbolischen Volumenformeln (Springborn 2008).",
)
# Grounded LLM output: claim text is a substring of the fixture chunk
_GROUNDED_CLAIM = (
"For an ideal tetrahedron with dihedral angles the hyperbolic volume is"
" V = L(gamma1) + L(gamma2) + L(gamma3). [Springborn2008 #chunk 16]"
)
GROUNDED_LLM_OUTPUT = (
"The Lobachevsky function L appears as the building block of hyperbolic volume.\n"
+ _GROUNDED_CLAIM
+ "\nThe volume function V0 is strictly concave on the angle domain."
" [Springborn2008 #chunk 16]\n"
)
# Ungrounded LLM output: claim text not present in any chunk
UNGROUNDED_LLM_OUTPUT = textwrap.dedent(
"""\
The closed form is L(x) = -integral from 0 to x of log|2 sin t| dt. [Springborn2008 #chunk 99]
"""
)
class MockLLM:
"""Deterministic mock LLM that returns a preset response."""
def __init__(self, response: str) -> None:
self._response = response
def generate(self, prompt: str, model: str) -> str:
return self._response
# ---------------------------------------------------------------------------
# _parse_claims
# ---------------------------------------------------------------------------
def test_parse_claims_finds_citation() -> None:
"""Claims with [BibKey #locator] format are parsed correctly."""
md = "Some result. [Springborn2008 #chunk 16]"
claims = _parse_claims(md)
assert len(claims) == 1
assert claims[0].bibkey == "Springborn2008"
assert claims[0].locator == "chunk 16"
def test_parse_claims_no_citations() -> None:
"""Text without citation markers returns empty list."""
md = "A standalone sentence with no citation."
claims = _parse_claims(md)
assert claims == []
def test_parse_claims_multiple() -> None:
"""Multiple citations in same text are all parsed."""
md = "First claim. [Smith2020 #page 5]\nSecond claim. [Jones2019 #eq 3]\n"
claims = _parse_claims(md)
assert len(claims) == 2
# ---------------------------------------------------------------------------
# _run_grounding_guard
# ---------------------------------------------------------------------------
def test_grounding_guard_marks_grounded_claim() -> None:
"""A claim whose text appears in the cited chunk is marked grounded."""
claim = Claim(
text="the hyperbolic volume is V = L(gamma1) + L(gamma2) + L(gamma3)",
bibkey="Springborn2008",
locator="chunk 16",
)
result = _run_grounding_guard([claim], FIXTURE_CHUNKS)
assert result[0].grounded is True
def test_grounding_guard_marks_ungrounded_claim() -> None:
"""A claim not present in the cited chunk is marked ungrounded."""
claim = Claim(
text="integral from 0 to x of log 2 sin t dt closed form definition",
bibkey="Springborn2008",
locator="chunk 99",
)
result = _run_grounding_guard([claim], FIXTURE_CHUNKS)
assert result[0].grounded is False
def test_grounding_guard_ungrounded_when_bibkey_missing() -> None:
"""A claim citing a bibkey not in any chunk is ungrounded."""
claim = Claim(text="some statement", bibkey="UnknownBib2000", locator="page 1")
result = _run_grounding_guard([claim], FIXTURE_CHUNKS)
assert result[0].grounded is False
# ---------------------------------------------------------------------------
# _inject_cross_refs
# ---------------------------------------------------------------------------
def test_inject_cross_refs_replaces_title() -> None:
"""Occurrences of another concept's title are replaced with [[slug]]."""
concepts = [
Concept(
slug="discrete-conformal-map",
title="Discrete Conformal Map",
aliases=[],
),
FIXTURE_CONCEPT,
]
text = "This is related to Discrete Conformal Map theory."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[discrete-conformal-map]]" in result
def test_inject_cross_refs_skips_current_slug() -> None:
"""The current concept's own title is not replaced."""
concepts = [FIXTURE_CONCEPT]
text = "The Lobachevsky Function is important."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[lobachevsky-function]]" not in result
def test_inject_cross_refs_replaces_alias() -> None:
"""Aliases are also replaced with [[slug]]."""
concepts = [
Concept(
slug="circle-packing",
title="Circle Packing",
aliases=["Koebe-Andreev-Thurston", "circle pattern"],
),
FIXTURE_CONCEPT,
]
text = "The Koebe-Andreev-Thurston theorem gives a packing."
result = _inject_cross_refs(text, concepts, current_slug="lobachevsky-function")
assert "[[circle-packing]]" in result
# ---------------------------------------------------------------------------
# compile_concept (full mock)
# ---------------------------------------------------------------------------
@pytest.fixture
def mock_retrieve(monkeypatch: pytest.MonkeyPatch) -> None:
"""Patch _retrieve_chunks to return deterministic fixture chunks."""
monkeypatch.setattr(
"codex.wiki._retrieve_chunks",
lambda queries, top_k: FIXTURE_CHUNKS,
)
@pytest.fixture
def mock_formulas(monkeypatch: pytest.MonkeyPatch) -> None:
"""Patch _try_embed_formulas to be a no-op (F-09 not present)."""
monkeypatch.setattr("codex.wiki._try_embed_formulas", lambda concept, chunks: None)
@pytest.fixture
def mock_settings(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
"""Patch get_settings to return a settings pointing at tmp_path."""
from codex.config import Settings
wiki_path = tmp_path / "wiki"
wiki_path.mkdir()
mock = MagicMock(spec=Settings)
mock.wiki_dir = str(wiki_path)
mock.wiki_llm_model = "test-model"
mock.wiki_llm_url = None
mock.ollama_base_url = "http://localhost:11434"
mock.wiki_top_k = 6
monkeypatch.setattr("codex.wiki.get_settings", lambda: mock)
monkeypatch.setattr("codex.cli.get_settings", lambda: mock, raising=False)
return wiki_path
def test_compile_concept_returns_concept_page(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""compile_concept returns a ConceptPage with non-empty markdown."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
assert isinstance(page, ConceptPage)
assert page.concept.slug == "lobachevsky-function"
assert len(page.markdown) > 0
def test_compile_concept_has_chunk_hash(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""Compiled page has a non-empty chunk_hash."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
assert len(page.chunk_hash) == 64 # SHA-256 hex
def test_compile_concept_grounded_claim_not_flagged(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""A grounded claim does NOT receive the ⚠ marker in the output markdown."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
# When all claims are grounded, no ⚠ in markdown
# (may still have 0 ⚠ if claims found; just check no false positive for grounded)
grounded = [c for c in page.claims if c.grounded]
for claim in grounded:
assert claim.grounded is True
def test_compile_concept_ungrounded_claim_marked(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""An ungrounded claim is marked ⚠ in the page markdown."""
llm = MockLLM(UNGROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
ungrounded = [c for c in page.claims if not c.grounded]
if ungrounded:
assert "" in page.markdown
def test_compile_concept_header_present(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
) -> None:
"""Page markdown starts with an H1 header for the concept title."""
llm = MockLLM(GROUNDED_LLM_OUTPUT)
page = compile_concept(FIXTURE_CONCEPT, top_k=6, llm=llm, wiki_dir=mock_settings)
assert page.markdown.startswith("# Lobachevsky Function")
# ---------------------------------------------------------------------------
# write_index
# ---------------------------------------------------------------------------
def test_index_creates_index_md(
mock_settings: Path,
) -> None:
"""write_index creates wiki/index.md."""
pages = [
("discrete-conformal-map", "Discrete Conformal Map"),
("circle-packing", "Circle Packing"),
]
write_index(pages, wiki_dir=mock_settings)
index_path = mock_settings / "index.md"
assert index_path.exists()
def test_index_contains_links(
mock_settings: Path,
) -> None:
"""wiki/index.md contains [[slug]] links for each page."""
pages = [
("discrete-conformal-map", "Discrete Conformal Map"),
("circle-packing", "Circle Packing"),
]
write_index(pages, wiki_dir=mock_settings)
content = (mock_settings / "index.md").read_text(encoding="utf-8")
assert "[[discrete-conformal-map]]" in content
assert "[[circle-packing]]" in content
def test_index_contains_header(
mock_settings: Path,
) -> None:
"""wiki/index.md starts with a # Wiki Index header."""
write_index([], wiki_dir=mock_settings)
content = (mock_settings / "index.md").read_text(encoding="utf-8")
assert content.startswith("# Wiki Index")
# ---------------------------------------------------------------------------
# append_log
# ---------------------------------------------------------------------------
def test_log_append_creates_log_md(
mock_settings: Path,
) -> None:
"""append_log creates wiki/log.md if it does not exist."""
report = CompileReport(
compiled=["lobachevsky-function"],
skipped=[],
ungrounded=[],
)
append_log(report, wiki_dir=mock_settings)
log_path = mock_settings / "log.md"
assert log_path.exists()
def test_log_append_does_not_overwrite(
mock_settings: Path,
) -> None:
"""Two calls to append_log accumulate entries — older entry is NOT overwritten."""
report1 = CompileReport(compiled=["first-concept"])
report2 = CompileReport(compiled=["second-concept"])
append_log(report1, wiki_dir=mock_settings)
append_log(report2, wiki_dir=mock_settings)
content = (mock_settings / "log.md").read_text(encoding="utf-8")
assert "first-concept" in content
assert "second-concept" in content
def test_log_append_records_ungrounded(
mock_settings: Path,
) -> None:
"""Ungrounded claims appear in the log entry."""
report = CompileReport(
compiled=["lobachevsky-function"],
ungrounded=[("lobachevsky-function", "some ungrounded claim text")],
)
append_log(report, wiki_dir=mock_settings)
content = (mock_settings / "log.md").read_text(encoding="utf-8")
assert "some ungrounded claim text" in content
assert "" in content
def test_log_append_records_skipped(
mock_settings: Path,
) -> None:
"""Skipped slugs appear in the log entry."""
report = CompileReport(
compiled=[],
skipped=["circle-packing"],
)
append_log(report, wiki_dir=mock_settings)
content = (mock_settings / "log.md").read_text(encoding="utf-8")
assert "circle-packing" in content
# ---------------------------------------------------------------------------
# Idempotency: second compile without chunk change → no rewrite
# ---------------------------------------------------------------------------
def test_idempotent_no_rewrite_when_unchanged(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Second compile_all with unchanged chunks skips the concept (changed_only=True)."""
from codex.wiki import compile_all
call_count = 0
original_compile = compile_concept
def counting_compile(concept: Concept, **kwargs: Any) -> ConceptPage:
nonlocal call_count
call_count += 1
return original_compile(concept, **kwargs)
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
# Write concepts.yaml into wiki_dir
yaml_content = textwrap.dedent(
"""\
concepts:
- slug: lobachevsky-function
title: Lobachevsky Function
aliases: [Milnor Lobachevsky]
"""
)
(mock_settings / "concepts.yaml").write_text(yaml_content, encoding="utf-8")
llm = MockLLM(GROUNDED_LLM_OUTPUT)
# First compile: should call compile_concept
compile_all(changed_only=True, llm=llm, output_dir=str(mock_settings))
first_count = call_count
# Second compile with same chunks: should skip
compile_all(changed_only=True, llm=llm, output_dir=str(mock_settings))
second_count = call_count
assert first_count >= 1, "First run should compile at least once"
assert second_count == first_count, "Second run should not recompile (unchanged chunks)"
def test_force_all_recompiles_even_when_unchanged(
mock_retrieve: None,
mock_formulas: None,
mock_settings: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""compile_all with changed_only=False recompiles regardless of hash."""
from codex.wiki import compile_all
call_count = 0
original_compile = compile_concept
def counting_compile(concept: Concept, **kwargs: Any) -> ConceptPage:
nonlocal call_count
call_count += 1
return original_compile(concept, **kwargs)
monkeypatch.setattr("codex.wiki.compile_concept", counting_compile)
yaml_content = textwrap.dedent(
"""\
concepts:
- slug: lobachevsky-function
title: Lobachevsky Function
aliases: [Milnor Lobachevsky]
"""
)
(mock_settings / "concepts.yaml").write_text(yaml_content, encoding="utf-8")
llm = MockLLM(GROUNDED_LLM_OUTPUT)
compile_all(changed_only=True, llm=llm, output_dir=str(mock_settings))
compile_all(changed_only=False, llm=llm, output_dir=str(mock_settings))
assert call_count >= 2, "Force --all should recompile even when unchanged"
# ---------------------------------------------------------------------------
# _chunk_hash
# ---------------------------------------------------------------------------
def test_chunk_hash_is_stable() -> None:
"""Same chunks produce the same hash each time."""
h1 = _chunk_hash(FIXTURE_CHUNKS)
h2 = _chunk_hash(FIXTURE_CHUNKS)
assert h1 == h2
assert len(h1) == 64
def test_chunk_hash_differs_on_content_change() -> None:
"""Different content produces different hash."""
modified = [dict(FIXTURE_CHUNKS[0], content="completely different content"), FIXTURE_CHUNKS[1]]
h1 = _chunk_hash(FIXTURE_CHUNKS)
h2 = _chunk_hash(modified)
assert h1 != h2

122
tests/wiki/test_concepts.py Normal file
View File

@@ -0,0 +1,122 @@
"""Tests for load_concepts() — YAML parsing of wiki/concepts.yaml."""
from __future__ import annotations
import textwrap
from pathlib import Path
import pytest
from codex.wiki import Concept, load_concepts
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
FIXTURE_YAML = textwrap.dedent(
"""\
concepts:
- slug: discrete-conformal-map
title: Discrete Conformal Map
aliases: [discrete conformal equivalence, discrete uniformization]
emphasis: Fokus auf die variationelle Charakterisierung (BPS 2015).
- slug: circle-packing
title: Circle Packing
aliases: [Koebe-Andreev-Thurston, circle pattern]
- slug: lobachevsky-function
title: Lobachevsky Function
aliases: [Milnor Lobachevsky, Clausen function]
emphasis: Verwendung in hyperbolischen Volumenformeln (Springborn 2008).
- slug: discrete-yamabe-flow
title: Discrete Yamabe Flow
aliases: [discrete Ricci flow, Chow-Luo]
- slug: hyperideal-tetrahedron
title: Hyperideal Tetrahedron
aliases: [hyperbolic volume, Schläfli, Ushijima]
"""
)
@pytest.fixture
def concepts_yaml(tmp_path: Path) -> Path:
"""Write fixture YAML to a temp file and return the path."""
p = tmp_path / "concepts.yaml"
p.write_text(FIXTURE_YAML, encoding="utf-8")
return p
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_load_concepts_returns_all_concepts(concepts_yaml: Path) -> None:
"""All five concept entries are returned."""
concepts = load_concepts(str(concepts_yaml))
assert len(concepts) == 5
def test_load_concepts_types(concepts_yaml: Path) -> None:
"""Every returned item is a Concept instance."""
concepts = load_concepts(str(concepts_yaml))
for c in concepts:
assert isinstance(c, Concept)
def test_load_concepts_first_slug_and_title(concepts_yaml: Path) -> None:
"""First concept has correct slug and title."""
concepts = load_concepts(str(concepts_yaml))
assert concepts[0].slug == "discrete-conformal-map"
assert concepts[0].title == "Discrete Conformal Map"
def test_load_concepts_aliases_are_lists(concepts_yaml: Path) -> None:
"""Aliases are parsed as a list of strings."""
concepts = load_concepts(str(concepts_yaml))
for c in concepts:
assert isinstance(c.aliases, list)
for alias in c.aliases:
assert isinstance(alias, str)
def test_load_concepts_first_has_two_aliases(concepts_yaml: Path) -> None:
"""First concept has exactly two aliases."""
concepts = load_concepts(str(concepts_yaml))
assert concepts[0].aliases == ["discrete conformal equivalence", "discrete uniformization"]
def test_load_concepts_emphasis_present(concepts_yaml: Path) -> None:
"""Concepts with emphasis field have it parsed correctly."""
concepts = load_concepts(str(concepts_yaml))
dcm = next(c for c in concepts if c.slug == "discrete-conformal-map")
assert dcm.emphasis is not None
assert "variationelle" in dcm.emphasis
def test_load_concepts_emphasis_absent_is_none(concepts_yaml: Path) -> None:
"""Concepts without emphasis field have emphasis=None."""
concepts = load_concepts(str(concepts_yaml))
cp = next(c for c in concepts if c.slug == "circle-packing")
assert cp.emphasis is None
def test_load_concepts_slugs_unique(concepts_yaml: Path) -> None:
"""All slugs are distinct."""
concepts = load_concepts(str(concepts_yaml))
slugs = [c.slug for c in concepts]
assert len(slugs) == len(set(slugs))
def test_load_concepts_unicode_in_aliases(concepts_yaml: Path) -> None:
"""Aliases with non-ASCII characters (Schläfli) are parsed without error."""
concepts = load_concepts(str(concepts_yaml))
ht = next(c for c in concepts if c.slug == "hyperideal-tetrahedron")
assert any("Schläfli" in a for a in ht.aliases)
def test_load_concepts_empty_yaml(tmp_path: Path) -> None:
"""An empty concepts list returns an empty list (no crash)."""
p = tmp_path / "concepts.yaml"
p.write_text("concepts: []\n", encoding="utf-8")
concepts = load_concepts(str(p))
assert concepts == []

415
uv.lock generated
View File

@@ -153,6 +153,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" },
]
[[package]]
name = "albucore"
version = "0.0.23"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "opencv-python-headless" },
{ name = "simsimd" },
{ name = "stringzilla" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c0/64/78d1716dd1496734d58705d68e02a9eadf4c10edd32c3ad641dde949efca/albucore-0.0.23.tar.gz", hash = "sha256:57823982b954913b84a9e2cf71058c4577b02397a62c41885be2d9b295efa8ab", size = 16437, upload-time = "2024-12-24T20:28:47.136Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/de/4d9298befa6ae0f21230378f55100dca364816e3734028ca2766f2eca263/albucore-0.0.23-py3-none-any.whl", hash = "sha256:99274ac0c15a1a7d9a726df9d54d5ab70d9d0c189e2a935399dba3d4bafad415", size = 14717, upload-time = "2024-12-24T20:28:46.03Z" },
]
[[package]]
name = "albumentations"
version = "1.4.24"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "albucore" },
{ name = "numpy" },
{ name = "opencv-python-headless" },
{ name = "pydantic" },
{ name = "pyyaml" },
{ name = "scipy" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/5a/151344c3ce5a6ee50437b07aa58364ada145d3a7b63e12849068ed0c5265/albumentations-1.4.24.tar.gz", hash = "sha256:cc8874cb0abccc9117aefacbe385c354ac824fa142f46a6c0896d65cec5f8a6d", size = 263557, upload-time = "2024-12-24T21:59:34.587Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/ae/904b7bf58281d2bc1f3d6d48813bbcee742d1f8e3f9c0e1c451b0f67eb5a/albumentations-1.4.24-py3-none-any.whl", hash = "sha256:2f639257a11e681071f4f7d10f6a6d874ae705bcce52746874cd8d7e317a16d7", size = 274921, upload-time = "2024-12-24T21:59:32.202Z" },
]
[[package]]
name = "annotated-doc"
version = "0.0.4"
@@ -342,8 +374,10 @@ dependencies = [
{ name = "flagembedding" },
{ name = "httpx" },
{ name = "pgvector" },
{ name = "pix2tex" },
{ name = "psycopg", extra = ["binary"] },
{ name = "pydantic-settings" },
{ name = "pymupdf" },
{ name = "sentence-transformers" },
{ name = "tenacity" },
{ name = "typer" },
@@ -362,8 +396,10 @@ requires-dist = [
{ name = "flagembedding", specifier = ">=1.2" },
{ name = "httpx", specifier = ">=0.27" },
{ name = "pgvector", specifier = ">=0.3" },
{ name = "pix2tex", specifier = ">=0.1.4" },
{ name = "psycopg", extras = ["binary"], specifier = ">=3.1" },
{ name = "pydantic-settings", specifier = ">=2" },
{ name = "pymupdf", specifier = ">=1.24" },
{ name = "sentence-transformers", specifier = ">=3" },
{ name = "tenacity", specifier = ">=8" },
{ name = "typer", specifier = ">=0.12" },
@@ -391,7 +427,7 @@ name = "cuda-bindings"
version = "13.3.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cuda-pathfinder" },
{ name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/67/5e7dba1ba576dd73da5dee894ca076ca5e959450dfff66d6d510a255d1f7/cuda_bindings-13.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7855c4868aabc0cfae28abbe83d56734bdfbd08f08fc234ac1912a12858bf49", size = 6025351, upload-time = "2026-05-29T23:11:49.685Z" },
@@ -422,34 +458,34 @@ wheels = [
[package.optional-dependencies]
cudart = [
{ name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" },
]
cufft = [
{ name = "nvidia-cufft", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cufft", marker = "sys_platform == 'linux'" },
]
cufile = [
{ name = "nvidia-cufile", marker = "sys_platform == 'linux'" },
]
cupti = [
{ name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" },
]
curand = [
{ name = "nvidia-curand", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-curand", marker = "sys_platform == 'linux'" },
]
cusolver = [
{ name = "nvidia-cusolver", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cusolver", marker = "sys_platform == 'linux'" },
]
cusparse = [
{ name = "nvidia-cusparse", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cusparse", marker = "sys_platform == 'linux'" },
]
nvjitlink = [
{ name = "nvidia-nvjitlink", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" },
]
nvrtc = [
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" },
]
nvtx = [
{ name = "nvidia-nvtx", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
{ name = "nvidia-nvtx", marker = "sys_platform == 'linux'" },
]
[[package]]
@@ -486,6 +522,26 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" },
]
[[package]]
name = "einops"
version = "0.8.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" },
]
[[package]]
name = "entmax"
version = "1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "torch" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/a0/71747f0d98e441d0670b06205afd24d832e88c0ee62129ca47ce88505304/entmax-1.3-py3-none-any.whl", hash = "sha256:41bb1edbd497a1b53b1f0fc80befd543499dc704b4e5436ddf6c5728a2d00546", size = 13359, upload-time = "2024-02-07T10:09:38.865Z" },
]
[[package]]
name = "filelock"
version = "3.29.1"
@@ -1237,6 +1293,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" },
]
[[package]]
name = "munch"
version = "4.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/2b/45098135b5f9f13221820d90f9e0516e11a2a0f55012c13b081d202b782a/munch-4.0.0.tar.gz", hash = "sha256:542cb151461263216a4e37c3fd9afc425feeaf38aaa3025cd2a981fadb422235", size = 19089, upload-time = "2023-07-01T09:49:35.98Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/b3/7c69b37f03260a061883bec0e7b05be7117c1b1c85f5212c72c8c2bc3c8c/munch-4.0.0-py2.py3-none-any.whl", hash = "sha256:71033c45db9fb677a0b7eb517a4ce70ae09258490e419b0e7f00d1e386ecb1b4", size = 9950, upload-time = "2023-07-01T09:49:34.472Z" },
]
[[package]]
name = "mypy"
version = "2.1.0"
@@ -1374,7 +1439,7 @@ name = "nvidia-cublas"
version = "13.1.1.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cuda-nvrtc" },
{ name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" },
@@ -1413,7 +1478,7 @@ name = "nvidia-cudnn-cu13"
version = "9.20.0.48"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
{ name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/56/c5/83384d846b2fd17c44bd499b36c75a45ed4f095fbbb2252294e89cea5c5c/nvidia_cudnn_cu13-9.20.0.48-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:e31454ae00094b0c55319d9d15b6fa2fc50a9e1c0f5c8c80fb75258234e731e1", size = 444574296, upload-time = "2026-03-09T19:28:27.751Z" },
@@ -1425,7 +1490,7 @@ name = "nvidia-cufft"
version = "12.0.0.61"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" },
@@ -1455,9 +1520,9 @@ name = "nvidia-cusolver"
version = "12.0.4.66"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-cublas" },
{ name = "nvidia-cusparse" },
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" },
@@ -1469,7 +1534,7 @@ name = "nvidia-cusparse"
version = "12.6.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "nvidia-nvjitlink" },
{ name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" },
@@ -1521,6 +1586,24 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" },
]
[[package]]
name = "opencv-python-headless"
version = "4.13.0.92"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" },
{ url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" },
{ url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" },
{ url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" },
{ url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" },
{ url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" },
{ url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" },
{ url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" },
]
[[package]]
name = "packaging"
version = "26.2"
@@ -1624,6 +1707,101 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/26/6cee8a1ce8c43625ec561aff19df07f9776b7525d9002c86bceb3e0ac970/pgvector-0.4.2-py3-none-any.whl", hash = "sha256:549d45f7a18593783d5eec609ea1684a724ba8405c4cb182a0b2b08aeff04e08", size = 27441, upload-time = "2025-12-05T01:07:16.536Z" },
]
[[package]]
name = "pillow"
version = "12.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5", size = 5308279, upload-time = "2026-04-01T14:43:13.246Z" },
{ url = "https://files.pythonhosted.org/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421", size = 4695490, upload-time = "2026-04-01T14:43:15.584Z" },
{ url = "https://files.pythonhosted.org/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987", size = 6284462, upload-time = "2026-04-01T14:43:18.268Z" },
{ url = "https://files.pythonhosted.org/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76", size = 8094744, upload-time = "2026-04-01T14:43:20.716Z" },
{ url = "https://files.pythonhosted.org/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005", size = 6398371, upload-time = "2026-04-01T14:43:23.443Z" },
{ url = "https://files.pythonhosted.org/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780", size = 7087215, upload-time = "2026-04-01T14:43:26.758Z" },
{ url = "https://files.pythonhosted.org/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5", size = 6509783, upload-time = "2026-04-01T14:43:29.56Z" },
{ url = "https://files.pythonhosted.org/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5", size = 7212112, upload-time = "2026-04-01T14:43:32.091Z" },
{ url = "https://files.pythonhosted.org/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940", size = 6378489, upload-time = "2026-04-01T14:43:34.601Z" },
{ url = "https://files.pythonhosted.org/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5", size = 7084129, upload-time = "2026-04-01T14:43:37.213Z" },
{ url = "https://files.pythonhosted.org/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414", size = 2463612, upload-time = "2026-04-01T14:43:39.421Z" },
{ url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
{ url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
{ url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
{ url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
{ url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
{ url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
{ url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
{ url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
{ url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
{ url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
{ url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
{ url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
{ url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
{ url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
{ url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
{ url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
{ url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
{ url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
{ url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
{ url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
{ url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
{ url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
{ url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
{ url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
{ url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
{ url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
{ url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
{ url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
{ url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
{ url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
{ url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
{ url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
{ url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
{ url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
{ url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
{ url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
{ url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
{ url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
{ url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
{ url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
{ url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
{ url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
{ url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
{ url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
{ url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
{ url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
{ url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
{ url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
]
[[package]]
name = "pix2tex"
version = "0.1.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "albumentations" },
{ name = "einops" },
{ name = "munch" },
{ name = "numpy" },
{ name = "opencv-python-headless" },
{ name = "pandas" },
{ name = "pillow" },
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
{ name = "pyyaml" },
{ name = "requests" },
{ name = "timm" },
{ name = "tokenizers" },
{ name = "torch" },
{ name = "tqdm" },
{ name = "transformers" },
{ name = "x-transformers" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/b8/673667ace0a131169502420810aa9dfca69aad960d5af88da68ddd46c7f0/pix2tex-0.1.4-py3-none-any.whl", hash = "sha256:a024309508fc3e8a4ce5241e095276626d098df53e609410013f2ec35a4b7612", size = 427031, upload-time = "2025-01-18T15:23:27.552Z" },
]
[[package]]
name = "pluggy"
version = "1.6.0"
@@ -1984,6 +2162,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" },
]
[[package]]
name = "pymupdf"
version = "1.27.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/32/708bedc9dde7b328d45abbc076091769d44f2f24ad151ad92d56a6ec142b/pymupdf-1.27.2.3.tar.gz", hash = "sha256:7a92faa25129e8bbec5e50eeb9214f187665428c31b05c4ef6e36c58c0b1c6d2", size = 85759618, upload-time = "2026-04-24T14:13:14.42Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dc/09/ddbdfa7ee91fbabd6f63d7d744884cbdfe3e7ff9b8604749fb38bddf5c5d/pymupdf-1.27.2.3-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc1bc3cae6e9e150b0dbb0a9221bdfd411d65f0db2fe359eaa22467d7cc2a05f", size = 24002636, upload-time = "2026-04-24T14:09:17.459Z" },
{ url = "https://files.pythonhosted.org/packages/01/89/3f8edd6c4f50ca370e2a2f2a3011face36f3760728ffe76dffec91c0fca0/pymupdf-1.27.2.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:660d93cb6da5bbddf11d3982ae27745dd3a9902d9f24cdb69adab83962294b5a", size = 23278238, upload-time = "2026-04-24T14:09:32.882Z" },
{ url = "https://files.pythonhosted.org/packages/c3/26/b7e5a70eb83bd189f8b5df87ec442746b992f2f632662839b288170d357d/pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1dd460a3ae4597a755f00a3bd9771f5ebf1531dc111f6a36bf05dd00a6b84425", size = 24333923, upload-time = "2026-04-24T14:09:47.341Z" },
{ url = "https://files.pythonhosted.org/packages/e4/a0/aa1ee2240f29481a04a827c313333b4ecd8a14d6ac3e15d3f41a30574781/pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:857842b4888827bd6155a1131341b2822a7ebe9a8c15a975fd7d490d7a64a30c", size = 24963198, upload-time = "2026-04-24T14:10:07.408Z" },
{ url = "https://files.pythonhosted.org/packages/69/49/4f742451f980840829fc00ba158bebb25d389c846d8f4f8c65936ee55de8/pymupdf-1.27.2.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:580983849c64a08d08344ca3d1580e87c01f046a8392421797bc850efd72a5b6", size = 25184609, upload-time = "2026-04-24T14:10:22.911Z" },
{ url = "https://files.pythonhosted.org/packages/f6/3f/3853d6608f394faf6eec2bd4e8ea9f6a00beea329b071abdb29f4164cc3d/pymupdf-1.27.2.3-cp310-abi3-win32.whl", hash = "sha256:a5c1088a87189891a4946ab314a14b7934ac4c5b6077f7e74ebee956f8906d0e", size = 18019286, upload-time = "2026-04-24T14:10:34.239Z" },
{ url = "https://files.pythonhosted.org/packages/44/47/5fb10fe73f96b31253a41647c362ea9e0380920bddf16028414a051247fc/pymupdf-1.27.2.3-cp310-abi3-win_amd64.whl", hash = "sha256:d20f68ef15195e073071dbc4ae7455257c7889af7584e39df490c0a92728526e", size = 19249102, upload-time = "2026-04-24T14:10:46.72Z" },
{ url = "https://files.pythonhosted.org/packages/53/a4/b9e91aac82293f9c954654c85581ee8212b5b05efadc534b581141241e6f/pymupdf-1.27.2.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:77691604c5d1d0233827139bbcdea61fd57879c84712b8e49b1f45520f7ab9c2", size = 25000393, upload-time = "2026-04-24T14:11:01.669Z" },
]
[[package]]
name = "pyreadline3"
version = "3.5.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b6/6d/f94028646d7bbe6d9d873c47ee7c246f2d29129d253f0d96cb6fcab70733/pyreadline3-3.5.6.tar.gz", hash = "sha256:61e53218b99656091ddb077df9e71f25850e72e030b6183b39c9b7e6e4f4a9bf", size = 100368, upload-time = "2026-05-14T17:55:04.471Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/5e/35c856e186b74678c24927847ad9895a51f1bc02a0c6126477a6c6040064/pyreadline3-3.5.6-py3-none-any.whl", hash = "sha256:8449b734232e42a5dcd74048e39b60db2839a4c38cf3ae2bf7707d58b5389c0d", size = 85243, upload-time = "2026-05-14T17:55:03.262Z" },
]
[[package]]
name = "pytest"
version = "9.0.3"
@@ -2427,6 +2630,60 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
]
[[package]]
name = "simsimd"
version = "6.5.16"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/99/8c/070a179eb509b689509dacbd0bc81aa2e36614aff2c8aa6dc6c440886206/simsimd-6.5.16.tar.gz", hash = "sha256:0a005c6e2dacec83f235a747f7dbecca46b5d4d1e183ecc1929ca556ee7d7564", size = 187216, upload-time = "2026-03-07T14:36:23.191Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/52/b8/53f89ca12a3526b86c4221de68497d2b1f4c3f7f6b47d8c153ef14c67d15/simsimd-6.5.16-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f8a207a23bc9060a46b234ec304a712f1cbb0a240d18b484bad5cabf0d01746", size = 105152, upload-time = "2026-03-07T14:34:52.203Z" },
{ url = "https://files.pythonhosted.org/packages/56/4f/0fa014163c6b846182f6355ebfc24f79e86ced7a2cce0ca95ba711f19e04/simsimd-6.5.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:51c6b0ad0078f8c6b4d3ae4ec256bcf861c2bf5909d4567440b86f9ad7f94fd3", size = 94599, upload-time = "2026-03-07T14:34:53.564Z" },
{ url = "https://files.pythonhosted.org/packages/fb/3c/35266c8d128ea42706d9436b54994039e2659fb37ed28f1c62e123a86631/simsimd-6.5.16-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13b8af340ad5cc1311cae6f8d778aef80bff1922260dee1a17ca60878eaac466", size = 385042, upload-time = "2026-03-07T14:34:54.873Z" },
{ url = "https://files.pythonhosted.org/packages/3c/28/7ae846998728326759eab771afd83ad721b6c10e9cef7da2b5ca9bdd4a7b/simsimd-6.5.16-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:12ae4f5f2ade1152d2d3a0094f56fae636204d40595b385ea9b304410647a353", size = 583515, upload-time = "2026-03-07T14:34:56.578Z" },
{ url = "https://files.pythonhosted.org/packages/ce/b9/3a5717c988b6093a5fb15484754f7ffe5451a7559f3c1d5f2b3183199441/simsimd-6.5.16-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97bcda199d4be8f4372af6b781e96e7e8cd1838ce256a83deef75ac660dcd464", size = 421418, upload-time = "2026-03-07T14:34:58.316Z" },
{ url = "https://files.pythonhosted.org/packages/bb/65/e218050eb89390c64ddc327f36da8e3b471483f11c0f3683c2bf891d2dab/simsimd-6.5.16-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a59ef1ab3d0f6d4f1dcac43e1b2db9b8e73c00e72714716e061bfd27dde2d652", size = 619558, upload-time = "2026-03-07T14:34:59.847Z" },
{ url = "https://files.pythonhosted.org/packages/45/89/a45ef421b70d557eac7d196b03e45ff9ff8c7c786b4e54dfb505c1efc0f1/simsimd-6.5.16-cp312-cp312-win_amd64.whl", hash = "sha256:e0ae95b0fe17c62532ecc66f03f6e9354641448249efabe6332eed0f5819150d", size = 87454, upload-time = "2026-03-07T14:35:01.778Z" },
{ url = "https://files.pythonhosted.org/packages/1d/68/620c859f8737990371f79a45e3dc7135374635011c89b9e403cadd746639/simsimd-6.5.16-cp312-cp312-win_arm64.whl", hash = "sha256:fcfcc79473141f42b1db05037cb626e196ed20cffa7f768d4cad34b2a1239965", size = 62912, upload-time = "2026-03-07T14:35:03.133Z" },
{ url = "https://files.pythonhosted.org/packages/1b/f2/e1dedb4b3644c76467c84ffb57fc6e7784f46f312c34be9d6b52144e3d90/simsimd-6.5.16-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0af914ab13741744ea1bd3521e719226633f2ab082dc5b07790c61685d88558", size = 105157, upload-time = "2026-03-07T14:35:04.455Z" },
{ url = "https://files.pythonhosted.org/packages/46/21/a52af2040ad608cc236583ada58b0bfa5ffbfdc83b1d3565f4793f28cade/simsimd-6.5.16-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:683f758d0261b3d8790f8c9fc63fdc64b7af4db66b59ba7a31556a755cb38df7", size = 94604, upload-time = "2026-03-07T14:35:05.982Z" },
{ url = "https://files.pythonhosted.org/packages/e1/39/c6c7f66368204f0aa544aa074fa84b42a4146cf9e4bc79c3896c155d9abc/simsimd-6.5.16-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc1e29d8fed1c2b89338062fa17283b78181c84d2b024cc9bf7ed75402810bfc", size = 385102, upload-time = "2026-03-07T14:35:07.32Z" },
{ url = "https://files.pythonhosted.org/packages/2d/f2/6d84388c6e0f0637321149bc84bbbfa54a12f65f29bc6a007dd1403bf6f7/simsimd-6.5.16-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec7e92323c820935475bc9ec84938eecc9d9bc625055ff057a6d0dcfffb7eb2a", size = 583601, upload-time = "2026-03-07T14:35:08.799Z" },
{ url = "https://files.pythonhosted.org/packages/14/53/26bf42b6f8ec1f5680d91e95e276e49662bc1b8e0522c4861a0c3349b7ba/simsimd-6.5.16-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5a4be386421726204f70e9f8601dc8818fc2df0032ef6dcd218cdf224a9fce18", size = 421445, upload-time = "2026-03-07T14:35:10.298Z" },
{ url = "https://files.pythonhosted.org/packages/67/05/31b5247c0e17cd82482fd1724881d49ce442ad6affb2776efae8f9cc4835/simsimd-6.5.16-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe922886957645e041618fddf242a89f5f7ded0c4bee13dc6537f749ccf75ba2", size = 619612, upload-time = "2026-03-07T14:35:12.109Z" },
{ url = "https://files.pythonhosted.org/packages/5d/54/dbc23d585a57c9b0e71ab10705c4121ce91a807df374d433dd86fb438caa/simsimd-6.5.16-cp313-cp313-win_amd64.whl", hash = "sha256:fe7a0fa49b09651cc1721f5928fa68665f4957c492937241bbdd6ed040dc4a5d", size = 87460, upload-time = "2026-03-07T14:35:13.948Z" },
{ url = "https://files.pythonhosted.org/packages/a3/3c/62a41c182ab6f7abfbfe8941fa12d08b8235b4498e988e5d1f29ac21504f/simsimd-6.5.16-cp313-cp313-win_arm64.whl", hash = "sha256:3fc01992b9d3be84d4826c0d9f8a894668ad931285c09f74bdbe61a5400c9f4d", size = 62922, upload-time = "2026-03-07T14:35:15.252Z" },
{ url = "https://files.pythonhosted.org/packages/df/df/6a1b62074968bbd2976611ac9f89fa60bde2c0c3171f1eb303314bd2bb40/simsimd-6.5.16-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:22624893c86cb9f07968a7e471ed81b2e59f68ba4941cea69ee7418b5cc6fe8e", size = 105335, upload-time = "2026-03-07T14:35:16.839Z" },
{ url = "https://files.pythonhosted.org/packages/09/4f/43bf19becc155e5efdd31dc220c1bd34f866172739a7a081a8bfa2cae840/simsimd-6.5.16-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10d8b32ecee86a86fe30abb35a7c47c1d76756838355bc4377b73bdc69d16ed4", size = 94782, upload-time = "2026-03-07T14:35:18.233Z" },
{ url = "https://files.pythonhosted.org/packages/27/91/c31085edffdc81343f81b937fb2930cd0e105cfab1b9b97845c45b3621c3/simsimd-6.5.16-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b5a632299ee145fa2eab53906922d1596ee63f5a182e3741cde9b18745afe68", size = 387117, upload-time = "2026-03-07T14:35:19.573Z" },
{ url = "https://files.pythonhosted.org/packages/1a/a6/faaf1633cf9d3fc5ebe46d2f145f42257accc6bd25420d722702b6b5adfb/simsimd-6.5.16-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:40a7e14e02acebd0cdadc88c3eeb262c6cbff550a10d4bce2c7771756cf68658", size = 275340, upload-time = "2026-03-07T15:13:14.926Z" },
{ url = "https://files.pythonhosted.org/packages/c5/c8/3c3fa982272ab7a5943ceacc04fd64a38d408fce2cd45e7890eb932e92d1/simsimd-6.5.16-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4b878a28a338c30768cb401f4fbb79bd5b911d95ca024717077f1c57746ad78", size = 297257, upload-time = "2026-03-07T15:13:17.076Z" },
{ url = "https://files.pythonhosted.org/packages/46/8c/81e83b57992f1ae1bb3fa3d55cd1c4a5bd5dafcec6bd44273eb59c8f8f79/simsimd-6.5.16-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:639bb66dbb15da8727267dc7b7fbf7cc59c18ccef901dd83cdff4f12651f0244", size = 286880, upload-time = "2026-03-07T15:13:19.428Z" },
{ url = "https://files.pythonhosted.org/packages/5e/96/de52bf9ffff59c71b9bc672d7a539c431d81a17d909c4ee734f7731b51d2/simsimd-6.5.16-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:999acb24a43c619af6217b513536ae28bfe23c8fa170a4120a3cca7fdd22acff", size = 585133, upload-time = "2026-03-07T14:35:21.53Z" },
{ url = "https://files.pythonhosted.org/packages/84/e8/190aead5370bc3e0bd0f5fbd938a27cac4678dd903e81ff16acae7d7c6e4/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8524c7fd12f7ef9b97e824c65db4e89919b7cc8d530780119b3417ce8643a3c2", size = 422963, upload-time = "2026-03-07T14:35:23.137Z" },
{ url = "https://files.pythonhosted.org/packages/c0/26/d6ecb102a16f01ea22e98bbf8da37b9a8cb4fb38459b939367afb401f1c4/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:973460e647b3f769e714caa40b64f56dcf95a4afca98cdd19e2c3c1c9527e438", size = 320199, upload-time = "2026-03-07T15:13:21.823Z" },
{ url = "https://files.pythonhosted.org/packages/07/08/920d1619df54ed2c377dbfb10e0a561e27731091995ba1093b600ed3f00c/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:141437e4d727872ab50fe3b19098816aee23b8c3519ee04c9831ef0326e444e1", size = 340041, upload-time = "2026-03-07T15:13:23.91Z" },
{ url = "https://files.pythonhosted.org/packages/0d/3e/995e875eca129b1acb35e4824f1f4fab30b8393da80d51883552e2edd60f/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3daee137ffc2dd8bbe64b7f0f95ca2b2302b2985c35a6a7be61626052aa74e5d", size = 317465, upload-time = "2026-03-07T15:13:25.697Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ce/892865784240c167624bf55f835ff74d52e24c7d7f1b9aa79f77358397ac/simsimd-6.5.16-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:03f4d0a8aff48160e3b0acb44ac5525a39d26348db907d6d5ef516369b309973", size = 620749, upload-time = "2026-03-07T14:35:25.077Z" },
{ url = "https://files.pythonhosted.org/packages/54/0d/b74a391fefe7d349230b58b1d0fe6d401d1625553b5375a99608f9d228a9/simsimd-6.5.16-cp313-cp313t-win_amd64.whl", hash = "sha256:01ef2ff8cf99fc3a8e23fb2cadc06b6aa4df9b5e6d001b184d42cf403b1cdc16", size = 87630, upload-time = "2026-03-07T14:35:26.598Z" },
{ url = "https://files.pythonhosted.org/packages/eb/ac/004dc381de9ac6634c785d0284dba8d1f12018584ddd992c09d9f85454b9/simsimd-6.5.16-cp313-cp313t-win_arm64.whl", hash = "sha256:a152c559298bae402ed8205b604e5b0418a2ce8a61a6a87f14973e53b68d5f6a", size = 63126, upload-time = "2026-03-07T14:35:28.295Z" },
{ url = "https://files.pythonhosted.org/packages/6b/5a/b70d670c67ca3d0284b4a52e32d65eb9767df51c0ff5b968db6a2bdc406c/simsimd-6.5.16-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c70924ce14c7ed1663ff131f34bdf3987042f569b41a4ed756a1ad65109de760", size = 105215, upload-time = "2026-03-07T14:35:29.677Z" },
{ url = "https://files.pythonhosted.org/packages/cd/16/59a7d17719a49d453d35a21d2fc40bd7915f78046f82b3325f1f5629505a/simsimd-6.5.16-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cfa1237885074a8e8aba7c203d82e189b84760ffa946fb53e82ece762f40f36c", size = 94618, upload-time = "2026-03-07T14:35:31.395Z" },
{ url = "https://files.pythonhosted.org/packages/02/75/8cb99c018b1c68b5048e19df9d4552d5f41f0512f2e32fdd6a5e58a5b2d1/simsimd-6.5.16-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7ecf8eb87e39a72e23126bf7ffa1a454830ec2daddd00ac89cef96aefce788a7", size = 385337, upload-time = "2026-03-07T14:35:32.863Z" },
{ url = "https://files.pythonhosted.org/packages/79/48/0fd0017b306422d950758e8077e00295d5d9dc2add4680c0aad437774128/simsimd-6.5.16-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0029256c39bafc3930884b47280628ff84a8eda3b7b55e64465f0e051df93cb8", size = 583769, upload-time = "2026-03-07T14:35:35.191Z" },
{ url = "https://files.pythonhosted.org/packages/03/4e/803bffa17b5d52bd545b906f28d947630f271d6a4dc53324d5177464babe/simsimd-6.5.16-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9afa80898b89cdb65317ca6f36efedb3320a000205a82b70dd2ea82872482d08", size = 421581, upload-time = "2026-03-07T14:35:36.719Z" },
{ url = "https://files.pythonhosted.org/packages/59/59/93bbf9c1a6b554b4cf21b32f436fc0de082fe929c4c459d295292ee8bcce/simsimd-6.5.16-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:fc6b72bf5a62afa66a9b51f6a01d751d8f217c9f7d4b1ea094e495c3dce87c33", size = 619710, upload-time = "2026-03-07T14:35:38.338Z" },
{ url = "https://files.pythonhosted.org/packages/04/4c/207158749eb6ad8576a1b3cd4e80b7f1e2a0fc59fb2b0730f8df43b3d4a9/simsimd-6.5.16-cp314-cp314-win_amd64.whl", hash = "sha256:96fdb750432ad6478177fb80612b3aea2da002dff613f1fddd19334da9b7f25e", size = 90117, upload-time = "2026-03-07T14:35:40.495Z" },
{ url = "https://files.pythonhosted.org/packages/66/67/38ab856761cc62fbb92b328350a6652f87b27ab2ca1d49fa934aaeca0d3c/simsimd-6.5.16-cp314-cp314-win_arm64.whl", hash = "sha256:2e3981bfa3f09fa9fac845037df7c3a684e0538ff297d3b2ccd26a2eed243f80", size = 64908, upload-time = "2026-03-07T14:35:42.169Z" },
{ url = "https://files.pythonhosted.org/packages/62/49/df617f9e5605b48b75d921b5361c88475879b95a43dd3f2b77fb4659382a/simsimd-6.5.16-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:864a0497c8d4bdc6948bedb016836ba777d14a93300c3735c6e84444241cd66e", size = 105371, upload-time = "2026-03-07T14:35:43.552Z" },
{ url = "https://files.pythonhosted.org/packages/45/3f/e0b8064146919d40436503032f331fc92fbd3d8e5b29ca01c40a675432cf/simsimd-6.5.16-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:492b86704d942fa3ec627523ba7f40e87203e4222d498aa6fc880a865e13fa76", size = 94790, upload-time = "2026-03-07T14:35:44.914Z" },
{ url = "https://files.pythonhosted.org/packages/74/6f/b3811e96e6582e4f04793b688eb1f85e2a74722f00089dc5c7932023d523/simsimd-6.5.16-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c4e0e257e191c2e1ac94737901ec3771b076f7b9c032b620c0bfb747ecefcd9", size = 387243, upload-time = "2026-03-07T14:35:46.408Z" },
{ url = "https://files.pythonhosted.org/packages/47/5b/46b52cd8df732e73799adb91af16e2bc872e597349b01f456df3008d4dd7/simsimd-6.5.16-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03ed0eec1d7d5124bc86256a8d7ac81b1c6363149e1f1cc957007418da04e8ed", size = 585270, upload-time = "2026-03-07T14:35:48.885Z" },
{ url = "https://files.pythonhosted.org/packages/9d/0b/92c7dc6b6478032cde9d65f997e8135f5e178c455b8585877b3a9f996bf7/simsimd-6.5.16-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b331c7c2222bc03139e0821c076103ea50f9fab5750571b4cd1e53c2ba3cb0d6", size = 423066, upload-time = "2026-03-07T14:35:50.63Z" },
{ url = "https://files.pythonhosted.org/packages/4d/03/ad761cc350e0f30cd52f798e39434ce68bd09a741e931f4458ddafd0d099/simsimd-6.5.16-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c51b74b8f9b096ddd98beea66e18751ad079c398600d8c877a5d228a1f23d20", size = 620824, upload-time = "2026-03-07T14:35:52.638Z" },
{ url = "https://files.pythonhosted.org/packages/bf/aa/b059b409ae311d4d5e936c07c506c62d5f547597933822fe8c54d32e276b/simsimd-6.5.16-cp314-cp314t-win_amd64.whl", hash = "sha256:4aedebecab2c776177c2db2cdd2f311892d9b1b71bcf66d889539ab1e22ad9a6", size = 90323, upload-time = "2026-03-07T14:35:54.438Z" },
{ url = "https://files.pythonhosted.org/packages/07/3a/2d0a48ef00dd495b5ded82a476ec4300ae3f67496cbd7c7fe2777de89a3c/simsimd-6.5.16-cp314-cp314t-win_arm64.whl", hash = "sha256:d63af5fbd32b0346ef949794451b6c1ec58a66139d3ca22177f93cf7c4be7877", size = 65109, upload-time = "2026-03-07T14:35:55.863Z" },
]
[[package]]
name = "six"
version = "1.17.0"
@@ -2445,6 +2702,71 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/f5/0c41cb68dcae6b7de4fac4188a3a9589e21fb31df21ea3a2e888db95e6c9/soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65", size = 37304, upload-time = "2026-05-24T13:55:55.406Z" },
]
[[package]]
name = "stringzilla"
version = "4.6.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/b5/40951e5eaef033aa377c111f4a18a62dbce6d6211eff754f88c73aeebe49/stringzilla-4.6.2.tar.gz", hash = "sha256:f30fb35d84578236723aaf9442e9281ae29c8ad5255eb118173c9370af4c0e71", size = 646294, upload-time = "2026-06-05T17:07:09.163Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a4/2c/71021ff18092efeef3bd9e9f75c04b2f5b648c66aa88f80981f12f1777f0/stringzilla-4.6.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4fd0728a45baa861927abf37d365c656695c89b13a671108f34b7bdc88653c20", size = 212166, upload-time = "2026-06-05T17:05:41.951Z" },
{ url = "https://files.pythonhosted.org/packages/88/2b/62dd1a69ee3bb55ac507da01f4312ecba338e77e158e8b03c01eb79884fe/stringzilla-4.6.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dae0ce637947ab3bfbca8e26236de1149e2a181a7b7286a174936a986b370689", size = 199218, upload-time = "2026-06-05T17:05:43.182Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c2/b7764bb3dfb08f221f0b96267b2b72715489a165c89973bbce2edc2d8f97/stringzilla-4.6.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b54e6be715a19faf407837bccf302082866134b09673d470c162cbbffb7b1570", size = 582298, upload-time = "2026-06-05T17:05:44.883Z" },
{ url = "https://files.pythonhosted.org/packages/25/4f/80eda1295021033ad67b775ef7e4933b3b52f6f715fb464676cd0330bbdf/stringzilla-4.6.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5737b993627d8994654dc8780b858ed610bd4ed6a9c1ed6bd9972c33b258b06", size = 647665, upload-time = "2026-06-05T17:05:46.529Z" },
{ url = "https://files.pythonhosted.org/packages/73/83/946c776fd76af08a20f21c4671bbc4cc5a8a0e9b3ba578b67e724a5639b1/stringzilla-4.6.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3c60de67871c87a4aebcdca3f120d87252446cc4cd344156ae5391d17147bed2", size = 651211, upload-time = "2026-06-05T17:05:48.034Z" },
{ url = "https://files.pythonhosted.org/packages/5b/f8/869e728e232645c93817c4bdd794bcd75bc895334b110011e46c5cbd90e9/stringzilla-4.6.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02a9979e6992ed6975ab6bc24d19148839a8cfad88ee828608296cef9002a141", size = 595069, upload-time = "2026-06-05T17:05:49.586Z" },
{ url = "https://files.pythonhosted.org/packages/44/8d/08a810a6b9a13f15a5885f290fb14ea6d75e783b95852c95c5a938016f25/stringzilla-4.6.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b307cd338cf719137db7efaa1cb3da3421d50ea26c61059424bac97ddd5138d5", size = 600468, upload-time = "2026-06-05T17:05:51.02Z" },
{ url = "https://files.pythonhosted.org/packages/0f/54/65df9199a334066cf9d8ead48573f80b6c3bc2cb7d2a0a0fce09f31b2860/stringzilla-4.6.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83bc6f26f02628c831ddfc547f1077c69a6fd7902231fea2aee8f93e9da8bd46", size = 1857818, upload-time = "2026-06-05T17:05:52.762Z" },
{ url = "https://files.pythonhosted.org/packages/48/b3/8a7ca75ee56f82e0d120932e6d4ed96c304b82f28d8e4c63f040f279b04e/stringzilla-4.6.2-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:da84c50cb0eb3d5b6f03de83b791bf891b1f49a296e3dac0301a27cf2a827d0c", size = 611350, upload-time = "2026-06-05T17:05:54.549Z" },
{ url = "https://files.pythonhosted.org/packages/7f/9c/8b1e21c13c94a7b405c36a84fe12aa746a19215a1eaf4610233356188c78/stringzilla-4.6.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbbeea11ddd1553a63650c1abb720710989879d94d151a43600f10af1d8a39a5", size = 661628, upload-time = "2026-06-05T17:05:55.98Z" },
{ url = "https://files.pythonhosted.org/packages/78/52/0f784920c1a4e5f2241772f961c4b72d2444d066ef588c34edb5490c7995/stringzilla-4.6.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:cadbc0ac8474cdfa85084f797f080b797c1eb951cd7aa0c73881616d1ddf0639", size = 597510, upload-time = "2026-06-05T17:05:57.21Z" },
{ url = "https://files.pythonhosted.org/packages/8e/47/0ca2d6131b65e925e8d5498d32f72b0353de58bd49346ccc0d193d79fc5d/stringzilla-4.6.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4ca9adcadc1fe1564e31d4bda14d76f5d7614c616c10c16d7bccc48ee486b0eb", size = 616539, upload-time = "2026-06-05T17:05:58.704Z" },
{ url = "https://files.pythonhosted.org/packages/59/05/48bbe7225e55f0ff5c7c4bc00296733ab88e09135dcb6a39f614fcaf0c4b/stringzilla-4.6.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a321e1b909378769f7c05697468fe88ea65089148785be50b65f26f591350204", size = 613551, upload-time = "2026-06-05T17:06:00.247Z" },
{ url = "https://files.pythonhosted.org/packages/f5/5e/84af0d65bc2f661b5df0a5e9202f722a746a67a77649fc5d1634d5b22455/stringzilla-4.6.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9248aa322f24f28a888991f38d23a75d4dd27587becd029b00e94a1f155f9276", size = 602450, upload-time = "2026-06-05T17:06:01.853Z" },
{ url = "https://files.pythonhosted.org/packages/85/62/03a153b149edcf433d87672e3b62917423c53ca2e30df8ac1cbb08fbaf72/stringzilla-4.6.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:50d61f06b66a9b4d737f9ed3611da41a53e9cb8aa3c25d9c3d34dbb67f272e6c", size = 632715, upload-time = "2026-06-05T17:06:03.209Z" },
{ url = "https://files.pythonhosted.org/packages/6f/6f/52211a7b7871497547ea71c93ac4a9e6911f99e6c412102792c29889dcb6/stringzilla-4.6.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:215d9e5ea1f3caeeebaac1b6cf18579a8f7e519a16afba036954230bae17d12c", size = 1976332, upload-time = "2026-06-05T17:06:04.689Z" },
{ url = "https://files.pythonhosted.org/packages/ea/3a/40848f18b3801019af4f0efd8a317f7953f8287560f32ff3c4cb4c38cea2/stringzilla-4.6.2-cp312-cp312-win32.whl", hash = "sha256:0638c6bdbcc45de73ab9c85eab622712174993ef91b5b0463f333a11e9a2bbfb", size = 114688, upload-time = "2026-06-05T17:06:06.43Z" },
{ url = "https://files.pythonhosted.org/packages/20/0f/14df6be0e23b3cdd7e28732eb7f93437ce57d20510ba43875e3d98be02d8/stringzilla-4.6.2-cp312-cp312-win_amd64.whl", hash = "sha256:42dc9bf6379edb174607b7061cb2c234d3f8a57f701fee31755cbcb5fd55e93a", size = 162453, upload-time = "2026-06-05T17:06:07.838Z" },
{ url = "https://files.pythonhosted.org/packages/e5/01/cb7608daeb23f427e0da01ecb4cb211b7f860055b2d94578845f5ac4f1a4/stringzilla-4.6.2-cp312-cp312-win_arm64.whl", hash = "sha256:ec028b7db898a2131a10fcebced14cc0dad5af0d45ad05f27e20ca0feb226446", size = 123179, upload-time = "2026-06-05T17:06:09.249Z" },
{ url = "https://files.pythonhosted.org/packages/68/d8/d92cc16d4e7b7e8b05f4f78d5546bfdd635d42ae5e39bcf80198d50f7d0c/stringzilla-4.6.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00a791addb965cc3268e1e983d933f7aa81f19ea1a5bfed78ed3c319623597f9", size = 212166, upload-time = "2026-06-05T17:06:10.556Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/9f626134a7e7a40e9cfc875aed379b123df2a5fd8484234914431840712e/stringzilla-4.6.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b2bbea488adb0320fd317cf07f35486fcb7d8e487aa5b23e60b55b36a6e9faa9", size = 199217, upload-time = "2026-06-05T17:06:11.864Z" },
{ url = "https://files.pythonhosted.org/packages/5d/24/47ac8d851ab53a8ee3178755a74eaf5987bb4b01b5a9b748bf2120d26022/stringzilla-4.6.2-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:23a2ebe9e8cceff2d0c2c0a664a72417ae7d6a9b10a5d9049c95bd152ec70b3e", size = 582265, upload-time = "2026-06-05T17:06:13.236Z" },
{ url = "https://files.pythonhosted.org/packages/1d/fb/b7f9f5f5068c47378017eb2747df52d3fc77f43bd4764b5719a30ed1cc9a/stringzilla-4.6.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0d7149226d0455caf1aa2eec2b425fa276f1e2a61ba3760ff492f6b9f25cc23", size = 647712, upload-time = "2026-06-05T17:06:14.643Z" },
{ url = "https://files.pythonhosted.org/packages/7c/00/ed9bad99ef5e9c002634c61822585b792b22c19d66f40954e4efad694a72/stringzilla-4.6.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6a01e629f2677522abae4e9e65ada2f983ead838266d3a9b99bb512c731f325c", size = 651073, upload-time = "2026-06-05T17:06:16.045Z" },
{ url = "https://files.pythonhosted.org/packages/14/d3/72119c2e900be6451114d1d1c5947e00c79dc725c0e15a7cb34c185f55bd/stringzilla-4.6.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53b7dbc30c3c22fb5761e90c4854213ab62bf153fe3b25011348b312bb49df03", size = 595114, upload-time = "2026-06-05T17:06:17.534Z" },
{ url = "https://files.pythonhosted.org/packages/aa/14/ff3e14615132504806a467819999ba5840a181cc4e907044987fece7eabb/stringzilla-4.6.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c2e30dfd179206e9bfced4c4e91d3f8af9eae7d6fce5ba5e5f4e66aa9215be7", size = 600463, upload-time = "2026-06-05T17:06:18.915Z" },
{ url = "https://files.pythonhosted.org/packages/c7/ae/c97a1689dc0cc466f71c2f958adca6ae1a8e6bf5bdbe33164c213a77525c/stringzilla-4.6.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a67387037c66d167819cfa8d4234c713fbc6df90131a6f61dc3cd84ab19fd4", size = 1857863, upload-time = "2026-06-05T17:06:20.373Z" },
{ url = "https://files.pythonhosted.org/packages/55/c2/33ace5325d88229b565ee144a350d77bdded7a3103e2e326de8acc961d3c/stringzilla-4.6.2-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcb95e147f7709527fa4e3eddc16b9a4e033ca48cba85cc20deda62270c85e0c", size = 611309, upload-time = "2026-06-05T17:06:22.074Z" },
{ url = "https://files.pythonhosted.org/packages/d2/9a/98d567f8633449e1cf659cd6ce7682688168403ca8195417fc41bb15e217/stringzilla-4.6.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:716c4120bfed0d0b75a16ff2c99b644de1c5c41d68c57543439ffdc0caf8bd05", size = 661656, upload-time = "2026-06-05T17:06:23.557Z" },
{ url = "https://files.pythonhosted.org/packages/0b/94/b7c71735aae038a5a74323f41691d581855fe906ba4090c63de733e2316e/stringzilla-4.6.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:8e8e831ce10fd629d4573a11d86bf6bf851ef456e4b757a3bb239b3ea30e5013", size = 597568, upload-time = "2026-06-05T17:06:25.038Z" },
{ url = "https://files.pythonhosted.org/packages/37/8b/9822536bd67d4c86e4919010e0b0cf397af9a9786f2bb04363df873ec718/stringzilla-4.6.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:24dbf7a571da484d1efb4f1674cc2f9fab8ebed4c1eb3c647833098b7cbc4b46", size = 616597, upload-time = "2026-06-05T17:06:26.599Z" },
{ url = "https://files.pythonhosted.org/packages/3f/b1/5a9ee7e66e320d13cc2a451c8fdc0ff186973754a12b227b0431b74bf9ca/stringzilla-4.6.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:2016e170f08bf956ba6f19cdf2eed6850e93a60cef7c4d481c32468ae877f340", size = 613611, upload-time = "2026-06-05T17:06:28.111Z" },
{ url = "https://files.pythonhosted.org/packages/e3/e6/cfe4bb910a4b806089bf920dc3a875c98318a5acde034f63087f728f52d9/stringzilla-4.6.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f0436f1f2f2f659335bcd132ce79b970703f63b2ae478547e4f3fe83032e04d2", size = 602475, upload-time = "2026-06-05T17:06:29.68Z" },
{ url = "https://files.pythonhosted.org/packages/b6/65/9dad1d7a415e8fd893f3106f787d499f2bd00e12ed0023a85960ecd68b22/stringzilla-4.6.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:9d7d3d89c903ff519111eb5f9614324e7640c912ce55975eb240de662d7196c9", size = 632736, upload-time = "2026-06-05T17:06:31.056Z" },
{ url = "https://files.pythonhosted.org/packages/03/c3/f4299a496892111aa200b92ac3e3a4295ab44f21238afeb23f0bff6d70a6/stringzilla-4.6.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c8c09a22da4bf6dd054a36b8eeed40760f4b5318b9f001145fa1159844475ab", size = 1976337, upload-time = "2026-06-05T17:06:32.538Z" },
{ url = "https://files.pythonhosted.org/packages/83/1f/e36f84703db11b568489cf950634ff55ba20b780476989992a4fb35497b3/stringzilla-4.6.2-cp313-cp313-win32.whl", hash = "sha256:66dace3fa65ebdbdac3644e513348678f75e94e59b7234f614eae33535be1bdc", size = 114686, upload-time = "2026-06-05T17:06:34.069Z" },
{ url = "https://files.pythonhosted.org/packages/d8/21/41039cf8b99f21a17709beeea7b63aef9fe8b627a1a843990e2af075af3c/stringzilla-4.6.2-cp313-cp313-win_amd64.whl", hash = "sha256:1bdd87d0a92a83bc013a817afe69bc906b55476370c210791f3e132457dd461a", size = 162456, upload-time = "2026-06-05T17:06:35.393Z" },
{ url = "https://files.pythonhosted.org/packages/0c/73/a052bc96a5166181c413c49ade7c8bd4bd6ea7bbb92f0791066c5b899053/stringzilla-4.6.2-cp313-cp313-win_arm64.whl", hash = "sha256:9c182254bd7943e4c61ce83c58e6096a180905fadbd2d8b4de8b57fc5cb7c9cc", size = 123186, upload-time = "2026-06-05T17:06:36.934Z" },
{ url = "https://files.pythonhosted.org/packages/04/43/73302749e84315461722068a89fcbc892ce35ee5204d4fb112d0c1aa00d4/stringzilla-4.6.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbf773dd128460ae44b3839c3a85f2587e42869f1004ea327b11950d1d4a1c11", size = 212195, upload-time = "2026-06-05T17:06:38.379Z" },
{ url = "https://files.pythonhosted.org/packages/9b/c1/b248df98ce0f25e795a1f5bee5e0cff9b337af3fc0c12f7e7040695e27e6/stringzilla-4.6.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f4e50aaddb4aae588b988925c6e5664db08fe31555a57c649fdfa217b0b88113", size = 199248, upload-time = "2026-06-05T17:06:39.986Z" },
{ url = "https://files.pythonhosted.org/packages/f0/61/afeb2ac52b6541e91562f89fa126d9b0380e02b0a4ca169cce4e6910a614/stringzilla-4.6.2-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1a612afc970c67bc9c5de0b1ae11ac2ad61bd80cb1c7f80fa68c16402ab4f656", size = 582391, upload-time = "2026-06-05T17:06:41.379Z" },
{ url = "https://files.pythonhosted.org/packages/f0/90/d09b435ef6dc56090f1b03a99d49cb16c0d4e7dd315320291f82137d7f6a/stringzilla-4.6.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3afa1595ddede82fff17df64d162ae8498ee61b35aa6c730a55419558cdc7663", size = 647818, upload-time = "2026-06-05T17:06:42.925Z" },
{ url = "https://files.pythonhosted.org/packages/9c/94/f67c9b02a495e45ecc7eab586b0dece8b7fdaded82a23fb132187192b488/stringzilla-4.6.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ca51a91d4606649789aef673e30f78b0381b915ca4963fffea18c0ca6e5bb9d", size = 650412, upload-time = "2026-06-05T17:06:44.618Z" },
{ url = "https://files.pythonhosted.org/packages/30/11/278fa91c272368e816929ac927172371d4629299b9afbf8be84c6b159ac8/stringzilla-4.6.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1ff91faf487f5cef54ce23375691d70562f7134dee485b0cb7bee215a0cd82c9", size = 595271, upload-time = "2026-06-05T17:06:46.406Z" },
{ url = "https://files.pythonhosted.org/packages/0a/09/4d8c852ab4d55ad7321672cddfc9ad5629293964b92b978afe8b4cfd363a/stringzilla-4.6.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9862fab01ccd8007b676c30e92c9c4978201954260340f962eb4a89d5af482cb", size = 600405, upload-time = "2026-06-05T17:06:48.222Z" },
{ url = "https://files.pythonhosted.org/packages/6f/d4/56be7c6ed92a96eb261a6f90cefbbea6f4f2e390ac587aa821c65c9b9a9e/stringzilla-4.6.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbd0df9824f32f3fc113d9348dfdcc976d4df2ac2118c53074126abb56351bd0", size = 1857367, upload-time = "2026-06-05T17:06:49.772Z" },
{ url = "https://files.pythonhosted.org/packages/0d/28/11e549e5fada3a92b62ca3d750e88043a47f75c835ba08bc792409050024/stringzilla-4.6.2-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f8475647325b1cb923aebdc3b22dad2a8805eb77c3cc300d27960a4a12293f9c", size = 611547, upload-time = "2026-06-05T17:06:51.535Z" },
{ url = "https://files.pythonhosted.org/packages/ae/7a/eece4bb72d24cdb08ac4acf5d73aec0ccf8b970508b6f4d1633aef2513e7/stringzilla-4.6.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:aba0640812f13520380318930ff716fa5a434393a70d8e7db57ced637ecd0705", size = 661977, upload-time = "2026-06-05T17:06:53.146Z" },
{ url = "https://files.pythonhosted.org/packages/a2/1f/3ed79edbbf6212e0f813f85aafb15f57b60508c862c73b470002a0bf24d1/stringzilla-4.6.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0a57f406cad5225ae266f1dbfa9f07a8468a33921bd18cd03797871aa334db5e", size = 595870, upload-time = "2026-06-05T17:06:54.69Z" },
{ url = "https://files.pythonhosted.org/packages/b1/ad/1890f124a9ff99c620d032ad637191ab7bcdd06c0881e90133d0bfe81298/stringzilla-4.6.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:573abfc3eca6d903e0aab4ab6a8de841a0e437a93536fd2f75a6c9e70db29729", size = 616634, upload-time = "2026-06-05T17:06:56.308Z" },
{ url = "https://files.pythonhosted.org/packages/88/1f/eeaa1a37a7feb7369642d17d001a8bb1c5f28ecfbd26088ed06af63b00f2/stringzilla-4.6.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4666e0b76da6e54a5a4d81561905ce09743adc8ea03ca59cbd306305095607cd", size = 613845, upload-time = "2026-06-05T17:06:57.846Z" },
{ url = "https://files.pythonhosted.org/packages/93/d8/64879da8fba98bed4c4645524c57579bbc13b4c4687152d22458d0ad71f4/stringzilla-4.6.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e6f4d02db0efb228b724d69cb7dbf0126b534ebf87dc47469057aabe591a91f6", size = 602664, upload-time = "2026-06-05T17:06:59.34Z" },
{ url = "https://files.pythonhosted.org/packages/29/17/5e2377010f92d34f3bfe4f811be6710fc495705e14901a24829a78847b51/stringzilla-4.6.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:64c10e42b88e84fa1cf36f42dc8ba97421f8700d1c11873bab8de13cb5723dc1", size = 632179, upload-time = "2026-06-05T17:07:01.024Z" },
{ url = "https://files.pythonhosted.org/packages/4d/64/65ea631fc3c6b1c836156041c2895aae4874efa03ac829355224bd60c31d/stringzilla-4.6.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1a4895a105fd3de40fc82ef96f0e8a1030645e10fa13b9b6d15efeaf02e52efc", size = 1976008, upload-time = "2026-06-05T17:07:02.653Z" },
{ url = "https://files.pythonhosted.org/packages/1f/ce/a3bfb84c0da5ca9347ec7f1774a821664763827bb8e854f38fc814ac24e2/stringzilla-4.6.2-cp314-cp314-win32.whl", hash = "sha256:8cad620429de33881cca0e78032bba301176342e987705ddefb827403bc8caf3", size = 117985, upload-time = "2026-06-05T17:07:04.44Z" },
{ url = "https://files.pythonhosted.org/packages/62/34/e13e520393cd35e9e2cd0c4b9392781a2e30c76888d997523c549829faaa/stringzilla-4.6.2-cp314-cp314-win_amd64.whl", hash = "sha256:34e3c30bd65bba91ccb6b73258e4899fa025e9e21fbb1c028ab32974fd51e377", size = 167485, upload-time = "2026-06-05T17:07:05.873Z" },
{ url = "https://files.pythonhosted.org/packages/66/12/37808d822c5c2ff188eff46f0ee2637530e0ce781b9aac0b147bb9713c67/stringzilla-4.6.2-cp314-cp314-win_arm64.whl", hash = "sha256:005b4450a5d1f5e199cd4fc28c60a67695555a034981dfc4a8b0fa0b0989f0f1", size = 127917, upload-time = "2026-06-05T17:07:07.544Z" },
]
[[package]]
name = "sympy"
version = "1.14.0"
@@ -2475,6 +2797,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" },
]
[[package]]
name = "timm"
version = "0.5.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "torch" },
{ name = "torchvision" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b7/fe/aecb1926f5237d889693b8b583330d507ef416d4fa1912aaea69131ff335/timm-0.5.4.tar.gz", hash = "sha256:5d7b92e66a76c432009aba90d515ea7a882aae573415a7c5269e3617df901c1f", size = 359836, upload-time = "2022-01-17T04:57:53.212Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/65/a83208746dc9c0d70feff7874b49780ff110810feb528df4b0ecadcbee60/timm-0.5.4-py3-none-any.whl", hash = "sha256:0592c8fd2d46d0769c0b7e954b3dacea93769eee40dabb4bd7f2acb85243b588", size = 431537, upload-time = "2022-01-17T04:57:50.572Z" },
]
[[package]]
name = "tokenizers"
version = "0.22.2"
@@ -2545,6 +2880,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/88/21/afadd25ecd81b3cea1e11c73cf1ab41a983a50271548c3ec7ec3b9efc3e9/torch-2.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f96b63f8287f66a005dd1b5a6abba2920f11156c5e5c4d815f3e2050fd1aa16", size = 123231092, upload-time = "2026-05-13T14:51:18.854Z" },
]
[[package]]
name = "torchvision"
version = "0.27.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "numpy" },
{ name = "pillow" },
{ name = "torch" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/c8/5cd91932f7f3671b0743dc4ae1a4c16b1d0b45bf4087976277d325bda718/torchvision-0.27.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1a6dd742a150645126df9e0b2e449874c1d635897c773b322c2e067e98382dfe", size = 1758824, upload-time = "2026-05-13T14:57:15.227Z" },
{ url = "https://files.pythonhosted.org/packages/d9/36/7fb7d19477b3d93283b52fea11fa8ee30ab9064a08c97b4a6b91445e26cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65772ff3ec4f4f5d680e30019835555dd239e7fefee4b0a846375fe1cb1592ef", size = 7831034, upload-time = "2026-05-13T14:57:06.483Z" },
{ url = "https://files.pythonhosted.org/packages/62/43/dfd894c3f8b01b5b33fde990f0159c1926ebc7b6e2c4193e2efb7da3c4cb/torchvision-0.27.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7a9966a088d06b4cf6c610e03be62de469efa6f2cd2e7c7eed8e925ed6af59ac", size = 7579774, upload-time = "2026-05-13T14:56:59.337Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0c/722e989f9cf026e97ef7cb24a9bb1859e099f72d247ae35388fb89729f73/torchvision-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c037709072ca9b19750c0cbe9e8bb6f91c9a1be1befa26df33e281deccbd8c7", size = 4021073, upload-time = "2026-05-13T14:57:00.848Z" },
{ url = "https://files.pythonhosted.org/packages/d8/ae/36547812e6e047c1d80bcacd1b17a340612b08a6e876e0aabf3d0b9228b0/torchvision-0.27.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:41d6dae73e1af09fa82ded597ae57f2a2314285acde54b25890a8f8e51b999d7", size = 1758826, upload-time = "2026-05-13T14:57:05.262Z" },
{ url = "https://files.pythonhosted.org/packages/ae/30/32c4ea842738728a14e3df8c576c62dedcf5ae5cb6a5c984c6429ebe7524/torchvision-0.27.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:70f071c6f74b60d5fe8851636d8d4cd5f4fa29d57fd9348a87a6f17b990b95ba", size = 7789501, upload-time = "2026-05-13T14:56:57.786Z" },
{ url = "https://files.pythonhosted.org/packages/f6/24/4d0d48684251bd0673f87d633d5d88ab00227983b00591156eed2f86c8d5/torchvision-0.27.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:aaafa6962c9d91f42503de1957d6fa349907d028c06f335bd95da7a5bc57147d", size = 7579868, upload-time = "2026-05-13T14:56:41.618Z" },
{ url = "https://files.pythonhosted.org/packages/ba/da/e6edd051d2ba25adf23b120fa97f458dff888d098c51e84724f17d2d1470/torchvision-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:aee384a2782c89517c4ab9061d2720ba59fd2ffe5ef89d0a149cc2d43abdf521", size = 4092700, upload-time = "2026-05-13T14:57:09.729Z" },
{ url = "https://files.pythonhosted.org/packages/fa/23/95dfa40431360f42ca949bf861434bed51164adfa8fb9801e05bf3194f50/torchvision-0.27.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:c5121f1b9ab09a7f73e837871deb8321551f7eaeb19d87aa00de9191968eae44", size = 1845008, upload-time = "2026-05-13T14:57:03.768Z" },
{ url = "https://files.pythonhosted.org/packages/23/b9/9dbdf76b2b49a75ba8088df6f7c755bdb520afb6c6dbac0102b46cde5e99/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1c01f0d1091ae22b9dfc082b0a0fe5faaf053686a29b4fb082ba7691375c73cf", size = 7791430, upload-time = "2026-05-13T14:56:56.206Z" },
{ url = "https://files.pythonhosted.org/packages/5c/6a/e4a16cf2f3310c2ea7760dc5d9054496844391e0f4c1fae87fefac2f3d9e/torchvision-0.27.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:dadea3c5ecfd05bbb2a3312ab0374f213c58bf6459cb059122e2f4dfe13d10ed", size = 7668441, upload-time = "2026-05-13T14:57:02.127Z" },
{ url = "https://files.pythonhosted.org/packages/00/70/01b6461117a6a94b5af3f8ee166bb0f045056f3cf187750c110dabfdfffa/torchvision-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a49e55055a39a8506fe7e59850522cab004efb2c3839f6057658889c1d69c815", size = 4141602, upload-time = "2026-05-13T14:56:53.449Z" },
{ url = "https://files.pythonhosted.org/packages/92/22/c0633677b3b3f3e69554a21ac087bf705f829c40cd5e3783507b8c006681/torchvision-0.27.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:c1fac0fc2a7adf29481fc1938a0e7845c57ba1147a986784109c4d98f434ea8c", size = 1758818, upload-time = "2026-05-13T14:56:54.988Z" },
{ url = "https://files.pythonhosted.org/packages/48/e8/55f9d9667b56dae470e69e31beac9b00d458ea393feec1aae95cc4f3f1c9/torchvision-0.27.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:cbf89764fc76f3f17fbf80c12d5a89c691e91cb9d82c38412aaf0568655ffb19", size = 7789667, upload-time = "2026-05-13T14:56:48.858Z" },
{ url = "https://files.pythonhosted.org/packages/00/bc/6f8681daf3bbc4c315bb0005110f99d28e3ecd675bf9c8f2c0d393fbac7a/torchvision-0.27.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:91f61b9865423037c327eb56afa207cc72de874e458c361840db9dcf5ce0c0eb", size = 7579848, upload-time = "2026-05-13T14:56:38.209Z" },
{ url = "https://files.pythonhosted.org/packages/19/6c/8d8020e6bd1e46c53e487c9c4e9457a07f2ee28931028fb5d71e2da40adc/torchvision-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:5bb82fc3c55daf1788621e504310b0a286f1069627a8742f692aebb075ef25a7", size = 4119284, upload-time = "2026-05-13T14:56:46.625Z" },
{ url = "https://files.pythonhosted.org/packages/8d/7e/e78c48662a8d551606efdbe11c6b9c1d6d2391b92cd0e4591b9e6a2412b8/torchvision-0.27.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:2c4099a15150143b9b034730b404a56d572efe0b79489b4c765d929cb4eac7f3", size = 1758828, upload-time = "2026-05-13T14:56:52.293Z" },
{ url = "https://files.pythonhosted.org/packages/21/dd/d03ee9f9ee7bf11a8c7c776fb8e7fd6102f59c013791a2a4e5175bd6cba7/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b4c6bb0a670dcba017b3643e21902c9b8a1cc1c127d602f1488fa29ec3c6e865", size = 7790618, upload-time = "2026-05-13T14:56:44.721Z" },
{ url = "https://files.pythonhosted.org/packages/39/08/4002336a74742be70728603ec1769feb2b55e0d19c532c9ec9f92008de76/torchvision-0.27.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1c2db4bde82bc48ebff73436a6adf34d4f809448268a70d9a1285f5c8f92313d", size = 7580217, upload-time = "2026-05-13T14:56:43.274Z" },
{ url = "https://files.pythonhosted.org/packages/ed/cb/4dd4783eb3565f526ba6e64b6f6ca26c00eacc924cdfe60455db9d91b84b/torchvision-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:72bf547e58ddb948689734eed6f4b6a2031f979dba4fb08e3690688b392e929f", size = 4226392, upload-time = "2026-05-13T14:56:40.235Z" },
]
[[package]]
name = "tqdm"
version = "4.67.3"
@@ -2685,6 +3052,20 @@ version = "0.2.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9f/c1/dd817bf57e0274dacb10e0ac868cb6cd70876950cf361c41879c030a2b8b/warc3-wet-clueweb09-0.2.5.tar.gz", hash = "sha256:3054bfc07da525d5967df8ca3175f78fa3f78514c82643f8c81fbca96300b836", size = 17853, upload-time = "2020-12-07T23:59:04.599Z" }
[[package]]
name = "x-transformers"
version = "0.15.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "einops" },
{ name = "entmax" },
{ name = "torch" },
]
sdist = { url = "https://files.pythonhosted.org/packages/1b/ce/18166d4cf4fdce9e1566f8882ac59f1cc960b91f81622750837780d6b991/x-transformers-0.15.0.tar.gz", hash = "sha256:968021a5df401b41b6ca649a0a0aceadd56f232bfad9880ea0b19e1571f0b168", size = 19663, upload-time = "2021-08-09T21:47:42.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/52/62fd9d73f4c3f56442c590bc020f25597df5ba37db789f7861922b991e5c/x_transformers-0.15.0-py3-none-any.whl", hash = "sha256:7feefcf32f46005c2b9141d7dc0b08a0a472e4a7f4ea1f95e9e8dd4e79227309", size = 11208, upload-time = "2021-08-09T21:47:41.252Z" },
]
[[package]]
name = "xxhash"
version = "3.7.0"

20
wiki/concepts.yaml Normal file
View File

@@ -0,0 +1,20 @@
# Schema-Datei: was die KB abdeckt + Emphasis-Hebel (analog CLAUDE.md im Second-Brain).
# Der Mensch kuratiert diese Liste — sie steuert, worüber die Wiki Seiten baut.
concepts:
- slug: discrete-conformal-map
title: Discrete Conformal Map
aliases: [discrete conformal equivalence, discrete uniformization]
emphasis: Fokus auf die variationelle Charakterisierung (BPS 2015).
- slug: circle-packing
title: Circle Packing
aliases: [Koebe-Andreev-Thurston, circle pattern]
- slug: lobachevsky-function
title: Lobachevsky Function
aliases: [Milnor Lobachevsky, Clausen function]
emphasis: Verwendung in hyperbolischen Volumenformeln (Springborn 2008).
- slug: discrete-yamabe-flow
title: Discrete Yamabe Flow
aliases: [discrete Ricci flow, Chow-Luo]
- slug: hyperideal-tetrahedron
title: Hyperideal Tetrahedron
aliases: [hyperbolic volume, Schläfli, Ushijima]