Files
fuelish/3_extract_data.sh
T
matthias 7678d28717 Add containerized OCR pipeline for fuel receipts
Self-hosted, verifiable pipeline for extracting structured data (station,
date/time, price per litre, litres, discount, total) from German fuel
receipt photos: grayscale + mild bilateral filter + contrast stretch,
Tesseract OCR, then LLM structuring via a small local model (Qwen 2.5,
served by Ollama). Each step is independently runnable and inspectable
through numbered scripts against a shared tooling container.

Ollama runs as its own long-lived container (0_run_ollama.sh), separate
from the stateless per-receipt pipeline steps, so the model stays loaded
in memory between runs. Includes a containerized ground-truth batch test
(test_batch.sh) that reproduces the full pipeline over the test receipts
and reports per-field accuracy.

Targets eventual deployment on a Raspberry Pi 4 k3s cluster.
2026-09-28 19:35:29 +02:00

69 lines
2.3 KiB
Bash

#!/usr/bin/env bash
# Step 3 of the pipeline: send raw OCR text to a small text-only LLM
# (served by Ollama on the host) to structure it into the receipt JSON
# schema. Runs with --network host so the container can reach Ollama at
# 127.0.0.1 on the host; override with OLLAMA_URL / OLLAMA_MODEL env vars
# if Ollama runs elsewhere or you want a different model.
#
# Usage: ./3_extract_data.sh -in <ocr.txt> -out <scratch-dir>
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
ROOT="$(pwd)"
fail() { echo "FAIL: $*" >&2; exit 1; }
IN_PATH=""
OUT_DIR=""
while [ $# -gt 0 ]; do
case "$1" in
-in) IN_PATH="${2:-}"; shift 2 ;;
-out) OUT_DIR="${2:-}"; shift 2 ;;
*) fail "unknown argument: $1" ;;
esac
done
[ -n "$IN_PATH" ] && [ -n "$OUT_DIR" ] \
|| fail "usage: $0 -in <ocr.txt> -out <scratch-dir>"
[ -f "$IN_PATH" ] || fail "input file not found: $IN_PATH"
command -v docker >/dev/null 2>&1 || fail "docker not found in PATH"
IMAGE="${REGISTRY_IMAGE:-gastronomy/tools}"
if [ -f "$ROOT/registry.env" ]; then
# shellcheck disable=SC1091
source "$ROOT/registry.env"
IMAGE="${REGISTRY_IMAGE:-$IMAGE}"
fi
VERSION="$(<"$ROOT/tooling-container/VERSION")"
IMAGE="$IMAGE:$VERSION"
docker image inspect "$IMAGE" >/dev/null 2>&1 \
|| fail "$IMAGE not found locally - run ./build-image.sh first"
IN_ABS="$(realpath "$IN_PATH")"
IN_DIR="$(dirname "$IN_ABS")"
IN_FILE="$(basename "$IN_ABS")"
mkdir -p "$OUT_DIR"
OUT_ABS="$(realpath "$OUT_DIR")"
# On a vboxsf-mounted repo, files are owned by root:vboxsf with no
# "other" access - join the mounted dirs' groups so --user can read/write
# them without needing root inside the container.
GROUP_ADD_ARGS=(--group-add "$(stat -c %g "$IN_DIR")")
OUT_GID="$(stat -c %g "$OUT_ABS")"
[ "$OUT_GID" = "$(stat -c %g "$IN_DIR")" ] || GROUP_ADD_ARGS+=(--group-add "$OUT_GID")
echo "== Extracting structured data from $IN_FILE =="
docker run --rm \
--network host \
--user "$(id -u):$(id -g)" \
"${GROUP_ADD_ARGS[@]}" \
-e OLLAMA_URL="${OLLAMA_URL:-http://127.0.0.1:11434/api/chat}" \
-e OLLAMA_MODEL="${OLLAMA_MODEL:-qwen2.5:3b}" \
-v "$IN_DIR:/data/in:ro" \
-v "$OUT_ABS:/data/out" \
"$IMAGE" \
python3 /opt/app/src/extract.py -in "/data/in/$IN_FILE" -out /data/out
echo "== Done =="
echo "Extracted JSON written to: $OUT_ABS/extracted.json"