Initial scaffold: local invoice OCR pipeline with Ollama, classifier branches, structured extraction, review, and Odoo XML export

- Add AGENTS.md and project-specific Zed skills (odoo-ocr-pipeline, odoo-xml-import, local-vlm-client).
- Implement Pydantic schemas for documents, invoices, review results, and VLM responses.
- Add unified BaseVLMClient with Ollama implementation and llama.cpp stub.
- Build pipeline stages: loader, classifier, digital_pdf/scanned_print/handwritten/mixed_unknown branches, extractor, reviewer, xml_builder.
- Add CLI entry point  with sidecar JSON and confidence-gated XML output.
- Include prompts for classifier, OCR, extraction, and review models.
- Add tests with FakeVLMClient; pytest, ruff, and mypy all pass.
This commit is contained in:
2026-08-21 14:04:42 +02:00
commit 7e706793fa
49 changed files with 2512 additions and 0 deletions
+54
View File
@@ -0,0 +1,54 @@
"""Shared pytest fixtures."""
from __future__ import annotations
import json
from typing import Any
import pytest
from PIL import Image
from odoo_ocr.clients.base import BaseVLMClient
from odoo_ocr.config import Settings
from odoo_ocr.schemas import VLMResponse
@pytest.fixture
def settings() -> Settings:
"""Default test settings with cache disabled."""
return Settings(
ollama_base_url="http://test-ollama:11435",
cache=Settings().model_dump()["cache"] | {"enabled": False},
)
class FakeVLMClient(BaseVLMClient):
"""Test double that returns canned responses based on prompt content."""
def __init__(self, responses: dict[str, str] | None = None) -> None:
super().__init__(Settings())
self.responses = responses or {}
self.calls: list[dict[str, Any]] = []
async def complete(
self,
system_prompt: str,
user_prompt: str,
images: list[Image.Image],
response_format: type[Any] | None = None,
temperature: float = 0.1,
max_tokens: int = 4096,
model: str | None = None,
) -> VLMResponse:
self.calls.append(
{
"system_prompt": system_prompt,
"user_prompt": user_prompt,
"image_count": len(images),
"model": model,
}
)
for key, value in self.responses.items():
if key in system_prompt or key in user_prompt:
return VLMResponse(content=json.dumps(value), model="fake")
return VLMResponse(content=json.dumps({}), model="fake")