mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
fix: make OCR portable and reliable across AMD64 and ARM64 (#519)
* fix: make OCR portable and reliable * fix: harden OCR installation portability * fix: pin OCR partials across downloads * fix: make OCR execution reliably asynchronous * fix: harden OCR portability and docs routes * fix: preserve decoder and docs safeguards
This commit is contained in:
+26
-6
@@ -190,8 +190,8 @@ RUN curl -fsSL --retry 3 --retry-delay 5 "https://github.com/strukturag/libheif/
|
||||
# Pin tags to specific major.minor for reproducible builds.
|
||||
# ============================================
|
||||
FROM node:22-bookworm@sha256:a25c9934ff6382cd4f08b6bc26c82bf4ea69b1e6f8dabfb2ead457374127c365 AS base-linux-arm64
|
||||
# CUDA base must match the AI bundles' wheels (torch/paddle/onnxruntime-gpu are all
|
||||
# cu126) and the libcublas-12-6 install below. It also sets the NVIDIA_REQUIRE_CUDA
|
||||
# CUDA base must match the remaining GPU AI bundles' torch/onnxruntime wheels
|
||||
# (cu126) and the libcublas-12-6 install below. It also sets the NVIDIA_REQUIRE_CUDA
|
||||
# driver gate enforced by nvidia-container-toolkit at container start: a 12.6 base
|
||||
# needs driver R560+, vs 12.9 which needs R575+ and fails to start on common
|
||||
# production drivers (e.g. 570.x / CUDA 12.8). Keep this at 12.6.x.
|
||||
@@ -210,6 +210,17 @@ FROM base-${TARGETOS}-${TARGETARCH} AS production
|
||||
|
||||
ARG TARGETARCH
|
||||
ARG PANDOC_VERSION=3.10
|
||||
ARG OCR_RUNTIME_INDEX_KEY_ID=
|
||||
ARG OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64=
|
||||
ARG SNAPOTTER_OFFICIAL_CONTAINER=0
|
||||
|
||||
# Public release-verification key pinned independently from the bundle host.
|
||||
# Official release builds inject these repository variables and opt into the
|
||||
# exact ABI target. Source builds fail closed unless their operator explicitly
|
||||
# opts in and provides the same trust identity or mounts a trust file.
|
||||
ENV OCR_RUNTIME_INDEX_KEY_ID=${OCR_RUNTIME_INDEX_KEY_ID} \
|
||||
OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64=${OCR_RUNTIME_INDEX_PUBLIC_KEY_PEM_B64} \
|
||||
SNAPOTTER_OFFICIAL_CONTAINER=${SNAPOTTER_OFFICIAL_CONTAINER}
|
||||
|
||||
# Pin corepack's cache during image build. It is removed after dependency and
|
||||
# browser installation so pnpm is not part of the production runtime surface.
|
||||
@@ -250,7 +261,7 @@ RUN for i in 1 2 3; do apt-get -o Acquire::Retries=3 update && break || sleep $(
|
||||
libimage-exiftool-perl \
|
||||
python3 python3-pip python3-venv python3-dev \
|
||||
tesseract-ocr tesseract-ocr-eng tesseract-ocr-deu tesseract-ocr-fra tesseract-ocr-spa \
|
||||
tesseract-ocr-chi-sim tesseract-ocr-jpn tesseract-ocr-kor \
|
||||
tesseract-ocr-chi-sim tesseract-ocr-jpn \
|
||||
# Document engine: qpdf + LibreOffice headless + WeasyPrint runtime deps
|
||||
# calibre deferred (5.2 GB ruling; pandoc covers epub/markdown families)
|
||||
qpdf \
|
||||
@@ -261,11 +272,16 @@ RUN for i in 1 2 3; do apt-get -o Acquire::Retries=3 update && break || sleep $(
|
||||
libgl1 libglib2.0-0 libgles2 \
|
||||
libegl1 libwayland-egl1 libwayland-client0 libwayland-cursor0 \
|
||||
libxkbcommon-x11-0 libxkbcommon0 libxcursor1 \
|
||||
&& rm -f /usr/share/tesseract-ocr/5/tessdata/osd.traineddata \
|
||||
&& test ! -e /usr/share/tesseract-ocr/5/tessdata/osd.traineddata \
|
||||
&& if apt-cache show libmagickcore-6.q16-7-extra >/dev/null 2>&1; then \
|
||||
apt-get install -y --no-install-recommends libmagickcore-6.q16-7-extra; \
|
||||
elif apt-cache show libmagickcore-6.q16-6-extra >/dev/null 2>&1; then \
|
||||
apt-get install -y --no-install-recommends libmagickcore-6.q16-6-extra; \
|
||||
else \
|
||||
echo "No supported ImageMagick EXR coder package found" >&2; exit 1; \
|
||||
fi \
|
||||
&& convert -list format | grep -Eq '^[[:space:]]*EXR([*[:space:]]|$)' \
|
||||
&& if apt-cache show libx265-199 >/dev/null 2>&1; then \
|
||||
apt-get install -y --no-install-recommends libx265-199; \
|
||||
elif apt-cache show libx265-209 >/dev/null 2>&1; then \
|
||||
@@ -304,7 +320,7 @@ RUN install -d /usr/share/postgresql-common/pgdg \
|
||||
> /etc/apt/sources.list.d/redis.list \
|
||||
&& for i in 1 2 3; do apt-get -o Acquire::Retries=3 update && break || sleep $((i * 15)); done \
|
||||
&& apt-get install -y --no-install-recommends postgresql-17 postgresql-client-17 redis-server \
|
||||
&& redis-server --version | grep -q 'v=8\.' \
|
||||
&& dpkg-query -W -f='${Version}\n' redis-server | grep -Eq '(^|:)8\.' \
|
||||
&& rm -f /etc/ssl/private/ssl-cert-snakeoil.key /etc/ssl/certs/ssl-cert-snakeoil.pem \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
@@ -398,6 +414,10 @@ COPY packages/ai/python/install_feature.py /app/packages/ai/python/install_featu
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# The immutable OCR artifact builder runs inside this final image and copies
|
||||
# the exact repository license into each signed runtime generation.
|
||||
COPY LICENSE ./LICENSE
|
||||
|
||||
# Copy workspace config
|
||||
COPY pnpm-workspace.yaml pnpm-lock.yaml package.json turbo.json tsconfig.base.json ./
|
||||
|
||||
@@ -502,6 +522,7 @@ RUN mkdir -p /data /data/files /data/ai/models /data/ai/pip-cache /tmp/workspace
|
||||
# Environment defaults
|
||||
ENV PORT=1349 \
|
||||
NODE_ENV=production \
|
||||
NODE_USE_ENV_PROXY=1 \
|
||||
STORAGE_MODE=local \
|
||||
WORKSPACE_PATH=/tmp/workspace \
|
||||
FILES_STORAGE_PATH=/data/files \
|
||||
@@ -560,8 +581,7 @@ ENV S6_KEEP_ENV=1 \
|
||||
|
||||
# Suppress noisy ML library output in docker logs
|
||||
ENV PYTHONWARNINGS=default \
|
||||
TF_CPP_MIN_LOG_LEVEL=3 \
|
||||
PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK=True
|
||||
TF_CPP_MIN_LOG_LEVEL=3
|
||||
|
||||
# Create non-root user for runtime
|
||||
# Home is /data/.home (created + chowned by the entrypoint at runtime). Runtime
|
||||
|
||||
@@ -42,6 +42,14 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
||||
libjxl-tools \
|
||||
ghostscript \
|
||||
qpdf \
|
||||
&& if apt-cache show libmagickcore-6.q16-7-extra >/dev/null 2>&1; then \
|
||||
apt-get install -y --no-install-recommends libmagickcore-6.q16-7-extra; \
|
||||
elif apt-cache show libmagickcore-6.q16-6-extra >/dev/null 2>&1; then \
|
||||
apt-get install -y --no-install-recommends libmagickcore-6.q16-6-extra; \
|
||||
else \
|
||||
echo "No supported ImageMagick EXR coder package found" >&2; exit 1; \
|
||||
fi \
|
||||
&& convert -list format | grep -Eq '^[[:space:]]*EXR([*[:space:]]|$)' \
|
||||
&& if apt-cache show libx265-199 >/dev/null 2>&1; then \
|
||||
apt-get install -y --no-install-recommends libx265-199; \
|
||||
elif apt-cache show libx265-209 >/dev/null 2>&1; then \
|
||||
|
||||
+50
-3
@@ -20,6 +20,36 @@ export BUNDLE_ID="${1:?Usage: build-bundle.sh <bundleId> <arch> <outputDir>}"
|
||||
export ARCH="${2:?Usage: build-bundle.sh <bundleId> <arch> <outputDir>}"
|
||||
OUTPUT_DIR="${3:?Usage: build-bundle.sh <bundleId> <arch> <outputDir>}"
|
||||
|
||||
# OCR uses the v3 complete-runtime format. Keep legacy aliases at this boundary
|
||||
# until every release caller has moved to explicit capability target names.
|
||||
if [[ "${BUNDLE_ID}" == "ocr" ]]; then
|
||||
case "${ARCH}" in
|
||||
amd64-gpu | amd64-cpu | linux-amd64-cpu-py312)
|
||||
OCR_TARGET="linux-amd64-cpu-py312"
|
||||
;;
|
||||
arm64-cpu | linux-arm64-cpu-py311)
|
||||
OCR_TARGET="linux-arm64-cpu-py311"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unsupported OCR runtime target alias: ${ARCH}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
OCR_BUILDER="${OCR_RUNTIME_BUILDER:-${SCRIPT_DIR}/build-ocr-runtime.sh}"
|
||||
if [[ ! -x "${OCR_BUILDER}" ]]; then
|
||||
echo "ERROR: OCR runtime builder is missing or not executable: ${OCR_BUILDER}" >&2
|
||||
exit 2
|
||||
fi
|
||||
exec "${OCR_BUILDER}" "${OCR_TARGET}" "${OUTPUT_DIR}"
|
||||
fi
|
||||
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-0}"
|
||||
if [[ ! "${SOURCE_DATE_EPOCH}" =~ ^[0-9]+$ ]]; then
|
||||
echo "ERROR: SOURCE_DATE_EPOCH must be a non-negative integer" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
MANIFEST="/app/docker/feature-manifest.json"
|
||||
VENV_PATH="${PYTHON_VENV_PATH:-/opt/venv}"
|
||||
export MODELS_DIR="/tmp/bundle-models"
|
||||
@@ -68,8 +98,9 @@ if '${BUNDLE_ID}' not in m['bundles']:
|
||||
# ── Step 0: Write pip constraints (numpy-1.x-ABI closure lock) ────────────
|
||||
# Bundle installs below can otherwise pull the latest transitive scientific
|
||||
# stack: numpy 2.x plus scipy/scikit-learn/scikit-image/pandas wheels built
|
||||
# against the numpy 2.x ABI (e.g. paddleocr[doc-parser] drags numpy 1.26.4 ->
|
||||
# 2.5.1 + scipy 1.18). The Step 4 re-pin snaps numpy back to 1.26.4 but leaves
|
||||
# against the numpy 2.x ABI. A legacy bundle's transitive dependency can drag
|
||||
# numpy 1.26.4 to 2.x plus a newer scipy. The Step 4 re-pin snaps numpy back to
|
||||
# 1.26.4 but leaves
|
||||
# those numpy-2.x wheels behind; the Step 6 diff (by dir name, not version)
|
||||
# then ships them, and once installed they strand on the numpy==1.26.4 base and
|
||||
# raise "numpy.dtype size changed" on import, crashing the dispatcher for
|
||||
@@ -419,7 +450,23 @@ echo "=== Creating archive ==="
|
||||
ARCHIVE_NAME="${BUNDLE_ID}-${ARCH}.tar.gz"
|
||||
ARCHIVE_PATH="${OUTPUT_DIR}/${ARCHIVE_NAME}"
|
||||
|
||||
tar -czf "${ARCHIVE_PATH}" -C "${BUILD_DIR}" .
|
||||
find "${BUILD_DIR}" -mindepth 1 -printf '%P\0' \
|
||||
| LC_ALL=C sort -z \
|
||||
| tar \
|
||||
--create \
|
||||
--file=- \
|
||||
--directory="${BUILD_DIR}" \
|
||||
--sort=name \
|
||||
--mtime="@${SOURCE_DATE_EPOCH}" \
|
||||
--owner=0 \
|
||||
--group=0 \
|
||||
--numeric-owner \
|
||||
--format=gnu \
|
||||
--no-recursion \
|
||||
--null \
|
||||
--verbatim-files-from \
|
||||
--files-from=- \
|
||||
| gzip -n > "${ARCHIVE_PATH}"
|
||||
|
||||
sha256sum "${ARCHIVE_PATH}" | awk '{print $1}' > "${ARCHIVE_PATH}.sha256"
|
||||
|
||||
|
||||
Executable
+723
@@ -0,0 +1,723 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Build one complete, immutable OCR runtime for the native host. This builder
|
||||
# deliberately has no cross-build mode: compiled wheels and the Python ABI must
|
||||
# be exercised on the same architecture that will publish the artifact.
|
||||
|
||||
TARGET="${1:?Usage: build-ocr-runtime.sh <target> <outputDir>}"
|
||||
OUTPUT_DIR="${2:?Usage: build-ocr-runtime.sh <target> <outputDir>}"
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
if [[ -d "/app/packages/ai/python" ]]; then
|
||||
REPO_ROOT="${SNAPOTTER_SOURCE_ROOT:-/app}"
|
||||
else
|
||||
REPO_ROOT="${SNAPOTTER_SOURCE_ROOT:-$(cd -- "${SCRIPT_DIR}/.." && pwd)}"
|
||||
fi
|
||||
|
||||
MODELS_MANIFEST="${OCR_MODELS_MANIFEST:-${SCRIPT_DIR}/ocr-runtime-models.json}"
|
||||
ADAPTER_SOURCE="${OCR_ADAPTER_SOURCE:-${REPO_ROOT}/packages/ai/python/ocr_runtime.py}"
|
||||
RUNNER_SOURCE="${OCR_RUNNER_SOURCE:-${REPO_ROOT}/packages/ai/python/ocr_runtime_entrypoint.py}"
|
||||
SOURCE_DATE_EPOCH="${SOURCE_DATE_EPOCH:-0}"
|
||||
SOURCE_IMAGE_DIGEST="${SNAPOTTER_OCR_SOURCE_IMAGE_DIGEST:?Set SNAPOTTER_OCR_SOURCE_IMAGE_DIGEST}"
|
||||
SOURCE_COMMIT="${SNAPOTTER_OCR_SOURCE_COMMIT:?Set SNAPOTTER_OCR_SOURCE_COMMIT}"
|
||||
BUILDER_ID="${SNAPOTTER_OCR_BUILDER_ID:?Set SNAPOTTER_OCR_BUILDER_ID}"
|
||||
|
||||
if [[ ! "${SOURCE_IMAGE_DIGEST}" =~ ^[a-f0-9]{64}$ ]]; then
|
||||
echo "ERROR: OCR source image digest must be 64 lowercase hex characters" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ ! "${SOURCE_COMMIT}" =~ ^[a-f0-9]{40}$ ]]; then
|
||||
echo "ERROR: OCR source commit must be a 40-character Git object ID" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ -z "${BUILDER_ID}" || ${#BUILDER_ID} -gt 512 || "${BUILDER_ID}" == *$'\n'* || "${BUILDER_ID}" == *$'\r'* ]]; then
|
||||
echo "ERROR: OCR builder identity is missing or unsafe" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
case "${TARGET}" in
|
||||
linux-amd64-cpu-py312)
|
||||
EXPECTED_MACHINE="x86_64"
|
||||
PYTHON_MINOR="3.12"
|
||||
ARCH="amd64"
|
||||
REQUIREMENTS="${SCRIPT_DIR}/ocr-runtime-requirements-amd64.txt"
|
||||
;;
|
||||
linux-arm64-cpu-py311)
|
||||
EXPECTED_MACHINE="aarch64"
|
||||
PYTHON_MINOR="3.11"
|
||||
ARCH="arm64"
|
||||
REQUIREMENTS="${SCRIPT_DIR}/ocr-runtime-requirements-arm64.txt"
|
||||
;;
|
||||
*)
|
||||
echo "ERROR: Unsupported OCR runtime target: ${TARGET}" >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$(uname -s)" != "Linux" || "$(uname -m)" != "${EXPECTED_MACHINE}" ]]; then
|
||||
echo "ERROR: ${TARGET} must be built natively on Linux/${EXPECTED_MACHINE}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
PYTHON_BIN="${PYTHON_BIN:-$(command -v "python${PYTHON_MINOR}" || true)}"
|
||||
if [[ -z "${PYTHON_BIN}" || ! -x "${PYTHON_BIN}" ]]; then
|
||||
echo "ERROR: python${PYTHON_MINOR} is required for ${TARGET}" >&2
|
||||
exit 2
|
||||
fi
|
||||
if [[ "$("${PYTHON_BIN}" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')" != "${PYTHON_MINOR}" ]]; then
|
||||
echo "ERROR: ${PYTHON_BIN} does not provide Python ${PYTHON_MINOR}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for input in \
|
||||
"${MODELS_MANIFEST}" \
|
||||
"${RUNNER_SOURCE}" \
|
||||
"${ADAPTER_SOURCE}" \
|
||||
"${REQUIREMENTS}"; do
|
||||
if [[ ! -f "${input}" ]]; then
|
||||
echo "ERROR: Required OCR runtime build input is missing: ${input}" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
|
||||
VERSION="${SNAPOTTER_VERSION:-$("${PYTHON_BIN}" - "${REPO_ROOT}" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
root = pathlib.Path(sys.argv[1])
|
||||
for candidate in (root / "package.json", root / "docker" / "feature-manifest.json"):
|
||||
if candidate.is_file():
|
||||
value = json.loads(candidate.read_text(encoding="utf-8"))
|
||||
version = value.get("version") or value.get("imageVersion")
|
||||
if isinstance(version, str) and version:
|
||||
print(version)
|
||||
raise SystemExit(0)
|
||||
raise SystemExit("cannot determine SnapOtter version")
|
||||
PY
|
||||
)}"
|
||||
if [[ ! "${VERSION}" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$ ]]; then
|
||||
echo "ERROR: Unsafe SnapOtter version: ${VERSION}" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
BUILD_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/snapotter-ocr-runtime.XXXXXXXX")"
|
||||
trap 'rm -rf "${BUILD_ROOT}"' EXIT
|
||||
RUNTIME_ROOT="${BUILD_ROOT}/runtime"
|
||||
WHEELHOUSE="${BUILD_ROOT}/wheelhouse"
|
||||
mkdir -p "${RUNTIME_ROOT}" "${WHEELHOUSE}" "${OUTPUT_DIR}"
|
||||
|
||||
echo "=== Building OCR runtime ${TARGET} (${VERSION}) ==="
|
||||
echo "Downloading the exact hash-locked Python inputs"
|
||||
"${PYTHON_BIN}" -m pip download \
|
||||
--disable-pip-version-check \
|
||||
--require-hashes \
|
||||
--no-deps \
|
||||
--dest "${WHEELHOUSE}" \
|
||||
--requirement "${REQUIREMENTS}"
|
||||
|
||||
# OmegaConf 2.3.0 requires antlr4-python3-runtime 4.9.x, whose upstream release
|
||||
# has no wheel. Its exact sdist is already hash-checked above; build it without
|
||||
# network access or build isolation, then bind the installed files in the final
|
||||
# per-file artifact manifest.
|
||||
ANTLR_SDIST="$(find "${WHEELHOUSE}" -maxdepth 1 -type f -name 'antlr4-python3-runtime-4.9.3.tar.gz' -print -quit)"
|
||||
if [[ -z "${ANTLR_SDIST}" ]]; then
|
||||
echo "ERROR: Hash-locked antlr4 source distribution was not downloaded" >&2
|
||||
exit 2
|
||||
fi
|
||||
mkdir -p "${WHEELHOUSE}/built"
|
||||
PIP_NO_INDEX=1 "${PYTHON_BIN}" -m pip wheel \
|
||||
--disable-pip-version-check \
|
||||
--no-build-isolation \
|
||||
--no-deps \
|
||||
--wheel-dir "${WHEELHOUSE}/built" \
|
||||
"${ANTLR_SDIST}"
|
||||
ANTLR_WHEEL="$(find "${WHEELHOUSE}/built" -maxdepth 1 -type f -name 'antlr4_python3_runtime-4.9.3-*.whl' -print -quit)"
|
||||
if [[ -z "${ANTLR_WHEEL}" ]]; then
|
||||
echo "ERROR: Failed to build the pinned antlr4 runtime wheel" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo "Creating a complete isolated Python runtime"
|
||||
"${PYTHON_BIN}" -m venv --copies "${RUNTIME_ROOT}/venv"
|
||||
grep -v '^antlr4-python3-runtime==' "${REQUIREMENTS}" > "${BUILD_ROOT}/wheel-requirements.txt"
|
||||
"${RUNTIME_ROOT}/venv/bin/python" -m pip install \
|
||||
--disable-pip-version-check \
|
||||
--no-index \
|
||||
--find-links "${WHEELHOUSE}" \
|
||||
--require-hashes \
|
||||
--no-deps \
|
||||
--requirement "${BUILD_ROOT}/wheel-requirements.txt"
|
||||
"${RUNTIME_ROOT}/venv/bin/python" -m pip install \
|
||||
--disable-pip-version-check \
|
||||
--no-index \
|
||||
--no-deps \
|
||||
"${ANTLR_WHEEL}"
|
||||
|
||||
# Remove build-root identity from otherwise relocatable venv metadata. The
|
||||
# interpreter ignores `command`; direct_url only records the temporary wheel
|
||||
# path. Keeping either would make byte-for-byte rebuilds depend on mktemp.
|
||||
sed -i '/^command = /d' "${RUNTIME_ROOT}/venv/pyvenv.cfg"
|
||||
ANTLR_DIST_INFO="$(find "${RUNTIME_ROOT}/venv" -type d -name 'antlr4_python3_runtime-4.9.3.dist-info' -print -quit)"
|
||||
if [[ -z "${ANTLR_DIST_INFO}" ]]; then
|
||||
echo "ERROR: Installed antlr4 runtime metadata is missing" >&2
|
||||
exit 2
|
||||
fi
|
||||
rm -f "${ANTLR_DIST_INFO}/direct_url.json"
|
||||
"${RUNTIME_ROOT}/venv/bin/python" - "${ANTLR_DIST_INFO}/RECORD" <<'PY'
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
record = pathlib.Path(sys.argv[1])
|
||||
lines = [line for line in record.read_text(encoding="utf-8").splitlines() if "direct_url.json" not in line]
|
||||
record.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
PY
|
||||
|
||||
echo "Downloading and verifying official OCR model objects"
|
||||
"${PYTHON_BIN}" - "${MODELS_MANIFEST}" "${RUNTIME_ROOT}" "${REPO_ROOT}" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import sys
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
manifest_path = pathlib.Path(sys.argv[1])
|
||||
runtime_root = pathlib.Path(sys.argv[2])
|
||||
repository_root = pathlib.Path(sys.argv[3]).resolve()
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
if manifest.get("schemaVersion") != 1 or manifest.get("family") != "ocr":
|
||||
raise SystemExit("invalid OCR model manifest")
|
||||
|
||||
|
||||
def safe_relative(value, label):
|
||||
if not isinstance(value, str) or not value or "\\" in value:
|
||||
raise SystemExit(f"invalid {label}")
|
||||
path = pathlib.PurePosixPath(value)
|
||||
if path.is_absolute() or any(part in ("", ".", "..") for part in path.parts):
|
||||
raise SystemExit(f"unsafe {label}: {value!r}")
|
||||
return pathlib.Path(*path.parts)
|
||||
|
||||
|
||||
def file_sha256(path):
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def object_size(value, label):
|
||||
if type(value) is not int or value <= 0:
|
||||
raise SystemExit(f"{label} has an invalid signed size")
|
||||
return value
|
||||
|
||||
|
||||
def download_exact(obj, label, allowed_hosts):
|
||||
url = obj.get("url", "")
|
||||
parsed = urllib.parse.urlparse(url)
|
||||
if parsed.scheme != "https" or parsed.hostname not in allowed_hosts:
|
||||
raise SystemExit(f"{label} has an untrusted download URL")
|
||||
destination = runtime_root / safe_relative(obj["path"], f"{label} path")
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
temporary = destination.with_suffix(destination.suffix + ".part")
|
||||
expected_size = object_size(obj.get("size"), label)
|
||||
request = urllib.request.Request(url, headers={"User-Agent": "SnapOtter-runtime-builder/1"})
|
||||
try:
|
||||
with urllib.request.urlopen(request, timeout=120) as response, temporary.open("wb") as output:
|
||||
# Read at most one byte beyond the signed size. A malicious or
|
||||
# misconfigured origin can never consume unbounded runner disk.
|
||||
remaining = expected_size + 1
|
||||
while remaining:
|
||||
chunk = response.read(min(1024 * 1024, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
output.write(chunk)
|
||||
remaining -= len(chunk)
|
||||
actual_size = temporary.stat().st_size
|
||||
digest = file_sha256(temporary)
|
||||
if actual_size != expected_size:
|
||||
raise SystemExit(
|
||||
f"{label} size mismatch: expected {expected_size}, got {actual_size}"
|
||||
)
|
||||
if digest != obj.get("sha256"):
|
||||
raise SystemExit(
|
||||
f"{label} digest mismatch: expected {obj.get('sha256')}, got {digest}"
|
||||
)
|
||||
temporary.replace(destination)
|
||||
finally:
|
||||
temporary.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def copy_exact_local(obj, label):
|
||||
source = repository_root / safe_relative(obj["source"], f"{label} source")
|
||||
destination = runtime_root / safe_relative(obj["path"], f"{label} path")
|
||||
if not source.is_file() or source.is_symlink():
|
||||
raise SystemExit(f"{label} is missing or unsafe: {source}")
|
||||
if source.stat().st_size != object_size(obj.get("size"), label):
|
||||
raise SystemExit(f"{label} size does not match its manifest")
|
||||
if file_sha256(source) != obj.get("sha256"):
|
||||
raise SystemExit(f"{label} digest does not match its manifest")
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copyfile(source, destination)
|
||||
|
||||
|
||||
for model in manifest.get("models", []):
|
||||
revision = model.get("revision", "")
|
||||
url = model.get("url", "")
|
||||
if not isinstance(revision, str) or len(revision) != 40:
|
||||
raise SystemExit(f"model {model.get('id')} has no immutable revision")
|
||||
if f"/resolve/{revision}/" not in url:
|
||||
raise SystemExit(f"model {model.get('id')} URL does not bind its revision")
|
||||
|
||||
download_exact(
|
||||
model,
|
||||
f"model {model['id']}",
|
||||
{"huggingface.co", "www.modelscope.cn"},
|
||||
)
|
||||
|
||||
for asset in manifest.get("localAssets", []):
|
||||
copy_exact_local(asset, f"local OCR asset {asset['id']}")
|
||||
|
||||
for asset in manifest.get("legalAssets", []):
|
||||
relative = safe_relative(asset["path"], "legal asset path")
|
||||
if relative.parts[0] not in {"LICENSES", "THIRD_PARTY_LICENSES"}:
|
||||
raise SystemExit(f"legal asset is outside a license directory: {asset['id']}")
|
||||
if asset.get("source"):
|
||||
copy_exact_local(asset, f"legal asset {asset['id']}")
|
||||
else:
|
||||
download_exact(
|
||||
asset,
|
||||
f"legal asset {asset['id']}",
|
||||
{"www.apache.org", "raw.githubusercontent.com"},
|
||||
)
|
||||
PY
|
||||
|
||||
install -m 0644 "${RUNNER_SOURCE}" "${RUNTIME_ROOT}/ocr_runner.py"
|
||||
install -m 0644 "${ADAPTER_SOURCE}" "${RUNTIME_ROOT}/ocr_runtime.py"
|
||||
|
||||
# Installed package managers are not part of the application runtime. Remove
|
||||
# their modules and every generated bytecode file so output paths and timestamps
|
||||
# from the temporary build root cannot leak into the immutable generation.
|
||||
SITE_PACKAGES="$("${RUNTIME_ROOT}/venv/bin/python" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
|
||||
rm -rf \
|
||||
"${SITE_PACKAGES}/pip" \
|
||||
"${SITE_PACKAGES}"/pip-*.dist-info \
|
||||
"${SITE_PACKAGES}/setuptools" \
|
||||
"${SITE_PACKAGES}"/setuptools-*.dist-info \
|
||||
"${SITE_PACKAGES}/_distutils_hack" \
|
||||
"${SITE_PACKAGES}/distutils-precedence.pth"
|
||||
# RapidOCR's universal wheel carries convenience defaults. SnapOtter binds every
|
||||
# model explicitly, so retaining those three duplicate ONNX files only bloats the
|
||||
# downloadable pack and risks an accidental fallback to the wrong classifier.
|
||||
find "${SITE_PACKAGES}/rapidocr/models" -type f -name '*.onnx' -delete
|
||||
find "${RUNTIME_ROOT}" -type d -name __pycache__ -prune -exec rm -rf {} +
|
||||
find "${RUNTIME_ROOT}" -type f \( -name '*.pyc' -o -name '*.pyo' \) -delete
|
||||
|
||||
# Only the interpreter is invoked from venv/bin. Console-script shebangs embed
|
||||
# the temporary build path, so remove them and retain one relocatable executable.
|
||||
find "${RUNTIME_ROOT}/venv/bin" -mindepth 1 -maxdepth 1 -type f ! -name python -delete
|
||||
find "${RUNTIME_ROOT}" -type l -delete
|
||||
if [[ ! -x "${RUNTIME_ROOT}/venv/bin/python" ]]; then
|
||||
echo "ERROR: Complete venv is missing its Python executable" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# pip records hashes for generated console scripts whose shebang contains the
|
||||
# random build-root path. Those scripts are intentionally removed above, but
|
||||
# leaving their stale hashes in *.dist-info/RECORD makes otherwise identical
|
||||
# runtime archives differ on every build. Rebuild every RECORD from the exact
|
||||
# retained regular files so metadata is truthful and byte-for-byte reproducible.
|
||||
"${RUNTIME_ROOT}/venv/bin/python" - "${RUNTIME_ROOT}" "${SITE_PACKAGES}" <<'PY'
|
||||
import base64
|
||||
import csv
|
||||
import hashlib
|
||||
import pathlib
|
||||
import stat
|
||||
import sys
|
||||
|
||||
runtime_root = pathlib.Path(sys.argv[1]).resolve()
|
||||
site_packages = pathlib.Path(sys.argv[2]).resolve()
|
||||
|
||||
|
||||
def contained(path: pathlib.Path) -> pathlib.Path:
|
||||
resolved = path.resolve(strict=False)
|
||||
try:
|
||||
resolved.relative_to(runtime_root)
|
||||
except ValueError as error:
|
||||
raise SystemExit(f"wheel RECORD path escapes the runtime: {path}") from error
|
||||
return resolved
|
||||
|
||||
|
||||
for record_path in sorted(site_packages.glob("*.dist-info/RECORD")):
|
||||
rows = list(csv.reader(record_path.read_text(encoding="utf-8").splitlines()))
|
||||
retained = []
|
||||
seen = set()
|
||||
record_relative = record_path.relative_to(site_packages).as_posix()
|
||||
for row in rows:
|
||||
if len(row) != 3 or not row[0]:
|
||||
raise SystemExit(f"malformed wheel RECORD: {record_path}")
|
||||
target = contained(site_packages / pathlib.PurePosixPath(row[0]))
|
||||
try:
|
||||
info = target.lstat()
|
||||
except FileNotFoundError:
|
||||
# Removed console scripts, bundled convenience models, and
|
||||
# direct_url metadata must not survive as stale manifest entries.
|
||||
continue
|
||||
if row[0] in seen:
|
||||
raise SystemExit(f"duplicate retained wheel RECORD path: {record_path}")
|
||||
seen.add(row[0])
|
||||
if stat.S_ISLNK(info.st_mode) or not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
|
||||
raise SystemExit(f"unsafe retained wheel RECORD target: {target}")
|
||||
if row[0] == record_relative:
|
||||
retained.append((row[0], "", ""))
|
||||
continue
|
||||
contents = target.read_bytes()
|
||||
digest = base64.urlsafe_b64encode(hashlib.sha256(contents).digest()).rstrip(b"=").decode()
|
||||
retained.append((row[0], f"sha256={digest}", str(len(contents))))
|
||||
if record_relative not in {row[0] for row in retained}:
|
||||
raise SystemExit(f"wheel RECORD does not describe itself: {record_path}")
|
||||
with record_path.open("w", encoding="utf-8", newline="") as output:
|
||||
writer = csv.writer(output, lineterminator="\n")
|
||||
writer.writerows(sorted(retained))
|
||||
PY
|
||||
|
||||
echo "Recording complete license and source attribution material"
|
||||
"${RUNTIME_ROOT}/venv/bin/python" - \
|
||||
"${MODELS_MANIFEST}" \
|
||||
"${RUNTIME_ROOT}" \
|
||||
"${SITE_PACKAGES}" \
|
||||
"${SOURCE_COMMIT}" \
|
||||
"${VERSION}" <<'PY'
|
||||
import json
|
||||
import pathlib
|
||||
import sys
|
||||
from email.parser import BytesParser
|
||||
from email.policy import default
|
||||
|
||||
manifest_path, runtime_root_raw, site_packages_raw, source_commit, version = sys.argv[1:]
|
||||
runtime_root = pathlib.Path(runtime_root_raw).resolve()
|
||||
site_packages = pathlib.Path(site_packages_raw).resolve()
|
||||
manifest = json.loads(pathlib.Path(manifest_path).read_text(encoding="utf-8"))
|
||||
legal_assets = {asset["id"]: asset for asset in manifest.get("legalAssets", [])}
|
||||
required_legal_assets = {"snapotter-agpl", "apache-2.0", "antlr-4.9.3-license"}
|
||||
if set(legal_assets) != required_legal_assets:
|
||||
raise SystemExit("OCR legal asset inventory is incomplete")
|
||||
|
||||
|
||||
def relative_regular(path):
|
||||
resolved = path.resolve(strict=True)
|
||||
try:
|
||||
relative = resolved.relative_to(runtime_root)
|
||||
except ValueError as error:
|
||||
raise SystemExit(f"license path escapes the runtime: {path}") from error
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise SystemExit(f"license path is not a regular file: {path}")
|
||||
return relative.as_posix()
|
||||
|
||||
|
||||
def license_file_names(root):
|
||||
markers = ("license", "licence", "copying", "notice", "copyright", "authors")
|
||||
return [
|
||||
relative_regular(path)
|
||||
for path in sorted(root.rglob("*"))
|
||||
if path.is_file() and any(marker in path.name.lower() for marker in markers)
|
||||
]
|
||||
|
||||
|
||||
manual_license_files = {
|
||||
"antlr4-python3-runtime": [legal_assets["antlr-4.9.3-license"]["path"]],
|
||||
"rapidocr": [legal_assets["apache-2.0"]["path"]],
|
||||
}
|
||||
package_license_files = {
|
||||
"onnxruntime": [
|
||||
relative_regular(site_packages / "onnxruntime" / "LICENSE"),
|
||||
relative_regular(site_packages / "onnxruntime" / "ThirdPartyNotices.txt"),
|
||||
],
|
||||
}
|
||||
python_packages = []
|
||||
seen_packages = set()
|
||||
for dist_info in sorted(site_packages.glob("*.dist-info")):
|
||||
metadata_path = dist_info / "METADATA"
|
||||
if not metadata_path.is_file() or metadata_path.is_symlink():
|
||||
raise SystemExit(f"Python distribution has no safe METADATA: {dist_info.name}")
|
||||
metadata = BytesParser(policy=default).parsebytes(metadata_path.read_bytes())
|
||||
name = metadata.get("Name")
|
||||
package_version = metadata.get("Version")
|
||||
if not isinstance(name, str) or not name or not isinstance(package_version, str) or not package_version:
|
||||
raise SystemExit(f"Python distribution has an invalid identity: {dist_info.name}")
|
||||
normalized_name = name.lower().replace("_", "-")
|
||||
if normalized_name in seen_packages:
|
||||
raise SystemExit(f"duplicate Python distribution metadata: {name}")
|
||||
seen_packages.add(normalized_name)
|
||||
|
||||
license_files = license_file_names(dist_info)
|
||||
license_files.extend(manual_license_files.get(normalized_name, []))
|
||||
license_files.extend(package_license_files.get(normalized_name, []))
|
||||
license_files = sorted(set(license_files))
|
||||
for relative in license_files:
|
||||
relative_regular(runtime_root / relative)
|
||||
if not license_files:
|
||||
raise SystemExit(f"package has no included license material: {name}=={package_version}")
|
||||
|
||||
raw_license = metadata.get("License-Expression") or metadata.get("License")
|
||||
if not raw_license or "\n" in raw_license or len(raw_license) > 200:
|
||||
raw_license = "See included license files"
|
||||
project_urls = []
|
||||
for value in metadata.get_all("Project-URL", []):
|
||||
label, separator, url = value.partition(",")
|
||||
if separator and url.strip().startswith("https://"):
|
||||
project_urls.append((label.strip().lower(), url.strip()))
|
||||
source_url = next(
|
||||
(
|
||||
url
|
||||
for label, url in project_urls
|
||||
if label in {"source", "source code", "repository", "code", "homepage"}
|
||||
),
|
||||
None,
|
||||
)
|
||||
homepage = metadata.get("Home-page")
|
||||
if source_url is None and isinstance(homepage, str) and homepage.startswith("https://"):
|
||||
source_url = homepage
|
||||
if source_url is None:
|
||||
source_url = f"https://pypi.org/project/{name}/{package_version}/"
|
||||
|
||||
python_packages.append(
|
||||
{
|
||||
"license": raw_license,
|
||||
"licenseFiles": license_files,
|
||||
"metadataFile": relative_regular(metadata_path),
|
||||
"name": name,
|
||||
"sourceUrl": source_url,
|
||||
"version": package_version,
|
||||
}
|
||||
)
|
||||
|
||||
license_path_by_id = {
|
||||
"AGPL-3.0": legal_assets["snapotter-agpl"]["path"],
|
||||
"Apache-2.0": legal_assets["apache-2.0"]["path"],
|
||||
}
|
||||
model_objects = [*manifest.get("models", []), *manifest.get("localAssets", [])]
|
||||
models = []
|
||||
for model in sorted(model_objects, key=lambda value: value["id"]):
|
||||
license_name = model.get("license")
|
||||
license_file = license_path_by_id.get(license_name)
|
||||
if license_file is None:
|
||||
raise SystemExit(f"model has no included license text: {model.get('id')}")
|
||||
models.append(
|
||||
{
|
||||
"file": model.get("file") or model.get("source"),
|
||||
"id": model["id"],
|
||||
"license": license_name,
|
||||
"licenseFile": license_file,
|
||||
"path": model["path"],
|
||||
"repository": model.get("repository", "snapotter-hq/SnapOtter"),
|
||||
"revision": model.get("revision", source_commit),
|
||||
"sha256": model["sha256"],
|
||||
"sourceUrl": model.get("url")
|
||||
or f"https://github.com/snapotter-hq/SnapOtter/tree/{source_commit}",
|
||||
}
|
||||
)
|
||||
|
||||
notice = {
|
||||
"modelObjects": models,
|
||||
"project": {
|
||||
"correspondingSource": f"https://github.com/snapotter-hq/SnapOtter/tree/{source_commit}",
|
||||
"license": "AGPL-3.0",
|
||||
"licenseFile": legal_assets["snapotter-agpl"]["path"],
|
||||
"name": "SnapOtter OCR runtime adapter",
|
||||
"sourceCommit": source_commit,
|
||||
"version": version,
|
||||
},
|
||||
"pythonPackages": sorted(python_packages, key=lambda value: value["name"].lower()),
|
||||
"schemaVersion": 1,
|
||||
}
|
||||
(runtime_root / "THIRD_PARTY_NOTICES.json").write_text(
|
||||
json.dumps(notice, sort_keys=True, separators=(",", ":")) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
PY
|
||||
|
||||
echo "Running a network-disabled CPU provider smoke test"
|
||||
SNAPOTTER_NETWORK_DISABLED=1 \
|
||||
SNAPOTTER_ALLOW_MODEL_DOWNLOAD=0 \
|
||||
HF_HUB_OFFLINE=1 \
|
||||
PIP_NO_INDEX=1 \
|
||||
PYTHONNOUSERSITE=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1 \
|
||||
SNAPOTTER_OCR_ARTIFACT_VERSION="${VERSION}" \
|
||||
SNAPOTTER_OCR_PROVIDERS_JSON='["CPUExecutionProvider"]' \
|
||||
SNAPOTTER_OCR_RUNTIME_TARGET="${TARGET}" \
|
||||
SNAPOTTER_RUNTIME_ROOT="${RUNTIME_ROOT}" \
|
||||
"${RUNTIME_ROOT}/venv/bin/python" "${RUNTIME_ROOT}/ocr_runner.py" --smoke
|
||||
|
||||
# Installer policy accepts regular files only and verifies exact modes. Normalize
|
||||
# everything before hashing and archiving.
|
||||
if find "${RUNTIME_ROOT}" ! -type d ! -type f -print -quit | grep -q .; then
|
||||
echo "ERROR: OCR runtime contains a non-regular filesystem entry" >&2
|
||||
exit 2
|
||||
fi
|
||||
find "${RUNTIME_ROOT}" -type d -exec chmod 0755 {} +
|
||||
find "${RUNTIME_ROOT}" -type f -exec chmod 0644 {} +
|
||||
chmod 0755 "${RUNTIME_ROOT}/venv/bin/python"
|
||||
find "${RUNTIME_ROOT}" -exec touch -h -d "@${SOURCE_DATE_EPOCH}" {} +
|
||||
|
||||
ARCHIVE_NAME="ocr-${TARGET}.tar.gz"
|
||||
STAGED_ARCHIVE_PATH="${BUILD_ROOT}/${ARCHIVE_NAME}"
|
||||
ARCHIVE_PATH="${OUTPUT_DIR}/${ARCHIVE_NAME}"
|
||||
|
||||
echo "Creating deterministic archive ${ARCHIVE_NAME}"
|
||||
find "${RUNTIME_ROOT}" -type f -printf '%P\0' \
|
||||
| LC_ALL=C sort -z \
|
||||
| tar \
|
||||
--create \
|
||||
--file=- \
|
||||
--directory="${RUNTIME_ROOT}" \
|
||||
--sort=name \
|
||||
--mtime="@${SOURCE_DATE_EPOCH}" \
|
||||
--owner=0 \
|
||||
--group=0 \
|
||||
--numeric-owner \
|
||||
--format=gnu \
|
||||
--null \
|
||||
--verbatim-files-from \
|
||||
--files-from=- \
|
||||
| gzip -n > "${STAGED_ARCHIVE_PATH}"
|
||||
gzip -t "${STAGED_ARCHIVE_PATH}"
|
||||
tar -tzf "${STAGED_ARCHIVE_PATH}" > /dev/null
|
||||
install -m 0644 "${STAGED_ARCHIVE_PATH}" "${ARCHIVE_PATH}"
|
||||
if [[ "$(sha256sum "${STAGED_ARCHIVE_PATH}" | awk '{print $1}')" != "$(sha256sum "${ARCHIVE_PATH}" | awk '{print $1}')" ]]; then
|
||||
echo "ERROR: OCR archive changed while copying to the output directory" >&2
|
||||
exit 2
|
||||
fi
|
||||
ARCHIVE_SHA256="$(sha256sum "${ARCHIVE_PATH}" | awk '{print $1}')"
|
||||
GENERATION="${VERSION}-${ARCHIVE_SHA256:0:16}"
|
||||
|
||||
ARTIFACT_PATH="${OUTPUT_DIR}/ocr-${TARGET}.artifact.json"
|
||||
INDEX_INPUT_PATH="${OUTPUT_DIR}/ocr-${TARGET}.index-input.json"
|
||||
"${PYTHON_BIN}" - \
|
||||
"${RUNTIME_ROOT}" \
|
||||
"${ARCHIVE_PATH}" \
|
||||
"${ARCHIVE_NAME}" \
|
||||
"${MODELS_MANIFEST}" \
|
||||
"${TARGET}" \
|
||||
"${ARCH}" \
|
||||
"${GENERATION}" \
|
||||
"${VERSION}" \
|
||||
"${ARTIFACT_PATH}" \
|
||||
"${INDEX_INPUT_PATH}" <<'PY'
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
import stat
|
||||
import sys
|
||||
|
||||
(
|
||||
runtime_root_raw,
|
||||
archive_path_raw,
|
||||
archive_name,
|
||||
models_manifest_raw,
|
||||
target,
|
||||
arch,
|
||||
generation,
|
||||
version,
|
||||
artifact_path_raw,
|
||||
index_input_path_raw,
|
||||
) = sys.argv[1:]
|
||||
runtime_root = pathlib.Path(runtime_root_raw)
|
||||
archive_path = pathlib.Path(archive_path_raw)
|
||||
|
||||
|
||||
def sha256(path: pathlib.Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
files = []
|
||||
for path in sorted((path for path in runtime_root.rglob("*") if path.is_file())):
|
||||
info = path.lstat()
|
||||
if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1:
|
||||
raise SystemExit(f"unsafe runtime file: {path}")
|
||||
files.append(
|
||||
{
|
||||
"path": path.relative_to(runtime_root).as_posix(),
|
||||
"sha256": sha256(path),
|
||||
"size": info.st_size,
|
||||
"mode": stat.S_IMODE(info.st_mode),
|
||||
}
|
||||
)
|
||||
|
||||
models_manifest = json.loads(pathlib.Path(models_manifest_raw).read_text(encoding="utf-8"))
|
||||
model_objects = [*models_manifest["models"], *models_manifest.get("localAssets", [])]
|
||||
models = {model["id"]: model["sha256"] for model in model_objects}
|
||||
legal_objects = models_manifest.get("legalAssets", [])
|
||||
artifact = {
|
||||
"family": "ocr",
|
||||
"target": target,
|
||||
"generation": generation,
|
||||
"version": version,
|
||||
"platform": "linux",
|
||||
"arch": arch,
|
||||
"archive": {
|
||||
"file": archive_name,
|
||||
"sha256": sha256(archive_path),
|
||||
"size": archive_path.stat().st_size,
|
||||
"expandedSize": sum(file["size"] for file in files),
|
||||
},
|
||||
"files": files,
|
||||
"runtime": {
|
||||
"pythonPath": "venv/bin/python",
|
||||
"entrypoint": "ocr_runner.py",
|
||||
"adapterPath": "ocr_runtime.py",
|
||||
},
|
||||
"models": models,
|
||||
# Preserve the full immutable source/license record in the signed index,
|
||||
# not only the digest map consumed at runtime.
|
||||
"modelObjects": sorted(model_objects, key=lambda model: model["id"]),
|
||||
"legalMaterials": sorted(legal_objects, key=lambda material: material["id"]),
|
||||
"compatibility": {
|
||||
"protocolVersion": 1,
|
||||
"snapotterVersion": version,
|
||||
},
|
||||
"capabilities": {
|
||||
"qualities": ["balanced", "best"],
|
||||
"providers": ["CPUExecutionProvider"],
|
||||
},
|
||||
"resources": {"minimumMemoryBytes": 4 * 1024 * 1024 * 1024},
|
||||
"provenance": {
|
||||
"builderId": os.environ["SNAPOTTER_OCR_BUILDER_ID"],
|
||||
"sourceCommit": os.environ["SNAPOTTER_OCR_SOURCE_COMMIT"],
|
||||
"sourceImageDigest": f"sha256:{os.environ['SNAPOTTER_OCR_SOURCE_IMAGE_DIGEST']}",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def canonical_write(path: pathlib.Path, value: object) -> None:
|
||||
path.write_text(
|
||||
json.dumps(value, sort_keys=True, separators=(",", ":")) + "\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
canonical_write(pathlib.Path(artifact_path_raw), artifact)
|
||||
canonical_write(pathlib.Path(index_input_path_raw), {"schemaVersion": 1, "artifacts": [artifact]})
|
||||
PY
|
||||
|
||||
"${PYTHON_BIN}" - "${ARCHIVE_PATH}" <<'PY' > "${ARCHIVE_PATH}.sha256"
|
||||
import hashlib
|
||||
import pathlib
|
||||
import sys
|
||||
|
||||
print(hashlib.sha256(pathlib.Path(sys.argv[1]).read_bytes()).hexdigest())
|
||||
PY
|
||||
|
||||
echo "Archive: ${ARCHIVE_PATH}"
|
||||
echo "Artifact: ${ARTIFACT_PATH}"
|
||||
echo "Unsigned canonical index input: ${INDEX_INPUT_PATH}"
|
||||
echo "SHA-256: $(cat "${ARCHIVE_PATH}.sha256")"
|
||||
@@ -64,10 +64,7 @@ def _hf_download(
|
||||
print(f" {label} ready ({size / 1_000_000:.1f} MB)")
|
||||
return dest
|
||||
|
||||
# Force CPU mode during build - no GPU driver available at build time.
|
||||
# Must be set before any ML library import.
|
||||
os.environ["PADDLE_DEVICE"] = "cpu"
|
||||
os.environ["FLAGS_use_cuda"] = "0"
|
||||
# No GPU driver is available during build-time model verification.
|
||||
os.environ["CUDA_VISIBLE_DEVICES"] = ""
|
||||
|
||||
# Prevent ONNX Runtime from loading the CUDA Execution Provider at build time.
|
||||
@@ -189,26 +186,6 @@ REMBG_MODELS = [
|
||||
"birefnet-hr-matting",
|
||||
]
|
||||
|
||||
# PaddleOCR PP-OCRv5 HuggingFace model repos to pre-download.
|
||||
# These are the models used by PaddleOCR(ocr_version="PP-OCRv5").
|
||||
# Downloaded via huggingface_hub to avoid initializing the PaddlePaddle
|
||||
# inference engine, which segfaults under QEMU emulation at build time.
|
||||
PADDLEOCR_MODELS = [
|
||||
"PaddlePaddle/PP-OCRv5_server_det",
|
||||
"PaddlePaddle/PP-OCRv5_server_rec",
|
||||
"PaddlePaddle/PP-OCRv5_mobile_det",
|
||||
"PaddlePaddle/PP-OCRv5_mobile_rec",
|
||||
"PaddlePaddle/latin_PP-OCRv5_mobile_rec",
|
||||
"PaddlePaddle/korean_PP-OCRv5_mobile_rec",
|
||||
"PaddlePaddle/PP-LCNet_x1_0_textline_ori",
|
||||
]
|
||||
|
||||
PADDLEOCR_VL_MODEL = "PaddlePaddle/PaddleOCR-VL-1.5"
|
||||
|
||||
# PaddleX stores models here by default
|
||||
PADDLEX_MODEL_DIR = "/opt/models/paddlex/official_models"
|
||||
|
||||
|
||||
def _register_birefnet_matting():
|
||||
"""Register BiRefNet-matting ONNX session for Ultra quality mode."""
|
||||
import os
|
||||
@@ -407,40 +384,6 @@ def download_codeformer_onnx_model():
|
||||
print(f" CodeFormer ONNX model ready ({size / 1_000_000:.1f} MB)\n")
|
||||
|
||||
|
||||
def download_paddleocr_models():
|
||||
"""Pre-download PaddleOCR PP-OCRv5 model weights from HuggingFace.
|
||||
|
||||
Uses huggingface_hub.snapshot_download() to fetch model files directly
|
||||
into the PaddleX cache directory. This avoids initializing PaddlePaddle's
|
||||
C++ inference engine, which segfaults under QEMU emulation (arm64 host
|
||||
building amd64 image).
|
||||
"""
|
||||
print("=== Downloading PaddleOCR PP-OCRv5 models ===")
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
os.makedirs(PADDLEX_MODEL_DIR, exist_ok=True)
|
||||
|
||||
for repo_id in PADDLEOCR_MODELS:
|
||||
model_name = repo_id.split("/", 1)[1]
|
||||
local_dir = os.path.join(PADDLEX_MODEL_DIR, model_name)
|
||||
print(f" Downloading {model_name}...")
|
||||
snapshot_download(repo_id=repo_id, local_dir=local_dir)
|
||||
print(f" {model_name} ready")
|
||||
print(f"All {len(PADDLEOCR_MODELS)} PaddleOCR PP-OCRv5 models downloaded.\n")
|
||||
|
||||
|
||||
def download_paddleocr_vl_model():
|
||||
"""Pre-download PaddleOCR-VL 1.5 model weights from HuggingFace."""
|
||||
print("=== Downloading PaddleOCR-VL 1.5 model ===")
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
model_name = PADDLEOCR_VL_MODEL.split("/", 1)[1]
|
||||
local_dir = os.path.join(PADDLEX_MODEL_DIR, model_name)
|
||||
print(f" Downloading {model_name} (~1.93 GB)...")
|
||||
snapshot_download(repo_id=PADDLEOCR_VL_MODEL, local_dir=local_dir)
|
||||
print(f" {model_name} ready\n")
|
||||
|
||||
|
||||
def download_scunet_model():
|
||||
"""Download SCUNet real-noise denoising model."""
|
||||
print(f"Downloading SCUNet model to {SCUNET_MODEL_PATH}...")
|
||||
@@ -582,7 +525,7 @@ def verify_mediapipe():
|
||||
def smoke_test():
|
||||
"""Final verification that all ML libraries and models are loadable.
|
||||
|
||||
GPU-dependent libraries (paddlepaddle-gpu, torch CUDA) cannot be imported
|
||||
GPU-dependent libraries such as the CUDA build of torch cannot be imported
|
||||
at build time because the CUDA driver is only available at runtime. We verify
|
||||
CPU-only imports and check that model files exist on disk.
|
||||
"""
|
||||
@@ -663,19 +606,6 @@ def smoke_test():
|
||||
assert os.path.getsize(NAFNET_MODEL_PATH) > NAFNET_MIN_SIZE
|
||||
print(" NAFNet model file verified")
|
||||
|
||||
# PaddleOCR model directories must exist
|
||||
for repo_id in PADDLEOCR_MODELS:
|
||||
model_name = repo_id.split("/", 1)[1]
|
||||
model_dir = os.path.join(PADDLEX_MODEL_DIR, model_name)
|
||||
assert os.path.isdir(model_dir), f"PaddleOCR model missing: {model_dir}"
|
||||
print(f" PaddleOCR models verified ({len(PADDLEOCR_MODELS)} models)")
|
||||
|
||||
# PaddleOCR-VL model directory must exist
|
||||
vl_name = PADDLEOCR_VL_MODEL.split("/", 1)[1]
|
||||
vl_dir = os.path.join(PADDLEX_MODEL_DIR, vl_name)
|
||||
assert os.path.isdir(vl_dir), f"PaddleOCR-VL model missing: {vl_dir}"
|
||||
print(" PaddleOCR-VL model verified")
|
||||
|
||||
# MediaPipe task models must exist (for mp.tasks fallback)
|
||||
assert os.path.exists(FACE_DETECT_MODEL_PATH), (
|
||||
f"MediaPipe face detector model missing: {FACE_DETECT_MODEL_PATH}"
|
||||
@@ -733,8 +663,6 @@ def main():
|
||||
download_codeformer_model,
|
||||
download_ddcolor_model,
|
||||
download_codeformer_onnx_model,
|
||||
download_paddleocr_models,
|
||||
download_paddleocr_vl_model,
|
||||
download_scunet_model,
|
||||
download_nafnet_model,
|
||||
download_facexlib_models,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
# Keep persistent runtime state private from unrelated users while making every
|
||||
# creation group-writable from its first inode state. This is required for
|
||||
# OpenShift/fsGroup rolling restarts where successive pods use different UIDs.
|
||||
umask 0007
|
||||
|
||||
# Shared permission helpers (dir_writable, ensure_writable). Lives beside this
|
||||
# script in the image; sourcing only defines functions (no side effects).
|
||||
. /usr/local/bin/entrypoint-lib.sh
|
||||
@@ -249,8 +254,8 @@ if [ "$(id -u)" = "0" ]; then
|
||||
# External mode: tini becomes PID 1 (reaps zombies, forwards signals) and runs
|
||||
# the app as snapotter, the same end state as the prior tini ENTRYPOINT.
|
||||
# gosu preserves the environment (unlike su), so HOME=/root would survive the
|
||||
# drop; set a writable HOME or libraries that write under ~ (PaddleOCR's
|
||||
# ~/.paddlex, plus the sidecar's expanduser caches) fail with EACCES.
|
||||
# drop; set a writable HOME or sidecar libraries using expanduser caches fail
|
||||
# with EACCES.
|
||||
export HOME=/data/.home
|
||||
exec tini -- gosu snapotter "$@"
|
||||
fi
|
||||
|
||||
@@ -426,79 +426,38 @@
|
||||
},
|
||||
"ocr": {
|
||||
"name": "OCR",
|
||||
"description": "Extract text from images",
|
||||
"estimatedSize": "3-4 GB",
|
||||
"archives": {
|
||||
"amd64-gpu": {
|
||||
"file": "v2.0.0/ocr-amd64-gpu.tar.gz",
|
||||
"sha256": "2a00a3184f6a635f1fa9ae2a6517ad740a11f9e5ff58c098d2fd369a2bb1e16b",
|
||||
"compressedSize": 5933794814,
|
||||
"extractedSize": 9343914078
|
||||
"description": "High-accuracy local OCR for images and PDFs",
|
||||
"estimatedSize": "~208-234 MiB download / ~409-488 MiB installed",
|
||||
"runtimeFormatVersion": 3,
|
||||
"runtimeFamily": "ocr",
|
||||
"modelManifest": "docker/ocr-runtime-models.json",
|
||||
"targets": {
|
||||
"linux-amd64-cpu-py312": {
|
||||
"file": "v3/ocr-linux-amd64-cpu-py312.tar.gz",
|
||||
"compressedSizeEstimate": 244622462,
|
||||
"extractedSizeEstimate": 510762511,
|
||||
"sizeKind": "measured-estimate",
|
||||
"minimumMemoryBytes": 4294967296,
|
||||
"requirements": "docker/ocr-runtime-requirements-amd64.txt"
|
||||
},
|
||||
"arm64-cpu": {
|
||||
"file": "v2.0.0/ocr-arm64-cpu.tar.gz",
|
||||
"sha256": "6868c264069dcb74c6675c0b1f58dc1c9f60d9aa4459725e3dbde07a99a6a09a",
|
||||
"compressedSize": 1984611355,
|
||||
"extractedSize": 3103671712
|
||||
"linux-arm64-cpu-py311": {
|
||||
"file": "v3/ocr-linux-arm64-cpu-py311.tar.gz",
|
||||
"compressedSizeEstimate": 218031562,
|
||||
"extractedSizeEstimate": 428565655,
|
||||
"sizeKind": "measured-estimate",
|
||||
"minimumMemoryBytes": 4294967296,
|
||||
"requirements": "docker/ocr-runtime-requirements-arm64.txt"
|
||||
}
|
||||
},
|
||||
"packages": {
|
||||
"common": ["huggingface-hub[hf_xet]==0.36.2"],
|
||||
"amd64": [
|
||||
"paddlepaddle-gpu>=3.2.1 --index-url https://www.paddlepaddle.org.cn/packages/stable/cu126/ --extra-index-url https://pypi.org/simple/",
|
||||
"paddleocr[doc-parser]>=3.4.0,<3.5.0"
|
||||
],
|
||||
"arm64": [
|
||||
"paddlepaddle>=3.2.1",
|
||||
"paddleocr[doc-parser]>=3.4.0,<3.5.0",
|
||||
"protobuf>=4.25.3,<5"
|
||||
]
|
||||
"common": ["rapidocr==3.9.1", "onnxruntime==1.20.1"],
|
||||
"amd64": [],
|
||||
"arm64": []
|
||||
},
|
||||
"pipFlags": {},
|
||||
"postInstall": [],
|
||||
"models": [
|
||||
{
|
||||
"id": "paddleocr-server-det",
|
||||
"downloadFn": "hf_snapshot",
|
||||
"args": ["PaddlePaddle/PP-OCRv5_server_det", "PP-OCRv5_server_det"]
|
||||
},
|
||||
{
|
||||
"id": "paddleocr-server-rec",
|
||||
"downloadFn": "hf_snapshot",
|
||||
"args": ["PaddlePaddle/PP-OCRv5_server_rec", "PP-OCRv5_server_rec"]
|
||||
},
|
||||
{
|
||||
"id": "paddleocr-mobile-det",
|
||||
"downloadFn": "hf_snapshot",
|
||||
"args": ["PaddlePaddle/PP-OCRv5_mobile_det", "PP-OCRv5_mobile_det"]
|
||||
},
|
||||
{
|
||||
"id": "paddleocr-mobile-rec",
|
||||
"downloadFn": "hf_snapshot",
|
||||
"args": ["PaddlePaddle/PP-OCRv5_mobile_rec", "PP-OCRv5_mobile_rec"]
|
||||
},
|
||||
{
|
||||
"id": "paddleocr-latin-rec",
|
||||
"downloadFn": "hf_snapshot",
|
||||
"args": ["PaddlePaddle/latin_PP-OCRv5_mobile_rec", "latin_PP-OCRv5_mobile_rec"]
|
||||
},
|
||||
{
|
||||
"id": "paddleocr-korean-rec",
|
||||
"downloadFn": "hf_snapshot",
|
||||
"args": ["PaddlePaddle/korean_PP-OCRv5_mobile_rec", "korean_PP-OCRv5_mobile_rec"]
|
||||
},
|
||||
{
|
||||
"id": "paddleocr-textline-ori",
|
||||
"downloadFn": "hf_snapshot",
|
||||
"args": ["PaddlePaddle/PP-LCNet_x1_0_textline_ori", "PP-LCNet_x1_0_textline_ori"]
|
||||
},
|
||||
{
|
||||
"id": "paddleocr-vl",
|
||||
"downloadFn": "hf_snapshot",
|
||||
"args": ["PaddlePaddle/PaddleOCR-VL-1.5", "PaddleOCR-VL-1.5"]
|
||||
}
|
||||
],
|
||||
"smokeImports": ["paddle", "paddleocr"],
|
||||
"models": [],
|
||||
"smokeImports": ["rapidocr", "onnxruntime"],
|
||||
"enablesTools": ["ocr", "ocr-pdf"]
|
||||
},
|
||||
"transcription": {
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Credentialed HuggingFace release tooling. Compile for CPython 3.11/Linux with:
|
||||
# uv pip compile docker/hf-release-requirements.in \
|
||||
# --python-version 3.11 --python-platform x86_64-manylinux_2_17 \
|
||||
# --generate-hashes --output-file docker/hf-release-requirements.txt
|
||||
huggingface_hub[hf_xet]==0.36.2
|
||||
@@ -0,0 +1,239 @@
|
||||
# This file was autogenerated by uv via the following command:
|
||||
# uv pip compile docker/hf-release-requirements.in --python-version 3.11 --python-platform x86_64-manylinux_2_17 --generate-hashes --output-file docker/hf-release-requirements.txt
|
||||
certifi==2026.6.17 \
|
||||
--hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \
|
||||
--hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db
|
||||
# via requests
|
||||
charset-normalizer==3.4.9 \
|
||||
--hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \
|
||||
--hash=sha256:03d07803992c6c7bbc976327f34b18b6160327fc81cb82c9d504720ac0be3b62 \
|
||||
--hash=sha256:04ce310cb89c15df659582aee80a0603788732a5e017d5bd5c81158106ce249c \
|
||||
--hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \
|
||||
--hash=sha256:0e94703ec9684807f20cfb5eed95c70f67f2a8f21ad620146d7b5a13677b93e5 \
|
||||
--hash=sha256:0fa1aec2d32bcc03c8fa0f6f1712caad1adc38509f31142112e5c9daf5b9c833 \
|
||||
--hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \
|
||||
--hash=sha256:16d10d789dd9bcca1173c95af82c58433122564b7bc39385124be735a35cbe99 \
|
||||
--hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \
|
||||
--hash=sha256:1d22856ffbe153a602df38e4a5464f0b748a54002e0d69ac6d2ad0a197cc99ec \
|
||||
--hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \
|
||||
--hash=sha256:231ddcbb35e2ff8973e1365db41fe0572662893b99a05deb183b68ad4c0c8bd4 \
|
||||
--hash=sha256:253a4a220747e8b5faf57ec320c4f5efb0cef05f647420bf267143ec15dba10a \
|
||||
--hash=sha256:280081916dc341820640489a66e4696049401ef1cf6dd672f672e70ad915aca3 \
|
||||
--hash=sha256:2a441ea71902098ffe78c5abe6c494f44160b4af614ed16c3d9a3b1d17fd8ee2 \
|
||||
--hash=sha256:304b13570067b2547562e308af560b3963857b1fa90bd6afd978130130fe2d6a \
|
||||
--hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \
|
||||
--hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \
|
||||
--hash=sha256:375b83ed0aecfce76c16d198fbc21f3b11b337d68662bea0a995046682a11419 \
|
||||
--hash=sha256:3c09a49d6cde137258beb3d551994a2927fd35ad5cf96aed573f61bbd67c5f84 \
|
||||
--hash=sha256:3d92613ec25e43b05f042302531ec0f00b8445190e43325880cbd6ab7c2581da \
|
||||
--hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \
|
||||
--hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \
|
||||
--hash=sha256:432786d3561e69aeeae6c7e8648964ce0ad05736120135601f87ac26b9c83381 \
|
||||
--hash=sha256:43b9e366a31fdd1c87d0eb08f579b4a82b723ea54338f040d6b4e518a026ea29 \
|
||||
--hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \
|
||||
--hash=sha256:45b0cc4e3556cd875e09102988d1ab8356c998b596c9fced84547c8138b487a0 \
|
||||
--hash=sha256:476743fe6dfe14a2da12e3ac79125dc84a3b2cf8094369a47a1529b0cd8549fe \
|
||||
--hash=sha256:4773092f8019072343a7447203308b176e10199920eb02d6195e81bbb3274c29 \
|
||||
--hash=sha256:4b3dac63058cc36820b0dd072f89898604e2d39686fe05321729d00d8ac185a0 \
|
||||
--hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \
|
||||
--hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \
|
||||
--hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \
|
||||
--hash=sha256:58150c9f9b9a552505912d182ccdf26f6396fb6094816ceebcbb20eecabaed94 \
|
||||
--hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \
|
||||
--hash=sha256:5e226f6218febc71f6c1fc2fafb91c226f75bdc1d8fb12d66823716e891608fd \
|
||||
--hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \
|
||||
--hash=sha256:60f44ade2cf573dad7a277e6f8ca9a51a21dda572b13bd7d8539bb3cd5dbedde \
|
||||
--hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \
|
||||
--hash=sha256:6366a16e1a25018694d6a5d784d09b046edc9eac40ea2b54065c3052672516a1 \
|
||||
--hash=sha256:65a7ff3f705e57d392f7261b6d0550fe137c3019477431f1c355e0db0a7d3e15 \
|
||||
--hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \
|
||||
--hash=sha256:67830fc78e67501f47bb950471b2dcb9b35b140084429318e862895a8e89c993 \
|
||||
--hash=sha256:68ce9f4d6b26d5ccbf7fd4459bf75f74a0a146677ebba80597df60cbdb20e6f4 \
|
||||
--hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \
|
||||
--hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \
|
||||
--hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \
|
||||
--hash=sha256:78841cccf1af7b40f6f716338d50c0902dbe88d9f800b3c973b7a9a0a693a642 \
|
||||
--hash=sha256:78fa18e436a1a0e58dbd7e02fc4473f3f32cceb12df9dfca542d075961c307d2 \
|
||||
--hash=sha256:79580094b00d1789d1f93ea55bc43cb2f611910c72235b7657f3482ddcc1b22d \
|
||||
--hash=sha256:7b86a2b16095d250c6f58b3d9b2eee6f4147754344f3dab0922f7c9bf7d226c9 \
|
||||
--hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \
|
||||
--hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \
|
||||
--hash=sha256:871ff67ea1aad4dfd91736464934d56b32dac49f9fbe16cddba36198a7b3a0db \
|
||||
--hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \
|
||||
--hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \
|
||||
--hash=sha256:8c041122946b7ba21bb32c45b1aa57b1be35527690aeb3c5c234521085632eee \
|
||||
--hash=sha256:90c44bc373b7687f6948b693cceaea1348ae0975d7474746559494468e3c1d84 \
|
||||
--hash=sha256:9104ed0bd76a429d46f9ec0dbc9b08ad1d2dcdf2b00a5a0daa1c145329b35b44 \
|
||||
--hash=sha256:920079c3f7456fa213e0829ed2073aaa727fd39d889ead5b4f35d0de5460d04f \
|
||||
--hash=sha256:93d59d504b230e83c7a843251681959a0b6a9cd76f6e146ce1b8a80eb8739af9 \
|
||||
--hash=sha256:9b2aff1c7b3884512b9512c3eaadd9bab39fb45042ffaaa1dd08ff2b9f8109d9 \
|
||||
--hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \
|
||||
--hash=sha256:9bb41182d93ea91f60b4bc8fbf4c820c69ef8a12ab2d917f3f1834f1acad07e8 \
|
||||
--hash=sha256:9cdef90ae47919cae358d8ab15797a800ed41da7aba5d72419fb510729e2ed4b \
|
||||
--hash=sha256:a1786910334ed46ab1dd73222f2cd1e05c2c3bb39f6dddb4f8b36fc382058a39 \
|
||||
--hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \
|
||||
--hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \
|
||||
--hash=sha256:aa99adc8f081b475a12843953db36831eaf83ec33eb46a90629ca6a5de45a616 \
|
||||
--hash=sha256:ac351b3b8014eead140e77e9717e2992c6bbe30b63bc3422422eb84865412e3d \
|
||||
--hash=sha256:ad41ba96094304aa090f5a30cb6e4fb3b3f1c264c523394b4c39bbacc4dc92ba \
|
||||
--hash=sha256:b5314963fce9b0b12743891de876e724997864ee22aa496f903f426c7e2fa5b2 \
|
||||
--hash=sha256:bcf74c1df76758a395bf0af608c04c82257523f55c9868b334f06270d0f2112b \
|
||||
--hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \
|
||||
--hash=sha256:c0323c9daef75ef2e5083624b4585018a0c9d5e3b40f607eed81a311270b934b \
|
||||
--hash=sha256:c1225416b463483160e4af85d5fc3a9690ccb53fd4b1865a6437825f5ede3209 \
|
||||
--hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \
|
||||
--hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \
|
||||
--hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \
|
||||
--hash=sha256:cd6280cf040f233bd7d3407b743b4b4c74f70e8e1c4199cb112a62c941c0772a \
|
||||
--hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \
|
||||
--hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \
|
||||
--hash=sha256:ddf4af30b417d9fe16481e9b81c27ab2a7cde1ff7ba3e85653b02db7d145dc7b \
|
||||
--hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \
|
||||
--hash=sha256:df7276909358e5635ae203673ab7e509ddd224225a8d6b0790bf13eb2bde1cc5 \
|
||||
--hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \
|
||||
--hash=sha256:e9701d0049d92c16703a42771b98d560b95248949f23f8cf7b4eddd201814fb9 \
|
||||
--hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \
|
||||
--hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \
|
||||
--hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \
|
||||
--hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \
|
||||
--hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \
|
||||
--hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115
|
||||
# via requests
|
||||
filelock==3.29.7 \
|
||||
--hash=sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d \
|
||||
--hash=sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51
|
||||
# via huggingface-hub
|
||||
fsspec==2026.6.0 \
|
||||
--hash=sha256:02e0b71817df9b2169dc30a16832045764def1191b43dcff5bb85bdee212d2a1 \
|
||||
--hash=sha256:f5bac145310fe30e16e1471bd6840b2d990d609e872251d7e674241822abf01a
|
||||
# via huggingface-hub
|
||||
hf-xet==1.5.1 \
|
||||
--hash=sha256:0c97106032ef70467b4f6bc2d0ccc266d7613ee076afc56516c502f87ce1c4a6 \
|
||||
--hash=sha256:3474760d10e3bb6f92ff3f024fcb00c0b3e4001e9b035c7483e49a5dd17aa70f \
|
||||
--hash=sha256:4f561cbbb92f80960772059864b7fb07eae879adde1b2e781ec6f86f6ac26c59 \
|
||||
--hash=sha256:51ef4500dab3764b41135ee1381a4b62ce56fc54d4c92b719b59e597d6df5bf6 \
|
||||
--hash=sha256:6071d5ccb4d8d2cbd5fea5cc798da4f0ba3f44e25369591c4e89a4987050e61d \
|
||||
--hash=sha256:6208adb15d192b90e4c2ad2a27ed864359b2cb0f2494eb6d7c7f3699ac02e2bf \
|
||||
--hash=sha256:6762d89b9e3267dfd502b29b2a327b4525f33b17e7b509a78d94e2151a30ce30 \
|
||||
--hash=sha256:6abd35c3221eff63836618ddfb954dcf84798603f71d8e33e3ed7b04acfdbe6e \
|
||||
--hash=sha256:6f7a04a8ad962422e225bc49fbbac99dc1806764b1f3e54dbd154bffa7593947 \
|
||||
--hash=sha256:8298485c1e36e7e67cbd01eeb1376619b7af43d4f1ec245caae306f890a8a32d \
|
||||
--hash=sha256:892e3a3a3aecc12aded8b93cf4f9cd059282c7de0732f7d55026f3abdf474350 \
|
||||
--hash=sha256:93d090b57b211133f6c0dab0205ef5cb6d89162979ba75a74845045cc3063b8e \
|
||||
--hash=sha256:94e761bbd266bf4c03cee73753916062665ce8365aa40ed321f45afcb934b41e \
|
||||
--hash=sha256:97f212a88d14bbf573619a74b7fecb238de77d08fc702e54dec6f78276ca3283 \
|
||||
--hash=sha256:a93df2039190502835b1db8cd7e178b0b7b889fe9ab51299d5ced26e0dd879a4 \
|
||||
--hash=sha256:bf67e6ed10260cef62e852789dc91ebb03f382d5bdc4b1dbeb64763ea275e7d6 \
|
||||
--hash=sha256:c6b6cd08ca095058780b50b8ce4d6cbf6787bcf27841705d58a9d32246e3e47a \
|
||||
--hash=sha256:d48199c2bf4f8df0adc55d31d1368b6ec0e4d4f45bc86b08038089c23db0bed8 \
|
||||
--hash=sha256:dbf48c0d02cf0b2e568944330c60d9120c272dabe013bd892d48e25bc6797577 \
|
||||
--hash=sha256:e1af0de8ca6f190d4294a28b88023db64a1e2d1d719cab044baf75bec569e7a9 \
|
||||
--hash=sha256:e78e4e5192ad2b674c2e1160b651cb9134db974f8ae1835bdfbfb0166b894a43 \
|
||||
--hash=sha256:e7dbb40617410f432182d918e37c12303fe6700fd6aa6c5964e30a535a4461d6 \
|
||||
--hash=sha256:f4ad3ebd4c32dd2b27099d69dc7b2df821e30767e46fb6ee6a0713778243b8ff \
|
||||
--hash=sha256:f61e3665892a6c8c5e765395838b8ddf36185da835253d4bc4509a81e49fb342 \
|
||||
--hash=sha256:f7b3002f95d1c13e24bcb4537baa8f0eb3838957067c91bb4959bc004a6435f5
|
||||
# via huggingface-hub
|
||||
huggingface-hub==0.36.2 \
|
||||
--hash=sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a \
|
||||
--hash=sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270
|
||||
# via -r docker/hf-release-requirements.in
|
||||
idna==3.18 \
|
||||
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
|
||||
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
|
||||
# via requests
|
||||
packaging==26.2 \
|
||||
--hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \
|
||||
--hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661
|
||||
# via huggingface-hub
|
||||
pyyaml==6.0.3 \
|
||||
--hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \
|
||||
--hash=sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a \
|
||||
--hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \
|
||||
--hash=sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956 \
|
||||
--hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \
|
||||
--hash=sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c \
|
||||
--hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \
|
||||
--hash=sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a \
|
||||
--hash=sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0 \
|
||||
--hash=sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b \
|
||||
--hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \
|
||||
--hash=sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6 \
|
||||
--hash=sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7 \
|
||||
--hash=sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e \
|
||||
--hash=sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007 \
|
||||
--hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \
|
||||
--hash=sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4 \
|
||||
--hash=sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9 \
|
||||
--hash=sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295 \
|
||||
--hash=sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea \
|
||||
--hash=sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0 \
|
||||
--hash=sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e \
|
||||
--hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \
|
||||
--hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \
|
||||
--hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \
|
||||
--hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \
|
||||
--hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \
|
||||
--hash=sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b \
|
||||
--hash=sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69 \
|
||||
--hash=sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5 \
|
||||
--hash=sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b \
|
||||
--hash=sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c \
|
||||
--hash=sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369 \
|
||||
--hash=sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd \
|
||||
--hash=sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824 \
|
||||
--hash=sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198 \
|
||||
--hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \
|
||||
--hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \
|
||||
--hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \
|
||||
--hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \
|
||||
--hash=sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196 \
|
||||
--hash=sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b \
|
||||
--hash=sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00 \
|
||||
--hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \
|
||||
--hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \
|
||||
--hash=sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e \
|
||||
--hash=sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28 \
|
||||
--hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \
|
||||
--hash=sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5 \
|
||||
--hash=sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4 \
|
||||
--hash=sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b \
|
||||
--hash=sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf \
|
||||
--hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \
|
||||
--hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \
|
||||
--hash=sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8 \
|
||||
--hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \
|
||||
--hash=sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da \
|
||||
--hash=sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d \
|
||||
--hash=sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc \
|
||||
--hash=sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c \
|
||||
--hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \
|
||||
--hash=sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f \
|
||||
--hash=sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917 \
|
||||
--hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \
|
||||
--hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \
|
||||
--hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \
|
||||
--hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \
|
||||
--hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \
|
||||
--hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \
|
||||
--hash=sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3 \
|
||||
--hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \
|
||||
--hash=sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926 \
|
||||
--hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0
|
||||
# via huggingface-hub
|
||||
requests==2.34.2 \
|
||||
--hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \
|
||||
--hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed
|
||||
# via huggingface-hub
|
||||
tqdm==4.68.4 \
|
||||
--hash=sha256:19829c9673638f2a0b8617da4cdcb927e831cd88bcfcb6e78d42a4d1af131520 \
|
||||
--hash=sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2
|
||||
# via huggingface-hub
|
||||
typing-extensions==4.16.0 \
|
||||
--hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \
|
||||
--hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5
|
||||
# via huggingface-hub
|
||||
urllib3==2.7.0 \
|
||||
--hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \
|
||||
--hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
# via requests
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"selectorVersion": "best-v1",
|
||||
"weights": {
|
||||
"confidenceCoverage": 0.65,
|
||||
"meanConfidence": 0.15,
|
||||
"polygonCoherence": 0.1,
|
||||
"readingOrderCoherence": 0.1
|
||||
},
|
||||
"variantPolicy": {
|
||||
"orientationMinConfidence": 0.75,
|
||||
"orientationMinScoreGain": 0.04,
|
||||
"preprocessMinScoreGain": 0.06
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"family": "ocr",
|
||||
"legalAssets": [
|
||||
{
|
||||
"id": "snapotter-agpl",
|
||||
"source": "LICENSE",
|
||||
"sourceUrl": "https://github.com/snapotter-hq/SnapOtter",
|
||||
"path": "LICENSES/SnapOtter-AGPL-3.0.txt",
|
||||
"sha256": "0d96a4ff68ad6d4b6f1f30f713b18d5184912ba8dd389f86aa7710db079abcb0",
|
||||
"size": 34523,
|
||||
"license": "AGPL-3.0"
|
||||
},
|
||||
{
|
||||
"id": "apache-2.0",
|
||||
"url": "https://www.apache.org/licenses/LICENSE-2.0.txt",
|
||||
"sourceUrl": "https://www.apache.org/licenses/LICENSE-2.0",
|
||||
"path": "THIRD_PARTY_LICENSES/Apache-2.0.txt",
|
||||
"sha256": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
|
||||
"size": 11358,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"id": "antlr-4.9.3-license",
|
||||
"url": "https://raw.githubusercontent.com/antlr/antlr4/4.9.3/LICENSE.txt",
|
||||
"sourceUrl": "https://github.com/antlr/antlr4/tree/4.9.3",
|
||||
"path": "THIRD_PARTY_LICENSES/ANTLR-4.9.3.txt",
|
||||
"sha256": "b1b379fcaf3219593a4c433feb1b35c780bed23fafaae440b1ae2771a9521e3a",
|
||||
"size": 2699,
|
||||
"license": "BSD-3-Clause"
|
||||
}
|
||||
],
|
||||
"localAssets": [
|
||||
{
|
||||
"id": "best-v1-calibration",
|
||||
"source": "docker/ocr-best-v1-calibration.json",
|
||||
"path": "models/best-v1-calibration.json",
|
||||
"sha256": "e481678c7979347059a1f48df3f9e306016eab733d148e2241d00e4868576576",
|
||||
"size": 335,
|
||||
"license": "AGPL-3.0"
|
||||
}
|
||||
],
|
||||
"models": [
|
||||
{
|
||||
"id": "pp-ocrv6-small-det",
|
||||
"repository": "PaddlePaddle/PP-OCRv6_small_det_onnx",
|
||||
"revision": "28fe5895c24fd108c19eb3e8479f4ab385fbfc62",
|
||||
"file": "inference.onnx",
|
||||
"url": "https://huggingface.co/PaddlePaddle/PP-OCRv6_small_det_onnx/resolve/28fe5895c24fd108c19eb3e8479f4ab385fbfc62/inference.onnx",
|
||||
"path": "models/PP-OCRv6_det_small.onnx",
|
||||
"sha256": "d73e0058b7a8086bbd57f3d10b8bcd4ff95363f67e06e2762b5e814fe9c9410e",
|
||||
"size": 9880512,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"id": "pp-ocrv6-small-rec",
|
||||
"repository": "PaddlePaddle/PP-OCRv6_small_rec_onnx",
|
||||
"revision": "b8f84f0b80c529de40b4fbb3544b84fa7233a513",
|
||||
"file": "inference.onnx",
|
||||
"url": "https://huggingface.co/PaddlePaddle/PP-OCRv6_small_rec_onnx/resolve/b8f84f0b80c529de40b4fbb3544b84fa7233a513/inference.onnx",
|
||||
"path": "models/PP-OCRv6_rec_small.onnx",
|
||||
"sha256": "5435fd747c9e0efe15a96d0b378d5bd157e9492ed8fd80edf08f30d02fa24634",
|
||||
"size": 21159378,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"id": "pp-ocrv6-medium-det",
|
||||
"repository": "PaddlePaddle/PP-OCRv6_medium_det_onnx",
|
||||
"revision": "61323801669c338b7891481ec7bac61ce31b576a",
|
||||
"file": "inference.onnx",
|
||||
"url": "https://huggingface.co/PaddlePaddle/PP-OCRv6_medium_det_onnx/resolve/61323801669c338b7891481ec7bac61ce31b576a/inference.onnx",
|
||||
"path": "models/PP-OCRv6_det_medium.onnx",
|
||||
"sha256": "eb13b44b25bb36f89528b68720af8a61d9cf381176107f465db1757b65d086e1",
|
||||
"size": 62032837,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"id": "pp-ocrv6-medium-rec",
|
||||
"repository": "PaddlePaddle/PP-OCRv6_medium_rec_onnx",
|
||||
"revision": "50c7eacafc52fa7bcf4194e8cd08e46f8558504b",
|
||||
"file": "inference.onnx",
|
||||
"url": "https://huggingface.co/PaddlePaddle/PP-OCRv6_medium_rec_onnx/resolve/50c7eacafc52fa7bcf4194e8cd08e46f8558504b/inference.onnx",
|
||||
"path": "models/PP-OCRv6_rec_medium.onnx",
|
||||
"sha256": "9c09abf0957f7968c7586464b7397b84ad2387a0497a351af40e9acc71b673ba",
|
||||
"size": 76554979,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"id": "korean-pp-ocrv5-mobile-rec",
|
||||
"repository": "PaddlePaddle/korean_PP-OCRv5_mobile_rec_onnx",
|
||||
"revision": "5c6f574b8e2230adf4287b33e736d71b9fabd28e",
|
||||
"file": "inference.onnx",
|
||||
"url": "https://huggingface.co/PaddlePaddle/korean_PP-OCRv5_mobile_rec_onnx/resolve/5c6f574b8e2230adf4287b33e736d71b9fabd28e/inference.onnx",
|
||||
"path": "models/korean_PP-OCRv5_mobile_rec.onnx",
|
||||
"sha256": "92f0b7785e64fc9090106a241cf4c1eb97472824558272751b88a2a4476d3a08",
|
||||
"size": 13418787,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"id": "pp-lcnet-x0-25-textline-ori",
|
||||
"repository": "PaddlePaddle/PP-LCNet_x0_25_textline_ori_onnx",
|
||||
"revision": "aea1f18d97338aaf84b463f90192f2820524a00a",
|
||||
"file": "inference.onnx",
|
||||
"url": "https://huggingface.co/PaddlePaddle/PP-LCNet_x0_25_textline_ori_onnx/resolve/aea1f18d97338aaf84b463f90192f2820524a00a/inference.onnx",
|
||||
"path": "models/PP-LCNet_x0_25_textline_ori.onnx",
|
||||
"sha256": "94a6a0a0425f2b5f08b5df72086f2d72abe40f1d22f6d12d2cd83674f11f2ff3",
|
||||
"size": 1019454,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"id": "pp-lcnet-x1-0-doc-ori",
|
||||
"repository": "PaddlePaddle/PP-LCNet_x1_0_doc_ori_onnx",
|
||||
"revision": "7330ab7039123e46af2dc03154b9969aa412c61d",
|
||||
"file": "inference.onnx",
|
||||
"url": "https://huggingface.co/PaddlePaddle/PP-LCNet_x1_0_doc_ori_onnx/resolve/7330ab7039123e46af2dc03154b9969aa412c61d/inference.onnx",
|
||||
"path": "models/PP-LCNet_x1_0_doc_ori.onnx",
|
||||
"sha256": "af9a0a4f317ff0709ce752067807f819cb15d883f8ecad89f28df1c6ee2d9c92",
|
||||
"size": 6788069,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"id": "pp-ocrv6-dictionary",
|
||||
"repository": "RapidAI/RapidOCR",
|
||||
"revision": "49043b4cb8eaf8388a6399394ebdc014851c217c",
|
||||
"file": "paddle/PP-OCRv6/rec/PP-OCRv6_rec_small/ppocrv6_dict.txt",
|
||||
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/49043b4cb8eaf8388a6399394ebdc014851c217c/paddle/PP-OCRv6/rec/PP-OCRv6_rec_small/ppocrv6_dict.txt",
|
||||
"path": "models/ppocrv6_dict.txt",
|
||||
"sha256": "b5f2bfe2bdd9448429e3e82b51c789775d9b42f2403d082b00662eb77e401c5d",
|
||||
"size": 74947,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
{
|
||||
"id": "korean-pp-ocrv5-dictionary",
|
||||
"repository": "RapidAI/RapidOCR",
|
||||
"revision": "49043b4cb8eaf8388a6399394ebdc014851c217c",
|
||||
"file": "paddle/PP-OCRv5/rec/korean_PP-OCRv5_rec_mobile/ppocrv5_korean_dict.txt",
|
||||
"url": "https://www.modelscope.cn/models/RapidAI/RapidOCR/resolve/49043b4cb8eaf8388a6399394ebdc014851c217c/paddle/PP-OCRv5/rec/korean_PP-OCRv5_rec_mobile/ppocrv5_korean_dict.txt",
|
||||
"path": "models/korean_dict.txt",
|
||||
"sha256": "a88071c68c01707489baa79ebe0405b7beb5cca229f4fc94cc3ef992328802d7",
|
||||
"size": 47451,
|
||||
"license": "Apache-2.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# SnapOtter OCR runtime: linux-amd64-cpu-py312
|
||||
# Every hash identifies the one Linux wheel used by this target. The sole
|
||||
# source distribution (antlr4-python3-runtime) is built offline from its
|
||||
# hash-pinned sdist by build-ocr-runtime.sh.
|
||||
rapidocr==3.9.1 --hash=sha256:600885e4e94e0b427abad394fccb0ec1d3c9118a215ca435bf7680aeae0e292b
|
||||
onnxruntime==1.20.1 --hash=sha256:bb71a814f66517a65628c9e4a2bb530a6edd2cd5d87ffa0af0f6f773a027d99e
|
||||
numpy==1.26.4 --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed
|
||||
opencv-python-headless==4.10.0.84 --hash=sha256:377d08a7e48a1405b5e84afcbe4798464ce7ee17081c1c23619c8b398ff18295
|
||||
pyclipper==1.3.0.post6 --hash=sha256:58eae2ff92a8cae1331568df076c4c5775bf946afab0068b217f0cf8e188eb3c
|
||||
six==1.17.0 --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274
|
||||
shapely==2.0.7 --hash=sha256:a9469f49ff873ef566864cb3516091881f217b5d231c8164f7883990eec88b73
|
||||
pyyaml==6.0.2 --hash=sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476
|
||||
pillow==12.3.0 --hash=sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91
|
||||
tqdm==4.67.1 --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2
|
||||
omegaconf==2.3.0 --hash=sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b
|
||||
antlr4-python3-runtime==4.9.3 --hash=sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b
|
||||
requests==2.32.3 --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
|
||||
colorlog==6.9.0 --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff
|
||||
certifi==2025.1.31 --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe
|
||||
charset-normalizer==3.4.1 --hash=sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d
|
||||
idna==3.10 --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
|
||||
urllib3==2.7.0 --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
coloredlogs==15.0.1 --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934
|
||||
humanfriendly==10.0 --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477
|
||||
flatbuffers==24.12.23 --hash=sha256:c418e0d48890f4142b92fd3e343e73a48f194e1f80075ddcc5793779b3585444
|
||||
packaging==24.2 --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759
|
||||
protobuf==5.29.6 --hash=sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9
|
||||
sympy==1.13.3 --hash=sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73
|
||||
mpmath==1.3.0 --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c
|
||||
@@ -0,0 +1,29 @@
|
||||
# SnapOtter OCR runtime: linux-arm64-cpu-py311
|
||||
# Every hash identifies the one Linux wheel used by this target. The sole
|
||||
# source distribution (antlr4-python3-runtime) is built offline from its
|
||||
# hash-pinned sdist by build-ocr-runtime.sh.
|
||||
rapidocr==3.9.1 --hash=sha256:600885e4e94e0b427abad394fccb0ec1d3c9118a215ca435bf7680aeae0e292b
|
||||
onnxruntime==1.20.1 --hash=sha256:f6243e34d74423bdd1edf0ae9596dd61023b260f546ee17d701723915f06a9f7
|
||||
numpy==1.26.4 --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e
|
||||
opencv-python-headless==4.10.0.84 --hash=sha256:46071015ff9ab40fccd8a163da0ee14ce9846349f06c6c8c0f2870856ffa45db
|
||||
pyclipper==1.3.0.post6 --hash=sha256:16cc1705a915896d2aff52131c427df02265631279eac849ebda766432714cc0
|
||||
six==1.17.0 --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274
|
||||
shapely==2.0.7 --hash=sha256:8f623b64bb219d62014781120f47499a7adc30cf7787e24b659e56651ceebcb0
|
||||
pyyaml==6.0.2 --hash=sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c
|
||||
pillow==12.3.0 --hash=sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd
|
||||
tqdm==4.67.1 --hash=sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2
|
||||
omegaconf==2.3.0 --hash=sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b
|
||||
antlr4-python3-runtime==4.9.3 --hash=sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b
|
||||
requests==2.32.3 --hash=sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6
|
||||
colorlog==6.9.0 --hash=sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff
|
||||
certifi==2025.1.31 --hash=sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe
|
||||
charset-normalizer==3.4.1 --hash=sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1
|
||||
idna==3.10 --hash=sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3
|
||||
urllib3==2.7.0 --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897
|
||||
coloredlogs==15.0.1 --hash=sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934
|
||||
humanfriendly==10.0 --hash=sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477
|
||||
flatbuffers==24.12.23 --hash=sha256:c418e0d48890f4142b92fd3e343e73a48f194e1f80075ddcc5793779b3585444
|
||||
packaging==24.2 --hash=sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759
|
||||
protobuf==5.29.6 --hash=sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6
|
||||
sympy==1.13.3 --hash=sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73
|
||||
mpmath==1.3.0 --hash=sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c
|
||||
+15
-51
@@ -7,7 +7,7 @@ set -euo pipefail
|
||||
# Usage: verify-bundle.sh <bundleId> <arch>
|
||||
#
|
||||
# bundleId - One of: background-removal, face-detection, object-eraser-colorize,
|
||||
# upscale-enhance, photo-restoration, ocr, transcription
|
||||
# upscale-enhance, photo-restoration, transcription
|
||||
# arch - Architecture variant: amd64-gpu or arm64-cpu
|
||||
#
|
||||
# Expects:
|
||||
@@ -30,7 +30,7 @@ STAGING="/tmp/verify-staging"
|
||||
|
||||
VENV="/opt/venv"
|
||||
PYTHON="${VENV}/bin/python3"
|
||||
SITE_PACKAGES="${VENV}/lib/python3.11/site-packages"
|
||||
SITE_PACKAGES="$("${PYTHON}" -c 'import sysconfig; print(sysconfig.get_paths()["purelib"])')"
|
||||
|
||||
export MODELS_PATH="${MODELS_PATH:-/tmp/verify-models}"
|
||||
export U2NET_HOME="${MODELS_PATH}/rembg"
|
||||
@@ -171,11 +171,7 @@ case "${BUNDLE_ID}" in
|
||||
check_imports "torch onnxruntime mediapipe"
|
||||
;;
|
||||
ocr)
|
||||
# scipy/scikit-learn ship inside this bundle via paddleocr's dependency
|
||||
# closure. Importing them here (not just paddleocr) catches a numpy-2.x-ABI
|
||||
# strand on the numpy==1.26.4 base; the "numpy.dtype size changed" class
|
||||
# that a paddleocr-only import misses because paddle lazy-loads them.
|
||||
check_imports "paddleocr paddle scipy sklearn"
|
||||
fail "OCR v3 runtimes must use verify-ocr-runtime.sh" 2
|
||||
;;
|
||||
transcription)
|
||||
check_imports "faster_whisper"
|
||||
@@ -194,10 +190,10 @@ log "Phase 4: Functional Smoke Tests"
|
||||
AI_SCRIPTS="/app/packages/ai/python"
|
||||
|
||||
smoke_background_removal() {
|
||||
local input="/fixtures/test-200x150.png"
|
||||
local input="/fixtures/image/valid/test-200x150.png"
|
||||
[[ -f "$input" ]] || fail "Fixture missing: $input" 3
|
||||
local output="/tmp/verify-smoke-rembg.png"
|
||||
timeout 300 run_python "${AI_SCRIPTS}/remove_bg.py" "${input}" "${output}" 2>/dev/null || true
|
||||
timeout 300 "${PYTHON}" "${AI_SCRIPTS}/remove_bg.py" "${input}" "${output}" 2>/dev/null || true
|
||||
[[ -f "${output}" && -s "${output}" ]] || fail "remove_bg output missing or empty" 3
|
||||
# Verify it's a valid PNG (check magic bytes)
|
||||
run_python -c "
|
||||
@@ -211,10 +207,10 @@ if magic[:4] != b'\\x89PNG':
|
||||
}
|
||||
|
||||
smoke_face_detection() {
|
||||
local input="/fixtures/sample-photo.jpg"
|
||||
local input="/fixtures/image/valid/sample-photo.jpg"
|
||||
[[ -f "$input" ]] || fail "Fixture missing: $input" 3
|
||||
local output="/tmp/verify-smoke-faces.png"
|
||||
timeout 300 run_python "${AI_SCRIPTS}/detect_faces.py" "${input}" "${output}" 2>/dev/null || true
|
||||
timeout 300 "${PYTHON}" "${AI_SCRIPTS}/detect_faces.py" "${input}" "${output}" 2>/dev/null || true
|
||||
[[ -f "${output}" ]] || fail "detect_faces output missing" 3
|
||||
pass "detect_faces produced output"
|
||||
}
|
||||
@@ -228,16 +224,16 @@ from PIL import Image
|
||||
img = Image.new('L', (64, 64), 128)
|
||||
img.save('${input}')
|
||||
" 2>/dev/null
|
||||
timeout 300 run_python "${AI_SCRIPTS}/colorize.py" "${input}" "${output}" '{"model":"ddcolor"}' 2>/dev/null || true
|
||||
timeout 300 "${PYTHON}" "${AI_SCRIPTS}/colorize.py" "${input}" "${output}" '{"model":"ddcolor"}' 2>/dev/null || true
|
||||
[[ -f "${output}" && -s "${output}" ]] || fail "colorize output missing or empty" 3
|
||||
pass "colorize produced output"
|
||||
}
|
||||
|
||||
smoke_upscale_enhance() {
|
||||
local input="/fixtures/test-100x100.jpg"
|
||||
local input="/fixtures/image/valid/test-100x100.jpg"
|
||||
[[ -f "$input" ]] || fail "Fixture missing: $input" 3
|
||||
local output="/tmp/verify-smoke-upscale.png"
|
||||
timeout 300 run_python "${AI_SCRIPTS}/upscale.py" "${input}" "${output}" '{"scale":2}' 2>/dev/null || true
|
||||
timeout 300 "${PYTHON}" "${AI_SCRIPTS}/upscale.py" "${input}" "${output}" '{"scale":2}' 2>/dev/null || true
|
||||
[[ -f "${output}" && -s "${output}" ]] || fail "upscale output missing or empty" 3
|
||||
run_python -c "
|
||||
from PIL import Image
|
||||
@@ -252,51 +248,19 @@ print(f' Upscale: {inp.size} -> {out.size}')
|
||||
}
|
||||
|
||||
smoke_photo_restoration() {
|
||||
local input="/fixtures/test-100x100.jpg"
|
||||
local input="/fixtures/image/valid/test-100x100.jpg"
|
||||
[[ -f "$input" ]] || fail "Fixture missing: $input" 3
|
||||
local output="/tmp/verify-smoke-restore.png"
|
||||
timeout 300 run_python "${AI_SCRIPTS}/restore.py" "${input}" "${output}" 2>/dev/null || true
|
||||
timeout 300 "${PYTHON}" "${AI_SCRIPTS}/restore.py" "${input}" "${output}" 2>/dev/null || true
|
||||
[[ -f "${output}" && -s "${output}" ]] || fail "restore output missing or empty" 3
|
||||
pass "restore produced output"
|
||||
}
|
||||
|
||||
smoke_ocr() {
|
||||
local input="/tmp/verify-smoke-ocr.png"
|
||||
# Generate a 400x100 image with large, clear text for reliable OCR
|
||||
run_python -c "
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
img = Image.new('RGB', (400, 100), 'white')
|
||||
draw = ImageDraw.Draw(img)
|
||||
try:
|
||||
font = ImageFont.load_default(size=40)
|
||||
except TypeError:
|
||||
font = ImageFont.load_default()
|
||||
draw.text((20, 20), 'Hello SnapOtter', fill='black', font=font)
|
||||
img.save('${input}')
|
||||
" 2>/dev/null
|
||||
local result
|
||||
result="$(timeout 300 run_python "${AI_SCRIPTS}/ocr.py" "${input}" '{"quality":"balanced","enhance":false}' 2>/dev/null)" || result=""
|
||||
# Check stdout JSON has success=true and non-empty text
|
||||
echo "${result}" | run_python -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
if not data.get('success'):
|
||||
print('OCR did not return success=true', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
text = data.get('text', '')
|
||||
if not text.strip():
|
||||
print('OCR returned empty text', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f' OCR text: {text[:80]}')
|
||||
" || fail "OCR smoke test assertion failed" 3
|
||||
pass "ocr returned valid result"
|
||||
}
|
||||
|
||||
smoke_transcription() {
|
||||
local input="/fixtures/content/speech-10s.wav"
|
||||
local input="/fixtures/audio/valid/speech-10s.wav"
|
||||
[[ -f "$input" ]] || fail "Fixture missing: $input" 3
|
||||
local result
|
||||
result="$(timeout 300 run_python "${AI_SCRIPTS}/transcribe.py" "${input}" 2>/dev/null)" || result=""
|
||||
result="$(timeout 300 "${PYTHON}" "${AI_SCRIPTS}/transcribe.py" "${input}" 2>/dev/null)" || result=""
|
||||
# Check stdout JSON has success=true and non-empty segments
|
||||
echo "${result}" | run_python -c "
|
||||
import json, sys
|
||||
@@ -319,7 +283,7 @@ case "${BUNDLE_ID}" in
|
||||
object-eraser-colorize) smoke_object_eraser_colorize ;;
|
||||
upscale-enhance) smoke_upscale_enhance ;;
|
||||
photo-restoration) smoke_photo_restoration ;;
|
||||
ocr) smoke_ocr ;;
|
||||
ocr) fail "OCR v3 runtimes must use verify-ocr-runtime.sh" 2 ;;
|
||||
transcription) smoke_transcription ;;
|
||||
esac
|
||||
|
||||
|
||||
Executable
+4266
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user