OpenCV Abhängigkeiten für Agent besser verpacken

This commit is contained in:
2026-06-03 09:03:26 +02:00
parent 0ecdff4d7d
commit 7a6bb4552e
8 changed files with 74 additions and 8 deletions

View File

@@ -10,4 +10,5 @@ FEDEO_SCAN_MODE=Color
FEDEO_SCAN_SOURCE= FEDEO_SCAN_SOURCE=
FEDEO_SCAN_POSTPROCESS=false FEDEO_SCAN_POSTPROCESS=false
FEDEO_SCAN_POSTPROCESS_PROFILE=document FEDEO_SCAN_POSTPROCESS_PROFILE=document
FEDEO_SCAN_POSTPROCESS_PYTHON=python3 FEDEO_SCAN_POSTPROCESS_PYTHON=
FEDEO_SCAN_POSTPROCESS_STRICT=false

View File

@@ -1,5 +1,6 @@
dist dist
node_modules node_modules
.venv-opencv
.env .env
*.log *.log
*.tmp *.tmp

View File

@@ -58,9 +58,7 @@ npm run dev
Für automatischen Zuschnitt, leichte Entzerrung, Rotation und Kontrastkorrektur kann die OpenCV-Pipeline aktiviert werden. Für automatischen Zuschnitt, leichte Entzerrung, Rotation und Kontrastkorrektur kann die OpenCV-Pipeline aktiviert werden.
```bash ```bash
python3 -m venv .venv-opencv npm run setup:opencv
. .venv-opencv/bin/activate
pip install -r requirements-opencv.txt
``` ```
Konfiguration: Konfiguration:
@@ -69,8 +67,11 @@ Konfiguration:
FEDEO_SCAN_POSTPROCESS=true FEDEO_SCAN_POSTPROCESS=true
FEDEO_SCAN_POSTPROCESS_PROFILE=receipt FEDEO_SCAN_POSTPROCESS_PROFILE=receipt
FEDEO_SCAN_POSTPROCESS_PYTHON=/pfad/zum/agent/.venv-opencv/bin/python FEDEO_SCAN_POSTPROCESS_PYTHON=/pfad/zum/agent/.venv-opencv/bin/python
FEDEO_SCAN_POSTPROCESS_STRICT=false
``` ```
Wenn `FEDEO_SCAN_POSTPROCESS_PYTHON` leer bleibt, verwendet der Agent automatisch `.venv-opencv/bin/python`, sofern diese Umgebung existiert. Falls OpenCV nicht installiert ist und `FEDEO_SCAN_POSTPROCESS_STRICT=false` gesetzt ist, lädt der Agent den Rohscan hoch, statt den Auftrag komplett fehlschlagen zu lassen.
Profile: Profile:
- `receipt`: Bons und schmale Belege werden bevorzugt hochkant zugeschnitten und kontrastiert. - `receipt`: Bons und schmale Belege werden bevorzugt hochkant zugeschnitten und kontrastiert.

View File

@@ -11,7 +11,8 @@
"scripts": { "scripts": {
"build": "tsc -p tsconfig.json", "build": "tsc -p tsconfig.json",
"dev": "tsx src/main.ts", "dev": "tsx src/main.ts",
"start": "node dist/main.js" "start": "node dist/main.js",
"setup:opencv": "sh scripts/setup-opencv.sh"
}, },
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {

View File

@@ -0,0 +1,26 @@
#!/usr/bin/env sh
set -eu
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"
AGENT_DIR="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd)"
VENV_DIR="${FEDEO_SCAN_POSTPROCESS_VENV:-$AGENT_DIR/.venv-opencv}"
PYTHON_BIN="${PYTHON:-python3}"
echo "FEDEO OpenCV-Umgebung wird vorbereitet"
echo "Agent: $AGENT_DIR"
echo "Venv: $VENV_DIR"
if ! command -v "$PYTHON_BIN" >/dev/null 2>&1; then
echo "Fehler: $PYTHON_BIN wurde nicht gefunden." >&2
exit 1
fi
"$PYTHON_BIN" -m venv "$VENV_DIR"
"$VENV_DIR/bin/python" -m pip install --upgrade pip
"$VENV_DIR/bin/python" -m pip install -r "$AGENT_DIR/requirements-opencv.txt"
"$VENV_DIR/bin/python" -c "import cv2, PIL, numpy; print('OpenCV OK')"
echo
echo "Fertig. Verwende in .env:"
echo "FEDEO_SCAN_POSTPROCESS=true"
echo "FEDEO_SCAN_POSTPROCESS_PYTHON=$VENV_DIR/bin/python"

View File

@@ -1,8 +1,13 @@
import path from "node:path" import path from "node:path"
import os from "node:os" import os from "node:os"
import { existsSync } from "node:fs"
import { fileURLToPath } from "node:url"
import { AgentConfig } from "./types.js" import { AgentConfig } from "./types.js"
import { loadDotEnv } from "./env.js" import { loadDotEnv } from "./env.js"
const currentFile = fileURLToPath(import.meta.url)
const agentRoot = path.resolve(path.dirname(currentFile), "..")
const optional = (value: string | undefined) => { const optional = (value: string | undefined) => {
const trimmed = value?.trim() const trimmed = value?.trim()
return trimmed ? trimmed : undefined return trimmed ? trimmed : undefined
@@ -30,6 +35,11 @@ const postprocessProfileFromEnv = (value: string | undefined): AgentConfig["post
return "document" return "document"
} }
const defaultPostprocessPython = () => {
const localVenvPython = path.join(agentRoot, ".venv-opencv", "bin", "python")
return existsSync(localVenvPython) ? localVenvPython : "python3"
}
export const loadConfig = (): AgentConfig => { export const loadConfig = (): AgentConfig => {
loadDotEnv(process.env.FEDEO_AGENT_ENV || ".env") loadDotEnv(process.env.FEDEO_AGENT_ENV || ".env")
@@ -52,6 +62,7 @@ export const loadConfig = (): AgentConfig => {
scanSource: optional(process.env.FEDEO_SCAN_SOURCE), scanSource: optional(process.env.FEDEO_SCAN_SOURCE),
scanPostprocess: booleanFromEnv(process.env.FEDEO_SCAN_POSTPROCESS, false), scanPostprocess: booleanFromEnv(process.env.FEDEO_SCAN_POSTPROCESS, false),
postprocessProfile: postprocessProfileFromEnv(process.env.FEDEO_SCAN_POSTPROCESS_PROFILE), postprocessProfile: postprocessProfileFromEnv(process.env.FEDEO_SCAN_POSTPROCESS_PROFILE),
postprocessPython: optional(process.env.FEDEO_SCAN_POSTPROCESS_PYTHON) || "python3", postprocessPython: optional(process.env.FEDEO_SCAN_POSTPROCESS_PYTHON) || defaultPostprocessPython(),
postprocessStrict: booleanFromEnv(process.env.FEDEO_SCAN_POSTPROCESS_STRICT, false),
} }
} }

View File

@@ -3,6 +3,7 @@ import path from "node:path"
import { AgentConfig, ScanJob, ScanResult } from "../types.js" import { AgentConfig, ScanJob, ScanResult } from "../types.js"
import { commandExists, runCommand } from "../commands.js" import { commandExists, runCommand } from "../commands.js"
import { hasOpenCvPostprocessRuntime, postprocessScan } from "./postprocess.js" import { hasOpenCvPostprocessRuntime, postprocessScan } from "./postprocess.js"
import { log } from "../logger.js"
const mimeTypes = { const mimeTypes = {
pdf: "application/pdf", pdf: "application/pdf",
@@ -51,6 +52,12 @@ const ensureFilenameExtension = (filename: string, format: AgentConfig["scanForm
return `${filename.slice(0, -ext.length)}${expectedExt}` return `${filename.slice(0, -ext.length)}${expectedExt}`
} }
const fallbackRawResult = (scanOutputPath: string, jobId: string): ScanResult => ({
path: scanOutputPath,
filename: `${jobId}.raw.png`,
mimeType: "image/png",
})
export const hasSane = () => commandExists("scanimage") export const hasSane = () => commandExists("scanimage")
export const listScanners = async () => { export const listScanners = async () => {
@@ -111,10 +118,27 @@ export const runScan = async (config: AgentConfig, job: ScanJob): Promise<ScanRe
if (shouldPostprocess) { if (shouldPostprocess) {
if (!await hasOpenCvPostprocessRuntime(config)) { if (!await hasOpenCvPostprocessRuntime(config)) {
throw new Error("OpenCV-Nachbearbeitung ist aktiviert, aber python3 mit cv2, Pillow und numpy ist nicht verfügbar") const message = "OpenCV-Nachbearbeitung ist aktiviert, aber python3 mit cv2, Pillow und numpy ist nicht verfügbar"
if (config.postprocessStrict) throw new Error(message)
log.warn(`${message}. Rohscan wird ohne Korrektur hochgeladen.`, {
jobId: job.id,
python: config.postprocessPython,
})
return fallbackRawResult(scanOutputPath, job.id)
} }
try {
return await postprocessScan(config, scanOutputPath, filename, format, postprocessProfile) return await postprocessScan(config, scanOutputPath, filename, format, postprocessProfile)
} catch (error) {
if (config.postprocessStrict) throw error
log.warn("OpenCV-Nachbearbeitung fehlgeschlagen. Rohscan wird ohne Korrektur hochgeladen.", {
jobId: job.id,
error: error instanceof Error ? error.message : String(error),
})
return fallbackRawResult(scanOutputPath, job.id)
}
} }
return { return {

View File

@@ -12,6 +12,7 @@ export type AgentConfig = {
scanPostprocess: boolean scanPostprocess: boolean
postprocessProfile: "document" | "receipt" | "raw" postprocessProfile: "document" | "receipt" | "raw"
postprocessPython: string postprocessPython: string
postprocessStrict: boolean
} }
export type AgentHeartbeat = { export type AgentHeartbeat = {