Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +1 -0
- huey_config.py +21 -0
- images/augmentoolkit-logo.png +3 -0
- linux.sh +614 -0
- local_linux.sh +738 -0
- local_macos.sh +713 -0
- logs/.gitkeep +3 -0
- macos.sh +612 -0
- ocr/.env.example +23 -0
- ocr/LICENSE +21 -0
- ocr/README.md +17 -0
- ocr/THIRD_PARTY.md +23 -0
- ocr/__init__.py +3 -0
- ocr/adapters/__init__.py +0 -0
- ocr/adapters/cpu_http.py +61 -0
- ocr/adapters/freellm_alias.py +160 -0
- ocr/adapters/http.py +30 -0
- ocr/adapters/rapidocr_cpu.py +146 -0
- ocr/anonymization/__init__.py +15 -0
- ocr/anonymization/__main__.py +40 -0
- ocr/anonymization/approval_ledger.py +109 -0
- ocr/anonymization/artifacts.json +11 -0
- ocr/anonymization/atomic_output.py +41 -0
- ocr/anonymization/candidate_io.py +22 -0
- ocr/anonymization/compare03d.py +111 -0
- ocr/anonymization/corrections.py +754 -0
- ocr/anonymization/demonstrate03f.py +220 -0
- ocr/anonymization/detectors.py +38 -0
- ocr/anonymization/develop03e.py +145 -0
- ocr/anonymization/engine.py +149 -0
- ocr/anonymization/evaluate.py +196 -0
- ocr/anonymization/fixtures/holdout03c/cases.json +3854 -0
- ocr/anonymization/fixtures/holdout03c/freeze.json +18 -0
- ocr/anonymization/fixtures/holdout03d/README.md +35 -0
- ocr/anonymization/fixtures/holdout03d/cases.json +2730 -0
- ocr/anonymization/fixtures/holdout03d/freeze.json +129 -0
- ocr/anonymization/fixtures/qualification.json +647 -0
- ocr/anonymization/fixtures/validation.json +210 -0
- ocr/anonymization/historical.py +36 -0
- ocr/anonymization/holdout.py +79 -0
- ocr/anonymization/host_adapter.py +167 -0
- ocr/anonymization/investigate.py +142 -0
- ocr/anonymization/model.py +81 -0
- ocr/anonymization/normalization.py +11 -0
- ocr/anonymization/outcomes.py +124 -0
- ocr/anonymization/private_review.py +200 -0
- ocr/anonymization/private_transport.py +155 -0
- ocr/anonymization/provenance.py +56 -0
- ocr/anonymization/qualify03d.py +121 -0
- ocr/anonymization/recognizer.py +152 -0
.gitattributes
CHANGED
|
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
images/augmentoolkit-logo.png filter=lfs diff=lfs merge=lfs -text
|
huey_config.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
# Import RedisHuey and the necessary connection details from redis_config
|
| 4 |
+
from huey import RedisHuey
|
| 5 |
+
from redis_config import REDIS_HOST, REDIS_PORT, REDIS_DB
|
| 6 |
+
|
| 7 |
+
# Define the Huey instance using Redis
|
| 8 |
+
# The 'name' parameter is optional but good practice, often set to the app name.
|
| 9 |
+
# It prefixes keys used by Huey in Redis to avoid collisions if Redis is shared.
|
| 10 |
+
huey = RedisHuey(
|
| 11 |
+
name="augmentoolkit_tasks", # Optional, helps namespace Huey keys in Redis
|
| 12 |
+
host=REDIS_HOST,
|
| 13 |
+
port=REDIS_PORT,
|
| 14 |
+
db=REDIS_DB,
|
| 15 |
+
# Add other RedisHuey parameters if needed (e.g., password, connection_pool)
|
| 16 |
+
)
|
| 17 |
+
|
| 18 |
+
# Remove SQLite specific code
|
| 19 |
+
# os.makedirs("huey")
|
| 20 |
+
# _db_path = os.path.join(os.path.dirname(__file__), 'huey', 'huey_queue.db')
|
| 21 |
+
# huey = SqliteHuey(filename=_db_path)
|
images/augmentoolkit-logo.png
ADDED
|
Git LFS Details
|
linux.sh
ADDED
|
@@ -0,0 +1,614 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Exit immediately if a command exits with a non-zero status.
|
| 4 |
+
# set -e # Disabled for manual error checking below
|
| 5 |
+
|
| 6 |
+
echo "Starting Augmentoolkit services..."
|
| 7 |
+
|
| 8 |
+
# --- Get script directory and change to it ---
|
| 9 |
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
| 10 |
+
cd "$SCRIPT_DIR"
|
| 11 |
+
if [ $? -ne 0 ]; then
|
| 12 |
+
echo "ERROR: Failed to change directory to script location '$SCRIPT_DIR'."
|
| 13 |
+
exit 1
|
| 14 |
+
fi
|
| 15 |
+
echo "Running in directory: $(pwd)"
|
| 16 |
+
|
| 17 |
+
# --- Cleanup Function ---
|
| 18 |
+
# Stores PIDs of background processes
|
| 19 |
+
HUEY_PID=""
|
| 20 |
+
UVICORN_PID=""
|
| 21 |
+
SERVE_PID="" # Though serve runs in foreground, trap might catch signals before it starts
|
| 22 |
+
VALKEY_PID="" # PID for valkey server
|
| 23 |
+
|
| 24 |
+
cleanup() {
|
| 25 |
+
echo "" # Newline after ^C
|
| 26 |
+
echo "----------------------------------------"
|
| 27 |
+
echo "Signal received, stopping background services..."
|
| 28 |
+
|
| 29 |
+
# Function to kill a process and wait for it to terminate
|
| 30 |
+
kill_and_wait() {
|
| 31 |
+
local pid=$1
|
| 32 |
+
local name=$2
|
| 33 |
+
local timeout=9
|
| 34 |
+
|
| 35 |
+
if [ -n "$pid" ] && kill -0 $pid &> /dev/null; then
|
| 36 |
+
echo "Stopping $name (PID $pid)..."
|
| 37 |
+
|
| 38 |
+
# First try SIGTERM (graceful shutdown)
|
| 39 |
+
kill -TERM $pid 2>/dev/null
|
| 40 |
+
|
| 41 |
+
# Wait for process to terminate gracefully
|
| 42 |
+
local count=0
|
| 43 |
+
while [ $count -lt $timeout ] && kill -0 $pid &> /dev/null; do
|
| 44 |
+
sleep 1
|
| 45 |
+
count=$((count + 1))
|
| 46 |
+
done
|
| 47 |
+
|
| 48 |
+
# If still running, force kill with SIGKILL
|
| 49 |
+
if kill -0 $pid &> /dev/null; then
|
| 50 |
+
echo "Process $pid ($name) didn't terminate gracefully, force killing..."
|
| 51 |
+
kill -KILL $pid 2>/dev/null
|
| 52 |
+
sleep 1
|
| 53 |
+
fi
|
| 54 |
+
|
| 55 |
+
# Final check
|
| 56 |
+
if kill -0 $pid &> /dev/null; then
|
| 57 |
+
echo "WARNING: Failed to stop $name (PID $pid)"
|
| 58 |
+
else
|
| 59 |
+
echo "$name stopped successfully."
|
| 60 |
+
fi
|
| 61 |
+
fi
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# Stop processes in reverse order of startup
|
| 65 |
+
kill_and_wait "$SERVE_PID" "frontend serve"
|
| 66 |
+
kill_and_wait "$UVICORN_PID" "Uvicorn API server"
|
| 67 |
+
kill_and_wait "$HUEY_PID" "Huey worker"
|
| 68 |
+
kill_and_wait "$VALKEY_PID" "Valkey server"
|
| 69 |
+
|
| 70 |
+
# Find and kill any remaining huey consumers specific to this Augmentoolkit instance
|
| 71 |
+
echo "Searching for huey consumer processes in current directory ($SCRIPT_DIR)..."
|
| 72 |
+
|
| 73 |
+
# Use ps with full command line and grep for huey_consumer processes in our specific directory
|
| 74 |
+
# This avoids killing huey consumers from other Augmentoolkit instances or projects
|
| 75 |
+
HUEY_PIDS=$(ps aux | grep -E "huey_consumer.*tasks\.huey" | grep "$SCRIPT_DIR" | grep -v grep | awk '{print $2}')
|
| 76 |
+
|
| 77 |
+
if [ -n "$HUEY_PIDS" ]; then
|
| 78 |
+
echo "Found huey consumer processes in current directory: $HUEY_PIDS"
|
| 79 |
+
for pid in $HUEY_PIDS; do
|
| 80 |
+
if kill -0 $pid &> /dev/null; then
|
| 81 |
+
echo "Stopping huey consumer from current directory (PID $pid)..."
|
| 82 |
+
kill -TERM $pid 2>/dev/null
|
| 83 |
+
sleep 2
|
| 84 |
+
if kill -0 $pid &> /dev/null; then
|
| 85 |
+
echo "Force killing huey consumer (PID $pid)..."
|
| 86 |
+
kill -KILL $pid 2>/dev/null
|
| 87 |
+
fi
|
| 88 |
+
fi
|
| 89 |
+
done
|
| 90 |
+
sleep 1
|
| 91 |
+
# Final check for any remaining huey processes in our directory
|
| 92 |
+
REMAINING_HUEY=$(ps aux | grep -E "huey_consumer.*tasks\.huey" | grep "$SCRIPT_DIR" | grep -v grep | awk '{print $2}')
|
| 93 |
+
if [ -n "$REMAINING_HUEY" ]; then
|
| 94 |
+
echo "WARNING: Some huey consumer processes in current directory may still be running: $REMAINING_HUEY"
|
| 95 |
+
else
|
| 96 |
+
echo "All huey consumer processes from current directory stopped successfully."
|
| 97 |
+
fi
|
| 98 |
+
else
|
| 99 |
+
echo "No additional huey consumer processes found in current directory."
|
| 100 |
+
fi
|
| 101 |
+
|
| 102 |
+
# Deactivate virtual environment if active
|
| 103 |
+
if command -v deactivate &> /dev/null; then
|
| 104 |
+
echo "Deactivating virtual environment..."
|
| 105 |
+
deactivate
|
| 106 |
+
fi
|
| 107 |
+
|
| 108 |
+
# Give a moment for any remaining output to flush
|
| 109 |
+
sleep 1
|
| 110 |
+
|
| 111 |
+
# Clear any remaining output and reset cursor
|
| 112 |
+
echo "Cleanup complete."
|
| 113 |
+
echo "----------------------------------------"
|
| 114 |
+
|
| 115 |
+
# Ensure cursor is visible and terminal is in normal state
|
| 116 |
+
printf "\033[?25h" # Show cursor
|
| 117 |
+
printf "\033[0m" # Reset all attributes
|
| 118 |
+
|
| 119 |
+
exit 0 # Exit script after cleanup
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
# Trap signals (INT: Ctrl+C, TERM: standard kill) and run cleanup function
|
| 123 |
+
trap cleanup SIGINT SIGTERM
|
| 124 |
+
|
| 125 |
+
# --- 1. Python Virtual Environment & Dependencies ---
|
| 126 |
+
echo "----------------------------------------"
|
| 127 |
+
echo "1. Setting up Python virtual environment and installing dependencies..."
|
| 128 |
+
VENV_DIR=".venv"
|
| 129 |
+
|
| 130 |
+
# Check if python3.11 command exists (preferred)
|
| 131 |
+
if command -v python3.11 &> /dev/null; then
|
| 132 |
+
PYTHON_CMD="python3.11"
|
| 133 |
+
echo "Using Python 3.11: $(python3.11 --version)"
|
| 134 |
+
elif command -v python3 &> /dev/null; then
|
| 135 |
+
PYTHON_CMD="python3"
|
| 136 |
+
PYTHON_VERSION=$(python3 --version 2>&1 | grep -oE '[0-9]+\.[0-9]+')
|
| 137 |
+
echo "Using Python: $(python3 --version)"
|
| 138 |
+
if [[ "$PYTHON_VERSION" != "3.11" ]]; then
|
| 139 |
+
echo "WARNING: Python 3.11 is recommended but not found. Using $PYTHON_VERSION instead."
|
| 140 |
+
echo "If you encounter issues, please install Python 3.11 specifically."
|
| 141 |
+
fi
|
| 142 |
+
else
|
| 143 |
+
echo "ERROR: Neither python3.11 nor python3 command found."
|
| 144 |
+
echo "Please install Python 3.11 (recommended) or Python 3.x and rerun the script."
|
| 145 |
+
echo "On Ubuntu/Debian, you can install Python 3.11 using:"
|
| 146 |
+
echo " sudo apt update && sudo apt install python3.11 python3.11-venv python3.11-dev"
|
| 147 |
+
echo "On RHEL/CentOS/Fedora, you can install Python 3.11 using:"
|
| 148 |
+
echo " sudo dnf install python3.11 python3.11-devel"
|
| 149 |
+
echo "Or download from: https://www.python.org/downloads/"
|
| 150 |
+
exit 1
|
| 151 |
+
fi
|
| 152 |
+
|
| 153 |
+
# Create virtual environment if it doesn't exist
|
| 154 |
+
if [ ! -d "$VENV_DIR" ]; then
|
| 155 |
+
echo "Creating virtual environment in '$VENV_DIR' using $PYTHON_CMD..."
|
| 156 |
+
$PYTHON_CMD -m venv "$VENV_DIR"
|
| 157 |
+
if [ $? -ne 0 ]; then
|
| 158 |
+
echo "ERROR: Failed to create Python virtual environment using $PYTHON_CMD."
|
| 159 |
+
exit 1
|
| 160 |
+
fi
|
| 161 |
+
else
|
| 162 |
+
echo "Virtual environment '$VENV_DIR' already exists."
|
| 163 |
+
fi
|
| 164 |
+
|
| 165 |
+
# Activate virtual environment
|
| 166 |
+
echo "Activating virtual environment..."
|
| 167 |
+
source "$VENV_DIR/bin/activate"
|
| 168 |
+
if [ $? -ne 0 ]; then
|
| 169 |
+
echo "ERROR: Failed to activate Python virtual environment."
|
| 170 |
+
exit 1
|
| 171 |
+
fi
|
| 172 |
+
echo "Virtual environment activated. Python executable: $(which python)"
|
| 173 |
+
|
| 174 |
+
# --- Check/Install uv ---
|
| 175 |
+
echo "Checking for uv..."
|
| 176 |
+
if ! command -v uv &> /dev/null; then
|
| 177 |
+
echo "'uv' command not found. Attempting to install uv using pip..."
|
| 178 |
+
# Ensure pip is available (it should be with the selected Python version)
|
| 179 |
+
if ! python -m pip --version &> /dev/null; then
|
| 180 |
+
echo "ERROR: python -m pip command failed. Cannot install uv."
|
| 181 |
+
exit 1
|
| 182 |
+
fi
|
| 183 |
+
python -m pip install uv
|
| 184 |
+
if [ $? -ne 0 ]; then
|
| 185 |
+
echo "ERROR: Failed to install uv using pip. Please install uv manually (e.g., 'pip install uv') and rerun the script."
|
| 186 |
+
exit 1
|
| 187 |
+
fi
|
| 188 |
+
echo "uv installed successfully."
|
| 189 |
+
else
|
| 190 |
+
echo "uv found."
|
| 191 |
+
fi
|
| 192 |
+
# --- End Check/Install uv ---
|
| 193 |
+
|
| 194 |
+
|
| 195 |
+
# Install dependencies using uv
|
| 196 |
+
echo "Installing Python dependencies from requirements.txt using uv..."
|
| 197 |
+
# Assuming uv is in PATH after the check above and venv is activated
|
| 198 |
+
uv pip install -r requirements.txt
|
| 199 |
+
if [ $? -ne 0 ]; then
|
| 200 |
+
echo "ERROR: Failed to install Python dependencies using uv. Please check requirements.txt and uv logs."
|
| 201 |
+
exit 1 # Cleanup trap will handle exit
|
| 202 |
+
fi
|
| 203 |
+
echo "Python dependencies installed successfully."
|
| 204 |
+
|
| 205 |
+
# Download spaCy model for anonymization
|
| 206 |
+
echo "Downloading spaCy model (en_core_web_lg) for anonymization..."
|
| 207 |
+
python -m spacy download en_core_web_lg
|
| 208 |
+
if [ $? -ne 0 ]; then
|
| 209 |
+
echo "WARNING: Failed to download spaCy model. Anonymization might fall back to NLTK."
|
| 210 |
+
fi
|
| 211 |
+
|
| 212 |
+
|
| 213 |
+
# --- Helper Process Logging Setup ---
|
| 214 |
+
echo "----------------------------------------"
|
| 215 |
+
echo "Setting up helper process logging..."
|
| 216 |
+
|
| 217 |
+
# Create helper_process_logs directory if it doesn't exist
|
| 218 |
+
HELPER_LOGS_DIR="helper_process_logs"
|
| 219 |
+
if [ ! -d "$HELPER_LOGS_DIR" ]; then
|
| 220 |
+
echo "Creating helper process logs directory: $HELPER_LOGS_DIR"
|
| 221 |
+
mkdir -p "$HELPER_LOGS_DIR"
|
| 222 |
+
if [ $? -ne 0 ]; then
|
| 223 |
+
echo "ERROR: Failed to create helper process logs directory."
|
| 224 |
+
exit 1
|
| 225 |
+
fi
|
| 226 |
+
else
|
| 227 |
+
echo "Helper process logs directory already exists: $HELPER_LOGS_DIR"
|
| 228 |
+
fi
|
| 229 |
+
|
| 230 |
+
# Generate timestamp for log files
|
| 231 |
+
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
| 232 |
+
VALKEY_LOG_FILE="$HELPER_LOGS_DIR/valkey_server_${TIMESTAMP}.log"
|
| 233 |
+
HUEY_LOG_FILE="$HELPER_LOGS_DIR/huey_worker_${TIMESTAMP}.log"
|
| 234 |
+
UVICORN_LOG_FILE="$HELPER_LOGS_DIR/uvicorn_api_${TIMESTAMP}.log"
|
| 235 |
+
FRONTEND_LOG_FILE="$HELPER_LOGS_DIR/frontend_serve_${TIMESTAMP}.log"
|
| 236 |
+
|
| 237 |
+
echo "Helper process logs will be saved to:"
|
| 238 |
+
echo " Valkey server: $VALKEY_LOG_FILE"
|
| 239 |
+
echo " Huey worker: $HUEY_LOG_FILE"
|
| 240 |
+
echo " Uvicorn API: $UVICORN_LOG_FILE"
|
| 241 |
+
echo " Frontend serve: $FRONTEND_LOG_FILE"
|
| 242 |
+
echo "Helper process logging setup complete."
|
| 243 |
+
|
| 244 |
+
#### if REDIS_HOST skip
|
| 245 |
+
if [ -z "$REDIS_HOST" ]; then
|
| 246 |
+
# --- 1.5 Valkey/Redis Check ---
|
| 247 |
+
echo "----------------------------------------"
|
| 248 |
+
echo "1.5. Checking for running Valkey/Redis service..."
|
| 249 |
+
|
| 250 |
+
SERVER_CMD=""
|
| 251 |
+
SERVICE_NAME=""
|
| 252 |
+
|
| 253 |
+
# Check for valkey-server first
|
| 254 |
+
if command -v valkey-server &> /dev/null; then
|
| 255 |
+
SERVER_CMD="valkey-server"
|
| 256 |
+
SERVICE_NAME="valkey-server"
|
| 257 |
+
# Fallback to redis-server
|
| 258 |
+
elif command -v redis-server &> /dev/null; then
|
| 259 |
+
SERVER_CMD="redis-server"
|
| 260 |
+
SERVICE_NAME="redis-server"
|
| 261 |
+
else
|
| 262 |
+
# No background services started yet, just exit
|
| 263 |
+
# Check if something is listening on the default Valkey/Redis port (6379)
|
| 264 |
+
if command -v ss &> /dev/null; then
|
| 265 |
+
PORT_CHECK_CMD="ss -ltn"
|
| 266 |
+
else
|
| 267 |
+
PORT_CHECK_CMD="netstat -ltn"
|
| 268 |
+
fi
|
| 269 |
+
|
| 270 |
+
if $PORT_CHECK_CMD | grep -q ':6379'; then
|
| 271 |
+
echo "WARNING: Neither 'valkey-server' nor 'redis-server' found in PATH, but something is listening on port 6379."
|
| 272 |
+
echo "Assuming a compatible Valkey/Redis server is running (possibly built from source or custom install)."
|
| 273 |
+
else
|
| 274 |
+
echo "Neither 'valkey-server' nor 'redis-server' found in PATH."
|
| 275 |
+
echo "Building Valkey from source..."
|
| 276 |
+
|
| 277 |
+
# Check if valkey directory already exists
|
| 278 |
+
if [ ! -d "valkey" ]; then
|
| 279 |
+
echo "Cloning Valkey repository..."
|
| 280 |
+
git clone https://github.com/valkey-io/valkey.git
|
| 281 |
+
if [ $? -ne 0 ]; then
|
| 282 |
+
echo "ERROR: Failed to clone Valkey repository. Please check your internet connection and git installation."
|
| 283 |
+
exit 1
|
| 284 |
+
fi
|
| 285 |
+
else
|
| 286 |
+
echo "Valkey directory already exists, skipping clone."
|
| 287 |
+
fi
|
| 288 |
+
|
| 289 |
+
echo "Building Valkey..."
|
| 290 |
+
cd valkey
|
| 291 |
+
make
|
| 292 |
+
if [ $? -ne 0 ]; then
|
| 293 |
+
echo "ERROR: Failed to build Valkey. Please check build dependencies (gcc, make, etc.)."
|
| 294 |
+
cd "$SCRIPT_DIR"
|
| 295 |
+
exit 1
|
| 296 |
+
fi
|
| 297 |
+
|
| 298 |
+
echo "Starting Valkey server in background..."
|
| 299 |
+
echo "Output will be logged to: $VALKEY_LOG_FILE"
|
| 300 |
+
cd src
|
| 301 |
+
./valkey-server > "../../$VALKEY_LOG_FILE" 2>&1 &
|
| 302 |
+
VALKEY_PID=$!
|
| 303 |
+
cd "$SCRIPT_DIR"
|
| 304 |
+
|
| 305 |
+
# Give it a moment to start/fail
|
| 306 |
+
sleep 4
|
| 307 |
+
if ! kill -0 $VALKEY_PID &> /dev/null; then
|
| 308 |
+
echo "ERROR: Valkey server (PID $VALKEY_PID) failed to start or crashed immediately."
|
| 309 |
+
echo "Check the log file for details: $VALKEY_LOG_FILE"
|
| 310 |
+
exit 1
|
| 311 |
+
fi
|
| 312 |
+
echo "Valkey server started successfully with PID $VALKEY_PID."
|
| 313 |
+
|
| 314 |
+
# Set SERVER_CMD and SERVICE_NAME for consistency with the rest of the script
|
| 315 |
+
SERVER_CMD="./valkey/src/valkey-server"
|
| 316 |
+
SERVICE_NAME="valkey-server"
|
| 317 |
+
fi
|
| 318 |
+
fi
|
| 319 |
+
echo "Found $SERVER_CMD."
|
| 320 |
+
|
| 321 |
+
# If we didn't just start Valkey server locally, perform service checks
|
| 322 |
+
if [ -z "$VALKEY_PID" ]; then
|
| 323 |
+
# Check if systemctl exists and is usable
|
| 324 |
+
if command -v systemctl &> /dev/null && systemctl status &> /dev/null; then
|
| 325 |
+
# systemctl seems to be working, so let's use it
|
| 326 |
+
if ! systemctl is-active --quiet $SERVICE_NAME; then
|
| 327 |
+
echo "WARNING: The $SERVICE_NAME service is not active according to systemctl."
|
| 328 |
+
# Check if something is listening on port 6379 as fallback
|
| 329 |
+
if ss -ltn | grep -q ':6379' 2>/dev/null; then
|
| 330 |
+
echo "However, something is listening on port 6379. Assuming a compatible Valkey/Redis server is running."
|
| 331 |
+
else
|
| 332 |
+
echo "ERROR: The $SERVICE_NAME service is not active and nothing is listening on port 6379."
|
| 333 |
+
echo "Please start the service, e.g., 'sudo systemctl start $SERVICE_NAME' and ensure it's enabled to start on boot, e.g., 'sudo systemctl enable $SERVICE_NAME'."
|
| 334 |
+
exit 1
|
| 335 |
+
fi
|
| 336 |
+
else
|
| 337 |
+
echo "The $SERVICE_NAME service is active."
|
| 338 |
+
fi
|
| 339 |
+
else
|
| 340 |
+
# systemctl not available or not working, fall back to simple port check
|
| 341 |
+
echo "WARNING: 'systemctl' not available/running. Checking for service via port."
|
| 342 |
+
if ss -ltn | grep -q ':6379' 2>/dev/null; then
|
| 343 |
+
echo "A service is listening on port 6379. Assuming a compatible Valkey/Redis server is running."
|
| 344 |
+
else
|
| 345 |
+
echo "ERROR: No running Valkey/Redis service detected on port 6379, and could not start one automatically as it was expected to be running."
|
| 346 |
+
echo "Please start valkey-server or redis-server manually in another terminal."
|
| 347 |
+
exit 1
|
| 348 |
+
fi
|
| 349 |
+
fi
|
| 350 |
+
else
|
| 351 |
+
echo "Valkey server was started by this script (PID $VALKEY_PID), skipping system service check."
|
| 352 |
+
fi
|
| 353 |
+
# --- End Valkey/Redis Check ---
|
| 354 |
+
fi
|
| 355 |
+
|
| 356 |
+
# --- 2. Huey Worker ---
|
| 357 |
+
echo "----------------------------------------"
|
| 358 |
+
echo "2. Starting Huey worker in the background (using venv)..."
|
| 359 |
+
echo "Output will be logged to: $HUEY_LOG_FILE"
|
| 360 |
+
# Call the huey_consumer script directly (should be in PATH from venv)
|
| 361 |
+
# Redirect both stdout and stderr to the log file
|
| 362 |
+
huey_consumer tasks.huey > "$HUEY_LOG_FILE" 2>&1 &
|
| 363 |
+
HUEY_PID=$!
|
| 364 |
+
# Give it a moment to start/fail
|
| 365 |
+
sleep 2
|
| 366 |
+
if ! kill -0 $HUEY_PID &> /dev/null; then
|
| 367 |
+
echo "ERROR: Huey worker (PID $HUEY_PID) failed to start or crashed immediately."
|
| 368 |
+
echo "Check the log file for details: $HUEY_LOG_FILE"
|
| 369 |
+
exit 1 # Cleanup trap will handle exit
|
| 370 |
+
fi
|
| 371 |
+
echo "Huey worker started successfully with PID $HUEY_PID."
|
| 372 |
+
|
| 373 |
+
|
| 374 |
+
# --- 3. API Server ---
|
| 375 |
+
echo "----------------------------------------"
|
| 376 |
+
echo "3. Starting FastAPI server (Uvicorn) in the background (using venv)..."
|
| 377 |
+
echo "Output will be logged to: $UVICORN_LOG_FILE"
|
| 378 |
+
# Host 0.0.0.0 makes it accessible on the network
|
| 379 |
+
# Use python -m uvicorn to ensure using the venv uvicorn
|
| 380 |
+
# Redirect both stdout and stderr to the log file
|
| 381 |
+
python -m uvicorn api:app --host 0.0.0.0 --port 8000 > "$UVICORN_LOG_FILE" 2>&1 &
|
| 382 |
+
UVICORN_PID=$!
|
| 383 |
+
# Give it a moment to start/fail
|
| 384 |
+
sleep 2
|
| 385 |
+
if ! kill -0 $UVICORN_PID &> /dev/null; then
|
| 386 |
+
echo "ERROR: Uvicorn server (PID $UVICORN_PID) failed to start or crashed immediately."
|
| 387 |
+
echo "Check the log file for details: $UVICORN_LOG_FILE"
|
| 388 |
+
# Cleanup trap will handle stopping Huey
|
| 389 |
+
exit 1 # Cleanup trap will handle exit
|
| 390 |
+
fi
|
| 391 |
+
echo "Uvicorn server started successfully with PID $UVICORN_PID. Access API at http://localhost:8000"
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
# --- 4. NPM Check ---
|
| 395 |
+
echo "----------------------------------------"
|
| 396 |
+
echo "4. Checking for npm..."
|
| 397 |
+
if ! command -v npm &> /dev/null; then
|
| 398 |
+
echo "ERROR: npm command not found."
|
| 399 |
+
echo "Please install Node.js and npm (e.g., via your system package manager like 'sudo apt install nodejs npm' or better still, install nvm and use that to get node (ask an AI how to get nodejs via nvm)) and run this script again."
|
| 400 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 401 |
+
kill $HUEY_PID $UVICORN_PID
|
| 402 |
+
exit 1
|
| 403 |
+
fi
|
| 404 |
+
echo "npm found."
|
| 405 |
+
|
| 406 |
+
# Check Node.js version
|
| 407 |
+
NODE_VERSION=$(node --version 2>/dev/null | sed 's/v//')
|
| 408 |
+
if [ -z "$NODE_VERSION" ]; then
|
| 409 |
+
echo "ERROR: Could not determine Node.js version."
|
| 410 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 411 |
+
kill $HUEY_PID $UVICORN_PID
|
| 412 |
+
exit 1
|
| 413 |
+
fi
|
| 414 |
+
|
| 415 |
+
# Extract major version number
|
| 416 |
+
NODE_MAJOR_VERSION=$(echo "$NODE_VERSION" | cut -d. -f1)
|
| 417 |
+
|
| 418 |
+
echo "Node.js version: v$NODE_VERSION"
|
| 419 |
+
|
| 420 |
+
# Check if Node.js version is at least 18
|
| 421 |
+
if [ "$NODE_MAJOR_VERSION" -lt 18 ]; then
|
| 422 |
+
echo "ERROR: Node.js version $NODE_VERSION is too old."
|
| 423 |
+
echo "This application requires Node.js 18.0.0 or higher (20+ recommended)."
|
| 424 |
+
echo ""
|
| 425 |
+
echo "To upgrade Node.js, run the following commands:"
|
| 426 |
+
echo ""
|
| 427 |
+
echo "1. Add NodeSource repository:"
|
| 428 |
+
echo " curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -"
|
| 429 |
+
echo ""
|
| 430 |
+
echo "2. Remove conflicting packages (if any):"
|
| 431 |
+
echo " sudo apt-get remove -y libnode-dev libnode72"
|
| 432 |
+
echo ""
|
| 433 |
+
echo "3. Install Node.js 20.x:"
|
| 434 |
+
echo " sudo apt-get install -y nodejs"
|
| 435 |
+
echo ""
|
| 436 |
+
echo " OR run all steps at once:"
|
| 437 |
+
echo " curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get remove -y libnode-dev libnode72 2>/dev/null || true && sudo apt-get install -y nodejs"
|
| 438 |
+
echo ""
|
| 439 |
+
echo "Alternative method using Node Version Manager (nvm):"
|
| 440 |
+
echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash"
|
| 441 |
+
echo " source ~/.bashrc"
|
| 442 |
+
echo " nvm install 20"
|
| 443 |
+
echo " nvm use 20"
|
| 444 |
+
echo ""
|
| 445 |
+
echo "After upgrading, please run this script again."
|
| 446 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 447 |
+
kill $HUEY_PID $UVICORN_PID
|
| 448 |
+
exit 1
|
| 449 |
+
fi
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
# --- 5. Frontend Dependencies ---
|
| 453 |
+
echo "----------------------------------------"
|
| 454 |
+
echo "5. Installing frontend dependencies in atk-interface..."
|
| 455 |
+
if [ ! -d "atk-interface" ]; then
|
| 456 |
+
echo "ERROR: Directory 'atk-interface' not found in $(pwd)."
|
| 457 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 458 |
+
kill $HUEY_PID $UVICORN_PID
|
| 459 |
+
exit 1
|
| 460 |
+
fi
|
| 461 |
+
cd atk-interface
|
| 462 |
+
npm install
|
| 463 |
+
if [ $? -ne 0 ]; then
|
| 464 |
+
echo "ERROR: Failed to install npm dependencies in atk-interface. Check package.json and npm logs."
|
| 465 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 466 |
+
kill $HUEY_PID $UVICORN_PID
|
| 467 |
+
cd "$SCRIPT_DIR" # Go back to root
|
| 468 |
+
exit 1
|
| 469 |
+
fi
|
| 470 |
+
echo "Frontend dependencies installed."
|
| 471 |
+
|
| 472 |
+
|
| 473 |
+
# --- 6. Frontend Build ---
|
| 474 |
+
echo "----------------------------------------"
|
| 475 |
+
echo "6. Building frontend application in atk-interface..."
|
| 476 |
+
npm run build
|
| 477 |
+
if [ $? -ne 0 ]; then
|
| 478 |
+
echo "ERROR: Failed to build frontend application. Check build script in package.json and logs."
|
| 479 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 480 |
+
kill $HUEY_PID $UVICORN_PID
|
| 481 |
+
cd "$SCRIPT_DIR" # Go back to root
|
| 482 |
+
exit 1
|
| 483 |
+
fi
|
| 484 |
+
echo "Frontend built successfully."
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
# --- 7. Frontend Serve ---
|
| 488 |
+
echo "----------------------------------------"
|
| 489 |
+
echo "7. Serving frontend application from atk-interface/dist/ in the FOREGROUND..."
|
| 490 |
+
echo "Output will be logged to: $FRONTEND_LOG_FILE"
|
| 491 |
+
# Ensure dist directory exists after build
|
| 492 |
+
if [ ! -d "dist" ]; then
|
| 493 |
+
echo "ERROR: 'dist' directory not found in atk-interface after build."
|
| 494 |
+
# Cleanup trap will handle stopping background services
|
| 495 |
+
exit 1
|
| 496 |
+
fi
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
# Show summary of all log files
|
| 500 |
+
echo ""
|
| 501 |
+
echo "═══════════════════════════════════════════════════════════════════════════════"
|
| 502 |
+
echo "All helper processes are now running with output redirected to log files:"
|
| 503 |
+
if [ -n "$VALKEY_PID" ]; then
|
| 504 |
+
echo " • Valkey server logs: $VALKEY_LOG_FILE"
|
| 505 |
+
fi
|
| 506 |
+
echo " • Huey worker logs: $HUEY_LOG_FILE"
|
| 507 |
+
echo " • Uvicorn API logs: $UVICORN_LOG_FILE"
|
| 508 |
+
echo " • Frontend serve logs: $FRONTEND_LOG_FILE"
|
| 509 |
+
echo ""
|
| 510 |
+
echo "You can monitor these logs in real-time with:"
|
| 511 |
+
if [ -n "$VALKEY_PID" ]; then
|
| 512 |
+
echo " tail -f $VALKEY_LOG_FILE"
|
| 513 |
+
fi
|
| 514 |
+
echo " tail -f $HUEY_LOG_FILE"
|
| 515 |
+
echo " tail -f $UVICORN_LOG_FILE"
|
| 516 |
+
echo " tail -f $FRONTEND_LOG_FILE"
|
| 517 |
+
echo "═══════════════════════════════════════════════════════════════════════════════"
|
| 518 |
+
echo ""
|
| 519 |
+
|
| 520 |
+
# Serve on port 5173 (matches CORS config)
|
| 521 |
+
# Run npx serve in the background first, then open browser once it's ready
|
| 522 |
+
echo "Starting serve process (output redirected to log file)..."
|
| 523 |
+
# Redirect both stdout and stderr to the log file - use ../ to go back to script directory
|
| 524 |
+
npx serve -s dist --listen 5173 > "../$FRONTEND_LOG_FILE" 2>&1 &
|
| 525 |
+
SERVE_PID=$!
|
| 526 |
+
|
| 527 |
+
# Wait for the serve process to be ready
|
| 528 |
+
echo "Waiting for frontend server to start..."
|
| 529 |
+
sleep 3
|
| 530 |
+
|
| 531 |
+
# Check if serve process is still running
|
| 532 |
+
if ! kill -0 $SERVE_PID &> /dev/null; then
|
| 533 |
+
echo "ERROR: Frontend serve process failed to start or crashed immediately."
|
| 534 |
+
echo "Check the log file for details: $FRONTEND_LOG_FILE"
|
| 535 |
+
cleanup
|
| 536 |
+
exit 1
|
| 537 |
+
fi
|
| 538 |
+
|
| 539 |
+
# Wait for the server to actually be listening on port 5173
|
| 540 |
+
echo "Checking if server is responding..."
|
| 541 |
+
for i in {1..10}; do
|
| 542 |
+
if command -v curl &> /dev/null; then
|
| 543 |
+
if curl -s http://localhost:5173 > /dev/null 2>&1; then
|
| 544 |
+
break
|
| 545 |
+
fi
|
| 546 |
+
elif command -v wget &> /dev/null; then
|
| 547 |
+
if wget -q --spider http://localhost:5173 > /dev/null 2>&1; then
|
| 548 |
+
break
|
| 549 |
+
fi
|
| 550 |
+
else
|
| 551 |
+
# Fallback: just check if something is listening on port 5173
|
| 552 |
+
if command -v ss &> /dev/null; then
|
| 553 |
+
if ss -ltn | grep -q ':5173'; then
|
| 554 |
+
break
|
| 555 |
+
fi
|
| 556 |
+
elif command -v netstat &> /dev/null; then
|
| 557 |
+
if netstat -ltn | grep -q ':5173'; then
|
| 558 |
+
break
|
| 559 |
+
fi
|
| 560 |
+
else
|
| 561 |
+
# No way to check, just wait a bit more
|
| 562 |
+
sleep 1
|
| 563 |
+
break
|
| 564 |
+
fi
|
| 565 |
+
fi
|
| 566 |
+
sleep 1
|
| 567 |
+
done
|
| 568 |
+
|
| 569 |
+
echo "Opening interface..."
|
| 570 |
+
|
| 571 |
+
# --- Check for xdg-open ---
|
| 572 |
+
echo "Checking for xdg-open..."
|
| 573 |
+
if ! command -v xdg-open &> /dev/null; then
|
| 574 |
+
echo "WARNING: 'xdg-open' command not found. Cannot automatically open browser."
|
| 575 |
+
echo "Please install xdg-utils (e.g., 'sudo apt install xdg-utils' or 'sudo dnf install xdg-utils') or open http://localhost:5173 manually."
|
| 576 |
+
# Not exiting, just warning
|
| 577 |
+
else
|
| 578 |
+
echo "xdg-open found. Opening http://localhost:5173 in default browser... (if this fails, open http://localhost:5173 in a web browser of your choice)"
|
| 579 |
+
xdg-open http://localhost:5173
|
| 580 |
+
fi
|
| 581 |
+
|
| 582 |
+
echo "Frontend server is now running. Press Ctrl+C to stop all services."
|
| 583 |
+
|
| 584 |
+
# Wait for the serve process to finish (it's now running in the background)
|
| 585 |
+
wait $SERVE_PID
|
| 586 |
+
SERVE_EXIT_CODE=$?
|
| 587 |
+
SERVE_PID="" # Clear PID as it has exited
|
| 588 |
+
|
| 589 |
+
if [ $SERVE_EXIT_CODE -ne 0 ]; then
|
| 590 |
+
echo "WARNING: Frontend serve process exited with code $SERVE_EXIT_CODE."
|
| 591 |
+
echo "Check the log file for details: $FRONTEND_LOG_FILE"
|
| 592 |
+
# Consider if we should stop background services here if serve crashes
|
| 593 |
+
# Calling cleanup manually if serve fails
|
| 594 |
+
cleanup
|
| 595 |
+
exit 1 # Exit with error code
|
| 596 |
+
fi
|
| 597 |
+
|
| 598 |
+
|
| 599 |
+
# --- 8. Done (Only reached if npx serve exits gracefully, e.g., if killed separately) ---
|
| 600 |
+
cd "$SCRIPT_DIR" # Go back to root directory
|
| 601 |
+
echo "----------------------------------------"
|
| 602 |
+
echo "Frontend serve process finished."
|
| 603 |
+
echo ""
|
| 604 |
+
echo "Helper process log files were saved to:"
|
| 605 |
+
if [ -n "$VALKEY_PID" ]; then
|
| 606 |
+
echo " • Valkey server logs: $VALKEY_LOG_FILE"
|
| 607 |
+
fi
|
| 608 |
+
echo " • Huey worker logs: $HUEY_LOG_FILE"
|
| 609 |
+
echo " • Uvicorn API logs: $UVICORN_LOG_FILE"
|
| 610 |
+
echo " • Frontend serve logs: $FRONTEND_LOG_FILE"
|
| 611 |
+
# Call cleanup to stop background services if serve finishes normally
|
| 612 |
+
cleanup
|
| 613 |
+
|
| 614 |
+
exit 0
|
local_linux.sh
ADDED
|
@@ -0,0 +1,738 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Exit immediately if a command exits with a non-zero status.
|
| 4 |
+
# set -e # Disabled for manual error checking below
|
| 5 |
+
|
| 6 |
+
# Parse command line arguments
|
| 7 |
+
MODEL_TYPE="normal"
|
| 8 |
+
TENSOR_PARALLELISM=1
|
| 9 |
+
|
| 10 |
+
# Parse arguments
|
| 11 |
+
while [[ $# -gt 0 ]]; do
|
| 12 |
+
case $1 in
|
| 13 |
+
--tensor-parallelism)
|
| 14 |
+
TENSOR_PARALLELISM="$2"
|
| 15 |
+
shift 2
|
| 16 |
+
;;
|
| 17 |
+
-h|--help)
|
| 18 |
+
echo "Usage: $0 [MODEL_TYPE] [OPTIONS]"
|
| 19 |
+
echo ""
|
| 20 |
+
echo "MODEL_TYPE options:"
|
| 21 |
+
echo " normal - Use default model (Heralax/Augmentoolkit-DataSpecialist-v0.1)"
|
| 22 |
+
echo " small - Use quantized model (Heralax/Augmentoolkit-DataSpecialist-gptqmodel-4bit)"
|
| 23 |
+
echo " path/to/custom/model - Use custom model path"
|
| 24 |
+
echo ""
|
| 25 |
+
echo "OPTIONS:"
|
| 26 |
+
echo " --tensor-parallelism INT - Number of tensor parallel processes for vLLM (default: 1)"
|
| 27 |
+
echo ""
|
| 28 |
+
echo "Examples:"
|
| 29 |
+
echo " $0 # Use normal model (default)"
|
| 30 |
+
echo " $0 small # Use quantized model"
|
| 31 |
+
echo " $0 deepseek-ai/DeepSeek-R1 # Use custom model"
|
| 32 |
+
echo " $0 normal --tensor-parallelism 2 # Use normal model with 2 tensor parallel processes"
|
| 33 |
+
echo " $0 --tensor-parallelism 4 # Use default model with 4 tensor parallel processes"
|
| 34 |
+
echo ""
|
| 35 |
+
exit 0
|
| 36 |
+
;;
|
| 37 |
+
-*)
|
| 38 |
+
echo "Unknown option: $1"
|
| 39 |
+
echo "Use --help for usage information."
|
| 40 |
+
exit 1
|
| 41 |
+
;;
|
| 42 |
+
*)
|
| 43 |
+
# This is the MODEL_TYPE (positional argument)
|
| 44 |
+
if [ "$MODEL_TYPE" = "normal" ]; then
|
| 45 |
+
MODEL_TYPE="$1"
|
| 46 |
+
else
|
| 47 |
+
echo "Error: Multiple model types specified"
|
| 48 |
+
echo "Use --help for usage information."
|
| 49 |
+
exit 1
|
| 50 |
+
fi
|
| 51 |
+
shift
|
| 52 |
+
;;
|
| 53 |
+
esac
|
| 54 |
+
done
|
| 55 |
+
|
| 56 |
+
# Validate tensor parallelism value
|
| 57 |
+
if ! [[ "$TENSOR_PARALLELISM" =~ ^[0-9]+$ ]] || [ "$TENSOR_PARALLELISM" -lt 1 ]; then
|
| 58 |
+
echo "Error: --tensor-parallelism must be a positive integer (got: $TENSOR_PARALLELISM)"
|
| 59 |
+
exit 1
|
| 60 |
+
fi
|
| 61 |
+
|
| 62 |
+
echo "Starting Augmentoolkit services with model type: $MODEL_TYPE"
|
| 63 |
+
echo "Tensor parallelism: $TENSOR_PARALLELISM"
|
| 64 |
+
echo "(Use '$0 --help' to see available model options)"
|
| 65 |
+
|
| 66 |
+
# --- Get script directory and change to it ---
|
| 67 |
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
| 68 |
+
cd "$SCRIPT_DIR"
|
| 69 |
+
if [ $? -ne 0 ]; then
|
| 70 |
+
echo "ERROR: Failed to change directory to script location '$SCRIPT_DIR'."
|
| 71 |
+
exit 1
|
| 72 |
+
fi
|
| 73 |
+
echo "Running in directory: $(pwd)"
|
| 74 |
+
|
| 75 |
+
# --- Cleanup Function ---
|
| 76 |
+
# Stores PIDs of background processes
|
| 77 |
+
HUEY_PID=""
|
| 78 |
+
UVICORN_PID=""
|
| 79 |
+
SERVE_PID="" # Though serve runs in foreground, trap might catch signals before it starts
|
| 80 |
+
LLAMA_SERVER_PID="" # PID for llama.cpp server
|
| 81 |
+
VALKEY_PID="" # PID for valkey server
|
| 82 |
+
|
| 83 |
+
cleanup() {
|
| 84 |
+
echo "" # Newline after ^C
|
| 85 |
+
echo "----------------------------------------"
|
| 86 |
+
echo "Signal received, stopping background services..."
|
| 87 |
+
|
| 88 |
+
# Function to kill a process and wait for it to terminate
|
| 89 |
+
kill_and_wait() {
|
| 90 |
+
local pid=$1
|
| 91 |
+
local name=$2
|
| 92 |
+
local timeout=9
|
| 93 |
+
|
| 94 |
+
if [ -n "$pid" ] && kill -0 $pid &> /dev/null; then
|
| 95 |
+
echo "Stopping $name (PID $pid)..."
|
| 96 |
+
|
| 97 |
+
# First try SIGTERM (graceful shutdown)
|
| 98 |
+
kill -TERM $pid 2>/dev/null
|
| 99 |
+
|
| 100 |
+
# Wait for process to terminate gracefully
|
| 101 |
+
local count=0
|
| 102 |
+
while [ $count -lt $timeout ] && kill -0 $pid &> /dev/null; do
|
| 103 |
+
sleep 1
|
| 104 |
+
count=$((count + 1))
|
| 105 |
+
done
|
| 106 |
+
|
| 107 |
+
# If still running, force kill with SIGKILL
|
| 108 |
+
if kill -0 $pid &> /dev/null; then
|
| 109 |
+
echo "Process $pid ($name) didn't terminate gracefully, force killing..."
|
| 110 |
+
kill -KILL $pid 2>/dev/null
|
| 111 |
+
sleep 1
|
| 112 |
+
fi
|
| 113 |
+
|
| 114 |
+
# Final check
|
| 115 |
+
if kill -0 $pid &> /dev/null; then
|
| 116 |
+
echo "WARNING: Failed to stop $name (PID $pid)"
|
| 117 |
+
else
|
| 118 |
+
echo "$name stopped successfully."
|
| 119 |
+
fi
|
| 120 |
+
fi
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
# Stop processes in reverse order of startup
|
| 124 |
+
kill_and_wait "$SERVE_PID" "frontend serve"
|
| 125 |
+
kill_and_wait "$UVICORN_PID" "Uvicorn API server"
|
| 126 |
+
kill_and_wait "$HUEY_PID" "Huey worker"
|
| 127 |
+
kill_and_wait "$LLAMA_SERVER_PID" "VLLM server"
|
| 128 |
+
kill_and_wait "$VALKEY_PID" "Valkey server"
|
| 129 |
+
|
| 130 |
+
# Find and kill any remaining huey consumers specific to this Augmentoolkit instance
|
| 131 |
+
echo "Searching for huey consumer processes in current directory ($SCRIPT_DIR)..."
|
| 132 |
+
|
| 133 |
+
# Use ps with full command line and grep for huey_consumer processes in our specific directory
|
| 134 |
+
# This avoids killing huey consumers from other Augmentoolkit instances or projects
|
| 135 |
+
HUEY_PIDS=$(ps aux | grep -E "huey_consumer.*tasks\.huey" | grep "$SCRIPT_DIR" | grep -v grep | awk '{print $2}')
|
| 136 |
+
|
| 137 |
+
if [ -n "$HUEY_PIDS" ]; then
|
| 138 |
+
echo "Found huey consumer processes in current directory: $HUEY_PIDS"
|
| 139 |
+
for pid in $HUEY_PIDS; do
|
| 140 |
+
if kill -0 $pid &> /dev/null; then
|
| 141 |
+
echo "Stopping huey consumer from current directory (PID $pid)..."
|
| 142 |
+
kill -TERM $pid 2>/dev/null
|
| 143 |
+
sleep 2
|
| 144 |
+
if kill -0 $pid &> /dev/null; then
|
| 145 |
+
echo "Force killing huey consumer (PID $pid)..."
|
| 146 |
+
kill -KILL $pid 2>/dev/null
|
| 147 |
+
fi
|
| 148 |
+
fi
|
| 149 |
+
done
|
| 150 |
+
sleep 1
|
| 151 |
+
# Final check for any remaining huey processes in our directory
|
| 152 |
+
REMAINING_HUEY=$(ps aux | grep -E "huey_consumer.*tasks\.huey" | grep "$SCRIPT_DIR" | grep -v grep | awk '{print $2}')
|
| 153 |
+
if [ -n "$REMAINING_HUEY" ]; then
|
| 154 |
+
echo "WARNING: Some huey consumer processes in current directory may still be running: $REMAINING_HUEY"
|
| 155 |
+
else
|
| 156 |
+
echo "All huey consumer processes from current directory stopped successfully."
|
| 157 |
+
fi
|
| 158 |
+
else
|
| 159 |
+
echo "No additional huey consumer processes found in current directory."
|
| 160 |
+
fi
|
| 161 |
+
|
| 162 |
+
# Deactivate virtual environment if active
|
| 163 |
+
if command -v deactivate &> /dev/null; then
|
| 164 |
+
echo "Deactivating virtual environment..."
|
| 165 |
+
deactivate
|
| 166 |
+
fi
|
| 167 |
+
|
| 168 |
+
# Give a moment for any remaining output to flush
|
| 169 |
+
sleep 1
|
| 170 |
+
|
| 171 |
+
# Clear any remaining output and reset cursor
|
| 172 |
+
echo "Cleanup complete."
|
| 173 |
+
echo "----------------------------------------"
|
| 174 |
+
|
| 175 |
+
# Ensure cursor is visible and terminal is in normal state
|
| 176 |
+
printf "\033[?25h" # Show cursor
|
| 177 |
+
printf "\033[0m" # Reset all attributes
|
| 178 |
+
|
| 179 |
+
echo "" # Add extra newline for visual clarity before prompt returns
|
| 180 |
+
}
|
| 181 |
+
|
| 182 |
+
# Trap signals (INT: Ctrl+C, TERM: standard kill) and run cleanup function
|
| 183 |
+
trap cleanup SIGINT SIGTERM
|
| 184 |
+
|
| 185 |
+
# --- 1. Python Virtual Environment & Dependencies ---
|
| 186 |
+
echo "----------------------------------------"
|
| 187 |
+
echo "1 / 9. Setting up Python virtual environment and installing dependencies..."
|
| 188 |
+
VENV_DIR=".venv"
|
| 189 |
+
|
| 190 |
+
# Check if python3.11 command exists (preferred)
|
| 191 |
+
if command -v python3.11 &> /dev/null; then
|
| 192 |
+
PYTHON_CMD="python3.11"
|
| 193 |
+
echo "Using Python 3.11: $(python3.11 --version)"
|
| 194 |
+
elif command -v python3 &> /dev/null; then
|
| 195 |
+
PYTHON_CMD="python3"
|
| 196 |
+
PYTHON_VERSION=$(python3 --version 2>&1 | grep -oE '[0-9]+\.[0-9]+')
|
| 197 |
+
echo "Using Python: $(python3 --version)"
|
| 198 |
+
if [[ "$PYTHON_VERSION" != "3.11" ]]; then
|
| 199 |
+
echo "WARNING: Python 3.11 is recommended but not found. Using $PYTHON_VERSION instead."
|
| 200 |
+
echo "If you encounter issues, please install Python 3.11 specifically."
|
| 201 |
+
fi
|
| 202 |
+
else
|
| 203 |
+
echo "ERROR: Neither python3.11 nor python3 command found."
|
| 204 |
+
echo "Please install Python 3.11 (recommended) or Python 3.x and rerun the script."
|
| 205 |
+
echo "On Ubuntu/Debian, you can install Python 3.11 using:"
|
| 206 |
+
echo " sudo apt update && sudo apt install python3.11 python3.11-venv python3.11-dev"
|
| 207 |
+
echo "On RHEL/CentOS/Fedora, you can install Python 3.11 using:"
|
| 208 |
+
echo " sudo dnf install python3.11 python3.11-devel"
|
| 209 |
+
echo "Or download from: https://www.python.org/downloads/"
|
| 210 |
+
exit 1
|
| 211 |
+
fi
|
| 212 |
+
|
| 213 |
+
# Create virtual environment if it doesn't exist
|
| 214 |
+
if [ ! -d "$VENV_DIR" ]; then
|
| 215 |
+
echo "Creating virtual environment in '$VENV_DIR' using $PYTHON_CMD..."
|
| 216 |
+
$PYTHON_CMD -m venv "$VENV_DIR"
|
| 217 |
+
if [ $? -ne 0 ]; then
|
| 218 |
+
echo "ERROR: Failed to create Python virtual environment using $PYTHON_CMD."
|
| 219 |
+
exit 1
|
| 220 |
+
fi
|
| 221 |
+
else
|
| 222 |
+
echo "Virtual environment '$VENV_DIR' already exists."
|
| 223 |
+
fi
|
| 224 |
+
|
| 225 |
+
# Activate virtual environment
|
| 226 |
+
echo "Activating virtual environment..."
|
| 227 |
+
source "$VENV_DIR/bin/activate"
|
| 228 |
+
if [ $? -ne 0 ]; then
|
| 229 |
+
echo "ERROR: Failed to activate Python virtual environment."
|
| 230 |
+
exit 1
|
| 231 |
+
fi
|
| 232 |
+
echo "Virtual environment activated. Python executable: $(which python)"
|
| 233 |
+
|
| 234 |
+
# --- Check/Install uv ---
|
| 235 |
+
echo "Checking for uv..."
|
| 236 |
+
if ! command -v uv &> /dev/null; then
|
| 237 |
+
echo "'uv' command not found. Attempting to install uv using pip..."
|
| 238 |
+
# Ensure pip is available (it should be with python3)
|
| 239 |
+
if ! python -m pip --version &> /dev/null; then
|
| 240 |
+
echo "ERROR: python -m pip command failed. Cannot install uv."
|
| 241 |
+
exit 1
|
| 242 |
+
fi
|
| 243 |
+
python -m pip install uv
|
| 244 |
+
if [ $? -ne 0 ]; then
|
| 245 |
+
echo "ERROR: Failed to install uv using pip. Please install uv manually (e.g., 'pip install uv') and rerun the script."
|
| 246 |
+
exit 1
|
| 247 |
+
fi
|
| 248 |
+
echo "uv installed successfully."
|
| 249 |
+
else
|
| 250 |
+
echo "uv found."
|
| 251 |
+
fi
|
| 252 |
+
# --- End Check/Install uv ---
|
| 253 |
+
|
| 254 |
+
|
| 255 |
+
# Install dependencies using uv
|
| 256 |
+
echo "Installing Python dependencies from requirements.txt using uv..."
|
| 257 |
+
# Assuming uv is in PATH after the check above and venv is activated
|
| 258 |
+
uv pip install -r requirements.txt
|
| 259 |
+
if [ $? -ne 0 ]; then
|
| 260 |
+
echo "ERROR: Failed to install Python dependencies using uv. Please check requirements.txt and uv logs."
|
| 261 |
+
exit 1 # Cleanup trap will handle exit
|
| 262 |
+
fi
|
| 263 |
+
echo "Installing vllm using uv..."
|
| 264 |
+
uv pip install vllm
|
| 265 |
+
if [ $? -ne 0 ]; then
|
| 266 |
+
echo "ERROR: Failed to install vllm using uv."
|
| 267 |
+
exit 1
|
| 268 |
+
fi
|
| 269 |
+
|
| 270 |
+
echo "Python dependencies installed successfully."
|
| 271 |
+
|
| 272 |
+
|
| 273 |
+
# --- Helper Process Logging Setup ---
|
| 274 |
+
echo "----------------------------------------"
|
| 275 |
+
echo "Setting up helper process logging..."
|
| 276 |
+
|
| 277 |
+
# Create helper_process_logs directory if it doesn't exist
|
| 278 |
+
HELPER_LOGS_DIR="helper_process_logs"
|
| 279 |
+
if [ ! -d "$HELPER_LOGS_DIR" ]; then
|
| 280 |
+
echo "Creating helper process logs directory: $HELPER_LOGS_DIR"
|
| 281 |
+
mkdir -p "$HELPER_LOGS_DIR"
|
| 282 |
+
if [ $? -ne 0 ]; then
|
| 283 |
+
echo "ERROR: Failed to create helper process logs directory."
|
| 284 |
+
exit 1
|
| 285 |
+
fi
|
| 286 |
+
else
|
| 287 |
+
echo "Helper process logs directory already exists: $HELPER_LOGS_DIR"
|
| 288 |
+
fi
|
| 289 |
+
|
| 290 |
+
# Generate timestamp for log files
|
| 291 |
+
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
| 292 |
+
VALKEY_LOG_FILE="$HELPER_LOGS_DIR/valkey_server_${TIMESTAMP}.log"
|
| 293 |
+
LLAMA_SERVER_LOG_FILE="$HELPER_LOGS_DIR/llama_server_${TIMESTAMP}.log"
|
| 294 |
+
HUEY_LOG_FILE="$HELPER_LOGS_DIR/huey_worker_${TIMESTAMP}.log"
|
| 295 |
+
UVICORN_LOG_FILE="$HELPER_LOGS_DIR/uvicorn_api_${TIMESTAMP}.log"
|
| 296 |
+
FRONTEND_LOG_FILE="$HELPER_LOGS_DIR/frontend_serve_${TIMESTAMP}.log"
|
| 297 |
+
|
| 298 |
+
echo "Helper process logs will be saved to:"
|
| 299 |
+
echo " Valkey server: $VALKEY_LOG_FILE"
|
| 300 |
+
echo " VLLM server: $LLAMA_SERVER_LOG_FILE"
|
| 301 |
+
echo " Huey worker: $HUEY_LOG_FILE"
|
| 302 |
+
echo " Uvicorn API: $UVICORN_LOG_FILE"
|
| 303 |
+
echo " Frontend serve: $FRONTEND_LOG_FILE"
|
| 304 |
+
echo "Helper process logging setup complete."
|
| 305 |
+
|
| 306 |
+
#### if REDIS_HOST skip
|
| 307 |
+
if [ -z "$REDIS_HOST" ]; then
|
| 308 |
+
# --- 1.5 Valkey/Redis Check ---
|
| 309 |
+
echo "----------------------------------------"
|
| 310 |
+
echo "2 / 9. Checking for running Valkey/Redis service..."
|
| 311 |
+
|
| 312 |
+
SERVER_CMD=""
|
| 313 |
+
SERVICE_NAME=""
|
| 314 |
+
|
| 315 |
+
# Check for valkey-server first
|
| 316 |
+
if command -v valkey-server &> /dev/null; then
|
| 317 |
+
SERVER_CMD="valkey-server"
|
| 318 |
+
SERVICE_NAME="valkey-server"
|
| 319 |
+
# Fallback to redis-server
|
| 320 |
+
elif command -v redis-server &> /dev/null; then
|
| 321 |
+
SERVER_CMD="redis-server"
|
| 322 |
+
SERVICE_NAME="redis-server"
|
| 323 |
+
else
|
| 324 |
+
# Check if something is listening on the default Valkey/Redis port (6379)
|
| 325 |
+
if command -v ss &> /dev/null; then
|
| 326 |
+
PORT_CHECK_CMD="ss -ltn"
|
| 327 |
+
else
|
| 328 |
+
PORT_CHECK_CMD="netstat -ltn"
|
| 329 |
+
fi
|
| 330 |
+
|
| 331 |
+
if $PORT_CHECK_CMD | grep -q ':6379'; then
|
| 332 |
+
echo "WARNING: Neither 'valkey-server' nor 'redis-server' found in PATH, but something is listening on port 6379."
|
| 333 |
+
echo "Assuming a compatible Valkey/Redis server is running (possibly built from source or custom install)."
|
| 334 |
+
else
|
| 335 |
+
echo "Neither 'valkey-server' nor 'redis-server' found in PATH."
|
| 336 |
+
echo "Building Valkey from source..."
|
| 337 |
+
|
| 338 |
+
# Check if valkey directory already exists
|
| 339 |
+
if [ ! -d "valkey" ]; then
|
| 340 |
+
echo "Cloning Valkey repository..."
|
| 341 |
+
git clone https://github.com/valkey-io/valkey.git
|
| 342 |
+
if [ $? -ne 0 ]; then
|
| 343 |
+
echo "ERROR: Failed to clone Valkey repository. Please check your internet connection and git installation."
|
| 344 |
+
exit 1
|
| 345 |
+
fi
|
| 346 |
+
else
|
| 347 |
+
echo "Valkey directory already exists, skipping clone."
|
| 348 |
+
fi
|
| 349 |
+
|
| 350 |
+
# Only build if valkey-server doesn't exist
|
| 351 |
+
if [ ! -f "valkey/src/valkey-server" ]; then
|
| 352 |
+
echo "Building Valkey..."
|
| 353 |
+
cd valkey
|
| 354 |
+
make
|
| 355 |
+
if [ $? -ne 0 ]; then
|
| 356 |
+
echo "ERROR: Failed to build Valkey. Please check build dependencies (gcc, make, etc.)."
|
| 357 |
+
cd "$SCRIPT_DIR"
|
| 358 |
+
exit 1
|
| 359 |
+
fi
|
| 360 |
+
cd "$SCRIPT_DIR"
|
| 361 |
+
else
|
| 362 |
+
echo "Valkey already built, skipping build."
|
| 363 |
+
fi
|
| 364 |
+
|
| 365 |
+
echo "Starting Valkey server in background..."
|
| 366 |
+
echo "Output will be logged to: $VALKEY_LOG_FILE"
|
| 367 |
+
cd valkey/src
|
| 368 |
+
./valkey-server > "../../$VALKEY_LOG_FILE" 2>&1 &
|
| 369 |
+
VALKEY_PID=$!
|
| 370 |
+
cd "$SCRIPT_DIR"
|
| 371 |
+
|
| 372 |
+
# Give it a moment to start/fail
|
| 373 |
+
sleep 4
|
| 374 |
+
if ! kill -0 $VALKEY_PID &> /dev/null; then
|
| 375 |
+
echo "ERROR: Valkey server (PID $VALKEY_PID) failed to start or crashed immediately."
|
| 376 |
+
echo "Check the log file for details: $VALKEY_LOG_FILE"
|
| 377 |
+
exit 1
|
| 378 |
+
fi
|
| 379 |
+
echo "Valkey server started successfully with PID $VALKEY_PID."
|
| 380 |
+
|
| 381 |
+
# Set SERVER_CMD and SERVICE_NAME for consistency with the rest of the script
|
| 382 |
+
SERVER_CMD="./valkey/src/valkey-server"
|
| 383 |
+
SERVICE_NAME="valkey-server"
|
| 384 |
+
fi
|
| 385 |
+
fi
|
| 386 |
+
echo "Found $SERVER_CMD."
|
| 387 |
+
|
| 388 |
+
# If we didn't just start Valkey server locally, perform service checks
|
| 389 |
+
if [ -z "$VALKEY_PID" ]; then
|
| 390 |
+
# Check if systemctl exists and is usable
|
| 391 |
+
if command -v systemctl &> /dev/null && systemctl status &> /dev/null; then
|
| 392 |
+
# systemctl seems to be working, so let's use it
|
| 393 |
+
if ! systemctl is-active --quiet $SERVICE_NAME; then
|
| 394 |
+
echo "WARNING: The $SERVICE_NAME service is not active according to systemctl."
|
| 395 |
+
# Check if something is listening on port 6379 as fallback
|
| 396 |
+
if ss -ltn | grep -q ':6379' 2>/dev/null; then
|
| 397 |
+
echo "However, something is listening on port 6379. Assuming a compatible Valkey/Redis server is running."
|
| 398 |
+
else
|
| 399 |
+
echo "ERROR: The $SERVICE_NAME service is not active and nothing is listening on port 6379."
|
| 400 |
+
echo "Please start the service, e.g., 'sudo systemctl start $SERVICE_NAME' and ensure it's enabled to start on boot, e.g., 'sudo systemctl enable $SERVICE_NAME'."
|
| 401 |
+
exit 1
|
| 402 |
+
fi
|
| 403 |
+
else
|
| 404 |
+
echo "The $SERVICE_NAME service is active."
|
| 405 |
+
fi
|
| 406 |
+
else
|
| 407 |
+
# systemctl not available or not working, fall back to simple port check
|
| 408 |
+
echo "WARNING: 'systemctl' not available/running. Checking for service via port."
|
| 409 |
+
if ss -ltn | grep -q ':6379' 2>/dev/null; then
|
| 410 |
+
echo "A service is listening on port 6379. Assuming a compatible Valkey/Redis server is running."
|
| 411 |
+
else
|
| 412 |
+
echo "ERROR: No running Valkey/Redis service detected on port 6379, and could not start one automatically as it was expected to be running."
|
| 413 |
+
echo "Please start valkey-server or redis-server manually in another terminal."
|
| 414 |
+
exit 1
|
| 415 |
+
fi
|
| 416 |
+
fi
|
| 417 |
+
else
|
| 418 |
+
echo "Valkey server was started by this script (PID $VALKEY_PID), skipping system service check."
|
| 419 |
+
fi
|
| 420 |
+
# --- End Valkey/Redis Check (Step 2) ---
|
| 421 |
+
fi
|
| 422 |
+
|
| 423 |
+
# --- 1.6. VLLM Setup and Server ---
|
| 424 |
+
echo "----------------------------------------"
|
| 425 |
+
echo "3 / 9. Starting VLLM server..."
|
| 426 |
+
echo "Output will be logged to: $LLAMA_SERVER_LOG_FILE"
|
| 427 |
+
|
| 428 |
+
# Determine which model to use based on argument
|
| 429 |
+
if [ "$MODEL_TYPE" = "normal" ]; then
|
| 430 |
+
MODEL_NAME="Heralax/Augmentoolkit-DataSpecialist-v0.1"
|
| 431 |
+
echo "Using normal model: $MODEL_NAME"
|
| 432 |
+
elif [ "$MODEL_TYPE" = "small" ]; then
|
| 433 |
+
MODEL_NAME="Heralax/Augmentoolkit-DataSpecialist-gptqmodel-4bit"
|
| 434 |
+
echo "Using small (quantized) model: $MODEL_NAME"
|
| 435 |
+
echo "=============================================="
|
| 436 |
+
echo "!!! IMPORTANT NOTICE !!!"
|
| 437 |
+
echo "=============================================="
|
| 438 |
+
echo "You are using the quantized model."
|
| 439 |
+
echo "When doing local dataset generation, you MUST use:"
|
| 440 |
+
echo " 'Heralax/Augmentoolkit-DataSpecialist-gptqmodel-4bit'"
|
| 441 |
+
echo "in your config files instead of the default model."
|
| 442 |
+
echo ""
|
| 443 |
+
echo "Update your config files to use the quantized model:"
|
| 444 |
+
echo " - pdf_cleaning_small_model: Heralax/Augmentoolkit-DataSpecialist-gptqmodel-4bit"
|
| 445 |
+
echo " - pdf_cleaning_large_model: Heralax/Augmentoolkit-DataSpecialist-gptqmodel-4bit"
|
| 446 |
+
echo " - representation_variation_small_model: Heralax/Augmentoolkit-DataSpecialist-gptqmodel-4bit"
|
| 447 |
+
echo " - representation_variation_large_model: Heralax/Augmentoolkit-DataSpecialist-gptqmodel-4bit"
|
| 448 |
+
echo " - ...etc... for any model. Find replace all cases of 'Heralax/Augmentoolkit-DataSpecialist-v0.1' with the gptq-4bit one."
|
| 449 |
+
echo "=============================================="
|
| 450 |
+
echo ""
|
| 451 |
+
else
|
| 452 |
+
MODEL_NAME="$MODEL_TYPE"
|
| 453 |
+
echo "Using custom model: $MODEL_NAME"
|
| 454 |
+
fi
|
| 455 |
+
|
| 456 |
+
# Start vllm server with output redirection
|
| 457 |
+
vllm serve "$MODEL_NAME" --port 8082 --max-model-len 20000 --tensor-parallel-size "$TENSOR_PARALLELISM" > "$LLAMA_SERVER_LOG_FILE" 2>&1 &
|
| 458 |
+
LLAMA_SERVER_PID=$!
|
| 459 |
+
sleep 5 # Give it a moment to start/fail
|
| 460 |
+
|
| 461 |
+
echo "VLLM server started with PID $LLAMA_SERVER_PID."
|
| 462 |
+
echo "Access VLLM server API at http://localhost:8082"
|
| 463 |
+
# --- End VLLM Setup (Step 3) ---
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
# --- 2. Huey Worker ---
|
| 467 |
+
echo "----------------------------------------"
|
| 468 |
+
echo "4 / 9. Starting Huey worker in the background (using venv)..."
|
| 469 |
+
echo "Output will be logged to: $HUEY_LOG_FILE"
|
| 470 |
+
# Call the huey_consumer script directly (should be in PATH from venv)
|
| 471 |
+
# Redirect both stdout and stderr to the log file
|
| 472 |
+
huey_consumer tasks.huey > "$HUEY_LOG_FILE" 2>&1 &
|
| 473 |
+
HUEY_PID=$!
|
| 474 |
+
# Give it a moment to start/fail
|
| 475 |
+
sleep 2
|
| 476 |
+
if ! kill -0 $HUEY_PID &> /dev/null; then
|
| 477 |
+
echo "ERROR: Huey worker (PID $HUEY_PID) failed to start or crashed immediately."
|
| 478 |
+
echo "Check the log file for details: $HUEY_LOG_FILE"
|
| 479 |
+
exit 1 # Cleanup trap will handle exit
|
| 480 |
+
fi
|
| 481 |
+
echo "Huey worker started successfully with PID $HUEY_PID."
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
# --- 3. API Server ---
|
| 485 |
+
echo "----------------------------------------"
|
| 486 |
+
echo "5 / 9. Starting FastAPI server (Uvicorn) in the background (using venv)..."
|
| 487 |
+
echo "Output will be logged to: $UVICORN_LOG_FILE"
|
| 488 |
+
# Host 0.0.0.0 makes it accessible on the network
|
| 489 |
+
# Use python -m uvicorn to ensure using the venv uvicorn
|
| 490 |
+
# Redirect both stdout and stderr to the log file
|
| 491 |
+
python -m uvicorn api:app --host 0.0.0.0 --port 8000 > "$UVICORN_LOG_FILE" 2>&1 &
|
| 492 |
+
UVICORN_PID=$!
|
| 493 |
+
# Give it a moment to start/fail
|
| 494 |
+
sleep 2
|
| 495 |
+
if ! kill -0 $UVICORN_PID &> /dev/null; then
|
| 496 |
+
echo "ERROR: Uvicorn server (PID $UVICORN_PID) failed to start or crashed immediately."
|
| 497 |
+
echo "Check the log file for details: $UVICORN_LOG_FILE"
|
| 498 |
+
# Cleanup trap will handle stopping Huey
|
| 499 |
+
exit 1 # Cleanup trap will handle exit
|
| 500 |
+
fi
|
| 501 |
+
echo "Uvicorn server started successfully with PID $UVICORN_PID. Access API at http://localhost:8000"
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
# --- 4. NPM Check ---
|
| 505 |
+
echo "----------------------------------------"
|
| 506 |
+
echo "6 / 9. Checking for npm..."
|
| 507 |
+
if ! command -v npm &> /dev/null; then
|
| 508 |
+
echo "ERROR: npm command not found."
|
| 509 |
+
echo "Please install Node.js and npm (e.g., via your system package manager like 'sudo apt install nodejs npm') and run this script again."
|
| 510 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 511 |
+
kill $HUEY_PID $UVICORN_PID
|
| 512 |
+
exit 1
|
| 513 |
+
fi
|
| 514 |
+
echo "npm found."
|
| 515 |
+
|
| 516 |
+
# Check Node.js version
|
| 517 |
+
NODE_VERSION=$(node --version 2>/dev/null | sed 's/v//')
|
| 518 |
+
if [ -z "$NODE_VERSION" ]; then
|
| 519 |
+
echo "ERROR: Could not determine Node.js version."
|
| 520 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 521 |
+
kill $HUEY_PID $UVICORN_PID
|
| 522 |
+
exit 1
|
| 523 |
+
fi
|
| 524 |
+
|
| 525 |
+
# Extract major version number
|
| 526 |
+
NODE_MAJOR_VERSION=$(echo "$NODE_VERSION" | cut -d. -f1)
|
| 527 |
+
|
| 528 |
+
echo "Node.js version: v$NODE_VERSION"
|
| 529 |
+
|
| 530 |
+
# Check if Node.js version is at least 18
|
| 531 |
+
if [ "$NODE_MAJOR_VERSION" -lt 18 ]; then
|
| 532 |
+
echo "ERROR: Node.js version $NODE_VERSION is too old."
|
| 533 |
+
echo "This application requires Node.js 18.0.0 or higher (20+ recommended)."
|
| 534 |
+
echo ""
|
| 535 |
+
echo "To upgrade Node.js, run the following commands:"
|
| 536 |
+
echo ""
|
| 537 |
+
echo "1. Add NodeSource repository:"
|
| 538 |
+
echo " curl -fsSL https://deb.nodesource.com/setup_20.x | bash -"
|
| 539 |
+
echo ""
|
| 540 |
+
echo "2. Remove conflicting packages (if any):"
|
| 541 |
+
echo " apt-get remove -y libnode-dev libnode72"
|
| 542 |
+
echo ""
|
| 543 |
+
echo "3. Install Node.js 20.x:"
|
| 544 |
+
echo " apt-get install -y nodejs"
|
| 545 |
+
echo ""
|
| 546 |
+
echo " OR run all steps at once:"
|
| 547 |
+
echo " curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && apt-get remove -y libnode-dev libnode72 2>/dev/null || true && apt-get install -y nodejs"
|
| 548 |
+
echo ""
|
| 549 |
+
echo "Alternative method using Node Version Manager (nvm):"
|
| 550 |
+
echo " curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.0/install.sh | bash"
|
| 551 |
+
echo " source ~/.bashrc"
|
| 552 |
+
echo " nvm install 20"
|
| 553 |
+
echo " nvm use 20"
|
| 554 |
+
echo ""
|
| 555 |
+
echo "After upgrading, please run this script again."
|
| 556 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 557 |
+
kill $HUEY_PID $UVICORN_PID
|
| 558 |
+
exit 1
|
| 559 |
+
fi
|
| 560 |
+
|
| 561 |
+
|
| 562 |
+
# --- 5. Frontend Dependencies ---
|
| 563 |
+
echo "----------------------------------------"
|
| 564 |
+
echo "7 / 9. Installing frontend dependencies in atk-interface..."
|
| 565 |
+
if [ ! -d "atk-interface" ]; then
|
| 566 |
+
echo "ERROR: Directory 'atk-interface' not found in $(pwd)."
|
| 567 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 568 |
+
kill $HUEY_PID $UVICORN_PID
|
| 569 |
+
exit 1
|
| 570 |
+
fi
|
| 571 |
+
cd atk-interface
|
| 572 |
+
npm install
|
| 573 |
+
if [ $? -ne 0 ]; then
|
| 574 |
+
echo "ERROR: Failed to install npm dependencies in atk-interface. Check package.json and npm logs."
|
| 575 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 576 |
+
kill $HUEY_PID $UVICORN_PID
|
| 577 |
+
cd "$SCRIPT_DIR" # Go back to root
|
| 578 |
+
exit 1
|
| 579 |
+
fi
|
| 580 |
+
echo "Frontend dependencies installed."
|
| 581 |
+
|
| 582 |
+
|
| 583 |
+
# --- 6. Frontend Build ---
|
| 584 |
+
echo "----------------------------------------"
|
| 585 |
+
echo "8 / 9. Building frontend application in atk-interface..."
|
| 586 |
+
npm run build
|
| 587 |
+
if [ $? -ne 0 ]; then
|
| 588 |
+
echo "ERROR: Failed to build frontend application. Check build script in package.json and logs."
|
| 589 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 590 |
+
kill $HUEY_PID $UVICORN_PID
|
| 591 |
+
cd "$SCRIPT_DIR" # Go back to root
|
| 592 |
+
exit 1
|
| 593 |
+
fi
|
| 594 |
+
echo "Frontend built successfully."
|
| 595 |
+
|
| 596 |
+
|
| 597 |
+
# --- 7. Frontend Serve ---
|
| 598 |
+
echo "----------------------------------------"
|
| 599 |
+
echo "9 / 9. Serving frontend application from atk-interface/dist/ in the FOREGROUND..."
|
| 600 |
+
echo "Output will be logged to: $FRONTEND_LOG_FILE"
|
| 601 |
+
# Ensure dist directory exists after build
|
| 602 |
+
if [ ! -d "dist" ]; then
|
| 603 |
+
echo "ERROR: 'dist' directory not found in atk-interface after build."
|
| 604 |
+
# Cleanup trap will handle stopping background services
|
| 605 |
+
exit 1
|
| 606 |
+
fi
|
| 607 |
+
|
| 608 |
+
|
| 609 |
+
# Show summary of all log files
|
| 610 |
+
echo ""
|
| 611 |
+
echo "═══════════════════════════════════════════════════════════════════════════════"
|
| 612 |
+
echo "All helper processes are now running with output redirected to log files:"
|
| 613 |
+
if [ -n "$VALKEY_PID" ]; then
|
| 614 |
+
echo " • Valkey server logs: $VALKEY_LOG_FILE"
|
| 615 |
+
fi
|
| 616 |
+
echo " • VLLM server logs: $LLAMA_SERVER_LOG_FILE"
|
| 617 |
+
echo " • Huey worker logs: $HUEY_LOG_FILE"
|
| 618 |
+
echo " • Uvicorn API logs: $UVICORN_LOG_FILE"
|
| 619 |
+
echo " • Frontend serve logs: $FRONTEND_LOG_FILE"
|
| 620 |
+
echo ""
|
| 621 |
+
echo "You can monitor these logs in real-time with:"
|
| 622 |
+
if [ -n "$VALKEY_PID" ]; then
|
| 623 |
+
echo " tail -f $VALKEY_LOG_FILE"
|
| 624 |
+
fi
|
| 625 |
+
echo " tail -f $LLAMA_SERVER_LOG_FILE"
|
| 626 |
+
echo " tail -f $HUEY_LOG_FILE"
|
| 627 |
+
echo " tail -f $UVICORN_LOG_FILE"
|
| 628 |
+
echo " tail -f $FRONTEND_LOG_FILE"
|
| 629 |
+
echo "═══════════════════════════════════════════════════════════════════════════════"
|
| 630 |
+
echo ""
|
| 631 |
+
|
| 632 |
+
# Serve on port 5173 (matches CORS config)
|
| 633 |
+
# Run npx serve in the background first, then open browser once it's ready
|
| 634 |
+
echo "Starting serve process (output redirected to log file)..."
|
| 635 |
+
# Redirect both stdout and stderr to the log file - use ../ to go back to script directory
|
| 636 |
+
npx serve -s dist --listen 5173 > "../$FRONTEND_LOG_FILE" 2>&1 &
|
| 637 |
+
SERVE_PID=$!
|
| 638 |
+
|
| 639 |
+
# Wait for the serve process to be ready
|
| 640 |
+
echo "Waiting for frontend server to start..."
|
| 641 |
+
sleep 3
|
| 642 |
+
|
| 643 |
+
# Check if serve process is still running
|
| 644 |
+
if ! kill -0 $SERVE_PID &> /dev/null; then
|
| 645 |
+
echo "ERROR: Frontend serve process failed to start or crashed immediately."
|
| 646 |
+
echo "Check the log file for details: $FRONTEND_LOG_FILE"
|
| 647 |
+
cleanup
|
| 648 |
+
exit 1
|
| 649 |
+
fi
|
| 650 |
+
|
| 651 |
+
# Wait for the server to actually be listening on port 5173
|
| 652 |
+
echo "Checking if server is responding..."
|
| 653 |
+
for i in {1..10}; do
|
| 654 |
+
if command -v curl &> /dev/null; then
|
| 655 |
+
if curl -s http://localhost:5173 > /dev/null 2>&1; then
|
| 656 |
+
break
|
| 657 |
+
fi
|
| 658 |
+
elif command -v wget &> /dev/null; then
|
| 659 |
+
if wget -q --spider http://localhost:5173 > /dev/null 2>&1; then
|
| 660 |
+
break
|
| 661 |
+
fi
|
| 662 |
+
else
|
| 663 |
+
# Fallback: just check if something is listening on port 5173
|
| 664 |
+
if command -v ss &> /dev/null; then
|
| 665 |
+
if ss -ltn | grep -q ':5173'; then
|
| 666 |
+
break
|
| 667 |
+
fi
|
| 668 |
+
elif command -v netstat &> /dev/null; then
|
| 669 |
+
if netstat -ltn | grep -q ':5173'; then
|
| 670 |
+
break
|
| 671 |
+
fi
|
| 672 |
+
else
|
| 673 |
+
# No way to check, just wait a bit more
|
| 674 |
+
sleep 1
|
| 675 |
+
break
|
| 676 |
+
fi
|
| 677 |
+
fi
|
| 678 |
+
sleep 1
|
| 679 |
+
done
|
| 680 |
+
|
| 681 |
+
if ! kill -0 $LLAMA_SERVER_PID &> /dev/null; then
|
| 682 |
+
echo "ERROR: VLLM server (PID $LLAMA_SERVER_PID) failed to start or crashed."
|
| 683 |
+
echo "Check the log file for details: $LLAMA_SERVER_LOG_FILE"
|
| 684 |
+
echo "The model probably wants to take too much memory. You may need to:"
|
| 685 |
+
echo " - Use 'small' argument to run quantized model: ./local_linux.sh small"
|
| 686 |
+
echo " - Use a custom model path: ./local_linux.sh path/to/your/model"
|
| 687 |
+
echo " - Use an API instead of local inference"
|
| 688 |
+
cleanup
|
| 689 |
+
exit 1
|
| 690 |
+
fi
|
| 691 |
+
|
| 692 |
+
echo "Opening interface..."
|
| 693 |
+
|
| 694 |
+
# --- Check for xdg-open ---
|
| 695 |
+
echo "Checking for xdg-open..."
|
| 696 |
+
if ! command -v xdg-open &> /dev/null; then
|
| 697 |
+
echo "WARNING: 'xdg-open' command not found. Cannot automatically open browser."
|
| 698 |
+
echo "Please install xdg-utils (e.g., 'sudo apt install xdg-utils' or 'sudo dnf install xdg-utils') or open http://localhost:5173 manually."
|
| 699 |
+
# Not exiting, just warning
|
| 700 |
+
else
|
| 701 |
+
echo "xdg-open found. Opening http://localhost:5173 in default browser..."
|
| 702 |
+
xdg-open http://localhost:5173
|
| 703 |
+
fi
|
| 704 |
+
|
| 705 |
+
echo "Frontend server is now running. Press Ctrl+C to stop all services."
|
| 706 |
+
|
| 707 |
+
# Wait for the serve process to finish (it's now running in the background)
|
| 708 |
+
wait $SERVE_PID
|
| 709 |
+
SERVE_EXIT_CODE=$?
|
| 710 |
+
SERVE_PID="" # Clear PID as it has exited
|
| 711 |
+
|
| 712 |
+
if [ $SERVE_EXIT_CODE -ne 0 ]; then
|
| 713 |
+
echo "WARNING: Frontend serve process exited with code $SERVE_EXIT_CODE."
|
| 714 |
+
echo "Check the log file for details: $FRONTEND_LOG_FILE"
|
| 715 |
+
# Consider if we should stop background services here if serve crashes
|
| 716 |
+
# Calling cleanup manually if serve fails
|
| 717 |
+
cleanup
|
| 718 |
+
exit 1 # Exit with error code
|
| 719 |
+
fi
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
# --- 8. Done (Only reached if npx serve exits gracefully, e.g., if killed separately) ---
|
| 723 |
+
cd "$SCRIPT_DIR" # Go back to root directory
|
| 724 |
+
echo "----------------------------------------"
|
| 725 |
+
echo "Frontend serve process finished."
|
| 726 |
+
echo ""
|
| 727 |
+
echo "Helper process log files were saved to:"
|
| 728 |
+
if [ -n "$VALKEY_PID" ]; then
|
| 729 |
+
echo " • Valkey server logs: $VALKEY_LOG_FILE"
|
| 730 |
+
fi
|
| 731 |
+
echo " • VLLM server logs: $LLAMA_SERVER_LOG_FILE"
|
| 732 |
+
echo " • Huey worker logs: $HUEY_LOG_FILE"
|
| 733 |
+
echo " • Uvicorn API logs: $UVICORN_LOG_FILE"
|
| 734 |
+
echo " • Frontend serve logs: $FRONTEND_LOG_FILE"
|
| 735 |
+
# Call cleanup to stop background services if serve finishes normally
|
| 736 |
+
cleanup
|
| 737 |
+
|
| 738 |
+
exit 0
|
local_macos.sh
ADDED
|
@@ -0,0 +1,713 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Exit immediately if a command exits with a non-zero status.
|
| 4 |
+
# set -e # Disabled for manual error checking below
|
| 5 |
+
|
| 6 |
+
echo "Starting Augmentoolkit services..."
|
| 7 |
+
|
| 8 |
+
# --- Get script directory and change to it ---
|
| 9 |
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
| 10 |
+
cd "$SCRIPT_DIR"
|
| 11 |
+
if [ $? -ne 0 ]; then
|
| 12 |
+
echo "ERROR: Failed to change directory to script location '$SCRIPT_DIR'."
|
| 13 |
+
exit 1
|
| 14 |
+
fi
|
| 15 |
+
echo "Running in directory: $(pwd)"
|
| 16 |
+
|
| 17 |
+
# --- Cleanup Function ---
|
| 18 |
+
# Stores PIDs of background processes
|
| 19 |
+
HUEY_PID=""
|
| 20 |
+
UVICORN_PID=""
|
| 21 |
+
SERVE_PID="" # Though serve runs in foreground, trap might catch signals before it starts
|
| 22 |
+
LLAMA_SERVER_PID="" # PID for llama.cpp server
|
| 23 |
+
|
| 24 |
+
cleanup() {
|
| 25 |
+
echo "" # Newline after ^C
|
| 26 |
+
echo "----------------------------------------"
|
| 27 |
+
echo "Signal received, stopping background services..."
|
| 28 |
+
|
| 29 |
+
# Function to kill a process and wait for it to terminate
|
| 30 |
+
kill_and_wait() {
|
| 31 |
+
local pid=$1
|
| 32 |
+
local name=$2
|
| 33 |
+
local timeout=9
|
| 34 |
+
|
| 35 |
+
if [ -n "$pid" ] && kill -0 $pid &> /dev/null; then
|
| 36 |
+
echo "Stopping $name (PID $pid)..."
|
| 37 |
+
|
| 38 |
+
# First try SIGTERM (graceful shutdown)
|
| 39 |
+
kill -TERM $pid 2>/dev/null
|
| 40 |
+
|
| 41 |
+
# Wait for process to terminate gracefully
|
| 42 |
+
local count=0
|
| 43 |
+
while [ $count -lt $timeout ] && kill -0 $pid &> /dev/null; do
|
| 44 |
+
sleep 1
|
| 45 |
+
count=$((count + 1))
|
| 46 |
+
done
|
| 47 |
+
|
| 48 |
+
# If still running, force kill with SIGKILL
|
| 49 |
+
if kill -0 $pid &> /dev/null; then
|
| 50 |
+
echo "Process $pid ($name) didn't terminate gracefully, force killing..."
|
| 51 |
+
kill -KILL $pid 2>/dev/null
|
| 52 |
+
sleep 1
|
| 53 |
+
fi
|
| 54 |
+
|
| 55 |
+
# Final check
|
| 56 |
+
if kill -0 $pid &> /dev/null; then
|
| 57 |
+
echo "WARNING: Failed to stop $name (PID $pid)"
|
| 58 |
+
else
|
| 59 |
+
echo "$name stopped successfully."
|
| 60 |
+
fi
|
| 61 |
+
fi
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
# Stop processes in reverse order of startup
|
| 65 |
+
kill_and_wait "$SERVE_PID" "frontend serve"
|
| 66 |
+
kill_and_wait "$UVICORN_PID" "Uvicorn API server"
|
| 67 |
+
kill_and_wait "$HUEY_PID" "Huey worker"
|
| 68 |
+
kill_and_wait "$LLAMA_SERVER_PID" "LLaMA.cpp server"
|
| 69 |
+
|
| 70 |
+
# Find and kill any remaining huey consumers specific to this Augmentoolkit instance
|
| 71 |
+
echo "Searching for huey consumer processes in current directory ($SCRIPT_DIR)..."
|
| 72 |
+
|
| 73 |
+
# Use ps with full command line and grep for huey_consumer processes in our specific directory
|
| 74 |
+
# This avoids killing huey consumers from other Augmentoolkit instances or projects
|
| 75 |
+
HUEY_PIDS=$(ps aux | grep -E "huey_consumer.*tasks\.huey" | grep "$SCRIPT_DIR" | grep -v grep | awk '{print $2}')
|
| 76 |
+
|
| 77 |
+
if [ -n "$HUEY_PIDS" ]; then
|
| 78 |
+
echo "Found huey consumer processes in current directory: $HUEY_PIDS"
|
| 79 |
+
for pid in $HUEY_PIDS; do
|
| 80 |
+
if kill -0 $pid &> /dev/null; then
|
| 81 |
+
echo "Stopping huey consumer from current directory (PID $pid)..."
|
| 82 |
+
kill -TERM $pid 2>/dev/null
|
| 83 |
+
sleep 2
|
| 84 |
+
if kill -0 $pid &> /dev/null; then
|
| 85 |
+
echo "Force killing huey consumer (PID $pid)..."
|
| 86 |
+
kill -KILL $pid 2>/dev/null
|
| 87 |
+
fi
|
| 88 |
+
fi
|
| 89 |
+
done
|
| 90 |
+
sleep 1
|
| 91 |
+
# Final check for any remaining huey processes in our directory
|
| 92 |
+
REMAINING_HUEY=$(ps aux | grep -E "huey_consumer.*tasks\.huey" | grep "$SCRIPT_DIR" | grep -v grep | awk '{print $2}')
|
| 93 |
+
if [ -n "$REMAINING_HUEY" ]; then
|
| 94 |
+
echo "WARNING: Some huey consumer processes in current directory may still be running: $REMAINING_HUEY"
|
| 95 |
+
else
|
| 96 |
+
echo "All huey consumer processes from current directory stopped successfully."
|
| 97 |
+
fi
|
| 98 |
+
else
|
| 99 |
+
echo "No additional huey consumer processes found in current directory."
|
| 100 |
+
fi
|
| 101 |
+
|
| 102 |
+
# Deactivate virtual environment if active
|
| 103 |
+
if command -v deactivate &> /dev/null; then
|
| 104 |
+
echo "Deactivating virtual environment..."
|
| 105 |
+
deactivate
|
| 106 |
+
fi
|
| 107 |
+
|
| 108 |
+
# Give a moment for any remaining output to flush
|
| 109 |
+
sleep 1
|
| 110 |
+
|
| 111 |
+
# Clear any remaining output and reset cursor
|
| 112 |
+
echo "Cleanup complete."
|
| 113 |
+
echo "----------------------------------------"
|
| 114 |
+
|
| 115 |
+
# Ensure cursor is visible and terminal is in normal state
|
| 116 |
+
printf "\033[?25h" # Show cursor
|
| 117 |
+
printf "\033[0m" # Reset all attributes
|
| 118 |
+
|
| 119 |
+
# Do not exit from cleanup function itself, let the caller decide exit status
|
| 120 |
+
echo "" # Add extra newline for visual clarity before prompt returns
|
| 121 |
+
}
|
| 122 |
+
|
| 123 |
+
# Trap signals (INT: Ctrl+C, TERM: standard kill) and run cleanup function
|
| 124 |
+
trap cleanup SIGINT SIGTERM
|
| 125 |
+
|
| 126 |
+
# --- 1. Python Virtual Environment & Dependencies ---
|
| 127 |
+
echo "----------------------------------------"
|
| 128 |
+
echo "1 / 10. Setting up Python virtual environment and installing dependencies..."
|
| 129 |
+
VENV_DIR=".venv"
|
| 130 |
+
|
| 131 |
+
# Check if python3.11 command exists (preferred)
|
| 132 |
+
if command -v python3.11 &> /dev/null; then
|
| 133 |
+
PYTHON_CMD="python3.11"
|
| 134 |
+
echo "Using Python 3.11: $(python3.11 --version)"
|
| 135 |
+
elif command -v python3 &> /dev/null; then
|
| 136 |
+
PYTHON_CMD="python3"
|
| 137 |
+
PYTHON_VERSION=$(python3 --version 2>&1 | grep -oE '[0-9]+\.[0-9]+')
|
| 138 |
+
echo "Using Python: $(python3 --version)"
|
| 139 |
+
if [[ "$PYTHON_VERSION" != "3.11" ]]; then
|
| 140 |
+
echo "WARNING: Python 3.11 is recommended but not found. Using $PYTHON_VERSION instead."
|
| 141 |
+
echo "If you encounter issues, please install Python 3.11 specifically."
|
| 142 |
+
fi
|
| 143 |
+
else
|
| 144 |
+
echo "ERROR: Neither python3.11 nor python3 command found."
|
| 145 |
+
echo "Please install Python 3.11 (recommended) or Python 3.x and rerun the script."
|
| 146 |
+
echo "On macOS, you can install Python 3.11 using:"
|
| 147 |
+
echo " brew install python@3.11"
|
| 148 |
+
echo "Or download from: https://www.python.org/downloads/"
|
| 149 |
+
exit 1
|
| 150 |
+
fi
|
| 151 |
+
|
| 152 |
+
# --- Check/Install uv ---
|
| 153 |
+
echo "Checking for uv..."
|
| 154 |
+
if ! command -v uv &> /dev/null; then
|
| 155 |
+
echo "'uv' command not found. Attempting to install uv using pip..."
|
| 156 |
+
# Ensure pip is available (it should be with the selected Python version)
|
| 157 |
+
if ! $PYTHON_CMD -m pip --version &> /dev/null; then
|
| 158 |
+
echo "ERROR: $PYTHON_CMD -m pip command failed. Cannot install uv."
|
| 159 |
+
exit 1
|
| 160 |
+
fi
|
| 161 |
+
$PYTHON_CMD -m pip install uv
|
| 162 |
+
if [ $? -ne 0 ]; then
|
| 163 |
+
echo "ERROR: Failed to install uv using pip. Please install uv manually (e.g., 'pip install uv' or 'brew install uv') and rerun the script."
|
| 164 |
+
exit 1
|
| 165 |
+
fi
|
| 166 |
+
echo "uv installed successfully."
|
| 167 |
+
else
|
| 168 |
+
echo "uv found."
|
| 169 |
+
fi
|
| 170 |
+
# --- End Check/Install uv ---
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
# Create virtual environment if it doesn't exist
|
| 174 |
+
if [ ! -d "$VENV_DIR" ]; then
|
| 175 |
+
echo "Creating virtual environment in '$VENV_DIR' using $PYTHON_CMD..."
|
| 176 |
+
$PYTHON_CMD -m venv "$VENV_DIR"
|
| 177 |
+
if [ $? -ne 0 ]; then
|
| 178 |
+
echo "ERROR: Failed to create Python virtual environment using $PYTHON_CMD."
|
| 179 |
+
exit 1
|
| 180 |
+
fi
|
| 181 |
+
else
|
| 182 |
+
echo "Virtual environment '$VENV_DIR' already exists."
|
| 183 |
+
fi
|
| 184 |
+
|
| 185 |
+
# Activate virtual environment
|
| 186 |
+
echo "Activating virtual environment..."
|
| 187 |
+
source "$VENV_DIR/bin/activate"
|
| 188 |
+
if [ $? -ne 0 ]; then
|
| 189 |
+
echo "ERROR: Failed to activate Python virtual environment."
|
| 190 |
+
exit 1
|
| 191 |
+
fi
|
| 192 |
+
echo "Virtual environment activated. Python executable: $(which python)"
|
| 193 |
+
|
| 194 |
+
# --- Attempt to install SSL certificates (macOS specific fix) ---
|
| 195 |
+
echo "Attempting to install SSL certificates for the active Python environment..."
|
| 196 |
+
# Get the full path to the python executable inside the venv
|
| 197 |
+
VENV_PYTHON_EXE=$(which python)
|
| 198 |
+
# Try to infer the Applications directory path (this is heuristic)
|
| 199 |
+
# Assumes Python was installed via the standard macOS installer
|
| 200 |
+
PYTHON_BASE_DIR=$(dirname "$(dirname "$VENV_PYTHON_EXE")") # Go up two levels from venv/bin/python
|
| 201 |
+
CERT_SCRIPT_PATH=""
|
| 202 |
+
# Check common installation patterns
|
| 203 |
+
if [ -d "$PYTHON_BASE_DIR/Install Certificates.command" ]; then
|
| 204 |
+
# Might be like /Library/Frameworks/Python.framework/Versions/3.11/Install Certificates.command
|
| 205 |
+
CERT_SCRIPT_PATH="$PYTHON_BASE_DIR/Install Certificates.command"
|
| 206 |
+
elif [ -d "/Applications/Python $(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')/Install Certificates.command" ]; then
|
| 207 |
+
# Check standard /Applications path using the version
|
| 208 |
+
PY_VERSION_SHORT=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
| 209 |
+
CERT_SCRIPT_PATH="/Applications/Python ${PY_VERSION_SHORT}/Install Certificates.command"
|
| 210 |
+
fi
|
| 211 |
+
|
| 212 |
+
if [ -n "$CERT_SCRIPT_PATH" ] && [ -f "$CERT_SCRIPT_PATH" ]; then
|
| 213 |
+
echo "Found certificate installation script: $CERT_SCRIPT_PATH"
|
| 214 |
+
echo "Running certificate installation script..."
|
| 215 |
+
# Execute the script using the venv's python
|
| 216 |
+
"$CERT_SCRIPT_PATH"
|
| 217 |
+
if [ $? -ne 0 ]; then
|
| 218 |
+
echo "WARNING: Certificate installation script finished with a non-zero status. SSL issues might persist."
|
| 219 |
+
# Continue anyway, maybe it partially worked or wasn't strictly needed
|
| 220 |
+
else
|
| 221 |
+
echo "Certificate installation script completed successfully."
|
| 222 |
+
fi
|
| 223 |
+
else
|
| 224 |
+
echo "WARNING: Could not find the 'Install Certificates.command' script automatically."
|
| 225 |
+
echo "If you encounter SSL errors (e.g., downloading NLTK data, used in chunking), you may need to run it manually."
|
| 226 |
+
echo "Look for it in your Python application folder (e.g., /Applications/Python 3.11/)."
|
| 227 |
+
fi
|
| 228 |
+
# --- End SSL Certificate Install (Step 2) ---
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
# Install dependencies using uv
|
| 232 |
+
echo "Installing Python dependencies from requirements.txt using uv..."
|
| 233 |
+
# Assuming uv is in PATH after the check above and venv is activated
|
| 234 |
+
uv pip install -r requirements.txt
|
| 235 |
+
if [ $? -ne 0 ]; then
|
| 236 |
+
echo "ERROR: Failed to install Python dependencies using uv. Please check requirements.txt and uv logs."
|
| 237 |
+
exit 1 # Cleanup trap will handle exit
|
| 238 |
+
fi
|
| 239 |
+
echo "Python dependencies installed successfully."
|
| 240 |
+
|
| 241 |
+
# --- Configure SSL Certificate Path (Part of Step 2 Logic) ---
|
| 242 |
+
echo "----------------------------------------"
|
| 243 |
+
echo "2 / 10. Configuring SSL certificate path..." # Renumbered
|
| 244 |
+
|
| 245 |
+
# Ensure certifi is installed (uv pip install should handle this if in requirements.txt,
|
| 246 |
+
# but explicit check adds robustness - though uv handles it well)
|
| 247 |
+
# It's likely already installed as a dependency of requests/huggingface_hub etc.
|
| 248 |
+
# If dependency issues arise, add 'certifi' explicitly to requirements.txt
|
| 249 |
+
|
| 250 |
+
# Get the certificate path from certifi
|
| 251 |
+
CERT_PATH=$(python -c "import certifi; print(certifi.where())" 2>/dev/null) # Suppress stderr in case certifi isn't installed yet
|
| 252 |
+
|
| 253 |
+
if [ -n "$CERT_PATH" ] && [ -f "$CERT_PATH" ]; then
|
| 254 |
+
echo "Setting SSL_CERT_FILE environment variable to: $CERT_PATH"
|
| 255 |
+
export SSL_CERT_FILE="$CERT_PATH"
|
| 256 |
+
echo "SSL_CERT_FILE set for this script session and its children."
|
| 257 |
+
# Optional: Add a connection test here if desired
|
| 258 |
+
# python -c "import requests; r = requests.get('https://huggingface.co', timeout=10); print(f'Connection test status: {r.status_code}')"
|
| 259 |
+
# if [ $? -ne 0 ]; then echo "WARNING: Connection test failed after setting SSL_CERT_FILE."; fi
|
| 260 |
+
else
|
| 261 |
+
echo "WARNING: Could not determine certificate path using certifi. Certifi might not be installed yet or there's an issue."
|
| 262 |
+
echo "Attempting to proceed, but SSL errors (like CERTIFICATE_VERIFY_FAILED) may occur."
|
| 263 |
+
echo "Ensure 'certifi' is listed in requirements.txt if SSL errors persist."
|
| 264 |
+
fi
|
| 265 |
+
# --- End SSL Configuration ---
|
| 266 |
+
|
| 267 |
+
|
| 268 |
+
# --- Helper Process Logging Setup ---
|
| 269 |
+
echo "----------------------------------------"
|
| 270 |
+
echo "Setting up helper process logging..."
|
| 271 |
+
|
| 272 |
+
# Create helper_process_logs directory if it doesn't exist
|
| 273 |
+
HELPER_LOGS_DIR="helper_process_logs"
|
| 274 |
+
if [ ! -d "$HELPER_LOGS_DIR" ]; then
|
| 275 |
+
echo "Creating helper process logs directory: $HELPER_LOGS_DIR"
|
| 276 |
+
mkdir -p "$HELPER_LOGS_DIR"
|
| 277 |
+
if [ $? -ne 0 ]; then
|
| 278 |
+
echo "ERROR: Failed to create helper process logs directory."
|
| 279 |
+
exit 1
|
| 280 |
+
fi
|
| 281 |
+
else
|
| 282 |
+
echo "Helper process logs directory already exists: $HELPER_LOGS_DIR"
|
| 283 |
+
fi
|
| 284 |
+
|
| 285 |
+
# Generate timestamp for log files
|
| 286 |
+
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
| 287 |
+
LLAMA_SERVER_LOG_FILE="$HELPER_LOGS_DIR/llama_server_${TIMESTAMP}.log"
|
| 288 |
+
HUEY_LOG_FILE="$HELPER_LOGS_DIR/huey_worker_${TIMESTAMP}.log"
|
| 289 |
+
UVICORN_LOG_FILE="$HELPER_LOGS_DIR/uvicorn_api_${TIMESTAMP}.log"
|
| 290 |
+
FRONTEND_LOG_FILE="$HELPER_LOGS_DIR/frontend_serve_${TIMESTAMP}.log"
|
| 291 |
+
|
| 292 |
+
echo "Helper process logs will be saved to:"
|
| 293 |
+
echo " LLaMA server: $LLAMA_SERVER_LOG_FILE"
|
| 294 |
+
echo " Huey worker: $HUEY_LOG_FILE"
|
| 295 |
+
echo " Uvicorn API: $UVICORN_LOG_FILE"
|
| 296 |
+
echo " Frontend serve: $FRONTEND_LOG_FILE"
|
| 297 |
+
echo "Helper process logging setup complete."
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
# --- 1.5 Valkey (Redis Fork) Check & Installation ---
|
| 301 |
+
echo "----------------------------------------"
|
| 302 |
+
echo "3 / 10. Checking for Valkey (Redis fork)..." # Renumbered
|
| 303 |
+
|
| 304 |
+
# First, check if something is already listening on port 6379
|
| 305 |
+
if command -v lsof &> /dev/null; then
|
| 306 |
+
PORT_CHECK_CMD="lsof -i :6379"
|
| 307 |
+
elif command -v netstat &> /dev/null; then
|
| 308 |
+
PORT_CHECK_CMD="netstat -an"
|
| 309 |
+
else
|
| 310 |
+
PORT_CHECK_CMD=""
|
| 311 |
+
fi
|
| 312 |
+
|
| 313 |
+
if [ -n "$PORT_CHECK_CMD" ]; then
|
| 314 |
+
if [ "$PORT_CHECK_CMD" = "lsof -i :6379" ]; then
|
| 315 |
+
if $PORT_CHECK_CMD &> /dev/null; then
|
| 316 |
+
echo "Something is already listening on port 6379. Assuming a compatible Valkey/Redis server is running."
|
| 317 |
+
echo "Skipping Valkey installation and setup."
|
| 318 |
+
else
|
| 319 |
+
NEED_VALKEY_SETUP=true
|
| 320 |
+
fi
|
| 321 |
+
else
|
| 322 |
+
# netstat case
|
| 323 |
+
if $PORT_CHECK_CMD | grep -q ':6379'; then
|
| 324 |
+
echo "Something is already listening on port 6379. Assuming a compatible Valkey/Redis server is running."
|
| 325 |
+
echo "Skipping Valkey installation and setup."
|
| 326 |
+
else
|
| 327 |
+
NEED_VALKEY_SETUP=true
|
| 328 |
+
fi
|
| 329 |
+
fi
|
| 330 |
+
else
|
| 331 |
+
echo "WARNING: Cannot check port 6379 (lsof and netstat not available). Proceeding with Valkey setup."
|
| 332 |
+
NEED_VALKEY_SETUP=true
|
| 333 |
+
fi
|
| 334 |
+
|
| 335 |
+
if [ "$NEED_VALKEY_SETUP" = true ]; then
|
| 336 |
+
# Check if brew command exists
|
| 337 |
+
if ! command -v brew &> /dev/null; then
|
| 338 |
+
echo "Homebrew (brew) command not found."
|
| 339 |
+
read -p "Homebrew is required to install Valkey. Install Homebrew now? (y/N) " -n 1 -r
|
| 340 |
+
echo # Move to a new line after input
|
| 341 |
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
| 342 |
+
echo "Attempting to install Homebrew. This may take a few minutes and will likely ask for your administrator password..."
|
| 343 |
+
# Execute the official Homebrew installation script
|
| 344 |
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
| 345 |
+
# Check if installation succeeded by seeing if 'brew' command is now available
|
| 346 |
+
# Might require sourcing profile or opening new shell, but let's check command first
|
| 347 |
+
if ! command -v brew &> /dev/null; then
|
| 348 |
+
echo "ERROR: Homebrew installation appears to have failed or is not available in this shell session."
|
| 349 |
+
echo "Please try installing Homebrew manually from https://brew.sh/ and run this script again, possibly in a new terminal window."
|
| 350 |
+
exit 1
|
| 351 |
+
else
|
| 352 |
+
# Add brew to PATH for the current script execution if possible
|
| 353 |
+
# This is complex as the path depends on architecture (Intel vs ARM)
|
| 354 |
+
# For simplicity, we'll rely on the installer potentially modifying the profile
|
| 355 |
+
# or the user running in a new shell later if 'brew' isn't found immediately after install.
|
| 356 |
+
# A more robust solution might involve detecting arch and adding the known path.
|
| 357 |
+
echo "Homebrew installed successfully. Proceeding..."
|
| 358 |
+
# Re-check explicitly here just in case the path isn't immediately active
|
| 359 |
+
if ! command -v brew &> /dev/null; then
|
| 360 |
+
echo "ERROR: Homebrew installed but 'brew' command still not found in PATH for this session."
|
| 361 |
+
echo "Please open a new terminal window and run this script again."
|
| 362 |
+
exit 1
|
| 363 |
+
fi
|
| 364 |
+
fi
|
| 365 |
+
else
|
| 366 |
+
echo "Homebrew installation declined. Valkey cannot be installed automatically."
|
| 367 |
+
echo "Please install Homebrew (https://brew.sh/) and Valkey ('brew install valkey') manually and rerun the script."
|
| 368 |
+
exit 1
|
| 369 |
+
fi
|
| 370 |
+
fi
|
| 371 |
+
echo "Homebrew found."
|
| 372 |
+
|
| 373 |
+
# Check if valkey is installed
|
| 374 |
+
if ! brew list valkey &>/dev/null; then
|
| 375 |
+
echo "Valkey not found via Homebrew. Attempting to install..."
|
| 376 |
+
# Update brew minimally and install valkey
|
| 377 |
+
# Using 'brew install' directly avoids running 'brew update' if possible,
|
| 378 |
+
# but Homebrew might still update itself or core formulae.
|
| 379 |
+
# We avoid 'brew upgrade' to prevent upgrading all packages.
|
| 380 |
+
brew install valkey
|
| 381 |
+
if [ $? -ne 0 ]; then
|
| 382 |
+
echo "ERROR: Failed to install Valkey using Homebrew. Please install it manually ('brew install valkey') and rerun the script."
|
| 383 |
+
exit 1
|
| 384 |
+
fi
|
| 385 |
+
echo "Valkey installed successfully."
|
| 386 |
+
else
|
| 387 |
+
echo "Valkey found via Homebrew."
|
| 388 |
+
fi
|
| 389 |
+
|
| 390 |
+
# Check if Valkey service is running, start if not
|
| 391 |
+
if ! brew services list | grep -q "valkey.*started"; then
|
| 392 |
+
echo "Valkey service is not running. Starting it now..."
|
| 393 |
+
brew services start valkey
|
| 394 |
+
if [ $? -ne 0 ]; then
|
| 395 |
+
echo "ERROR: Failed to start Valkey service using Homebrew. Please check Homebrew services ('brew services list') and start Valkey manually if needed."
|
| 396 |
+
exit 1
|
| 397 |
+
fi
|
| 398 |
+
# Give it a moment to start
|
| 399 |
+
sleep 2
|
| 400 |
+
if ! brew services list | grep -q "valkey.*started"; then
|
| 401 |
+
echo "ERROR: Valkey service failed to stay running after starting. Please investigate ('brew services list', check logs)."
|
| 402 |
+
exit 1
|
| 403 |
+
fi
|
| 404 |
+
echo "Valkey service started successfully."
|
| 405 |
+
else
|
| 406 |
+
echo "Valkey service is already running."
|
| 407 |
+
fi
|
| 408 |
+
fi # End NEED_VALKEY_SETUP conditional
|
| 409 |
+
# --- End Valkey Check ---
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
# --- 1.6. LLaMA.cpp Setup and Server ---
|
| 413 |
+
echo "----------------------------------------"
|
| 414 |
+
echo "4 / 10. Setting up LLaMA.cpp server..." # Renumbered
|
| 415 |
+
LLAMA_CPP_DIR_NAME="llama.cpp" # Relative to SCRIPT_DIR
|
| 416 |
+
LLAMA_CPP_FULL_PATH="$SCRIPT_DIR/$LLAMA_CPP_DIR_NAME"
|
| 417 |
+
LLAMA_MODEL_SUBDIR="models" # Subdirectory within LLAMA_CPP_DIR
|
| 418 |
+
|
| 419 |
+
# Model details
|
| 420 |
+
MODEL_URL="https://huggingface.co/Heralax/Augmentoolkit-DataSpecialist-v0.1-GGUF/resolve/main/Augmentoolkit-DataSpecialist-7.2B-Q8_0.gguf?download=true"
|
| 421 |
+
MODEL_FILENAME="augmentoolkit-datagen-7.2B-Q8_0.gguf"
|
| 422 |
+
LLAMA_MODEL_PATH="$LLAMA_CPP_FULL_PATH/$LLAMA_MODEL_SUBDIR/$MODEL_FILENAME"
|
| 423 |
+
|
| 424 |
+
# Check for prerequisites
|
| 425 |
+
if ! command -v git &> /dev/null; then
|
| 426 |
+
echo "ERROR: git command not found. Please install git and rerun the script."
|
| 427 |
+
cleanup
|
| 428 |
+
exit 1
|
| 429 |
+
fi
|
| 430 |
+
if ! command -v cmake &> /dev/null; then
|
| 431 |
+
echo "ERROR: cmake command not found. Please install cmake (e.g., 'brew install cmake') and rerun the script."
|
| 432 |
+
cleanup
|
| 433 |
+
exit 1
|
| 434 |
+
fi
|
| 435 |
+
DOWNLOAD_CMD=""
|
| 436 |
+
if command -v curl &> /dev/null; then # Prefer curl as it's more common on macOS by default
|
| 437 |
+
DOWNLOAD_CMD="curl"
|
| 438 |
+
elif command -v wget &> /dev/null; then
|
| 439 |
+
DOWNLOAD_CMD="wget"
|
| 440 |
+
else
|
| 441 |
+
echo "ERROR: Neither curl nor wget found. Please install one of them to download the model."
|
| 442 |
+
cleanup
|
| 443 |
+
exit 1
|
| 444 |
+
fi
|
| 445 |
+
|
| 446 |
+
# Clone llama.cpp if not already cloned
|
| 447 |
+
if [ ! -d "$LLAMA_CPP_FULL_PATH" ]; then
|
| 448 |
+
echo "Cloning llama.cpp repository into '$LLAMA_CPP_FULL_PATH'..."
|
| 449 |
+
git clone https://github.com/ggml-org/llama.cpp.git "$LLAMA_CPP_FULL_PATH"
|
| 450 |
+
if [ $? -ne 0 ]; then
|
| 451 |
+
echo "ERROR: Failed to clone llama.cpp repository."
|
| 452 |
+
cleanup
|
| 453 |
+
exit 1
|
| 454 |
+
fi
|
| 455 |
+
echo "Checking out specific commit..."
|
| 456 |
+
cd "$LLAMA_CPP_FULL_PATH"
|
| 457 |
+
# git checkout 3cb203c89f60483e349f841684173446ed23c28f
|
| 458 |
+
git checkout 3cb203c89f60483e349f841684173446ed23c28f
|
| 459 |
+
if [ $? -ne 0 ]; then
|
| 460 |
+
echo "ERROR: Failed to checkout specific commit."
|
| 461 |
+
cleanup
|
| 462 |
+
exit 1
|
| 463 |
+
fi
|
| 464 |
+
cd - > /dev/null
|
| 465 |
+
else
|
| 466 |
+
echo "llama.cpp directory '$LLAMA_CPP_FULL_PATH' already exists. Skipping clone."
|
| 467 |
+
# Optional: Add logic to pull latest changes if desired
|
| 468 |
+
# echo "Pulling latest changes for llama.cpp..."
|
| 469 |
+
# (cd "$LLAMA_CPP_FULL_PATH" && git pull)
|
| 470 |
+
fi
|
| 471 |
+
|
| 472 |
+
# Create model directory inside llama.cpp if it doesn't exist
|
| 473 |
+
mkdir -p "$LLAMA_CPP_FULL_PATH/$LLAMA_MODEL_SUBDIR"
|
| 474 |
+
if [ $? -ne 0 ]; then
|
| 475 |
+
echo "ERROR: Failed to create model directory '$LLAMA_CPP_FULL_PATH/$LLAMA_MODEL_SUBDIR'."
|
| 476 |
+
cleanup
|
| 477 |
+
exit 1
|
| 478 |
+
fi
|
| 479 |
+
|
| 480 |
+
# Download model if it doesn't exist
|
| 481 |
+
if [ ! -f "$LLAMA_MODEL_PATH" ]; then
|
| 482 |
+
echo "LLaMA.cpp model file '$MODEL_FILENAME' not found at '$LLAMA_MODEL_PATH'."
|
| 483 |
+
read -p "This will download a ~7.7GB model file. This may take a while and obviously requires a bit of disk space. Proceed? (y/N) " -n 1 -r
|
| 484 |
+
echo # Newline
|
| 485 |
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
| 486 |
+
echo "Downloading LLaMA.cpp model to '$LLAMA_MODEL_PATH'..."
|
| 487 |
+
if [ "$DOWNLOAD_CMD" = "curl" ]; then
|
| 488 |
+
curl -L "$MODEL_URL" -o "$LLAMA_MODEL_PATH"
|
| 489 |
+
else # wget
|
| 490 |
+
wget "$MODEL_URL" -O "$LLAMA_MODEL_PATH"
|
| 491 |
+
fi
|
| 492 |
+
if [ $? -ne 0 ]; then
|
| 493 |
+
echo "ERROR: Failed to download LLaMA.cpp model."
|
| 494 |
+
rm -f "$LLAMA_MODEL_PATH" # Remove partial download if any
|
| 495 |
+
cleanup
|
| 496 |
+
exit 1
|
| 497 |
+
fi
|
| 498 |
+
echo "Model downloaded successfully."
|
| 499 |
+
else
|
| 500 |
+
echo "Model download declined. LLaMA.cpp server cannot start without the model."
|
| 501 |
+
cleanup
|
| 502 |
+
exit 1
|
| 503 |
+
fi
|
| 504 |
+
else
|
| 505 |
+
echo "LLaMA.cpp model '$MODEL_FILENAME' already exists at '$LLAMA_MODEL_PATH'."
|
| 506 |
+
fi
|
| 507 |
+
|
| 508 |
+
# Build llama.cpp
|
| 509 |
+
LLAMA_SERVER_BINARY_PATH="$LLAMA_CPP_FULL_PATH/build/bin/llama-server"
|
| 510 |
+
if [ ! -f "$LLAMA_SERVER_BINARY_PATH" ]; then
|
| 511 |
+
echo "Building llama.cpp (this may take several minutes)..."
|
| 512 |
+
cd "$LLAMA_CPP_FULL_PATH" # Change to llama.cpp directory for build
|
| 513 |
+
|
| 514 |
+
NUM_CORES=$(sysctl -n hw.ncpu 2>/dev/null || echo 4) # Default to 4 if sysctl fails
|
| 515 |
+
|
| 516 |
+
cmake -B build # Metal is enabled by default on macOS
|
| 517 |
+
if [ $? -ne 0 ]; then
|
| 518 |
+
echo "ERROR: cmake configuration for llama.cpp failed."
|
| 519 |
+
cd "$SCRIPT_DIR" # Return to original script directory
|
| 520 |
+
cleanup
|
| 521 |
+
exit 1
|
| 522 |
+
fi
|
| 523 |
+
cmake --build build --config Release -j "$NUM_CORES"
|
| 524 |
+
if [ $? -ne 0 ]; then
|
| 525 |
+
echo "ERROR: cmake build for llama.cpp failed."
|
| 526 |
+
cd "$SCRIPT_DIR" # Return to original script directory
|
| 527 |
+
cleanup
|
| 528 |
+
exit 1
|
| 529 |
+
fi
|
| 530 |
+
echo "llama.cpp built successfully. Server binary at '$LLAMA_SERVER_BINARY_PATH'."
|
| 531 |
+
cd "$SCRIPT_DIR" # Return to original script directory
|
| 532 |
+
else
|
| 533 |
+
echo "llama-server binary already exists at '$LLAMA_SERVER_BINARY_PATH'. Skipping build."
|
| 534 |
+
fi
|
| 535 |
+
|
| 536 |
+
# Start llama-server
|
| 537 |
+
echo "Starting LLaMA.cpp server in the background..."
|
| 538 |
+
echo "Output will be logged to: $LLAMA_SERVER_LOG_FILE"
|
| 539 |
+
LLAMA_SERVER_PORT=8082 # Port for llama.cpp server
|
| 540 |
+
# The model path for llama-server is absolute: $LLAMA_MODEL_PATH
|
| 541 |
+
# The server binary path is absolute: $LLAMA_SERVER_BINARY_PATH
|
| 542 |
+
|
| 543 |
+
# Redirect both stdout and stderr to the log file
|
| 544 |
+
"$LLAMA_SERVER_BINARY_PATH" -m "$LLAMA_MODEL_PATH" -ngl 100 -c 30000 --port "$LLAMA_SERVER_PORT" --host 0.0.0.0 > "$LLAMA_SERVER_LOG_FILE" 2>&1 &
|
| 545 |
+
LLAMA_SERVER_PID=$!
|
| 546 |
+
sleep 3 # Give it a moment to start/fail
|
| 547 |
+
|
| 548 |
+
if ! kill -0 $LLAMA_SERVER_PID &> /dev/null; then
|
| 549 |
+
echo "ERROR: LLaMA.cpp server (PID $LLAMA_SERVER_PID) failed to start or crashed."
|
| 550 |
+
echo "Check the log file for details: $LLAMA_SERVER_LOG_FILE"
|
| 551 |
+
# No need to cd back as we are already in SCRIPT_DIR if build was skipped or after build
|
| 552 |
+
cleanup
|
| 553 |
+
exit 1
|
| 554 |
+
fi
|
| 555 |
+
echo "LLaMA.cpp server started successfully with PID $LLAMA_SERVER_PID."
|
| 556 |
+
echo "Access LLaMA.cpp server API at http://localhost:$LLAMA_SERVER_PORT"
|
| 557 |
+
# --- End LLaMA.cpp Setup (Step 4) ---
|
| 558 |
+
|
| 559 |
+
|
| 560 |
+
# --- 2. Huey Worker ---
|
| 561 |
+
echo "----------------------------------------"
|
| 562 |
+
echo "5 / 10. Starting Huey worker in the background (using venv)..." # Renumbered
|
| 563 |
+
echo "Output will be logged to: $HUEY_LOG_FILE"
|
| 564 |
+
# Call the huey_consumer script directly (should be in PATH from venv)
|
| 565 |
+
# Redirect both stdout and stderr to the log file
|
| 566 |
+
huey_consumer tasks.huey > "$HUEY_LOG_FILE" 2>&1 &
|
| 567 |
+
HUEY_PID=$!
|
| 568 |
+
# Give it a moment to start/fail
|
| 569 |
+
sleep 2
|
| 570 |
+
if ! kill -0 $HUEY_PID &> /dev/null; then
|
| 571 |
+
echo "ERROR: Huey worker (PID $HUEY_PID) failed to start or crashed immediately."
|
| 572 |
+
echo "Check the log file for details: $HUEY_LOG_FILE"
|
| 573 |
+
exit 1 # Cleanup trap will handle exit
|
| 574 |
+
fi
|
| 575 |
+
echo "Huey worker started successfully with PID $HUEY_PID."
|
| 576 |
+
|
| 577 |
+
|
| 578 |
+
# --- 3. API Server ---
|
| 579 |
+
echo "----------------------------------------"
|
| 580 |
+
echo "6 / 10. Starting FastAPI server (Uvicorn) in the background (using venv)..." # Renumbered
|
| 581 |
+
echo "Output will be logged to: $UVICORN_LOG_FILE"
|
| 582 |
+
# Host 0.0.0.0 makes it accessible on the network
|
| 583 |
+
# Use python -m uvicorn to ensure using the venv uvicorn
|
| 584 |
+
# Redirect both stdout and stderr to the log file
|
| 585 |
+
python -m uvicorn api:app --host 0.0.0.0 --port 8000 > "$UVICORN_LOG_FILE" 2>&1 &
|
| 586 |
+
UVICORN_PID=$!
|
| 587 |
+
# Give it a moment to start/fail
|
| 588 |
+
sleep 2
|
| 589 |
+
if ! kill -0 $UVICORN_PID &> /dev/null; then
|
| 590 |
+
echo "ERROR: Uvicorn server (PID $UVICORN_PID) failed to start or crashed immediately."
|
| 591 |
+
echo "Check the log file for details: $UVICORN_LOG_FILE"
|
| 592 |
+
# Cleanup trap will handle stopping Huey
|
| 593 |
+
exit 1 # Cleanup trap will handle exit
|
| 594 |
+
fi
|
| 595 |
+
echo "Uvicorn server started successfully with PID $UVICORN_PID. Access API at http://localhost:8000"
|
| 596 |
+
|
| 597 |
+
|
| 598 |
+
# --- 4. NPM Check ---
|
| 599 |
+
echo "----------------------------------------"
|
| 600 |
+
echo "7 / 10. Checking for npm..." # Renumbered
|
| 601 |
+
if ! command -v npm &> /dev/null; then
|
| 602 |
+
echo "ERROR: npm command not found."
|
| 603 |
+
echo "Please install Node.js and npm (e.g., from https://nodejs.org/) and run this script again."
|
| 604 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 605 |
+
kill $HUEY_PID $UVICORN_PID
|
| 606 |
+
exit 1
|
| 607 |
+
fi
|
| 608 |
+
echo "npm found."
|
| 609 |
+
|
| 610 |
+
|
| 611 |
+
# --- 5. Frontend Dependencies ---
|
| 612 |
+
echo "----------------------------------------"
|
| 613 |
+
echo "8 / 10. Installing frontend dependencies in atk-interface..." # Renumbered
|
| 614 |
+
if [ ! -d "atk-interface" ]; then
|
| 615 |
+
echo "ERROR: Directory 'atk-interface' not found in $(pwd)."
|
| 616 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 617 |
+
kill $HUEY_PID $UVICORN_PID
|
| 618 |
+
exit 1
|
| 619 |
+
fi
|
| 620 |
+
cd atk-interface
|
| 621 |
+
npm install
|
| 622 |
+
if [ $? -ne 0 ]; then
|
| 623 |
+
echo "ERROR: Failed to install npm dependencies in atk-interface. Check package.json and npm logs."
|
| 624 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 625 |
+
kill $HUEY_PID $UVICORN_PID
|
| 626 |
+
cd "$SCRIPT_DIR" # Go back to root
|
| 627 |
+
exit 1
|
| 628 |
+
fi
|
| 629 |
+
echo "Frontend dependencies installed."
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
# --- 6. Frontend Build ---
|
| 633 |
+
echo "----------------------------------------"
|
| 634 |
+
echo "9 / 10. Building frontend application in atk-interface..." # Renumbered
|
| 635 |
+
npm run build
|
| 636 |
+
if [ $? -ne 0 ]; then
|
| 637 |
+
echo "ERROR: Failed to build frontend application. Check build script in package.json and logs."
|
| 638 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 639 |
+
kill $HUEY_PID $UVICORN_PID
|
| 640 |
+
cd "$SCRIPT_DIR" # Go back to root
|
| 641 |
+
exit 1
|
| 642 |
+
fi
|
| 643 |
+
echo "Frontend built successfully."
|
| 644 |
+
|
| 645 |
+
|
| 646 |
+
# --- 7. Frontend Serve ---
|
| 647 |
+
echo "----------------------------------------"
|
| 648 |
+
echo "10 / 10. Serving frontend application from atk-interface/dist/ in the FOREGROUND..." # Renumbered
|
| 649 |
+
echo "Output will be logged to: $FRONTEND_LOG_FILE"
|
| 650 |
+
# Ensure dist directory exists after build
|
| 651 |
+
if [ ! -d "dist" ]; then
|
| 652 |
+
echo "ERROR: 'dist' directory not found in atk-interface after build."
|
| 653 |
+
# Cleanup trap will handle stopping background services
|
| 654 |
+
exit 1
|
| 655 |
+
fi
|
| 656 |
+
|
| 657 |
+
|
| 658 |
+
echo "Opening interface..."
|
| 659 |
+
|
| 660 |
+
|
| 661 |
+
# Open the URL in the default browser (macOS)
|
| 662 |
+
echo "Opening http://localhost:5173 in default browser..."
|
| 663 |
+
open http://localhost:5173
|
| 664 |
+
|
| 665 |
+
# Show summary of all log files
|
| 666 |
+
echo ""
|
| 667 |
+
echo "═══════════════════════════════════════════════════════════════════════════════"
|
| 668 |
+
echo "All helper processes are now running with output redirected to log files:"
|
| 669 |
+
echo " • LLaMA server logs: $LLAMA_SERVER_LOG_FILE"
|
| 670 |
+
echo " • Huey worker logs: $HUEY_LOG_FILE"
|
| 671 |
+
echo " • Uvicorn API logs: $UVICORN_LOG_FILE"
|
| 672 |
+
echo " • Frontend serve logs: $FRONTEND_LOG_FILE"
|
| 673 |
+
echo ""
|
| 674 |
+
echo "You can monitor these logs in real-time with:"
|
| 675 |
+
echo " tail -f $LLAMA_SERVER_LOG_FILE"
|
| 676 |
+
echo " tail -f $HUEY_LOG_FILE"
|
| 677 |
+
echo " tail -f $UVICORN_LOG_FILE"
|
| 678 |
+
echo " tail -f $FRONTEND_LOG_FILE"
|
| 679 |
+
echo "═══════════════════════════════════════════════════════════════════════════════"
|
| 680 |
+
echo ""
|
| 681 |
+
|
| 682 |
+
# Serve on port 5173 (matches CORS config)
|
| 683 |
+
# Run npx serve in the foreground with output redirected to log file
|
| 684 |
+
echo "Starting serve process (output redirected to log file)..."
|
| 685 |
+
# Redirect both stdout and stderr to the log file - use ../ to go back to script directory
|
| 686 |
+
npx serve -s dist --listen 5173 > "../$FRONTEND_LOG_FILE" 2>&1
|
| 687 |
+
SERVE_EXIT_CODE=$?
|
| 688 |
+
SERVE_PID="" # Clear PID as it has exited
|
| 689 |
+
|
| 690 |
+
if [ $SERVE_EXIT_CODE -ne 0 ]; then
|
| 691 |
+
echo "WARNING: Frontend serve process exited with code $SERVE_EXIT_CODE."
|
| 692 |
+
echo "Check the log file for details: $FRONTEND_LOG_FILE"
|
| 693 |
+
# Consider if we should stop background services here if serve crashes
|
| 694 |
+
# Calling cleanup manually if serve fails
|
| 695 |
+
cleanup
|
| 696 |
+
exit 1 # Exit with error code
|
| 697 |
+
fi
|
| 698 |
+
|
| 699 |
+
|
| 700 |
+
# --- 8. Done (Only reached if npx serve exits gracefully, e.g., if killed separately) ---
|
| 701 |
+
cd "$SCRIPT_DIR" # Go back to root directory
|
| 702 |
+
echo "----------------------------------------"
|
| 703 |
+
echo "Frontend serve process finished."
|
| 704 |
+
echo ""
|
| 705 |
+
echo "Helper process log files were saved to:"
|
| 706 |
+
echo " • LLaMA server logs: $LLAMA_SERVER_LOG_FILE"
|
| 707 |
+
echo " • Huey worker logs: $HUEY_LOG_FILE"
|
| 708 |
+
echo " • Uvicorn API logs: $UVICORN_LOG_FILE"
|
| 709 |
+
echo " • Frontend serve logs: $FRONTEND_LOG_FILE"
|
| 710 |
+
# Call cleanup to stop background services if serve finishes normally
|
| 711 |
+
cleanup
|
| 712 |
+
|
| 713 |
+
exit 0
|
logs/.gitkeep
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
|
| 3 |
+
|
macos.sh
ADDED
|
@@ -0,0 +1,612 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
# Exit immediately if a command exits with a non-zero status.
|
| 4 |
+
# set -e # Disabled for manual error checking below
|
| 5 |
+
|
| 6 |
+
echo "Starting Augmentoolkit services..."
|
| 7 |
+
|
| 8 |
+
# --- Get script directory and change to it ---
|
| 9 |
+
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
| 10 |
+
cd "$SCRIPT_DIR"
|
| 11 |
+
if [ $? -ne 0 ]; then
|
| 12 |
+
echo "ERROR: Failed to change directory to script location '$SCRIPT_DIR'."
|
| 13 |
+
exit 1
|
| 14 |
+
fi
|
| 15 |
+
echo "Running in directory: $(pwd)"
|
| 16 |
+
|
| 17 |
+
# --- Cleanup Function ---
|
| 18 |
+
# Stores PIDs of background processes
|
| 19 |
+
HUEY_PID=""
|
| 20 |
+
UVICORN_PID=""
|
| 21 |
+
SERVE_PID="" # Though serve runs in foreground, trap might catch signals before it starts
|
| 22 |
+
|
| 23 |
+
cleanup() {
|
| 24 |
+
echo "" # Newline after ^C
|
| 25 |
+
echo "----------------------------------------"
|
| 26 |
+
echo "Signal received, stopping background services..."
|
| 27 |
+
|
| 28 |
+
# Function to kill a process and wait for it to terminate
|
| 29 |
+
kill_and_wait() {
|
| 30 |
+
local pid=$1
|
| 31 |
+
local name=$2
|
| 32 |
+
local timeout=9
|
| 33 |
+
|
| 34 |
+
if [ -n "$pid" ] && kill -0 $pid &> /dev/null; then
|
| 35 |
+
echo "Stopping $name (PID $pid)..."
|
| 36 |
+
|
| 37 |
+
# First try SIGTERM (graceful shutdown)
|
| 38 |
+
kill -TERM $pid 2>/dev/null
|
| 39 |
+
|
| 40 |
+
# Wait for process to terminate gracefully
|
| 41 |
+
local count=0
|
| 42 |
+
while [ $count -lt $timeout ] && kill -0 $pid &> /dev/null; do
|
| 43 |
+
sleep 1
|
| 44 |
+
count=$((count + 1))
|
| 45 |
+
done
|
| 46 |
+
|
| 47 |
+
# If still running, force kill with SIGKILL
|
| 48 |
+
if kill -0 $pid &> /dev/null; then
|
| 49 |
+
echo "Process $pid ($name) didn't terminate gracefully, force killing..."
|
| 50 |
+
kill -KILL $pid 2>/dev/null
|
| 51 |
+
sleep 1
|
| 52 |
+
fi
|
| 53 |
+
|
| 54 |
+
# Final check
|
| 55 |
+
if kill -0 $pid &> /dev/null; then
|
| 56 |
+
echo "WARNING: Failed to stop $name (PID $pid)"
|
| 57 |
+
else
|
| 58 |
+
echo "$name stopped successfully."
|
| 59 |
+
fi
|
| 60 |
+
fi
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
# Stop processes in reverse order of startup
|
| 64 |
+
kill_and_wait "$SERVE_PID" "frontend serve"
|
| 65 |
+
kill_and_wait "$UVICORN_PID" "Uvicorn API server"
|
| 66 |
+
kill_and_wait "$HUEY_PID" "Huey worker"
|
| 67 |
+
|
| 68 |
+
# Find and kill any remaining huey consumers specific to this Augmentoolkit instance
|
| 69 |
+
echo "Searching for huey consumer processes in current directory ($SCRIPT_DIR)..."
|
| 70 |
+
|
| 71 |
+
# Use ps with full command line and grep for huey_consumer processes in our specific directory
|
| 72 |
+
# This avoids killing huey consumers from other Augmentoolkit instances or projects
|
| 73 |
+
HUEY_PIDS=$(ps aux | grep -E "huey_consumer.*tasks\.huey" | grep "$SCRIPT_DIR" | grep -v grep | awk '{print $2}')
|
| 74 |
+
|
| 75 |
+
if [ -n "$HUEY_PIDS" ]; then
|
| 76 |
+
echo "Found huey consumer processes in current directory: $HUEY_PIDS"
|
| 77 |
+
for pid in $HUEY_PIDS; do
|
| 78 |
+
if kill -0 $pid &> /dev/null; then
|
| 79 |
+
echo "Stopping huey consumer from current directory (PID $pid)..."
|
| 80 |
+
kill -TERM $pid 2>/dev/null
|
| 81 |
+
sleep 2
|
| 82 |
+
if kill -0 $pid &> /dev/null; then
|
| 83 |
+
echo "Force killing huey consumer (PID $pid)..."
|
| 84 |
+
kill -KILL $pid 2>/dev/null
|
| 85 |
+
fi
|
| 86 |
+
fi
|
| 87 |
+
done
|
| 88 |
+
sleep 1
|
| 89 |
+
# Final check for any remaining huey processes in our directory
|
| 90 |
+
REMAINING_HUEY=$(ps aux | grep -E "huey_consumer.*tasks\.huey" | grep "$SCRIPT_DIR" | grep -v grep | awk '{print $2}')
|
| 91 |
+
if [ -n "$REMAINING_HUEY" ]; then
|
| 92 |
+
echo "WARNING: Some huey consumer processes in current directory may still be running: $REMAINING_HUEY"
|
| 93 |
+
else
|
| 94 |
+
echo "All huey consumer processes from current directory stopped successfully."
|
| 95 |
+
fi
|
| 96 |
+
else
|
| 97 |
+
echo "No additional huey consumer processes found in current directory."
|
| 98 |
+
fi
|
| 99 |
+
|
| 100 |
+
# Deactivate virtual environment if active
|
| 101 |
+
if command -v deactivate &> /dev/null; then
|
| 102 |
+
echo "Deactivating virtual environment..."
|
| 103 |
+
deactivate
|
| 104 |
+
fi
|
| 105 |
+
|
| 106 |
+
# Give a moment for any remaining output to flush
|
| 107 |
+
sleep 1
|
| 108 |
+
|
| 109 |
+
# Clear any remaining output and reset cursor
|
| 110 |
+
echo "Cleanup complete."
|
| 111 |
+
echo "----------------------------------------"
|
| 112 |
+
|
| 113 |
+
# Ensure cursor is visible and terminal is in normal state
|
| 114 |
+
printf "\033[?25h" # Show cursor
|
| 115 |
+
printf "\033[0m" # Reset all attributes
|
| 116 |
+
|
| 117 |
+
exit 0 # Exit script after cleanup
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
# Trap signals (INT: Ctrl+C, TERM: standard kill) and run cleanup function
|
| 121 |
+
trap cleanup SIGINT SIGTERM
|
| 122 |
+
|
| 123 |
+
# --- 1. Python Virtual Environment & Dependencies ---
|
| 124 |
+
echo "----------------------------------------"
|
| 125 |
+
echo "1. Setting up Python virtual environment and installing dependencies..."
|
| 126 |
+
VENV_DIR=".venv"
|
| 127 |
+
|
| 128 |
+
# Check if python3.11 command exists (preferred)
|
| 129 |
+
if command -v python3.11 &> /dev/null; then
|
| 130 |
+
PYTHON_CMD="python3.11"
|
| 131 |
+
echo "Using Python 3.11: $(python3.11 --version)"
|
| 132 |
+
elif command -v python3 &> /dev/null; then
|
| 133 |
+
PYTHON_CMD="python3"
|
| 134 |
+
PYTHON_VERSION=$(python3 --version 2>&1 | grep -oE '[0-9]+\.[0-9]+')
|
| 135 |
+
echo "Using Python: $(python3 --version)"
|
| 136 |
+
if [[ "$PYTHON_VERSION" != "3.11" ]]; then
|
| 137 |
+
echo "WARNING: Python 3.11 is recommended but not found. Using $PYTHON_VERSION instead."
|
| 138 |
+
echo "If you encounter issues, please install Python 3.11 specifically."
|
| 139 |
+
fi
|
| 140 |
+
else
|
| 141 |
+
echo "ERROR: Neither python3.11 nor python3 command found."
|
| 142 |
+
echo "Please install Python 3.11 (recommended) or Python 3.x and rerun the script."
|
| 143 |
+
echo "On macOS, you can install Python 3.11 using:"
|
| 144 |
+
echo " brew install python@3.11"
|
| 145 |
+
echo "Or download from: https://www.python.org/downloads/"
|
| 146 |
+
exit 1
|
| 147 |
+
fi
|
| 148 |
+
|
| 149 |
+
# --- Check/Install uv ---
|
| 150 |
+
echo "Checking for uv..."
|
| 151 |
+
if ! command -v uv &> /dev/null; then
|
| 152 |
+
echo "'uv' command not found. Attempting to install uv using pip..."
|
| 153 |
+
# Ensure pip is available (it should be with the selected Python version)
|
| 154 |
+
if ! $PYTHON_CMD -m pip --version &> /dev/null; then
|
| 155 |
+
echo "ERROR: $PYTHON_CMD -m pip command failed. Cannot install uv."
|
| 156 |
+
exit 1
|
| 157 |
+
fi
|
| 158 |
+
$PYTHON_CMD -m pip install uv
|
| 159 |
+
if [ $? -ne 0 ]; then
|
| 160 |
+
echo "ERROR: Failed to install uv using pip. Please install uv manually (e.g., 'pip install uv' or 'brew install uv') and rerun the script."
|
| 161 |
+
exit 1
|
| 162 |
+
fi
|
| 163 |
+
echo "uv installed successfully."
|
| 164 |
+
else
|
| 165 |
+
echo "uv found."
|
| 166 |
+
fi
|
| 167 |
+
# --- End Check/Install uv ---
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# Create virtual environment if it doesn't exist
|
| 171 |
+
if [ ! -d "$VENV_DIR" ]; then
|
| 172 |
+
echo "Creating virtual environment in '$VENV_DIR' using $PYTHON_CMD..."
|
| 173 |
+
$PYTHON_CMD -m venv "$VENV_DIR"
|
| 174 |
+
if [ $? -ne 0 ]; then
|
| 175 |
+
echo "ERROR: Failed to create Python virtual environment using $PYTHON_CMD."
|
| 176 |
+
exit 1
|
| 177 |
+
fi
|
| 178 |
+
else
|
| 179 |
+
echo "Virtual environment '$VENV_DIR' already exists."
|
| 180 |
+
fi
|
| 181 |
+
|
| 182 |
+
# Activate virtual environment
|
| 183 |
+
echo "Activating virtual environment..."
|
| 184 |
+
source "$VENV_DIR/bin/activate"
|
| 185 |
+
if [ $? -ne 0 ]; then
|
| 186 |
+
echo "ERROR: Failed to activate Python virtual environment."
|
| 187 |
+
exit 1
|
| 188 |
+
fi
|
| 189 |
+
echo "Virtual environment activated. Python executable: $(which python)"
|
| 190 |
+
|
| 191 |
+
# --- Attempt to install SSL certificates (macOS specific fix) ---
|
| 192 |
+
echo "Attempting to install SSL certificates for the active Python environment..."
|
| 193 |
+
# Get the full path to the python executable inside the venv
|
| 194 |
+
VENV_PYTHON_EXE=$(which python)
|
| 195 |
+
# Try to infer the Applications directory path (this is heuristic)
|
| 196 |
+
# Assumes Python was installed via the standard macOS installer
|
| 197 |
+
PYTHON_BASE_DIR=$(dirname "$(dirname "$VENV_PYTHON_EXE")") # Go up two levels from venv/bin/python
|
| 198 |
+
CERT_SCRIPT_PATH=""
|
| 199 |
+
# Check common installation patterns
|
| 200 |
+
if [ -d "$PYTHON_BASE_DIR/Install Certificates.command" ]; then
|
| 201 |
+
# Might be like /Library/Frameworks/Python.framework/Versions/3.11/Install Certificates.command
|
| 202 |
+
CERT_SCRIPT_PATH="$PYTHON_BASE_DIR/Install Certificates.command"
|
| 203 |
+
elif [ -d "/Applications/Python $(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')/Install Certificates.command" ]; then
|
| 204 |
+
# Check standard /Applications path using the version
|
| 205 |
+
PY_VERSION_SHORT=$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')
|
| 206 |
+
CERT_SCRIPT_PATH="/Applications/Python ${PY_VERSION_SHORT}/Install Certificates.command"
|
| 207 |
+
fi
|
| 208 |
+
|
| 209 |
+
if [ -n "$CERT_SCRIPT_PATH" ] && [ -f "$CERT_SCRIPT_PATH" ]; then
|
| 210 |
+
echo "Found certificate installation script: $CERT_SCRIPT_PATH"
|
| 211 |
+
echo "Running certificate installation script..."
|
| 212 |
+
# Execute the script using the venv's python
|
| 213 |
+
"$CERT_SCRIPT_PATH"
|
| 214 |
+
if [ $? -ne 0 ]; then
|
| 215 |
+
echo "WARNING: Certificate installation script finished with a non-zero status. SSL issues might persist."
|
| 216 |
+
# Continue anyway, maybe it partially worked or wasn't strictly needed
|
| 217 |
+
else
|
| 218 |
+
echo "Certificate installation script completed successfully."
|
| 219 |
+
fi
|
| 220 |
+
else
|
| 221 |
+
echo "WARNING: Could not find the 'Install Certificates.command' script automatically."
|
| 222 |
+
echo "If you encounter SSL errors (e.g., downloading NLTK data, used in chunking), you may need to run it manually."
|
| 223 |
+
echo "Look for it in your Python application folder (e.g., /Applications/Python 3.11/)."
|
| 224 |
+
fi
|
| 225 |
+
# --- End SSL Certificate Install ---
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
# Install dependencies using uv
|
| 229 |
+
echo "Installing Python dependencies from requirements.txt using uv..."
|
| 230 |
+
# Assuming uv is in PATH after the check above and venv is activated
|
| 231 |
+
uv pip install -r requirements.txt
|
| 232 |
+
if [ $? -ne 0 ]; then
|
| 233 |
+
echo "ERROR: Failed to install Python dependencies using uv. Please check requirements.txt and uv logs."
|
| 234 |
+
exit 1 # Cleanup trap will handle exit
|
| 235 |
+
fi
|
| 236 |
+
echo "Python dependencies installed successfully."
|
| 237 |
+
|
| 238 |
+
# Download spaCy model for anonymization
|
| 239 |
+
echo "Downloading spaCy model (en_core_web_lg) for anonymization..."
|
| 240 |
+
python3 -m spacy download en_core_web_lg
|
| 241 |
+
if [ $? -ne 0 ]; then
|
| 242 |
+
echo "WARNING: Failed to download spaCy model. Anonymization might fall back to NLTK."
|
| 243 |
+
fi
|
| 244 |
+
|
| 245 |
+
# --- Configure SSL Certificate Path ---
|
| 246 |
+
echo "----------------------------------------"
|
| 247 |
+
echo "Configuring SSL certificate path..."
|
| 248 |
+
|
| 249 |
+
# Ensure certifi is installed (uv pip install should handle this if in requirements.txt,
|
| 250 |
+
# but explicit check adds robustness - though uv handles it well)
|
| 251 |
+
# It's likely already installed as a dependency of requests/huggingface_hub etc.
|
| 252 |
+
# If dependency issues arise, add 'certifi' explicitly to requirements.txt
|
| 253 |
+
|
| 254 |
+
# Get the certificate path from certifi
|
| 255 |
+
CERT_PATH=$(python -c "import certifi; print(certifi.where())" 2>/dev/null) # Suppress stderr in case certifi isn't installed yet
|
| 256 |
+
|
| 257 |
+
if [ -n "$CERT_PATH" ] && [ -f "$CERT_PATH" ]; then
|
| 258 |
+
echo "Setting SSL_CERT_FILE environment variable to: $CERT_PATH"
|
| 259 |
+
export SSL_CERT_FILE="$CERT_PATH"
|
| 260 |
+
echo "SSL_CERT_FILE set for this script session and its children."
|
| 261 |
+
# Optional: Add a connection test here if desired
|
| 262 |
+
# python -c "import requests; r = requests.get('https://huggingface.co', timeout=10); print(f'Connection test status: {r.status_code}')"
|
| 263 |
+
# if [ $? -ne 0 ]; then echo "WARNING: Connection test failed after setting SSL_CERT_FILE."; fi
|
| 264 |
+
else
|
| 265 |
+
echo "WARNING: Could not determine certificate path using certifi. Certifi might not be installed yet or there's an issue."
|
| 266 |
+
echo "Attempting to proceed, but SSL errors (like CERTIFICATE_VERIFY_FAILED) may occur."
|
| 267 |
+
echo "Ensure 'certifi' is listed in requirements.txt if SSL errors persist."
|
| 268 |
+
fi
|
| 269 |
+
# --- End SSL Configuration ---
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
# --- Helper Process Logging Setup ---
|
| 273 |
+
echo "----------------------------------------"
|
| 274 |
+
echo "Setting up helper process logging..."
|
| 275 |
+
|
| 276 |
+
# Create helper_process_logs directory if it doesn't exist
|
| 277 |
+
HELPER_LOGS_DIR="helper_process_logs"
|
| 278 |
+
if [ ! -d "$HELPER_LOGS_DIR" ]; then
|
| 279 |
+
echo "Creating helper process logs directory: $HELPER_LOGS_DIR"
|
| 280 |
+
mkdir -p "$HELPER_LOGS_DIR"
|
| 281 |
+
if [ $? -ne 0 ]; then
|
| 282 |
+
echo "ERROR: Failed to create helper process logs directory."
|
| 283 |
+
exit 1
|
| 284 |
+
fi
|
| 285 |
+
else
|
| 286 |
+
echo "Helper process logs directory already exists: $HELPER_LOGS_DIR"
|
| 287 |
+
fi
|
| 288 |
+
|
| 289 |
+
# Generate timestamp for log files
|
| 290 |
+
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
| 291 |
+
HUEY_LOG_FILE="$HELPER_LOGS_DIR/huey_worker_${TIMESTAMP}.log"
|
| 292 |
+
UVICORN_LOG_FILE="$HELPER_LOGS_DIR/uvicorn_api_${TIMESTAMP}.log"
|
| 293 |
+
FRONTEND_LOG_FILE="$HELPER_LOGS_DIR/frontend_serve_${TIMESTAMP}.log"
|
| 294 |
+
|
| 295 |
+
echo "Helper process logs will be saved to:"
|
| 296 |
+
echo " Huey worker: $HUEY_LOG_FILE"
|
| 297 |
+
echo " Uvicorn API: $UVICORN_LOG_FILE"
|
| 298 |
+
echo " Frontend serve: $FRONTEND_LOG_FILE"
|
| 299 |
+
echo "Helper process logging setup complete."
|
| 300 |
+
|
| 301 |
+
|
| 302 |
+
# --- 1.5 Valkey (Redis Fork) Check & Installation ---
|
| 303 |
+
echo "----------------------------------------"
|
| 304 |
+
echo "1.5. Checking for Valkey (Redis fork)..."
|
| 305 |
+
|
| 306 |
+
# First, check if something is already listening on port 6379
|
| 307 |
+
if command -v lsof &> /dev/null; then
|
| 308 |
+
PORT_CHECK_CMD="lsof -i :6379"
|
| 309 |
+
elif command -v netstat &> /dev/null; then
|
| 310 |
+
PORT_CHECK_CMD="netstat -an"
|
| 311 |
+
else
|
| 312 |
+
PORT_CHECK_CMD=""
|
| 313 |
+
fi
|
| 314 |
+
|
| 315 |
+
if [ -n "$PORT_CHECK_CMD" ]; then
|
| 316 |
+
if [ "$PORT_CHECK_CMD" = "lsof -i :6379" ]; then
|
| 317 |
+
if $PORT_CHECK_CMD &> /dev/null; then
|
| 318 |
+
echo "Something is already listening on port 6379. Assuming a compatible Valkey/Redis server is running."
|
| 319 |
+
echo "Skipping Valkey installation and setup."
|
| 320 |
+
else
|
| 321 |
+
NEED_VALKEY_SETUP=true
|
| 322 |
+
fi
|
| 323 |
+
else
|
| 324 |
+
# netstat case
|
| 325 |
+
if $PORT_CHECK_CMD | grep -q ':6379'; then
|
| 326 |
+
echo "Something is already listening on port 6379. Assuming a compatible Valkey/Redis server is running."
|
| 327 |
+
echo "Skipping Valkey installation and setup."
|
| 328 |
+
else
|
| 329 |
+
NEED_VALKEY_SETUP=true
|
| 330 |
+
fi
|
| 331 |
+
fi
|
| 332 |
+
else
|
| 333 |
+
echo "WARNING: Cannot check port 6379 (lsof and netstat not available). Proceeding with Valkey setup."
|
| 334 |
+
NEED_VALKEY_SETUP=true
|
| 335 |
+
fi
|
| 336 |
+
|
| 337 |
+
if [ "$NEED_VALKEY_SETUP" = true ]; then
|
| 338 |
+
# Check if brew command exists
|
| 339 |
+
|
| 340 |
+
# Check if brew command exists
|
| 341 |
+
if ! command -v brew &> /dev/null; then
|
| 342 |
+
echo "Homebrew (brew) command not found."
|
| 343 |
+
read -p "Homebrew is required to install Valkey. Install Homebrew now? (y/N) " -n 1 -r
|
| 344 |
+
echo # Move to a new line after input
|
| 345 |
+
if [[ $REPLY =~ ^[Yy]$ ]]; then
|
| 346 |
+
echo "Attempting to install Homebrew. This may take a few minutes and will likely ask for your administrator password..."
|
| 347 |
+
# Execute the official Homebrew installation script
|
| 348 |
+
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
|
| 349 |
+
# Check if installation succeeded by seeing if 'brew' command is now available
|
| 350 |
+
# Might require sourcing profile or opening new shell, but let's check command first
|
| 351 |
+
if ! command -v brew &> /dev/null; then
|
| 352 |
+
echo "ERROR: Homebrew installation appears to have failed or is not available in this shell session."
|
| 353 |
+
echo "Please try installing Homebrew manually from https://brew.sh/ and run this script again, possibly in a new terminal window."
|
| 354 |
+
exit 1
|
| 355 |
+
else
|
| 356 |
+
# Add brew to PATH for the current script execution if possible
|
| 357 |
+
# This is complex as the path depends on architecture (Intel vs ARM)
|
| 358 |
+
# For simplicity, we'll rely on the installer potentially modifying the profile
|
| 359 |
+
# or the user running in a new shell later if 'brew' isn't found immediately after install.
|
| 360 |
+
# A more robust solution might involve detecting arch and adding the known path.
|
| 361 |
+
echo "Homebrew installed successfully. Proceeding..."
|
| 362 |
+
# Re-check explicitly here just in case the path isn't immediately active
|
| 363 |
+
if ! command -v brew &> /dev/null; then
|
| 364 |
+
echo "ERROR: Homebrew installed but 'brew' command still not found in PATH for this session."
|
| 365 |
+
echo "Please open a new terminal window and run this script again."
|
| 366 |
+
exit 1
|
| 367 |
+
fi
|
| 368 |
+
fi
|
| 369 |
+
else
|
| 370 |
+
echo "Homebrew installation declined. Valkey cannot be installed automatically."
|
| 371 |
+
echo "Please install Homebrew (https://brew.sh/) and Valkey ('brew install valkey') manually and rerun the script."
|
| 372 |
+
exit 1
|
| 373 |
+
fi
|
| 374 |
+
fi
|
| 375 |
+
echo "Homebrew found."
|
| 376 |
+
|
| 377 |
+
# Check if valkey is installed
|
| 378 |
+
if ! brew list valkey &>/dev/null; then
|
| 379 |
+
echo "Valkey not found via Homebrew. Attempting to install..."
|
| 380 |
+
# Update brew minimally and install valkey
|
| 381 |
+
# Using 'brew install' directly avoids running 'brew update' if possible,
|
| 382 |
+
# but Homebrew might still update itself or core formulae.
|
| 383 |
+
# We avoid 'brew upgrade' to prevent upgrading all packages.
|
| 384 |
+
brew install valkey
|
| 385 |
+
if [ $? -ne 0 ]; then
|
| 386 |
+
echo "ERROR: Failed to install Valkey using Homebrew. Please install it manually ('brew install valkey') and rerun the script."
|
| 387 |
+
exit 1
|
| 388 |
+
fi
|
| 389 |
+
echo "Valkey installed successfully."
|
| 390 |
+
else
|
| 391 |
+
echo "Valkey found via Homebrew."
|
| 392 |
+
fi
|
| 393 |
+
|
| 394 |
+
# Check if Valkey service is running, start if not
|
| 395 |
+
if ! brew services list | grep -q "valkey.*started"; then
|
| 396 |
+
echo "Valkey service is not running. Starting it now..."
|
| 397 |
+
brew services start valkey
|
| 398 |
+
if [ $? -ne 0 ]; then
|
| 399 |
+
echo "ERROR: Failed to start Valkey service using Homebrew. Please check Homebrew services ('brew services list') and start Valkey manually if needed."
|
| 400 |
+
exit 1
|
| 401 |
+
fi
|
| 402 |
+
# Give it a moment to start
|
| 403 |
+
sleep 2
|
| 404 |
+
if ! brew services list | grep -q "valkey.*started"; then
|
| 405 |
+
echo "ERROR: Valkey service failed to stay running after starting. Please investigate ('brew services list', check logs)."
|
| 406 |
+
exit 1
|
| 407 |
+
fi
|
| 408 |
+
echo "Valkey service started successfully."
|
| 409 |
+
else
|
| 410 |
+
echo "Valkey service is already running."
|
| 411 |
+
fi
|
| 412 |
+
fi # End NEED_VALKEY_SETUP conditional
|
| 413 |
+
# --- End Valkey Check ---
|
| 414 |
+
|
| 415 |
+
|
| 416 |
+
# --- 2. Huey Worker ---
|
| 417 |
+
echo "----------------------------------------"
|
| 418 |
+
echo "2. Starting Huey worker in the background (using venv)..."
|
| 419 |
+
echo "Output will be logged to: $HUEY_LOG_FILE"
|
| 420 |
+
# Call the huey_consumer script directly (should be in PATH from venv)
|
| 421 |
+
# Redirect both stdout and stderr to the log file
|
| 422 |
+
huey_consumer tasks.huey > "$HUEY_LOG_FILE" 2>&1 &
|
| 423 |
+
HUEY_PID=$!
|
| 424 |
+
# Give it a moment to start/fail
|
| 425 |
+
sleep 2
|
| 426 |
+
if ! kill -0 $HUEY_PID &> /dev/null; then
|
| 427 |
+
echo "ERROR: Huey worker (PID $HUEY_PID) failed to start or crashed immediately."
|
| 428 |
+
echo "Check the log file for details: $HUEY_LOG_FILE"
|
| 429 |
+
exit 1 # Cleanup trap will handle exit
|
| 430 |
+
fi
|
| 431 |
+
echo "Huey worker started successfully with PID $HUEY_PID."
|
| 432 |
+
|
| 433 |
+
|
| 434 |
+
# --- 3. API Server ---
|
| 435 |
+
echo "----------------------------------------"
|
| 436 |
+
echo "3. Starting FastAPI server (Uvicorn) in the background (using venv)..."
|
| 437 |
+
echo "Output will be logged to: $UVICORN_LOG_FILE"
|
| 438 |
+
# Host 0.0.0.0 makes it accessible on the network
|
| 439 |
+
# Use python -m uvicorn to ensure using the venv uvicorn
|
| 440 |
+
# Redirect both stdout and stderr to the log file
|
| 441 |
+
python -m uvicorn api:app --host 0.0.0.0 --port 8000 > "$UVICORN_LOG_FILE" 2>&1 &
|
| 442 |
+
UVICORN_PID=$!
|
| 443 |
+
# Give it a moment to start/fail
|
| 444 |
+
sleep 2
|
| 445 |
+
if ! kill -0 $UVICORN_PID &> /dev/null; then
|
| 446 |
+
echo "ERROR: Uvicorn server (PID $UVICORN_PID) failed to start or crashed immediately."
|
| 447 |
+
echo "Check the log file for details: $UVICORN_LOG_FILE"
|
| 448 |
+
# Cleanup trap will handle stopping Huey
|
| 449 |
+
exit 1 # Cleanup trap will handle exit
|
| 450 |
+
fi
|
| 451 |
+
echo "Uvicorn server started successfully with PID $UVICORN_PID. Access API at http://localhost:8000"
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
# --- 4. NPM Check ---
|
| 455 |
+
echo "----------------------------------------"
|
| 456 |
+
echo "4. Checking for npm..."
|
| 457 |
+
if ! command -v npm &> /dev/null; then
|
| 458 |
+
echo "ERROR: npm command not found."
|
| 459 |
+
echo "Please install Node.js and npm (e.g., from https://nodejs.org/) and run this script again."
|
| 460 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 461 |
+
kill $HUEY_PID $UVICORN_PID
|
| 462 |
+
exit 1
|
| 463 |
+
fi
|
| 464 |
+
echo "npm found."
|
| 465 |
+
|
| 466 |
+
|
| 467 |
+
# --- 5. Frontend Dependencies ---
|
| 468 |
+
echo "----------------------------------------"
|
| 469 |
+
echo "5. Installing frontend dependencies in atk-interface..."
|
| 470 |
+
if [ ! -d "atk-interface" ]; then
|
| 471 |
+
echo "ERROR: Directory 'atk-interface' not found in $(pwd)."
|
| 472 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 473 |
+
kill $HUEY_PID $UVICORN_PID
|
| 474 |
+
exit 1
|
| 475 |
+
fi
|
| 476 |
+
cd atk-interface
|
| 477 |
+
npm install
|
| 478 |
+
if [ $? -ne 0 ]; then
|
| 479 |
+
echo "ERROR: Failed to install npm dependencies in atk-interface. Check package.json and npm logs."
|
| 480 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 481 |
+
kill $HUEY_PID $UVICORN_PID
|
| 482 |
+
cd "$SCRIPT_DIR" # Go back to root
|
| 483 |
+
exit 1
|
| 484 |
+
fi
|
| 485 |
+
echo "Frontend dependencies installed."
|
| 486 |
+
|
| 487 |
+
|
| 488 |
+
# --- 6. Frontend Build ---
|
| 489 |
+
echo "----------------------------------------"
|
| 490 |
+
echo "6. Building frontend application in atk-interface..."
|
| 491 |
+
npm run build
|
| 492 |
+
if [ $? -ne 0 ]; then
|
| 493 |
+
echo "ERROR: Failed to build frontend application. Check build script in package.json and logs."
|
| 494 |
+
echo "Stopping background services (PIDs $HUEY_PID, $UVICORN_PID)..."
|
| 495 |
+
kill $HUEY_PID $UVICORN_PID
|
| 496 |
+
cd "$SCRIPT_DIR" # Go back to root
|
| 497 |
+
exit 1
|
| 498 |
+
fi
|
| 499 |
+
echo "Frontend built successfully."
|
| 500 |
+
|
| 501 |
+
|
| 502 |
+
# --- 7. Frontend Serve ---
|
| 503 |
+
echo "----------------------------------------"
|
| 504 |
+
echo "7. Serving frontend application from atk-interface/dist/ in the FOREGROUND..."
|
| 505 |
+
echo "Output will be logged to: $FRONTEND_LOG_FILE"
|
| 506 |
+
# Ensure dist directory exists after build
|
| 507 |
+
if [ ! -d "dist" ]; then
|
| 508 |
+
echo "ERROR: 'dist' directory not found in atk-interface after build."
|
| 509 |
+
# Cleanup trap will handle stopping background services
|
| 510 |
+
exit 1
|
| 511 |
+
fi
|
| 512 |
+
|
| 513 |
+
# Show summary of all log files
|
| 514 |
+
echo ""
|
| 515 |
+
echo "═══════════════════════════════════════════════════════════════════════════════"
|
| 516 |
+
echo "All helper processes are now running with output redirected to log files:"
|
| 517 |
+
echo " • Huey worker logs: $HUEY_LOG_FILE"
|
| 518 |
+
echo " • Uvicorn API logs: $UVICORN_LOG_FILE"
|
| 519 |
+
echo " • Frontend serve logs: $FRONTEND_LOG_FILE"
|
| 520 |
+
echo ""
|
| 521 |
+
echo "You can monitor these logs in real-time with:"
|
| 522 |
+
echo " tail -f $HUEY_LOG_FILE"
|
| 523 |
+
echo " tail -f $UVICORN_LOG_FILE"
|
| 524 |
+
echo " tail -f $FRONTEND_LOG_FILE"
|
| 525 |
+
echo "═══════════════════════════════════════════════════════════════════════════════"
|
| 526 |
+
echo ""
|
| 527 |
+
|
| 528 |
+
# Serve on port 5173 (matches CORS config)
|
| 529 |
+
# Run npx serve in the background first, then open browser once it's ready
|
| 530 |
+
echo "Starting serve process (output redirected to log file)..."
|
| 531 |
+
# Redirect both stdout and stderr to the log file - use ../ to go back to script directory
|
| 532 |
+
npx serve -s dist --listen 5173 > "../$FRONTEND_LOG_FILE" 2>&1 &
|
| 533 |
+
SERVE_PID=$!
|
| 534 |
+
|
| 535 |
+
# Wait for the serve process to be ready
|
| 536 |
+
echo "Waiting for frontend server to start..."
|
| 537 |
+
sleep 3
|
| 538 |
+
|
| 539 |
+
# Check if serve process is still running
|
| 540 |
+
if ! kill -0 $SERVE_PID &> /dev/null; then
|
| 541 |
+
echo "ERROR: Frontend serve process failed to start or crashed immediately."
|
| 542 |
+
echo "Check the log file for details: $FRONTEND_LOG_FILE"
|
| 543 |
+
cleanup
|
| 544 |
+
exit 1
|
| 545 |
+
fi
|
| 546 |
+
|
| 547 |
+
# Wait for the server to actually be listening on port 5173
|
| 548 |
+
echo "Checking if server is responding..."
|
| 549 |
+
for i in {1..10}; do
|
| 550 |
+
if command -v curl &> /dev/null; then
|
| 551 |
+
if curl -s http://localhost:5173 > /dev/null 2>&1; then
|
| 552 |
+
break
|
| 553 |
+
fi
|
| 554 |
+
elif command -v nc &> /dev/null; then
|
| 555 |
+
if nc -z localhost 5173 > /dev/null 2>&1; then
|
| 556 |
+
break
|
| 557 |
+
fi
|
| 558 |
+
else
|
| 559 |
+
# Fallback: just check if something is listening on port 5173
|
| 560 |
+
if command -v lsof &> /dev/null; then
|
| 561 |
+
if lsof -i :5173 > /dev/null 2>&1; then
|
| 562 |
+
break
|
| 563 |
+
fi
|
| 564 |
+
elif command -v netstat &> /dev/null; then
|
| 565 |
+
if netstat -an | grep -q ':5173'; then
|
| 566 |
+
break
|
| 567 |
+
fi
|
| 568 |
+
else
|
| 569 |
+
# No way to check, just wait a bit more
|
| 570 |
+
sleep 1
|
| 571 |
+
break
|
| 572 |
+
fi
|
| 573 |
+
fi
|
| 574 |
+
sleep 1
|
| 575 |
+
done
|
| 576 |
+
|
| 577 |
+
echo "Opening interface..."
|
| 578 |
+
|
| 579 |
+
# Open the URL in the default browser (macOS)
|
| 580 |
+
echo "Opening http://localhost:5173 in default browser..."
|
| 581 |
+
open http://localhost:5173
|
| 582 |
+
|
| 583 |
+
echo "Frontend server is now running. Press Ctrl+C to stop all services."
|
| 584 |
+
|
| 585 |
+
# Wait for the serve process to finish (it's now running in the background)
|
| 586 |
+
wait $SERVE_PID
|
| 587 |
+
SERVE_EXIT_CODE=$?
|
| 588 |
+
SERVE_PID="" # Clear PID as it has exited
|
| 589 |
+
|
| 590 |
+
if [ $SERVE_EXIT_CODE -ne 0 ]; then
|
| 591 |
+
echo "WARNING: Frontend serve process exited with code $SERVE_EXIT_CODE."
|
| 592 |
+
echo "Check the log file for details: $FRONTEND_LOG_FILE"
|
| 593 |
+
# Consider if we should stop background services here if serve crashes
|
| 594 |
+
# Calling cleanup manually if serve fails
|
| 595 |
+
cleanup
|
| 596 |
+
exit 1 # Exit with error code
|
| 597 |
+
fi
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
# --- 8. Done (Only reached if npx serve exits gracefully, e.g., if killed separately) ---
|
| 601 |
+
cd "$SCRIPT_DIR" # Go back to root directory
|
| 602 |
+
echo "----------------------------------------"
|
| 603 |
+
echo "Frontend serve process finished."
|
| 604 |
+
echo ""
|
| 605 |
+
echo "Helper process log files were saved to:"
|
| 606 |
+
echo " • Huey worker logs: $HUEY_LOG_FILE"
|
| 607 |
+
echo " • Uvicorn API logs: $UVICORN_LOG_FILE"
|
| 608 |
+
echo " • Frontend serve logs: $FRONTEND_LOG_FILE"
|
| 609 |
+
# Call cleanup to stop background services if serve finishes normally
|
| 610 |
+
cleanup
|
| 611 |
+
|
| 612 |
+
exit 0
|
ocr/.env.example
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Securely provision values, do not commit .env or print the process environment.
|
| 2 |
+
OCR_DISPATCHER_TOKEN=
|
| 3 |
+
OCR_CPU_AUTH_TOKEN=
|
| 4 |
+
OCR_MODELS_DIR=/opt/ocr-models
|
| 5 |
+
# Empty means trusted local CPU. Remote URL must be HTTPS.
|
| 6 |
+
OCR_CPU_URL=
|
| 7 |
+
OCR_MIN_SENSITIVITY=D2
|
| 8 |
+
OCR_ALLOW_EXTERNAL_CPU_D0=true
|
| 9 |
+
OCR_ALLOW_EXTERNAL_CPU_D1=false
|
| 10 |
+
OCR_ALLOW_EXTERNAL_CPU_D2=false
|
| 11 |
+
OCR_ENABLE_GPU=false
|
| 12 |
+
FREELLMAPI_BASE_URL=
|
| 13 |
+
FREELLMAPI_API_KEY=
|
| 14 |
+
OCR_GPU_MODEL=alias-ocr
|
| 15 |
+
OCR_CPU_CONCURRENCY=1
|
| 16 |
+
OCR_GPU_CONCURRENCY=2
|
| 17 |
+
OCR_ENABLE_GPU_OVERFLOW=false
|
| 18 |
+
OCR_OVERFLOW_BENCHMARK_QUALIFIED=false
|
| 19 |
+
OCR_QUALITY_DUAL_RUN=false
|
| 20 |
+
OCR_MAX_UPLOAD_MIB=25
|
| 21 |
+
OCR_MAX_PAGES=100
|
| 22 |
+
OCR_MAX_PIXELS=20000000
|
| 23 |
+
OCR_JOB_TTL_MIN=30
|
ocr/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2024 Evan Armstrong
|
| 4 |
+
|
| 5 |
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
+
of this software and associated documentation files (the "Software"), to deal
|
| 7 |
+
in the Software without restriction, including without limitation the rights
|
| 8 |
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
| 9 |
+
copies of the Software, and to permit persons to whom the Software is
|
| 10 |
+
furnished to do so, subject to the following conditions:
|
| 11 |
+
|
| 12 |
+
The above copyright notice and this permission notice shall be included in all
|
| 13 |
+
copies or substantial portions of the Software.
|
| 14 |
+
|
| 15 |
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
| 16 |
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
| 17 |
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
| 18 |
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
| 19 |
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
| 20 |
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
| 21 |
+
SOFTWARE.
|
ocr/README.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Hybrid CPU-first OCR
|
| 2 |
+
|
| 3 |
+
Separate private dispatcher and authenticated PP-OCRv6 Small CPU worker.
|
| 4 |
+
No production integration or startup occurs on import. See:
|
| 5 |
+
|
| 6 |
+
- [API and schemas](docs/api.md)
|
| 7 |
+
- [Install, HF packaging, secrets and rollback](docs/deployment.md)
|
| 8 |
+
- [Routing, limits and cancellation](docs/routing.md)
|
| 9 |
+
- [Reproducible benchmarks](docs/benchmark.md) and [distribution notices](THIRD_PARTY.md)
|
| 10 |
+
- [Security boundaries](docs/threat-model.md)
|
| 11 |
+
- [Audit](docs/audit.md) and [decisions/deviations](docs/adr.md)
|
| 12 |
+
- [Release-readiness report](../docs/OCR_RELEASE_READINESS.md)
|
| 13 |
+
|
| 14 |
+
`python -m ocr.cli run file.pdf --format markdown` runs trusted local CPU.
|
| 15 |
+
`python -m ocr.cli probe-alias` uses a synthetic public image and credentials from
|
| 16 |
+
runtime environment. `python -m ocr.benchmark` generates a CC0 synthetic DE/EN
|
| 17 |
+
corpus and measures real CPU OCR. Live alias and all D2 remote routing default off.
|
ocr/THIRD_PARTY.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Distribution notices
|
| 2 |
+
|
| 3 |
+
The application code inherits the repository's MIT license, copied to `ocr/LICENSE`
|
| 4 |
+
for a standalone worker staging directory. The Space README's license field refers
|
| 5 |
+
to that application code, not to every bundled model/dependency.
|
| 6 |
+
|
| 7 |
+
PP-OCRv6 Small detector/recognizer artifacts come from the RapidAI/RapidOCR
|
| 8 |
+
ModelScope repository, revision `v3.10.0`; its public metadata declares Apache
|
| 9 |
+
License 2.0. Exact HTTPS URLs and upstream SHA256s are in `models.json`. Preserve
|
| 10 |
+
Apache license/attribution requirements when distributing the baked model files.
|
| 11 |
+
The pinned RapidOCR distribution includes its Apache-2.0 full license in installed
|
| 12 |
+
distribution metadata. No additional model redistribution rights are asserted.
|
| 13 |
+
|
| 14 |
+
Runtime dependencies retain their installed `.dist-info` license files in the
|
| 15 |
+
Docker image. Key metadata: RapidOCR Apache-2.0; ONNX Runtime MIT; Pillow MIT-CMU;
|
| 16 |
+
pypdf BSD-3-Clause; pypdfium2 BSD-3-Clause/Apache-2.0 plus bundled PDFium dependency
|
| 17 |
+
licenses; FastAPI MIT; HTTPX BSD-3-Clause. See those full installed notices rather
|
| 18 |
+
than treating this summary as a substitute. Debian's DejaVu font copyright notice
|
| 19 |
+
is retained under `/usr/share/doc/fonts-dejavu-core/copyright`.
|
| 20 |
+
|
| 21 |
+
Review all transitive notices and operator/provider terms before publication.
|
| 22 |
+
Document ownership/processing rights and downstream provider compliance are the
|
| 23 |
+
operator's responsibility; the software makes no regulatory certification claim.
|
ocr/__init__.py
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CPU-first OCR. No backend or network is initialized on import."""
|
| 2 |
+
|
| 3 |
+
__version__ = "1.0.0"
|
ocr/adapters/__init__.py
ADDED
|
File without changes
|
ocr/adapters/cpu_http.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import httpx
|
| 2 |
+
|
| 3 |
+
from ..schemas import BackendCapabilities, BackendHealth, OCRPageResult, SafeError
|
| 4 |
+
from .http import request_json
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class RemoteCPU:
|
| 8 |
+
name = "cpu"
|
| 9 |
+
external = True
|
| 10 |
+
|
| 11 |
+
def __init__(self, url, token, timeout=90, transport=None):
|
| 12 |
+
if not token:
|
| 13 |
+
raise ValueError("worker_token_required")
|
| 14 |
+
self.url = url
|
| 15 |
+
self.destination = url
|
| 16 |
+
self.client = httpx.AsyncClient(
|
| 17 |
+
timeout=timeout,
|
| 18 |
+
follow_redirects=False,
|
| 19 |
+
transport=transport,
|
| 20 |
+
headers={"Authorization": "Bearer " + token},
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
async def close(self):
|
| 24 |
+
await self.client.aclose()
|
| 25 |
+
|
| 26 |
+
async def capabilities(self):
|
| 27 |
+
# Conservative local contract; don't inherit a remote service's sensitive-class claims.
|
| 28 |
+
return BackendCapabilities(
|
| 29 |
+
name="cpu",
|
| 30 |
+
bounding_boxes=True,
|
| 31 |
+
languages=["de", "en"],
|
| 32 |
+
supports_sensitive_classes=["D0"],
|
| 33 |
+
multimodal_verified=True,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
async def health(self):
|
| 37 |
+
try:
|
| 38 |
+
r = await request_json(self.client, "GET", self.url + "/ready", limit=4096)
|
| 39 |
+
return BackendHealth(ready=r.get("ready") is True)
|
| 40 |
+
except SafeError:
|
| 41 |
+
return BackendHealth(ready=False, code="cpu_worker_unavailable")
|
| 42 |
+
|
| 43 |
+
async def recognize(self, page, options):
|
| 44 |
+
r = await request_json(
|
| 45 |
+
self.client,
|
| 46 |
+
"POST",
|
| 47 |
+
self.url + "/internal/ocr/page",
|
| 48 |
+
files={"file": ("page.png", page.image, "image/png")},
|
| 49 |
+
data={"request_id": page.page_id, "options": "{}"},
|
| 50 |
+
)
|
| 51 |
+
try:
|
| 52 |
+
result = OCRPageResult.model_validate(r)
|
| 53 |
+
except Exception:
|
| 54 |
+
raise SafeError("invalid_cpu_response") from None
|
| 55 |
+
if result.source != "cpu_ocr" or result.engine != "pp-ocrv6-small":
|
| 56 |
+
raise SafeError("unexpected_cpu_engine")
|
| 57 |
+
if result.image_width != page.width or result.image_height != page.height:
|
| 58 |
+
raise SafeError("cpu_geometry_mismatch")
|
| 59 |
+
result.page_index = page.index
|
| 60 |
+
result.provider_destination = self.destination
|
| 61 |
+
return result
|
ocr/adapters/freellm_alias.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import io
|
| 3 |
+
import time
|
| 4 |
+
|
| 5 |
+
import httpx
|
| 6 |
+
from PIL import Image, ImageDraw
|
| 7 |
+
|
| 8 |
+
from ..normalize import grounding
|
| 9 |
+
from ..schemas import BackendCapabilities, BackendHealth, OCRPageResult, SafeError
|
| 10 |
+
from .http import request_json
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class FreeLLMAlias:
|
| 14 |
+
name = "gpu"
|
| 15 |
+
external = True
|
| 16 |
+
|
| 17 |
+
def __init__(self, url, key, timeout=90, transport=None):
|
| 18 |
+
if not url or not key:
|
| 19 |
+
raise ValueError("alias_configuration_missing")
|
| 20 |
+
self.url = url
|
| 21 |
+
self.destination = None
|
| 22 |
+
self.qualified = False
|
| 23 |
+
self.qualified_at = 0
|
| 24 |
+
self.actual_model = None
|
| 25 |
+
self.grounding_verified = False
|
| 26 |
+
self.client = httpx.AsyncClient(
|
| 27 |
+
timeout=timeout,
|
| 28 |
+
follow_redirects=False,
|
| 29 |
+
transport=transport,
|
| 30 |
+
headers={"Authorization": "Bearer " + key},
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
async def close(self):
|
| 34 |
+
await self.client.aclose()
|
| 35 |
+
|
| 36 |
+
async def capabilities(self):
|
| 37 |
+
return BackendCapabilities(
|
| 38 |
+
name="gpu",
|
| 39 |
+
layout_blocks=self.grounding_verified,
|
| 40 |
+
bounding_boxes=self.grounding_verified,
|
| 41 |
+
grounding_verified=self.grounding_verified,
|
| 42 |
+
multimodal_verified=self.qualified,
|
| 43 |
+
effective_destination_verifiable=False,
|
| 44 |
+
supports_sensitive_classes=["D0"] if self.qualified else [],
|
| 45 |
+
)
|
| 46 |
+
|
| 47 |
+
async def health(self):
|
| 48 |
+
return BackendHealth(
|
| 49 |
+
ready=self.qualified and time.monotonic() - self.qualified_at < 3600,
|
| 50 |
+
code="ok" if self.qualified else "alias_unqualified",
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
async def _completion(self, image, *, probe=False):
|
| 54 |
+
# Separate copy: CPU source raster and coordinate system remain untouched.
|
| 55 |
+
with Image.open(io.BytesIO(image)) as im:
|
| 56 |
+
im = im.convert("RGB")
|
| 57 |
+
im.thumbnail((2048, 2048))
|
| 58 |
+
b = io.BytesIO()
|
| 59 |
+
im.save(b, format="PNG")
|
| 60 |
+
content = [
|
| 61 |
+
{
|
| 62 |
+
"type": "image_url",
|
| 63 |
+
"image_url": {
|
| 64 |
+
"url": "data:image/png;base64," + base64.b64encode(b.getvalue()).decode()
|
| 65 |
+
},
|
| 66 |
+
}
|
| 67 |
+
]
|
| 68 |
+
# No unverified provider-specific options, tools or thinking flags.
|
| 69 |
+
content.append(
|
| 70 |
+
{
|
| 71 |
+
"type": "text",
|
| 72 |
+
"text": "Transcribe the document verbatim. Return only document text or Markdown.",
|
| 73 |
+
}
|
| 74 |
+
)
|
| 75 |
+
data = await request_json(
|
| 76 |
+
self.client,
|
| 77 |
+
"POST",
|
| 78 |
+
self.url + "/chat/completions",
|
| 79 |
+
json={
|
| 80 |
+
"model": "alias-ocr",
|
| 81 |
+
"messages": [{"role": "user", "content": content}],
|
| 82 |
+
"stream": False,
|
| 83 |
+
"temperature": 0.1,
|
| 84 |
+
},
|
| 85 |
+
)
|
| 86 |
+
try:
|
| 87 |
+
choice = data["choices"][0]
|
| 88 |
+
text = choice["message"]["content"]
|
| 89 |
+
if choice.get("finish_reason") not in ("stop",):
|
| 90 |
+
raise SafeError("alias_truncated")
|
| 91 |
+
if not isinstance(text, str) or not text.strip():
|
| 92 |
+
raise SafeError("alias_empty")
|
| 93 |
+
if "<think>" in text or "<analysis>" in text:
|
| 94 |
+
raise SafeError("alias_reasoning_output")
|
| 95 |
+
if len(text) > 2_000_000:
|
| 96 |
+
raise SafeError("backend_response_limit")
|
| 97 |
+
model = data.get("model")
|
| 98 |
+
# Response metadata is recorded as an observation, NEVER proof of enforced routing.
|
| 99 |
+
observed = data.get("provider") if isinstance(data.get("provider"), str) else None
|
| 100 |
+
return (
|
| 101 |
+
text,
|
| 102 |
+
model if isinstance(model, str) and model != "alias-ocr" else None,
|
| 103 |
+
observed,
|
| 104 |
+
)
|
| 105 |
+
except SafeError:
|
| 106 |
+
raise
|
| 107 |
+
except (KeyError, IndexError, TypeError):
|
| 108 |
+
raise SafeError("alias_invalid_response") from None
|
| 109 |
+
|
| 110 |
+
async def probe(self):
|
| 111 |
+
self.qualified = False
|
| 112 |
+
# Synthetic D0 challenge text is not included in the prompt; a text-only echo cannot pass.
|
| 113 |
+
import secrets
|
| 114 |
+
|
| 115 |
+
challenge = "OCR " + secrets.token_hex(3).upper()
|
| 116 |
+
im = Image.new("RGB", (800, 160), "white")
|
| 117 |
+
d = ImageDraw.Draw(im)
|
| 118 |
+
from PIL import ImageFont
|
| 119 |
+
|
| 120 |
+
font = ImageFont.truetype("DejaVuSans.ttf", 48)
|
| 121 |
+
d.text((25, 40), challenge, fill="black", font=font)
|
| 122 |
+
b = io.BytesIO()
|
| 123 |
+
im.save(b, format="PNG")
|
| 124 |
+
text, model, provider = await self._completion(b.getvalue(), probe=True)
|
| 125 |
+
if challenge.casefold() not in text.casefold():
|
| 126 |
+
raise SafeError("alias_multimodal_probe_failed")
|
| 127 |
+
self.qualified = True
|
| 128 |
+
self.qualified_at = time.monotonic()
|
| 129 |
+
self.actual_model = model
|
| 130 |
+
# Observed metadata is not a verified effective destination/fallback guarantee.
|
| 131 |
+
return {
|
| 132 |
+
"multimodal_verified": True,
|
| 133 |
+
"observed_model": model,
|
| 134 |
+
"observed_provider": provider,
|
| 135 |
+
"effective_destination_verified": False,
|
| 136 |
+
"grounding_verified": False,
|
| 137 |
+
"d2_qualified": False,
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
async def recognize(self, page, options):
|
| 141 |
+
if not (await self.health()).ready:
|
| 142 |
+
raise SafeError("alias_unqualified")
|
| 143 |
+
text, model, provider = await self._completion(page.image)
|
| 144 |
+
# Model changes revoke prior probe qualification, even for D0.
|
| 145 |
+
if model != self.actual_model:
|
| 146 |
+
self.qualified = False
|
| 147 |
+
raise SafeError("alias_model_changed")
|
| 148 |
+
blocks = grounding(text) if self.grounding_verified else []
|
| 149 |
+
return OCRPageResult(
|
| 150 |
+
page_index=page.index,
|
| 151 |
+
source="alias_ocr",
|
| 152 |
+
engine=model,
|
| 153 |
+
provider_destination=None,
|
| 154 |
+
text=text,
|
| 155 |
+
markdown=text,
|
| 156 |
+
image_width=page.width,
|
| 157 |
+
image_height=page.height,
|
| 158 |
+
blocks=blocks,
|
| 159 |
+
flags=["destination_unverified"] + ([] if blocks else ["boxes_unavailable"]),
|
| 160 |
+
)
|
ocr/adapters/http.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
import httpx
|
| 4 |
+
|
| 5 |
+
from ..schemas import SafeError
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
async def request_json(client, method, url, *, limit=4 * 1024 * 1024, **kwargs):
|
| 9 |
+
try:
|
| 10 |
+
async with client.stream(method, url, **kwargs) as r:
|
| 11 |
+
if r.status_code >= 400:
|
| 12 |
+
raise SafeError(
|
| 13 |
+
f"backend_http_{r.status_code}"
|
| 14 |
+
if r.status_code in (429, 502, 503, 401, 403)
|
| 15 |
+
else "backend_http_error",
|
| 16 |
+
r.status_code in (429, 502, 503),
|
| 17 |
+
)
|
| 18 |
+
data = bytearray()
|
| 19 |
+
async for part in r.aiter_bytes():
|
| 20 |
+
data.extend(part)
|
| 21 |
+
if len(data) > limit:
|
| 22 |
+
raise SafeError("backend_response_limit")
|
| 23 |
+
try:
|
| 24 |
+
return json.loads(data)
|
| 25 |
+
except (ValueError, TypeError):
|
| 26 |
+
raise SafeError("backend_invalid_json") from None
|
| 27 |
+
except SafeError:
|
| 28 |
+
raise
|
| 29 |
+
except (httpx.TimeoutException, httpx.TransportError):
|
| 30 |
+
raise SafeError("backend_unavailable", True) from None
|
ocr/adapters/rapidocr_cpu.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import asyncio
|
| 2 |
+
import hashlib
|
| 3 |
+
import importlib.metadata
|
| 4 |
+
import io
|
| 5 |
+
import json
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
from PIL import Image
|
| 10 |
+
|
| 11 |
+
from ..normalize import polygon
|
| 12 |
+
from ..schemas import (
|
| 13 |
+
BackendCapabilities,
|
| 14 |
+
BackendHealth,
|
| 15 |
+
Block,
|
| 16 |
+
OCRPageResult,
|
| 17 |
+
SafeError,
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
class RapidCPU:
|
| 22 |
+
name = "cpu"
|
| 23 |
+
destination = "local-cpu"
|
| 24 |
+
external = False
|
| 25 |
+
|
| 26 |
+
def __init__(self, models_dir):
|
| 27 |
+
self.models_dir = Path(models_dir)
|
| 28 |
+
self.engine = None
|
| 29 |
+
self.lock = asyncio.Lock()
|
| 30 |
+
|
| 31 |
+
def _initialize(self):
|
| 32 |
+
import cv2
|
| 33 |
+
from rapidocr import EngineType, ModelType, OCRVersion, RapidOCR
|
| 34 |
+
|
| 35 |
+
cv2.setNumThreads(2)
|
| 36 |
+
|
| 37 |
+
manifest = json.loads((Path(__file__).parents[1] / "models.json").read_text())
|
| 38 |
+
params = {
|
| 39 |
+
"Global.use_cls": False,
|
| 40 |
+
"Global.log_level": "critical",
|
| 41 |
+
"EngineConfig.onnxruntime.intra_op_num_threads": 2,
|
| 42 |
+
"EngineConfig.onnxruntime.inter_op_num_threads": 1,
|
| 43 |
+
"EngineConfig.onnxruntime.use_cuda": False,
|
| 44 |
+
}
|
| 45 |
+
for kind in ("Det", "Rec"):
|
| 46 |
+
item = manifest[kind]
|
| 47 |
+
path = self.models_dir / item["filename"]
|
| 48 |
+
if (
|
| 49 |
+
not path.is_file()
|
| 50 |
+
or hashlib.sha256(path.read_bytes()).hexdigest() != item["sha256"]
|
| 51 |
+
):
|
| 52 |
+
raise SafeError("model_integrity_unavailable")
|
| 53 |
+
params.update(
|
| 54 |
+
{
|
| 55 |
+
f"{kind}.model_path": str(path),
|
| 56 |
+
f"{kind}.model_type": ModelType.SMALL,
|
| 57 |
+
f"{kind}.ocr_version": OCRVersion.PPOCRV6,
|
| 58 |
+
f"{kind}.engine_type": EngineType.ONNXRUNTIME,
|
| 59 |
+
}
|
| 60 |
+
)
|
| 61 |
+
self.engine = RapidOCR(params=params)
|
| 62 |
+
# Force lazy models to initialize before readiness; never download implicitly.
|
| 63 |
+
self.engine(np.full((96, 256, 3), 255, dtype=np.uint8), use_cls=False)
|
| 64 |
+
self.engine(
|
| 65 |
+
np.full((48, 256, 3), 255, dtype=np.uint8),
|
| 66 |
+
use_det=False,
|
| 67 |
+
use_cls=False,
|
| 68 |
+
use_rec=True,
|
| 69 |
+
)
|
| 70 |
+
self.engine.update_params(use_det=True, use_cls=False, use_rec=True)
|
| 71 |
+
for model in (self.engine.text_det, self.engine.text_rec):
|
| 72 |
+
if model is None or model.session.session.get_providers() != ["CPUExecutionProvider"]:
|
| 73 |
+
self.engine = None
|
| 74 |
+
raise SafeError("unexpected_cpu_execution_provider")
|
| 75 |
+
|
| 76 |
+
async def initialize(self):
|
| 77 |
+
async with self.lock:
|
| 78 |
+
if self.engine is None:
|
| 79 |
+
try:
|
| 80 |
+
await asyncio.to_thread(self._initialize)
|
| 81 |
+
except SafeError:
|
| 82 |
+
raise
|
| 83 |
+
except Exception:
|
| 84 |
+
self.engine = None
|
| 85 |
+
raise SafeError("cpu_initialization_failed") from None
|
| 86 |
+
|
| 87 |
+
async def capabilities(self):
|
| 88 |
+
return BackendCapabilities(
|
| 89 |
+
name="cpu",
|
| 90 |
+
bounding_boxes=True,
|
| 91 |
+
languages=["de", "en"],
|
| 92 |
+
supports_sensitive_classes=["D0", "D1", "D2"],
|
| 93 |
+
effective_destination_verifiable=True,
|
| 94 |
+
multimodal_verified=self.engine is not None,
|
| 95 |
+
)
|
| 96 |
+
|
| 97 |
+
async def health(self):
|
| 98 |
+
return BackendHealth(
|
| 99 |
+
ready=self.engine is not None,
|
| 100 |
+
code="ok" if self.engine else "model_not_ready",
|
| 101 |
+
)
|
| 102 |
+
|
| 103 |
+
def _recognize(self, page):
|
| 104 |
+
image = np.asarray(Image.open(io.BytesIO(page.image)).convert("RGB"))[:, :, ::-1].copy()
|
| 105 |
+
out = self.engine(image, use_cls=False)
|
| 106 |
+
blocks = []
|
| 107 |
+
if out.txts is not None:
|
| 108 |
+
for text, score, box in zip(out.txts, out.scores, out.boxes, strict=True):
|
| 109 |
+
p, b = polygon(box, page.width, page.height)
|
| 110 |
+
blocks.append(
|
| 111 |
+
Block(
|
| 112 |
+
text=text,
|
| 113 |
+
confidence=float(score),
|
| 114 |
+
confidence_kind="rapidocr-recognition",
|
| 115 |
+
polygon_1000=p,
|
| 116 |
+
bbox_1000=b,
|
| 117 |
+
)
|
| 118 |
+
)
|
| 119 |
+
# Preserve engine reading order; multi-column order is explicitly flagged by quality checks.
|
| 120 |
+
text = "\n".join(b.text for b in blocks)
|
| 121 |
+
return OCRPageResult(
|
| 122 |
+
page_index=page.index,
|
| 123 |
+
source="cpu_ocr",
|
| 124 |
+
engine="pp-ocrv6-small",
|
| 125 |
+
backend_version=f"rapidocr/{importlib.metadata.version('rapidocr')};onnxruntime/{importlib.metadata.version('onnxruntime')}",
|
| 126 |
+
provider_destination=self.destination,
|
| 127 |
+
text=text,
|
| 128 |
+
markdown=text,
|
| 129 |
+
image_width=page.width,
|
| 130 |
+
image_height=page.height,
|
| 131 |
+
blocks=blocks,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
async def recognize(self, page, options):
|
| 135 |
+
if self.engine is None:
|
| 136 |
+
raise SafeError("model_not_ready")
|
| 137 |
+
# Shield native work and retain the lock through cancellation: ORT cannot be force-cancelled.
|
| 138 |
+
async with self.lock:
|
| 139 |
+
task = asyncio.create_task(asyncio.to_thread(self._recognize, page))
|
| 140 |
+
try:
|
| 141 |
+
return await asyncio.shield(task)
|
| 142 |
+
except asyncio.CancelledError:
|
| 143 |
+
await task
|
| 144 |
+
raise
|
| 145 |
+
except Exception:
|
| 146 |
+
raise SafeError("cpu_inference_failed") from None
|
ocr/anonymization/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit trusted-layer anonymization; no legacy fallback or network inference."""
|
| 2 |
+
|
| 3 |
+
from .candidate_io import parse_candidate
|
| 4 |
+
from .engine import AnonymizationEngine
|
| 5 |
+
from .release_policy import ReleaseAuthority
|
| 6 |
+
from .schemas import Entity, LegacyReleaseCandidate, ReleaseCandidate
|
| 7 |
+
|
| 8 |
+
__all__ = [
|
| 9 |
+
"AnonymizationEngine",
|
| 10 |
+
"Entity",
|
| 11 |
+
"ReleaseAuthority",
|
| 12 |
+
"ReleaseCandidate",
|
| 13 |
+
"LegacyReleaseCandidate",
|
| 14 |
+
"parse_candidate",
|
| 15 |
+
]
|
ocr/anonymization/__main__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trusted local workflow: OCRResult JSON -> candidate JSON. No release shortcut."""
|
| 2 |
+
|
| 3 |
+
import argparse
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
|
| 6 |
+
from ocr.handoff import anonymization_handoff
|
| 7 |
+
from ocr.schemas import OCRResult, SafeError
|
| 8 |
+
|
| 9 |
+
from .atomic_output import write_candidate
|
| 10 |
+
from .engine import AnonymizationEngine
|
| 11 |
+
from .recognizer import GLiNERRecognizer
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def main():
|
| 15 |
+
parser = argparse.ArgumentParser(
|
| 16 |
+
description="Local OCR anonymization candidate (review required)"
|
| 17 |
+
)
|
| 18 |
+
parser.add_argument("--model-cache", required=True)
|
| 19 |
+
parser.add_argument(
|
| 20 |
+
"--input", required=True, help="Classified OCRResult JSON; authorized local filesystem only"
|
| 21 |
+
)
|
| 22 |
+
parser.add_argument(
|
| 23 |
+
"--output", required=True, help="New candidate JSON file (still classified)"
|
| 24 |
+
)
|
| 25 |
+
args = parser.parse_args()
|
| 26 |
+
try:
|
| 27 |
+
path = Path(args.input)
|
| 28 |
+
if path.stat().st_size > 8 * 1024 * 1024:
|
| 29 |
+
raise SafeError("anonymization_input_limit")
|
| 30 |
+
evidence = OCRResult.model_validate_json(path.read_bytes())
|
| 31 |
+
model = GLiNERRecognizer(args.model_cache)
|
| 32 |
+
candidate = anonymization_handoff(evidence, AnonymizationEngine(model))
|
| 33 |
+
write_candidate(args.output, (candidate.model_dump_json(indent=2) + "\n").encode())
|
| 34 |
+
except Exception:
|
| 35 |
+
raise SystemExit("anonymization_candidate_failed") from None
|
| 36 |
+
print("candidate_created_review_required")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
if __name__ == "__main__":
|
| 40 |
+
main()
|
ocr/anonymization/approval_ledger.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicitly provisioned, persistent single-host SQLite replay ledger."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import os
|
| 5 |
+
import sqlite3
|
| 6 |
+
import stat
|
| 7 |
+
import time
|
| 8 |
+
import uuid
|
| 9 |
+
from contextlib import closing
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from ocr.schemas import SafeError
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class SQLiteApprovalLedger:
|
| 16 |
+
"""Provision once with initialize=True; ordinary startup NEVER creates storage.
|
| 17 |
+
|
| 18 |
+
Persist ledger_id in private configuration and supply expected_id on restart.
|
| 19 |
+
Local ledgers must not share a signing key across hosts. A genuinely shared
|
| 20 |
+
durable backend is required for a shared key domain, not SQLite copies/NFS.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(self, path, *, initialize=False, expected_id=None):
|
| 24 |
+
self.path = Path(path).absolute()
|
| 25 |
+
try:
|
| 26 |
+
if initialize:
|
| 27 |
+
fd = os.open(self.path, os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW, 0o600)
|
| 28 |
+
os.close(fd)
|
| 29 |
+
self._metadata()
|
| 30 |
+
with closing(self._connect()) as connection, connection:
|
| 31 |
+
if initialize:
|
| 32 |
+
connection.execute("PRAGMA synchronous=FULL")
|
| 33 |
+
connection.execute(
|
| 34 |
+
"CREATE TABLE consumed (id TEXT PRIMARY KEY, expires INTEGER NOT NULL)"
|
| 35 |
+
)
|
| 36 |
+
connection.execute("CREATE TABLE identity (id TEXT NOT NULL)")
|
| 37 |
+
connection.execute("INSERT INTO identity VALUES (?)", (uuid.uuid4().hex,))
|
| 38 |
+
if connection.execute("PRAGMA quick_check").fetchone() != ("ok",):
|
| 39 |
+
raise ValueError()
|
| 40 |
+
self.ledger_id = connection.execute("SELECT id FROM identity").fetchone()[0]
|
| 41 |
+
if expected_id is not None and expected_id != self.ledger_id:
|
| 42 |
+
raise ValueError()
|
| 43 |
+
metadata = self.path.lstat()
|
| 44 |
+
self._inode = (metadata.st_dev, metadata.st_ino)
|
| 45 |
+
except Exception:
|
| 46 |
+
raise SafeError("approval_ledger_unavailable") from None
|
| 47 |
+
|
| 48 |
+
def _metadata(self):
|
| 49 |
+
metadata, parent = self.path.lstat(), self.path.parent.lstat()
|
| 50 |
+
if (
|
| 51 |
+
not stat.S_ISREG(metadata.st_mode)
|
| 52 |
+
or metadata.st_uid != os.getuid()
|
| 53 |
+
or metadata.st_mode & 0o077
|
| 54 |
+
or not stat.S_ISDIR(parent.st_mode)
|
| 55 |
+
or parent.st_uid != os.getuid()
|
| 56 |
+
or parent.st_mode & 0o077
|
| 57 |
+
):
|
| 58 |
+
raise ValueError()
|
| 59 |
+
return metadata
|
| 60 |
+
|
| 61 |
+
def _connect(self):
|
| 62 |
+
return sqlite3.connect(self.path.as_uri() + "?mode=rw", uri=True, timeout=5)
|
| 63 |
+
|
| 64 |
+
def consume(self, approval_id, expires_at):
|
| 65 |
+
try:
|
| 66 |
+
now = int(time.time())
|
| 67 |
+
if type(expires_at) is not int or expires_at <= now:
|
| 68 |
+
return False
|
| 69 |
+
metadata = self._metadata()
|
| 70 |
+
if (metadata.st_dev, metadata.st_ino) != self._inode:
|
| 71 |
+
raise ValueError()
|
| 72 |
+
with closing(self._connect()) as connection, connection:
|
| 73 |
+
connection.execute("PRAGMA synchronous=FULL")
|
| 74 |
+
connection.execute("BEGIN IMMEDIATE")
|
| 75 |
+
# Lock acquisition may have waited beyond approval expiration.
|
| 76 |
+
# Recheck at the consumption transaction, not only before waiting.
|
| 77 |
+
if expires_at <= int(time.time()):
|
| 78 |
+
return False
|
| 79 |
+
if connection.execute("SELECT id FROM identity").fetchone() != (self.ledger_id,):
|
| 80 |
+
raise ValueError()
|
| 81 |
+
# Retain consumed IDs: clock rollback must not resurrect approvals
|
| 82 |
+
# after expiry-based garbage collection. Offline archive is operator work.
|
| 83 |
+
connection.execute("INSERT INTO consumed VALUES (?, ?)", (approval_id, expires_at))
|
| 84 |
+
return True
|
| 85 |
+
except sqlite3.IntegrityError:
|
| 86 |
+
return False
|
| 87 |
+
except Exception:
|
| 88 |
+
raise SafeError("approval_ledger_unavailable") from None
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def validate_key_topology(bindings, *, signing_keys):
|
| 92 |
+
"""Compare actual key fingerprints, never caller-chosen key aliases.
|
| 93 |
+
|
| 94 |
+
This implementation qualifies local per-host key domains only. A shared
|
| 95 |
+
backend declaration is NOT evidence of shared durability; that integration
|
| 96 |
+
remains blocked until a real backend is implemented and qualified separately.
|
| 97 |
+
"""
|
| 98 |
+
try:
|
| 99 |
+
groups = {}
|
| 100 |
+
for entry in bindings:
|
| 101 |
+
key = signing_keys[entry["key_id"]]
|
| 102 |
+
if not isinstance(key, bytes) or len(key) < 32:
|
| 103 |
+
raise ValueError()
|
| 104 |
+
fingerprint = hashlib.sha256(key).hexdigest()
|
| 105 |
+
groups.setdefault(fingerprint, set()).add(entry["host_id"])
|
| 106 |
+
if any(len(hosts) > 1 for hosts in groups.values()):
|
| 107 |
+
raise ValueError()
|
| 108 |
+
except Exception:
|
| 109 |
+
raise SafeError("shared_key_multi_host_backend_not_qualified") from None
|
ocr/anonymization/artifacts.json
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"model": "VAGOsolutions/SauerkrautLM-GLiNER",
|
| 3 |
+
"revision": "ba967ee030b7e679dd5422cb16fcd1b379da38e1",
|
| 4 |
+
"files": {
|
| 5 |
+
"gliner_config.json": "f0659e7440bff3dd8630c8c553c30e0244d3949b7e6fa6e02938fb4bb4e2e9da",
|
| 6 |
+
"pytorch_model.bin": "a4f25b51796426ca39bf5b59ea2d142902693b690ffc1458a792267e45e3c269",
|
| 7 |
+
"special_tokens_map.json": "6e3204c5a4004719c185007a745d1940471a6fb02521cfa0a00306dbb6bc5903",
|
| 8 |
+
"tokenizer.json": "4f65190517e4633347d84e0225b8c5c66a1a62321107ebc73c09d5cefa2b5cd5",
|
| 9 |
+
"tokenizer_config.json": "ae8ad41b7039b6f1121db8be5790eae14ca7226ec14110406d4068797f505705"
|
| 10 |
+
}
|
| 11 |
+
}
|
ocr/anonymization/atomic_output.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Publish a complete private file atomically, without overwriting any target."""
|
| 2 |
+
|
| 3 |
+
import fcntl
|
| 4 |
+
import os
|
| 5 |
+
import tempfile
|
| 6 |
+
from pathlib import Path
|
| 7 |
+
|
| 8 |
+
from ocr.schemas import SafeError
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
def write_candidate(path, data):
|
| 12 |
+
temporary, directory = None, None
|
| 13 |
+
try:
|
| 14 |
+
path = Path(path)
|
| 15 |
+
directory = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
| 16 |
+
fcntl.flock(directory, fcntl.LOCK_SH)
|
| 17 |
+
# Same-directory staging guarantees a same-filesystem atomic hard link.
|
| 18 |
+
fd, temporary = tempfile.mkstemp(prefix=".anonymization-", dir=path.parent)
|
| 19 |
+
with os.fdopen(fd, "wb") as stream:
|
| 20 |
+
if stream.write(data) != len(data):
|
| 21 |
+
raise OSError("incomplete_write")
|
| 22 |
+
stream.flush()
|
| 23 |
+
os.fsync(stream.fileno())
|
| 24 |
+
# link() fails if ANY target (including a symlink) already exists.
|
| 25 |
+
# Never use rename()/replace(), which can clobber another process's file.
|
| 26 |
+
os.link(temporary, path)
|
| 27 |
+
except Exception:
|
| 28 |
+
raise SafeError("anonymization_candidate_write_failed") from None
|
| 29 |
+
finally:
|
| 30 |
+
if temporary is not None:
|
| 31 |
+
try:
|
| 32 |
+
os.unlink(temporary)
|
| 33 |
+
except FileNotFoundError:
|
| 34 |
+
pass
|
| 35 |
+
except OSError:
|
| 36 |
+
# Never print classified bytes or paths on cleanup failure. A
|
| 37 |
+
# leftover staging file is private (0600), never an approved export.
|
| 38 |
+
pass
|
| 39 |
+
|
| 40 |
+
if directory is not None:
|
| 41 |
+
os.close(directory)
|
ocr/anonymization/candidate_io.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit candidate version dispatch. No rewriting, migration or approval copying."""
|
| 2 |
+
|
| 3 |
+
import json
|
| 4 |
+
|
| 5 |
+
from ocr.schemas import SafeError
|
| 6 |
+
|
| 7 |
+
from .schemas import CorrectedCandidate, LegacyReleaseCandidate, ReleaseCandidate
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
def parse_candidate(data):
|
| 11 |
+
try:
|
| 12 |
+
if not isinstance(data, (str, bytes)) or len(data) > 16 * 1024 * 1024:
|
| 13 |
+
raise ValueError()
|
| 14 |
+
version = json.loads(data)["schema_version"]
|
| 15 |
+
cls = {
|
| 16 |
+
"ocr-anonymization-candidate/v1": LegacyReleaseCandidate,
|
| 17 |
+
"ocr-anonymization-candidate/v2": ReleaseCandidate,
|
| 18 |
+
"ocr-anonymization-candidate/v3": CorrectedCandidate,
|
| 19 |
+
}[version]
|
| 20 |
+
return cls.model_validate_json(data)
|
| 21 |
+
except Exception:
|
| 22 |
+
raise SafeError("unsupported_or_invalid_candidate") from None
|
ocr/anonymization/compare03d.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Development-only fixed experiments, never a claim of independent holdout quality.
|
| 2 |
+
|
| 3 |
+
All input paths are fixed repository synthetic corpora. Per-case payload evidence
|
| 4 |
+
is intentionally synthetic. The three datasets are development data from Gate03d.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import hashlib
|
| 8 |
+
import json
|
| 9 |
+
import resource
|
| 10 |
+
import subprocess
|
| 11 |
+
import sys
|
| 12 |
+
import time
|
| 13 |
+
from pathlib import Path
|
| 14 |
+
|
| 15 |
+
from .atomic_output import write_candidate
|
| 16 |
+
from .detectors import detect_contacts as detect
|
| 17 |
+
from .evaluate import score_cases
|
| 18 |
+
from .investigate import Recorded
|
| 19 |
+
from .recognizer import DEFAULT_PROMPTS, GLiNERRecognizer
|
| 20 |
+
from .replacement import replace, resolve_overlaps, validate_entities
|
| 21 |
+
from .terminology import DEFAULT_TERMS, ReligiousTerminology
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def run(cache, output):
|
| 25 |
+
denied = []
|
| 26 |
+
|
| 27 |
+
def offline(event, args):
|
| 28 |
+
if event in ("socket.connect", "socket.bind", "socket.getaddrinfo"):
|
| 29 |
+
denied.append(event)
|
| 30 |
+
raise PermissionError("qualification_network_disabled")
|
| 31 |
+
|
| 32 |
+
sys.addaudithook(offline)
|
| 33 |
+
start = time.perf_counter()
|
| 34 |
+
model = GLiNERRecognizer(cache)
|
| 35 |
+
load = (time.perf_counter() - start) * 1000
|
| 36 |
+
model.recognize("Synthetic warmup text.")
|
| 37 |
+
root = Path(__file__).parent / "fixtures"
|
| 38 |
+
paths = {
|
| 39 |
+
"qualification": root / "qualification.json",
|
| 40 |
+
"validation": root / "validation.json",
|
| 41 |
+
"development03c": root / "holdout03c/cases.json",
|
| 42 |
+
}
|
| 43 |
+
suites = {n: json.loads(p.read_bytes()) for n, p in paths.items()}
|
| 44 |
+
terminology = ReligiousTerminology()
|
| 45 |
+
|
| 46 |
+
def hybrid(text):
|
| 47 |
+
return detect(text) + terminology.detect(text)
|
| 48 |
+
|
| 49 |
+
report = dict(
|
| 50 |
+
schema_version="gate03d-development-comparison/v1",
|
| 51 |
+
source_sha=subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
|
| 52 |
+
model_revision=model.revision,
|
| 53 |
+
corpus_sha256={n: hashlib.sha256(p.read_bytes()).hexdigest() for n, p in paths.items()},
|
| 54 |
+
terminology=list(DEFAULT_TERMS),
|
| 55 |
+
loading_ms=load,
|
| 56 |
+
experiments={},
|
| 57 |
+
automated_release_eligible=False,
|
| 58 |
+
)
|
| 59 |
+
baseline = {}
|
| 60 |
+
# Fixed before inference. No low-threshold sweep or fixture-specific patterns.
|
| 61 |
+
for name, prompts, detector in (
|
| 62 |
+
("control", DEFAULT_PROMPTS, detect),
|
| 63 |
+
(
|
| 64 |
+
"semantic_status_prompt",
|
| 65 |
+
DEFAULT_PROMPTS | {"RELIGION": "religion or religious non-affiliation"},
|
| 66 |
+
detect,
|
| 67 |
+
),
|
| 68 |
+
("hybrid_terminology", DEFAULT_PROMPTS, hybrid),
|
| 69 |
+
):
|
| 70 |
+
model.prompts = dict(prompts)
|
| 71 |
+
recorder = Recorded(model)
|
| 72 |
+
result = dict(prompts=model.prompts, threshold=model.threshold, suites={}, changes=[])
|
| 73 |
+
for suite, cases in suites.items():
|
| 74 |
+
result["suites"][suite] = score_cases(recorder, cases, detector=detector)
|
| 75 |
+
for case in cases:
|
| 76 |
+
text = case["text"]
|
| 77 |
+
raw = recorder.predictions[text]
|
| 78 |
+
combined = resolve_overlaps(validate_entities(text, raw + detector(text)))
|
| 79 |
+
output_text, _ = replace(text, combined)
|
| 80 |
+
predicted = {(p.start, p.end, p.label) for p in combined}
|
| 81 |
+
gold = {(p["start"], p["end"], p["label"]) for p in case["spans"]}
|
| 82 |
+
item = dict(
|
| 83 |
+
suite=suite,
|
| 84 |
+
case_id=case["id"],
|
| 85 |
+
false_positives=sorted(predicted - gold),
|
| 86 |
+
false_negatives=sorted(gold - predicted),
|
| 87 |
+
exact_match=output_text == case["expected"],
|
| 88 |
+
predictions=[p.model_dump() for p in combined],
|
| 89 |
+
)
|
| 90 |
+
key = (suite, case["id"])
|
| 91 |
+
if name == "control":
|
| 92 |
+
baseline[key] = item
|
| 93 |
+
if item["false_positives"] or item["false_negatives"]:
|
| 94 |
+
result["changes"].append(item)
|
| 95 |
+
elif item["predictions"] != baseline[key]["predictions"]:
|
| 96 |
+
result["changes"].append(dict(before=baseline[key], after=item))
|
| 97 |
+
report["experiments"][name] = result
|
| 98 |
+
report["peak_rss_mib"] = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
| 99 |
+
report["denied_network_calls"] = denied
|
| 100 |
+
write_candidate(output, (json.dumps(report, ensure_ascii=False, indent=2) + "\n").encode())
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
if __name__ == "__main__":
|
| 104 |
+
import argparse
|
| 105 |
+
|
| 106 |
+
parser = argparse.ArgumentParser(description="Compare fixed Gate03d development alternatives")
|
| 107 |
+
parser.add_argument("--cache", required=True)
|
| 108 |
+
parser.add_argument("--output", required=True)
|
| 109 |
+
args = parser.parse_args()
|
| 110 |
+
run(args.cache, args.output)
|
| 111 |
+
print("comparison_recorded_check_metrics_before_selection")
|
ocr/anonymization/corrections.py
ADDED
|
@@ -0,0 +1,754 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Source-bound, authorized human corrections. No recognition optimization/export.
|
| 2 |
+
|
| 3 |
+
Machine snapshots, proposals and published effective states are immutable private
|
| 4 |
+
records. The host must implement durable atomic CAS publication and current ACLs;
|
| 5 |
+
no production identity/store is supplied. Candidate metadata contains hashes only.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import hashlib
|
| 9 |
+
import json
|
| 10 |
+
import re
|
| 11 |
+
import time
|
| 12 |
+
import uuid
|
| 13 |
+
from typing import Literal, Protocol
|
| 14 |
+
|
| 15 |
+
from pydantic import Field, model_validator
|
| 16 |
+
|
| 17 |
+
from ocr.schemas import OCRResult, SafeError
|
| 18 |
+
|
| 19 |
+
from .private_review import (
|
| 20 |
+
PrivateReviewBridge,
|
| 21 |
+
PrivateReviewHost,
|
| 22 |
+
PrivateReviewMaterial,
|
| 23 |
+
ReviewBinding,
|
| 24 |
+
SourcePageReference,
|
| 25 |
+
)
|
| 26 |
+
from .provenance import provenance
|
| 27 |
+
from .release_policy import digest
|
| 28 |
+
from .replacement import replace, validate_entities
|
| 29 |
+
from .schemas import (
|
| 30 |
+
LABELS,
|
| 31 |
+
CandidatePage,
|
| 32 |
+
CorrectedCandidate,
|
| 33 |
+
CorrectionProvenance,
|
| 34 |
+
Entity,
|
| 35 |
+
Label,
|
| 36 |
+
RecognitionProvenance,
|
| 37 |
+
ReleaseCandidate,
|
| 38 |
+
StrictModel,
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
POLICY = "reviewed-corrections-v1"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def commitment(value):
|
| 45 |
+
if isinstance(value, StrictModel):
|
| 46 |
+
value = value.model_dump(mode="json")
|
| 47 |
+
return hashlib.sha256(
|
| 48 |
+
json.dumps(
|
| 49 |
+
value, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False
|
| 50 |
+
).encode()
|
| 51 |
+
).hexdigest()
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
def author_digest(subject):
|
| 55 |
+
return commitment(dict(domain="private-correction-author/v1", subject=subject))
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class Observation(StrictModel):
|
| 59 |
+
observation_id: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 60 |
+
page_index: int = Field(ge=0)
|
| 61 |
+
entity: Entity = Field(repr=False)
|
| 62 |
+
human_correction_id: str | None = Field(default=None, pattern=r"^[a-f0-9]{32}$")
|
| 63 |
+
|
| 64 |
+
@model_validator(mode="after")
|
| 65 |
+
def explicit_human_provenance(self):
|
| 66 |
+
if self.human_correction_id is not None and (
|
| 67 |
+
self.entity.source != "deterministic"
|
| 68 |
+
or self.entity.model_revision != POLICY
|
| 69 |
+
or self.entity.score is not None
|
| 70 |
+
or self.entity.normalization != "identity"
|
| 71 |
+
):
|
| 72 |
+
raise ValueError("invalid_human_observation")
|
| 73 |
+
return self
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
class Placeholder(StrictModel):
|
| 77 |
+
category: Label
|
| 78 |
+
token: str = Field(pattern=r"^\[[A-Z_]{1,40}\]$")
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
class MachineSnapshot(StrictModel):
|
| 82 |
+
schema_version: Literal["private-machine-snapshot/v1"] = "private-machine-snapshot/v1"
|
| 83 |
+
evidence_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 84 |
+
sources: tuple[SourcePageReference, ...] = Field(min_length=1, max_length=100, repr=False)
|
| 85 |
+
observations: tuple[Observation, ...] = Field(max_length=100_000, repr=False)
|
| 86 |
+
provenance: RecognitionProvenance
|
| 87 |
+
placeholders: tuple[Placeholder, ...]
|
| 88 |
+
page_sources: tuple[Literal["embedded_text", "cpu_ocr", "alias_ocr"], ...]
|
| 89 |
+
machine_candidate: ReleaseCandidate | None = Field(repr=False)
|
| 90 |
+
|
| 91 |
+
@model_validator(mode="after")
|
| 92 |
+
def machine_only(self):
|
| 93 |
+
if (
|
| 94 |
+
any(o.human_correction_id is not None for o in self.observations)
|
| 95 |
+
or {p.category for p in self.placeholders} != set(LABELS)
|
| 96 |
+
or len(self.placeholders) != len(LABELS)
|
| 97 |
+
or len(self.sources) != len(self.page_sources)
|
| 98 |
+
or len({s.page_index for s in self.sources}) != len(self.sources)
|
| 99 |
+
or sum(s.characters for s in self.sources) > 2_000_000
|
| 100 |
+
or len({s.sensitivity for s in self.sources}) != 1
|
| 101 |
+
or (
|
| 102 |
+
self.machine_candidate is not None
|
| 103 |
+
and type(self.machine_candidate) is not ReleaseCandidate
|
| 104 |
+
)
|
| 105 |
+
):
|
| 106 |
+
raise ValueError("invalid_machine_snapshot")
|
| 107 |
+
return self
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class CorrectionContext(StrictModel):
|
| 111 |
+
schema_version: Literal["private-correction-context/v1"] = "private-correction-context/v1"
|
| 112 |
+
material: PrivateReviewMaterial = Field(repr=False)
|
| 113 |
+
machine: MachineSnapshot = Field(repr=False)
|
| 114 |
+
effective: tuple[Observation, ...] = Field(max_length=100_000, repr=False)
|
| 115 |
+
|
| 116 |
+
@model_validator(mode="after")
|
| 117 |
+
def root_binding(self):
|
| 118 |
+
if (
|
| 119 |
+
self.material.evidence_digest != self.machine.evidence_digest
|
| 120 |
+
or self.material.sources != self.machine.sources
|
| 121 |
+
or self.material.provenance != self.machine.provenance
|
| 122 |
+
):
|
| 123 |
+
raise ValueError("correction_context_mismatch")
|
| 124 |
+
candidate = self.material.candidate
|
| 125 |
+
if type(candidate) is CorrectedCandidate:
|
| 126 |
+
c = candidate.correction
|
| 127 |
+
if (
|
| 128 |
+
c.machine_snapshot_digest != commitment(self.machine)
|
| 129 |
+
or c.original_evidence_digest != self.machine.evidence_digest
|
| 130 |
+
or c.source_references_digest
|
| 131 |
+
!= commitment([s.model_dump(mode="json") for s in self.machine.sources])
|
| 132 |
+
or c.effective_state_digest != state_digest(self.machine, self.effective)
|
| 133 |
+
):
|
| 134 |
+
raise ValueError("correction_context_mismatch")
|
| 135 |
+
elif (
|
| 136 |
+
self.effective != self.machine.observations
|
| 137 |
+
or candidate != self.machine.machine_candidate
|
| 138 |
+
):
|
| 139 |
+
raise ValueError("correction_context_mismatch")
|
| 140 |
+
if candidate is None and self.material.blocking_reason != "conflicting_spans":
|
| 141 |
+
raise ValueError("unsupported_failed_correction_context")
|
| 142 |
+
return self
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
def state_digest(machine, effective):
|
| 146 |
+
return commitment(
|
| 147 |
+
dict(
|
| 148 |
+
machine=commitment(machine), observations=[o.model_dump(mode="json") for o in effective]
|
| 149 |
+
)
|
| 150 |
+
)
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
class SpanBinding(StrictModel):
|
| 154 |
+
page_index: int = Field(ge=0)
|
| 155 |
+
start: int = Field(ge=0)
|
| 156 |
+
end: int = Field(gt=0)
|
| 157 |
+
source_span_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 158 |
+
|
| 159 |
+
|
| 160 |
+
class CorrectionEdit(StrictModel):
|
| 161 |
+
span: SpanBinding
|
| 162 |
+
category: Label
|
| 163 |
+
action: Literal["mask", "suppress"]
|
| 164 |
+
rationale: Literal[
|
| 165 |
+
"missing_entity", "incomplete_boundary", "wrong_category", "span_conflict", "false_positive"
|
| 166 |
+
]
|
| 167 |
+
target_observation_ids: tuple[str, ...] = Field(max_length=100_000)
|
| 168 |
+
|
| 169 |
+
@model_validator(mode="after")
|
| 170 |
+
def valid_action(self):
|
| 171 |
+
if (
|
| 172 |
+
any(not re.fullmatch(r"[a-f0-9]{64}", s) for s in self.target_observation_ids)
|
| 173 |
+
or len(set(self.target_observation_ids)) != len(self.target_observation_ids)
|
| 174 |
+
or (self.action == "suppress") != (self.rationale == "false_positive")
|
| 175 |
+
or (not self.target_observation_ids) != (self.rationale == "missing_entity")
|
| 176 |
+
):
|
| 177 |
+
raise ValueError("invalid_correction_action")
|
| 178 |
+
return self
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
class CorrectionProposal(StrictModel):
|
| 182 |
+
schema_version: Literal["private-ocr-correction/v1"] = "private-ocr-correction/v1"
|
| 183 |
+
policy_version: Literal["reviewed-corrections-v1"] = "reviewed-corrections-v1"
|
| 184 |
+
correction_id: str = Field(pattern=r"^[a-f0-9]{32}$")
|
| 185 |
+
review_ref: str = Field(pattern=r"^[a-zA-Z0-9_-]{1,128}$")
|
| 186 |
+
author_subject: str = Field(pattern=r"^[a-zA-Z0-9:_-]{1,128}$", repr=False)
|
| 187 |
+
evidence_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 188 |
+
context_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 189 |
+
machine_snapshot_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 190 |
+
prior_candidate_digest: str | None = Field(pattern=r"^[a-f0-9]{64}$")
|
| 191 |
+
provenance: RecognitionProvenance
|
| 192 |
+
edits: tuple[CorrectionEdit, ...] = Field(min_length=1, max_length=1000)
|
| 193 |
+
created_at: int
|
| 194 |
+
expires_at: int
|
| 195 |
+
|
| 196 |
+
@model_validator(mode="after")
|
| 197 |
+
def short_lived(self):
|
| 198 |
+
if not 0 <= self.created_at < self.expires_at <= self.created_at + 900:
|
| 199 |
+
raise ValueError("invalid_correction_expiration")
|
| 200 |
+
return self
|
| 201 |
+
|
| 202 |
+
|
| 203 |
+
class StoredProposal(StrictModel):
|
| 204 |
+
proposal: CorrectionProposal = Field(repr=False)
|
| 205 |
+
revoked: bool
|
| 206 |
+
published_candidate_digest: str | None = Field(pattern=r"^[a-f0-9]{64}$")
|
| 207 |
+
|
| 208 |
+
published_review_ref: str | None = Field(default=None, pattern=r"^[a-zA-Z0-9_-]{1,128}$")
|
| 209 |
+
|
| 210 |
+
@model_validator(mode="after")
|
| 211 |
+
def complete_publication_pointer(self):
|
| 212 |
+
if (self.published_review_ref is None) != (self.published_candidate_digest is None):
|
| 213 |
+
raise ValueError("invalid_publication_pointer")
|
| 214 |
+
return self
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
class PublicationReceipt(StrictModel):
|
| 218 |
+
correction_id: str = Field(pattern=r"^[a-f0-9]{32}$")
|
| 219 |
+
proposal_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 220 |
+
candidate_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 221 |
+
context_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 222 |
+
review_ref: str = Field(pattern=r"^[a-zA-Z0-9_-]{1,128}$")
|
| 223 |
+
automated_release_eligible: Literal[False] = False
|
| 224 |
+
release_approved: Literal[False] = False
|
| 225 |
+
|
| 226 |
+
|
| 227 |
+
class CorrectionReview(StrictModel):
|
| 228 |
+
context: CorrectionContext = Field(repr=False)
|
| 229 |
+
record: StoredProposal = Field(repr=False)
|
| 230 |
+
release_approved: Literal[False] = False
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
class CorrectionHost(PrivateReviewHost, Protocol):
|
| 234 |
+
"""Required host contract, NOT a durable storage implementation.
|
| 235 |
+
|
| 236 |
+
store/publish must durably commit reauthorization, compare-and-swap of the
|
| 237 |
+
active head, immutable records and single-use publication in one transaction.
|
| 238 |
+
An RLock fixture demonstrates only in-process semantics. Crash/restart and
|
| 239 |
+
multi-process durability remain unqualified until a real adapter is supplied.
|
| 240 |
+
"""
|
| 241 |
+
|
| 242 |
+
def load_correction_context(self, review_ref: str) -> CorrectionContext: ...
|
| 243 |
+
def correction_binding(self, correction_id: str) -> str: ...
|
| 244 |
+
def load_correction(self, correction_id: str) -> StoredProposal: ...
|
| 245 |
+
def store_correction(
|
| 246 |
+
self, credential: object, proposal: CorrectionProposal, expected_context: str
|
| 247 |
+
) -> bool: ...
|
| 248 |
+
def publish_correction(
|
| 249 |
+
self,
|
| 250 |
+
credential: object,
|
| 251 |
+
proposal: CorrectionProposal,
|
| 252 |
+
context: CorrectionContext,
|
| 253 |
+
expected_context: str,
|
| 254 |
+
) -> PublicationReceipt: ...
|
| 255 |
+
|
| 256 |
+
# Atomic operations must reauthorize and CAS the active context, retain all
|
| 257 |
+
# old snapshots/proposals and consume this correction once in DURABLE storage.
|
| 258 |
+
|
| 259 |
+
|
| 260 |
+
def span_binding(context, page_index, start, end, text):
|
| 261 |
+
source = next(s for s in context.machine.sources if s.page_index == page_index)
|
| 262 |
+
if (
|
| 263 |
+
type(start) is not int
|
| 264 |
+
or type(end) is not int
|
| 265 |
+
or not 0 <= start < end <= len(text)
|
| 266 |
+
or len(text) != source.characters
|
| 267 |
+
or hashlib.sha256(text.encode()).hexdigest() != source.sha256
|
| 268 |
+
):
|
| 269 |
+
raise ValueError()
|
| 270 |
+
value = commitment(
|
| 271 |
+
dict(
|
| 272 |
+
domain="classified-original-span/v1",
|
| 273 |
+
evidence=context.machine.evidence_digest,
|
| 274 |
+
page=source.model_dump(mode="json"),
|
| 275 |
+
start=start,
|
| 276 |
+
end=end,
|
| 277 |
+
text_sha256=hashlib.sha256(text[start:end].encode()).hexdigest(),
|
| 278 |
+
)
|
| 279 |
+
)
|
| 280 |
+
return SpanBinding(page_index=page_index, start=start, end=end, source_span_digest=value)
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
def render(machine, effective, texts):
|
| 284 |
+
pages = []
|
| 285 |
+
allowed_pages = {s.page_index for s in machine.sources}
|
| 286 |
+
if any(o.page_index not in allowed_pages for o in effective):
|
| 287 |
+
raise ValueError()
|
| 288 |
+
for source, page_source in zip(machine.sources, machine.page_sources, strict=True):
|
| 289 |
+
text = texts[source.page_index]
|
| 290 |
+
if (
|
| 291 |
+
len(text) != source.characters
|
| 292 |
+
or hashlib.sha256(text.encode()).hexdigest() != source.sha256
|
| 293 |
+
):
|
| 294 |
+
raise ValueError()
|
| 295 |
+
observations = [o for o in effective if o.page_index == source.page_index]
|
| 296 |
+
entities = validate_entities(text, [o.entity for o in observations])
|
| 297 |
+
output, tags = replace(text, entities, {p.category: p.token for p in machine.placeholders})
|
| 298 |
+
human = {
|
| 299 |
+
(o.entity.start, o.entity.end, o.entity.label)
|
| 300 |
+
for o in observations
|
| 301 |
+
if o.human_correction_id is not None
|
| 302 |
+
}
|
| 303 |
+
tags = tuple(
|
| 304 |
+
t.model_copy(update={"source": "human_correction"})
|
| 305 |
+
if (t.start, t.end, t.label) in human and t.model_revision == POLICY
|
| 306 |
+
else t
|
| 307 |
+
for t in tags
|
| 308 |
+
)
|
| 309 |
+
pages.append(
|
| 310 |
+
CandidatePage(
|
| 311 |
+
page_index=source.page_index,
|
| 312 |
+
anonymized_text=output,
|
| 313 |
+
tags=tags,
|
| 314 |
+
source=page_source,
|
| 315 |
+
original_sensitivity=source.sensitivity,
|
| 316 |
+
)
|
| 317 |
+
)
|
| 318 |
+
return tuple(pages)
|
| 319 |
+
|
| 320 |
+
|
| 321 |
+
def prepare_review(engine, result, *, review_ref, environment_id, expires_at):
|
| 322 |
+
"""Trusted capture. Runtime failure aborts; recoverable span conflicts are retained.
|
| 323 |
+
|
| 324 |
+
The supplied review/env references are server-operator registration inputs, not
|
| 325 |
+
public request fields. Return objects must be stored in an immutable private host.
|
| 326 |
+
"""
|
| 327 |
+
try:
|
| 328 |
+
snapshot = OCRResult.model_validate_json(result.model_dump_json())
|
| 329 |
+
engine.validate_document(snapshot)
|
| 330 |
+
original_digest = hashlib.sha256(snapshot.model_dump_json().encode()).hexdigest()
|
| 331 |
+
config = provenance(
|
| 332 |
+
engine.recognizer, engine.terminology, engine.status_policy, engine.placeholders
|
| 333 |
+
)
|
| 334 |
+
sources, observations, texts = [], [], {}
|
| 335 |
+
for page in snapshot.pages:
|
| 336 |
+
text = page.text
|
| 337 |
+
texts[page.page_index] = text
|
| 338 |
+
sources.append(
|
| 339 |
+
SourcePageReference(
|
| 340 |
+
evidence_ref=commitment(dict(evidence=original_digest, page=page.page_index)),
|
| 341 |
+
page_index=page.page_index,
|
| 342 |
+
sensitivity=snapshot.sensitivity,
|
| 343 |
+
sha256=hashlib.sha256(text.encode()).hexdigest(),
|
| 344 |
+
characters=len(text),
|
| 345 |
+
)
|
| 346 |
+
)
|
| 347 |
+
for entity in engine.observe(text):
|
| 348 |
+
payload = dict(
|
| 349 |
+
evidence=original_digest,
|
| 350 |
+
config=config.configuration_digest,
|
| 351 |
+
page=page.page_index,
|
| 352 |
+
entity=entity.model_dump(mode="json"),
|
| 353 |
+
)
|
| 354 |
+
observations.append(
|
| 355 |
+
Observation(
|
| 356 |
+
observation_id=commitment(payload),
|
| 357 |
+
page_index=page.page_index,
|
| 358 |
+
entity=entity,
|
| 359 |
+
)
|
| 360 |
+
)
|
| 361 |
+
if config != provenance(
|
| 362 |
+
engine.recognizer, engine.terminology, engine.status_policy, engine.placeholders
|
| 363 |
+
):
|
| 364 |
+
raise ValueError()
|
| 365 |
+
placeholders = (
|
| 366 |
+
engine.placeholders
|
| 367 |
+
if engine.placeholders is not None
|
| 368 |
+
else {k: f"[{k}]" for k in LABELS}
|
| 369 |
+
)
|
| 370 |
+
machine = MachineSnapshot(
|
| 371 |
+
evidence_digest=original_digest,
|
| 372 |
+
sources=tuple(sources),
|
| 373 |
+
observations=tuple(observations),
|
| 374 |
+
provenance=config,
|
| 375 |
+
placeholders=tuple(
|
| 376 |
+
Placeholder(category=k, token=v) for k, v in sorted(placeholders.items())
|
| 377 |
+
),
|
| 378 |
+
page_sources=tuple(p.source for p in snapshot.pages),
|
| 379 |
+
machine_candidate=None,
|
| 380 |
+
)
|
| 381 |
+
blocked = None
|
| 382 |
+
try:
|
| 383 |
+
pages = render(machine, machine.observations, texts)
|
| 384 |
+
reasons = ("recognition_incomplete_possible",) + (
|
| 385 |
+
("empty_prediction",)
|
| 386 |
+
if any(
|
| 387 |
+
not any(
|
| 388 |
+
o.page_index == source.page_index and o.entity.source == "multilingual_ner"
|
| 389 |
+
for o in observations
|
| 390 |
+
)
|
| 391 |
+
for source in sources
|
| 392 |
+
)
|
| 393 |
+
else ()
|
| 394 |
+
)
|
| 395 |
+
candidate = ReleaseCandidate(
|
| 396 |
+
candidate_id=uuid.uuid4().hex,
|
| 397 |
+
sensitivity=snapshot.sensitivity,
|
| 398 |
+
pages=pages,
|
| 399 |
+
model_revision=config.model_revision,
|
| 400 |
+
provenance=config,
|
| 401 |
+
review_reasons=reasons,
|
| 402 |
+
)
|
| 403 |
+
machine = machine.model_copy(update={"machine_candidate": candidate})
|
| 404 |
+
except SafeError as exc:
|
| 405 |
+
if exc.code != "ambiguous_entity_overlap":
|
| 406 |
+
raise
|
| 407 |
+
candidate, blocked = None, "conflicting_spans"
|
| 408 |
+
material = PrivateReviewMaterial(
|
| 409 |
+
binding=ReviewBinding(
|
| 410 |
+
review_ref=review_ref,
|
| 411 |
+
environment_id=environment_id,
|
| 412 |
+
sensitivity=snapshot.sensitivity,
|
| 413 |
+
expires_at=expires_at,
|
| 414 |
+
revoked=False,
|
| 415 |
+
),
|
| 416 |
+
evidence_digest=original_digest,
|
| 417 |
+
sources=tuple(sources),
|
| 418 |
+
candidate=candidate,
|
| 419 |
+
candidate_digest=digest(candidate) if candidate else None,
|
| 420 |
+
provenance=config,
|
| 421 |
+
blocking_reason=blocked,
|
| 422 |
+
)
|
| 423 |
+
return CorrectionContext(material=material, machine=machine, effective=machine.observations)
|
| 424 |
+
except Exception:
|
| 425 |
+
raise SafeError("correction_capture_failed") from None
|
| 426 |
+
|
| 427 |
+
|
| 428 |
+
def apply_edits(context, proposal, texts):
|
| 429 |
+
if (
|
| 430 |
+
proposal.context_digest != commitment(context)
|
| 431 |
+
or proposal.evidence_digest != context.machine.evidence_digest
|
| 432 |
+
or proposal.provenance != context.machine.provenance
|
| 433 |
+
or proposal.machine_snapshot_digest != commitment(context.machine)
|
| 434 |
+
or proposal.prior_candidate_digest != context.material.candidate_digest
|
| 435 |
+
or proposal.review_ref != context.material.binding.review_ref
|
| 436 |
+
):
|
| 437 |
+
raise ValueError()
|
| 438 |
+
effective = {o.observation_id: o for o in context.effective}
|
| 439 |
+
if any(effective[o.observation_id] != o for o in context.effective):
|
| 440 |
+
raise ValueError()
|
| 441 |
+
ordered = sorted(proposal.edits, key=lambda e: (e.span.page_index, e.span.start, e.span.end))
|
| 442 |
+
used = set()
|
| 443 |
+
previous = None
|
| 444 |
+
for index, edit in enumerate(ordered):
|
| 445 |
+
s = edit.span
|
| 446 |
+
if previous is not None and previous.page_index == s.page_index and s.start < previous.end:
|
| 447 |
+
raise SafeError("correction_conflict")
|
| 448 |
+
previous = s
|
| 449 |
+
if span_binding(context, s.page_index, s.start, s.end, texts[s.page_index]) != s:
|
| 450 |
+
raise ValueError()
|
| 451 |
+
targets = set(edit.target_observation_ids)
|
| 452 |
+
if used & targets or not targets <= set(effective):
|
| 453 |
+
raise SafeError("correction_conflict")
|
| 454 |
+
used |= targets
|
| 455 |
+
target_obs = [effective[k] for k in targets]
|
| 456 |
+
overlap = {
|
| 457 |
+
k
|
| 458 |
+
for k, o in effective.items()
|
| 459 |
+
if o.page_index == s.page_index and o.entity.start < s.end and s.start < o.entity.end
|
| 460 |
+
}
|
| 461 |
+
if edit.action == "mask":
|
| 462 |
+
if targets != overlap:
|
| 463 |
+
raise SafeError("correction_conflict")
|
| 464 |
+
if edit.rationale == "wrong_category" and any(
|
| 465 |
+
(o.entity.start, o.entity.end) != (s.start, s.end) for o in target_obs
|
| 466 |
+
):
|
| 467 |
+
raise ValueError()
|
| 468 |
+
elif any(
|
| 469 |
+
o.page_index != s.page_index
|
| 470 |
+
or o.entity.start != s.start
|
| 471 |
+
or o.entity.end != s.end
|
| 472 |
+
or o.entity.label != edit.category
|
| 473 |
+
for o in target_obs
|
| 474 |
+
):
|
| 475 |
+
raise ValueError()
|
| 476 |
+
for key in targets:
|
| 477 |
+
del effective[key]
|
| 478 |
+
if edit.action == "mask":
|
| 479 |
+
entity = Entity(
|
| 480 |
+
start=s.start,
|
| 481 |
+
end=s.end,
|
| 482 |
+
label=edit.category,
|
| 483 |
+
text=texts[s.page_index][s.start : s.end],
|
| 484 |
+
source="deterministic",
|
| 485 |
+
model_revision=POLICY,
|
| 486 |
+
)
|
| 487 |
+
key = commitment(
|
| 488 |
+
dict(
|
| 489 |
+
correction=proposal.correction_id, ordinal=index, span=s.model_dump(mode="json")
|
| 490 |
+
)
|
| 491 |
+
)
|
| 492 |
+
effective[key] = Observation(
|
| 493 |
+
observation_id=key,
|
| 494 |
+
page_index=s.page_index,
|
| 495 |
+
entity=entity,
|
| 496 |
+
human_correction_id=proposal.correction_id,
|
| 497 |
+
)
|
| 498 |
+
result = tuple(
|
| 499 |
+
sorted(
|
| 500 |
+
effective.values(),
|
| 501 |
+
key=lambda o: (o.page_index, o.entity.start, o.entity.end, o.observation_id),
|
| 502 |
+
)
|
| 503 |
+
)
|
| 504 |
+
render(
|
| 505 |
+
context.machine, result, texts
|
| 506 |
+
) # Unresolved machine conflicts still block the whole job.
|
| 507 |
+
return result
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
class CorrectionService:
|
| 511 |
+
def __init__(self, host: CorrectionHost):
|
| 512 |
+
self.host = host
|
| 513 |
+
self.review = PrivateReviewBridge(host)
|
| 514 |
+
|
| 515 |
+
def _authorize(self, credential, review_ref, scope):
|
| 516 |
+
binding = self.review._binding(credential, review_ref)
|
| 517 |
+
identity = self.review.authorization.identity(credential)
|
| 518 |
+
if (
|
| 519 |
+
self.host.current_acl(
|
| 520 |
+
identity,
|
| 521 |
+
"anonymization:correction:" + scope,
|
| 522 |
+
binding.sensitivity,
|
| 523 |
+
binding.environment_id,
|
| 524 |
+
)
|
| 525 |
+
is not True
|
| 526 |
+
):
|
| 527 |
+
raise ValueError()
|
| 528 |
+
return identity, binding
|
| 529 |
+
|
| 530 |
+
def _context(self, credential, review_ref, scope):
|
| 531 |
+
identity, binding = self._authorize(credential, review_ref, scope)
|
| 532 |
+
material = self.review.open(credential, review_ref)
|
| 533 |
+
context = CorrectionContext.model_validate(self.host.load_correction_context(review_ref))
|
| 534 |
+
if context.material != material or self._authorize(credential, review_ref, scope) != (
|
| 535 |
+
identity,
|
| 536 |
+
binding,
|
| 537 |
+
):
|
| 538 |
+
raise ValueError()
|
| 539 |
+
return identity, context
|
| 540 |
+
|
| 541 |
+
def inspect(self, credential, review_ref):
|
| 542 |
+
try:
|
| 543 |
+
return self._context(credential, review_ref, "inspect")[1]
|
| 544 |
+
except Exception:
|
| 545 |
+
raise SafeError("private_correction_denied") from None
|
| 546 |
+
|
| 547 |
+
def _texts(self, credential, context):
|
| 548 |
+
texts = {
|
| 549 |
+
s.page_index: self.review.source_page(
|
| 550 |
+
credential, context.material.binding.review_ref, s.page_index
|
| 551 |
+
).text
|
| 552 |
+
for s in context.machine.sources
|
| 553 |
+
}
|
| 554 |
+
# Validate every captured/effective source slice, including observations
|
| 555 |
+
# the proposed correction intends to supersede. No corrupt evidence repair.
|
| 556 |
+
for observation in context.machine.observations:
|
| 557 |
+
expected = commitment(
|
| 558 |
+
dict(
|
| 559 |
+
evidence=context.machine.evidence_digest,
|
| 560 |
+
config=context.machine.provenance.configuration_digest,
|
| 561 |
+
page=observation.page_index,
|
| 562 |
+
entity=observation.entity.model_dump(mode="json"),
|
| 563 |
+
)
|
| 564 |
+
)
|
| 565 |
+
if observation.observation_id != expected:
|
| 566 |
+
raise ValueError()
|
| 567 |
+
for observations in (context.machine.observations, context.effective):
|
| 568 |
+
for source in context.machine.sources:
|
| 569 |
+
validate_entities(
|
| 570 |
+
texts[source.page_index],
|
| 571 |
+
[o.entity for o in observations if o.page_index == source.page_index],
|
| 572 |
+
)
|
| 573 |
+
if any(o.page_index not in texts for o in observations):
|
| 574 |
+
raise ValueError()
|
| 575 |
+
try:
|
| 576 |
+
original = render(context.machine, context.machine.observations, texts)
|
| 577 |
+
except SafeError as exc:
|
| 578 |
+
if (
|
| 579 |
+
exc.code != "ambiguous_entity_overlap"
|
| 580 |
+
or context.machine.machine_candidate is not None
|
| 581 |
+
):
|
| 582 |
+
raise
|
| 583 |
+
else:
|
| 584 |
+
if (
|
| 585 |
+
context.machine.machine_candidate is None
|
| 586 |
+
or original != context.machine.machine_candidate.pages
|
| 587 |
+
):
|
| 588 |
+
raise ValueError()
|
| 589 |
+
candidate = context.material.candidate
|
| 590 |
+
if (
|
| 591 |
+
candidate is not None
|
| 592 |
+
and render(context.machine, context.effective, texts) != candidate.pages
|
| 593 |
+
):
|
| 594 |
+
raise ValueError()
|
| 595 |
+
return texts
|
| 596 |
+
|
| 597 |
+
def bind(self, credential, review_ref, page_index, start, end):
|
| 598 |
+
try:
|
| 599 |
+
_, context = self._context(credential, review_ref, "submit")
|
| 600 |
+
text = self.review.source_page(credential, review_ref, page_index).text
|
| 601 |
+
value = span_binding(context, page_index, start, end, text)
|
| 602 |
+
self._authorize(credential, review_ref, "submit")
|
| 603 |
+
return value
|
| 604 |
+
except Exception:
|
| 605 |
+
raise SafeError("private_correction_denied") from None
|
| 606 |
+
|
| 607 |
+
def propose(self, credential, review_ref, edits):
|
| 608 |
+
try:
|
| 609 |
+
identity, context = self._context(credential, review_ref, "submit")
|
| 610 |
+
if type(edits) not in (tuple, list) or not 1 <= len(edits) <= 1000:
|
| 611 |
+
raise ValueError()
|
| 612 |
+
edits = tuple(CorrectionEdit.model_validate(e) for e in edits)
|
| 613 |
+
now = int(time.time())
|
| 614 |
+
proposal = CorrectionProposal(
|
| 615 |
+
correction_id=uuid.uuid4().hex,
|
| 616 |
+
review_ref=review_ref,
|
| 617 |
+
author_subject=identity.subject,
|
| 618 |
+
evidence_digest=context.machine.evidence_digest,
|
| 619 |
+
context_digest=commitment(context),
|
| 620 |
+
machine_snapshot_digest=commitment(context.machine),
|
| 621 |
+
prior_candidate_digest=context.material.candidate_digest,
|
| 622 |
+
provenance=context.machine.provenance,
|
| 623 |
+
edits=edits,
|
| 624 |
+
created_at=now,
|
| 625 |
+
expires_at=min(now + 900, context.material.binding.expires_at),
|
| 626 |
+
)
|
| 627 |
+
texts = self._texts(credential, context)
|
| 628 |
+
apply_edits(context, proposal, texts)
|
| 629 |
+
if self._context(credential, review_ref, "submit") != (identity, context):
|
| 630 |
+
raise ValueError()
|
| 631 |
+
if self.host.store_correction(credential, proposal, commitment(context)) is not True:
|
| 632 |
+
raise ValueError()
|
| 633 |
+
stored = StoredProposal.model_validate(
|
| 634 |
+
self.host.load_correction(proposal.correction_id)
|
| 635 |
+
)
|
| 636 |
+
if (
|
| 637 |
+
stored.proposal != proposal
|
| 638 |
+
or stored.revoked
|
| 639 |
+
or stored.published_candidate_digest is not None
|
| 640 |
+
):
|
| 641 |
+
raise ValueError()
|
| 642 |
+
return proposal
|
| 643 |
+
except Exception:
|
| 644 |
+
raise SafeError("private_correction_denied") from None
|
| 645 |
+
|
| 646 |
+
def inspect_proposal(self, credential, correction_id):
|
| 647 |
+
try:
|
| 648 |
+
if type(correction_id) is not str or not re.fullmatch(r"[a-f0-9]{32}", correction_id):
|
| 649 |
+
raise ValueError()
|
| 650 |
+
# Authenticate first; the host must authorize this opaque-ID lookup
|
| 651 |
+
# against its record binding before returning any proposal data.
|
| 652 |
+
self.review.authorization.identity(credential)
|
| 653 |
+
reference = self.host.correction_binding(correction_id)
|
| 654 |
+
_, context = self._context(credential, reference, "inspect")
|
| 655 |
+
record = StoredProposal.model_validate(self.host.load_correction(correction_id))
|
| 656 |
+
if (
|
| 657 |
+
record.proposal.correction_id != correction_id
|
| 658 |
+
or record.proposal.review_ref != reference
|
| 659 |
+
):
|
| 660 |
+
raise ValueError()
|
| 661 |
+
self._authorize(credential, reference, "inspect")
|
| 662 |
+
return CorrectionReview(context=context, record=record)
|
| 663 |
+
except Exception:
|
| 664 |
+
raise SafeError("private_correction_denied") from None
|
| 665 |
+
|
| 666 |
+
def apply(self, credential, correction_id):
|
| 667 |
+
try:
|
| 668 |
+
view = self.inspect_proposal(credential, correction_id)
|
| 669 |
+
proposal = view.record.proposal
|
| 670 |
+
identity, context = self._context(credential, proposal.review_ref, "apply")
|
| 671 |
+
now = int(time.time())
|
| 672 |
+
if (
|
| 673 |
+
view.record.revoked
|
| 674 |
+
or view.record.published_candidate_digest is not None
|
| 675 |
+
or identity.subject != proposal.author_subject
|
| 676 |
+
or not proposal.created_at <= now < proposal.expires_at
|
| 677 |
+
):
|
| 678 |
+
raise ValueError()
|
| 679 |
+
texts = self._texts(credential, context)
|
| 680 |
+
effective = apply_edits(context, proposal, texts)
|
| 681 |
+
pages = render(context.machine, effective, texts)
|
| 682 |
+
previous = context.material.candidate
|
| 683 |
+
authors = (
|
| 684 |
+
previous.correction.author_subject_digests
|
| 685 |
+
if type(previous) is CorrectedCandidate
|
| 686 |
+
else ()
|
| 687 |
+
)
|
| 688 |
+
authors = tuple(sorted(set(authors + (author_digest(identity.subject),))))
|
| 689 |
+
correction = CorrectionProvenance(
|
| 690 |
+
correction_id=proposal.correction_id,
|
| 691 |
+
proposal_digest=commitment(proposal),
|
| 692 |
+
original_evidence_digest=context.machine.evidence_digest,
|
| 693 |
+
machine_snapshot_digest=commitment(context.machine),
|
| 694 |
+
source_references_digest=commitment(
|
| 695 |
+
[s.model_dump(mode="json") for s in context.machine.sources]
|
| 696 |
+
),
|
| 697 |
+
prior_candidate_digest=context.material.candidate_digest,
|
| 698 |
+
prior_state_digest=state_digest(context.machine, context.effective),
|
| 699 |
+
effective_state_digest=state_digest(context.machine, effective),
|
| 700 |
+
author_subject_digests=authors,
|
| 701 |
+
edit_count=len(proposal.edits),
|
| 702 |
+
)
|
| 703 |
+
candidate = CorrectedCandidate(
|
| 704 |
+
candidate_id=uuid.uuid4().hex,
|
| 705 |
+
sensitivity=context.material.binding.sensitivity,
|
| 706 |
+
pages=pages,
|
| 707 |
+
model_revision=context.machine.provenance.model_revision,
|
| 708 |
+
provenance=context.machine.provenance,
|
| 709 |
+
correction=correction,
|
| 710 |
+
review_reasons=("recognition_incomplete_possible",)
|
| 711 |
+
+ (
|
| 712 |
+
("empty_prediction",)
|
| 713 |
+
if any(
|
| 714 |
+
not any(
|
| 715 |
+
o.page_index == source.page_index
|
| 716 |
+
and o.entity.source == "multilingual_ner"
|
| 717 |
+
for o in context.machine.observations
|
| 718 |
+
)
|
| 719 |
+
for source in context.machine.sources
|
| 720 |
+
)
|
| 721 |
+
else ()
|
| 722 |
+
)
|
| 723 |
+
+ ("human_correction_requires_review",),
|
| 724 |
+
)
|
| 725 |
+
binding = context.material.binding.model_copy(
|
| 726 |
+
update={"review_ref": "correction-" + uuid.uuid4().hex}
|
| 727 |
+
)
|
| 728 |
+
material = PrivateReviewMaterial(
|
| 729 |
+
binding=binding,
|
| 730 |
+
evidence_digest=context.machine.evidence_digest,
|
| 731 |
+
sources=context.machine.sources,
|
| 732 |
+
candidate=candidate,
|
| 733 |
+
candidate_digest=digest(candidate),
|
| 734 |
+
provenance=context.machine.provenance,
|
| 735 |
+
blocking_reason=None,
|
| 736 |
+
)
|
| 737 |
+
new = CorrectionContext(material=material, machine=context.machine, effective=effective)
|
| 738 |
+
if self._context(credential, proposal.review_ref, "apply") != (identity, context):
|
| 739 |
+
raise ValueError()
|
| 740 |
+
expected = PublicationReceipt(
|
| 741 |
+
correction_id=proposal.correction_id,
|
| 742 |
+
proposal_digest=commitment(proposal),
|
| 743 |
+
candidate_digest=digest(candidate),
|
| 744 |
+
context_digest=commitment(new),
|
| 745 |
+
review_ref=binding.review_ref,
|
| 746 |
+
)
|
| 747 |
+
actual = PublicationReceipt.model_validate(
|
| 748 |
+
self.host.publish_correction(credential, proposal, new, commitment(context))
|
| 749 |
+
)
|
| 750 |
+
if actual != expected:
|
| 751 |
+
raise ValueError()
|
| 752 |
+
return actual
|
| 753 |
+
except Exception:
|
| 754 |
+
raise SafeError("private_correction_denied") from None
|
ocr/anonymization/demonstrate03f.py
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pinned-model utility demo on EXISTING synthetic development cases only.
|
| 2 |
+
|
| 3 |
+
The isolated host simulates a human choosing existing gold spans; these are not
|
| 4 |
+
model predictions or a new holdout. No completed release review is manufactured.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import argparse
|
| 8 |
+
import hashlib
|
| 9 |
+
import importlib.metadata
|
| 10 |
+
import json
|
| 11 |
+
import resource
|
| 12 |
+
import subprocess
|
| 13 |
+
import sys
|
| 14 |
+
import tempfile
|
| 15 |
+
import time
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
from ocr.schemas import SafeError
|
| 19 |
+
from ocr.tests.fixtures.correction_host import CorrectionHostFixture
|
| 20 |
+
from ocr.tests.test_anonymization_security import evidence
|
| 21 |
+
|
| 22 |
+
from .approval_ledger import SQLiteApprovalLedger
|
| 23 |
+
from .atomic_output import write_candidate
|
| 24 |
+
from .corrections import CorrectionEdit, CorrectionService, commitment, prepare_review
|
| 25 |
+
from .engine import AnonymizationEngine
|
| 26 |
+
from .host_adapter import HostAuthorizationBridge
|
| 27 |
+
from .recognizer import GLiNERRecognizer
|
| 28 |
+
from .release_policy import digest
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def coverage(candidate, case):
|
| 32 |
+
if candidate is None:
|
| 33 |
+
return None
|
| 34 |
+
tags = candidate.pages[0].tags
|
| 35 |
+
return all(
|
| 36 |
+
any(t.start <= i < t.end for t in tags)
|
| 37 |
+
for span in case["spans"]
|
| 38 |
+
for i in range(span["start"], span["end"])
|
| 39 |
+
if case["text"][i].isalnum()
|
| 40 |
+
)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def run(cache, output, *, correct_controls_only=False):
|
| 44 |
+
if Path(output).exists():
|
| 45 |
+
raise FileExistsError("correction_demo_exists")
|
| 46 |
+
denied = []
|
| 47 |
+
|
| 48 |
+
def offline(event, args):
|
| 49 |
+
if event in ("socket.connect", "socket.bind", "socket.getaddrinfo"):
|
| 50 |
+
denied.append(event)
|
| 51 |
+
raise PermissionError("correction_demo_network_disabled")
|
| 52 |
+
|
| 53 |
+
sys.addaudithook(offline)
|
| 54 |
+
source = Path(__file__).parent / "fixtures/holdout03d/cases.json"
|
| 55 |
+
cases = json.loads(source.read_bytes())
|
| 56 |
+
selected = {4, 5} if correct_controls_only else {1, 2, 3, 55, 63, 78, 125, 127, 137}
|
| 57 |
+
start = time.perf_counter()
|
| 58 |
+
model = GLiNERRecognizer(cache)
|
| 59 |
+
loading_ms = (time.perf_counter() - start) * 1000
|
| 60 |
+
rows = []
|
| 61 |
+
for case in cases:
|
| 62 |
+
number = int(case["id"].split("-")[-1])
|
| 63 |
+
if number not in selected:
|
| 64 |
+
continue
|
| 65 |
+
start = time.perf_counter()
|
| 66 |
+
context = prepare_review(
|
| 67 |
+
AnonymizationEngine(model),
|
| 68 |
+
evidence("D2", (case["text"],)),
|
| 69 |
+
review_ref=case["id"],
|
| 70 |
+
environment_id="synthetic-room",
|
| 71 |
+
expires_at=int(time.time()) + 600,
|
| 72 |
+
)
|
| 73 |
+
inference_ms = (time.perf_counter() - start) * 1000
|
| 74 |
+
host = CorrectionHostFixture(context, {0: case["text"]}, separate_approver=True)
|
| 75 |
+
service = CorrectionService(host)
|
| 76 |
+
candidate = context.material.candidate
|
| 77 |
+
before = coverage(candidate, case)
|
| 78 |
+
original = (
|
| 79 |
+
"blocked_conflicting_spans"
|
| 80 |
+
if candidate is None
|
| 81 |
+
else "complete_no_measured_leakage"
|
| 82 |
+
if before
|
| 83 |
+
else "candidate_partial_mask"
|
| 84 |
+
)
|
| 85 |
+
edits = []
|
| 86 |
+
if number not in (1, 2, 3, 4, 5):
|
| 87 |
+
for span in case["spans"]:
|
| 88 |
+
if not (number == 125 and span["label"] == "ORGANIZATION") and not (
|
| 89 |
+
number != 125 and span["label"] == "RELIGION"
|
| 90 |
+
):
|
| 91 |
+
continue
|
| 92 |
+
targets = tuple(
|
| 93 |
+
sorted(
|
| 94 |
+
{
|
| 95 |
+
o.observation_id
|
| 96 |
+
for o in context.effective
|
| 97 |
+
if o.page_index == 0
|
| 98 |
+
and o.entity.start < span["end"]
|
| 99 |
+
and span["start"] < o.entity.end
|
| 100 |
+
}
|
| 101 |
+
)
|
| 102 |
+
)
|
| 103 |
+
edits.append(
|
| 104 |
+
CorrectionEdit(
|
| 105 |
+
span=service.bind(
|
| 106 |
+
host.author_credential, case["id"], 0, span["start"], span["end"]
|
| 107 |
+
),
|
| 108 |
+
category=span["label"],
|
| 109 |
+
action="mask",
|
| 110 |
+
rationale="incomplete_boundary" if number == 125 else "span_conflict",
|
| 111 |
+
target_observation_ids=targets,
|
| 112 |
+
)
|
| 113 |
+
)
|
| 114 |
+
malicious_rejected = unauthorized_rejected = False
|
| 115 |
+
if edits:
|
| 116 |
+
forged = edits[0].model_copy(
|
| 117 |
+
update={"span": edits[0].span.model_copy(update={"source_span_digest": "f" * 64})}
|
| 118 |
+
)
|
| 119 |
+
try:
|
| 120 |
+
service.propose(host.author_credential, case["id"], (forged,))
|
| 121 |
+
except SafeError as exc:
|
| 122 |
+
malicious_rejected = exc.code == "private_correction_denied"
|
| 123 |
+
try:
|
| 124 |
+
service.propose(host.approver_credential, case["id"], tuple(edits))
|
| 125 |
+
except SafeError as exc:
|
| 126 |
+
unauthorized_rejected = exc.code == "private_correction_denied"
|
| 127 |
+
proposal = service.propose(host.author_credential, case["id"], tuple(edits))
|
| 128 |
+
receipt = service.apply(host.author_credential, proposal.correction_id)
|
| 129 |
+
candidate = host.candidates[receipt.candidate_digest]
|
| 130 |
+
assert host.contexts[case["id"]] == context
|
| 131 |
+
with tempfile.TemporaryDirectory() as directory:
|
| 132 |
+
authority = HostAuthorizationBridge(host).authority(
|
| 133 |
+
b"synthetic-demo-signing-key-not-production",
|
| 134 |
+
SQLiteApprovalLedger(Path(directory) / "approvals.sqlite", initialize=True),
|
| 135 |
+
("synthetic-private",),
|
| 136 |
+
)
|
| 137 |
+
blocked = False
|
| 138 |
+
try:
|
| 139 |
+
authority.export(
|
| 140 |
+
candidate, None, actor=host.author_credential, destination="synthetic-private"
|
| 141 |
+
)
|
| 142 |
+
except SafeError as exc:
|
| 143 |
+
blocked = exc.code == "release_not_approved"
|
| 144 |
+
rows.append(
|
| 145 |
+
dict(
|
| 146 |
+
case_id=case["id"],
|
| 147 |
+
original_outcome=original,
|
| 148 |
+
machine_candidate_digest=context.material.candidate_digest,
|
| 149 |
+
machine_observations_digest=commitment(context.machine),
|
| 150 |
+
source_canaries_covered_before=before,
|
| 151 |
+
proposed_edits=[e.model_dump(mode="json") for e in edits],
|
| 152 |
+
new_candidate_produced=bool(edits),
|
| 153 |
+
final_candidate_digest=digest(candidate),
|
| 154 |
+
source_canaries_covered_after=coverage(candidate, case),
|
| 155 |
+
document_exact_match_after=candidate.pages[0].anonymized_text == case["expected"],
|
| 156 |
+
final_sensitivity=candidate.sensitivity,
|
| 157 |
+
release_blocked=blocked,
|
| 158 |
+
completed_release_reviews=len(host.completed),
|
| 159 |
+
malformed_correction_rejected=malicious_rejected if edits else None,
|
| 160 |
+
unauthorized_correction_rejected=unauthorized_rejected if edits else None,
|
| 161 |
+
capture_inference_ms=inference_ms,
|
| 162 |
+
)
|
| 163 |
+
)
|
| 164 |
+
report = dict(
|
| 165 |
+
schema_version="gate03f-synthetic-reviewed-correction-demo/v1",
|
| 166 |
+
source_sha=subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
|
| 167 |
+
corpus_sha256=hashlib.sha256(source.read_bytes()).hexdigest(),
|
| 168 |
+
model_revision=model.revision,
|
| 169 |
+
prompts=model.prompts,
|
| 170 |
+
threshold=model.threshold,
|
| 171 |
+
config_digest=context.machine.provenance.configuration_digest,
|
| 172 |
+
actual_inference_calls=len(rows),
|
| 173 |
+
correct_controls_only=correct_controls_only,
|
| 174 |
+
loading_ms=loading_ms,
|
| 175 |
+
peak_rss_mib=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024,
|
| 176 |
+
runtime_versions={
|
| 177 |
+
k: importlib.metadata.version(k)
|
| 178 |
+
for k in ("gliner", "torch", "transformers", "numpy", "pydantic")
|
| 179 |
+
},
|
| 180 |
+
denied_network_calls=denied,
|
| 181 |
+
cases=rows,
|
| 182 |
+
correction_source="simulated authorized human using existing synthetic gold spans",
|
| 183 |
+
production_identity_qualified=False,
|
| 184 |
+
durable_correction_store_qualified=False,
|
| 185 |
+
independent_qualification=False,
|
| 186 |
+
model_qualification_passed=False,
|
| 187 |
+
corrected_outputs_count_as_model_true_positives=False,
|
| 188 |
+
automated_release_eligible=False,
|
| 189 |
+
)
|
| 190 |
+
write_candidate(output, (json.dumps(report, indent=2) + "\n").encode())
|
| 191 |
+
return report
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
def main():
|
| 195 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 196 |
+
parser.add_argument("--cache", required=True)
|
| 197 |
+
parser.add_argument("--output", required=True)
|
| 198 |
+
parser.add_argument("--correct-controls-only", action="store_true")
|
| 199 |
+
args = parser.parse_args()
|
| 200 |
+
try:
|
| 201 |
+
report = run(args.cache, args.output, correct_controls_only=args.correct_controls_only)
|
| 202 |
+
except Exception:
|
| 203 |
+
raise SystemExit("private_correction_demo_failed") from None
|
| 204 |
+
# Emit only aggregate results; source text and paths are never printed.
|
| 205 |
+
print(
|
| 206 |
+
json.dumps(
|
| 207 |
+
dict(
|
| 208 |
+
cases=len(report["cases"]),
|
| 209 |
+
manual_corrections=sum(c["new_candidate_produced"] for c in report["cases"]),
|
| 210 |
+
all_seeded_canaries_covered=all(
|
| 211 |
+
c["source_canaries_covered_after"] for c in report["cases"]
|
| 212 |
+
),
|
| 213 |
+
all_release_blocked=all(c["release_blocked"] for c in report["cases"]),
|
| 214 |
+
)
|
| 215 |
+
)
|
| 216 |
+
)
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
if __name__ == "__main__":
|
| 220 |
+
main()
|
ocr/anonymization/detectors.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Deterministic contact patterns, not an exhaustive identifier detector."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
from .schemas import Entity
|
| 6 |
+
from .status import StatusPolicy
|
| 7 |
+
from .terminology import ReligiousTerminology
|
| 8 |
+
|
| 9 |
+
DEFAULT_TERMINOLOGY = ReligiousTerminology()
|
| 10 |
+
|
| 11 |
+
EMAIL = re.compile(r"(?<![\w.+-])[\w.!#$%&'*+/=?^`{|}~-]+@[\w-]+(?:\.[\w-]+)+(?![\w-])")
|
| 12 |
+
PHONE = re.compile(
|
| 13 |
+
r"(?<!\w)(?:\+\d{1,3}[ .-]?)?(?:\(\d{2,5}\)[ .-]?)?\d{2,8}(?:[ ./-]\d{2,10}){1,4}(?!\w)"
|
| 14 |
+
)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def detect_contacts(text):
|
| 18 |
+
found = []
|
| 19 |
+
for label, pattern in (("EMAIL", EMAIL), ("PHONE", PHONE)):
|
| 20 |
+
for match in pattern.finditer(text):
|
| 21 |
+
if label == "PHONE" and not 7 <= sum(c.isdigit() for c in match[0]) <= 15:
|
| 22 |
+
continue
|
| 23 |
+
found.append(
|
| 24 |
+
Entity(
|
| 25 |
+
start=match.start(),
|
| 26 |
+
end=match.end(),
|
| 27 |
+
label=label,
|
| 28 |
+
text=match[0],
|
| 29 |
+
source="deterministic",
|
| 30 |
+
model_revision="contact-patterns-v1",
|
| 31 |
+
)
|
| 32 |
+
)
|
| 33 |
+
return found
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
def detect(text, *, terminology=DEFAULT_TERMINOLOGY, status=StatusPolicy()):
|
| 37 |
+
"""Protect literal religious/status terms as well as contact identifiers."""
|
| 38 |
+
return detect_contacts(text) + terminology.detect(text) + status.detect(text)
|
ocr/anonymization/develop03e.py
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""One pinned inference pass, controlled replay of existing development evidence.
|
| 2 |
+
|
| 3 |
+
Synthetic-only, fixed fixture paths. No fresh holdout or release qualification.
|
| 4 |
+
Model timing is recorded independently from replay/deterministic evaluation time.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import hashlib
|
| 8 |
+
import importlib.metadata
|
| 9 |
+
import json
|
| 10 |
+
import resource
|
| 11 |
+
import statistics
|
| 12 |
+
import subprocess
|
| 13 |
+
import sys
|
| 14 |
+
import time
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
from .atomic_output import write_candidate
|
| 18 |
+
from .detectors import detect_contacts
|
| 19 |
+
from .outcomes import score_outcomes
|
| 20 |
+
from .recognizer import GLiNERRecognizer
|
| 21 |
+
from .status import StatusPolicy
|
| 22 |
+
from .terminology import ReligiousTerminology
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def run(cache, output):
|
| 26 |
+
if Path(output).exists():
|
| 27 |
+
raise FileExistsError("development_result_exists")
|
| 28 |
+
denied = []
|
| 29 |
+
|
| 30 |
+
def offline(event, args):
|
| 31 |
+
if event in ("socket.connect", "socket.bind", "socket.getaddrinfo"):
|
| 32 |
+
denied.append(event)
|
| 33 |
+
raise PermissionError("development_network_disabled")
|
| 34 |
+
|
| 35 |
+
sys.addaudithook(offline)
|
| 36 |
+
root = Path(__file__).parent / "fixtures"
|
| 37 |
+
paths = {
|
| 38 |
+
"qualification": root / "qualification.json",
|
| 39 |
+
"validation": root / "validation.json",
|
| 40 |
+
"development03c": root / "holdout03c/cases.json",
|
| 41 |
+
"development03d": root / "holdout03d/cases.json",
|
| 42 |
+
}
|
| 43 |
+
suites = {n: json.loads(p.read_bytes()) for n, p in paths.items()}
|
| 44 |
+
start = time.perf_counter()
|
| 45 |
+
model = GLiNERRecognizer(cache)
|
| 46 |
+
loading_ms = (time.perf_counter() - start) * 1000
|
| 47 |
+
model.recognize("An independent synthetic warmup sentence.")
|
| 48 |
+
predictions, inference = {}, {}
|
| 49 |
+
for cases in suites.values():
|
| 50 |
+
for case in cases:
|
| 51 |
+
text = case["text"]
|
| 52 |
+
if text in predictions:
|
| 53 |
+
continue
|
| 54 |
+
start = time.perf_counter()
|
| 55 |
+
predictions[text] = model.recognize(text)
|
| 56 |
+
inference[text] = (time.perf_counter() - start) * 1000
|
| 57 |
+
|
| 58 |
+
class Replay:
|
| 59 |
+
def recognize(self, text):
|
| 60 |
+
return predictions[text]
|
| 61 |
+
|
| 62 |
+
terms = ReligiousTerminology()
|
| 63 |
+
report = dict(
|
| 64 |
+
schema_version="gate03e-development-comparison/v1",
|
| 65 |
+
source_sha=subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
|
| 66 |
+
corpus_sha256={n: hashlib.sha256(p.read_bytes()).hexdigest() for n, p in paths.items()},
|
| 67 |
+
model_revision=model.revision,
|
| 68 |
+
prompts=model.prompts,
|
| 69 |
+
threshold=model.threshold,
|
| 70 |
+
loading_ms=loading_ms,
|
| 71 |
+
actual_inference_calls=len(predictions),
|
| 72 |
+
experiments={},
|
| 73 |
+
runtime_versions={
|
| 74 |
+
k: importlib.metadata.version(k)
|
| 75 |
+
for k in ("gliner", "torch", "transformers", "numpy", "pydantic")
|
| 76 |
+
},
|
| 77 |
+
actual_inference_latency_ms={
|
| 78 |
+
n: dict(
|
| 79 |
+
median=statistics.median(inference[c["text"]] for c in cases),
|
| 80 |
+
max=max(inference[c["text"]] for c in cases),
|
| 81 |
+
)
|
| 82 |
+
for n, cases in suites.items()
|
| 83 |
+
},
|
| 84 |
+
automated_release_eligible=False,
|
| 85 |
+
independent_qualification=False,
|
| 86 |
+
)
|
| 87 |
+
for name, status in (
|
| 88 |
+
("gate03d_literal_control", None),
|
| 89 |
+
("contextual_status", StatusPolicy(ocr_enabled=False)),
|
| 90 |
+
("status_scoped_ocr", StatusPolicy()),
|
| 91 |
+
):
|
| 92 |
+
|
| 93 |
+
def detector(text, status=status):
|
| 94 |
+
return (
|
| 95 |
+
detect_contacts(text)
|
| 96 |
+
+ terms.detect(text)
|
| 97 |
+
+ ([] if status is None else status.detect(text))
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
results = {
|
| 101 |
+
n: score_outcomes(Replay(), cases, detector=detector) for n, cases in suites.items()
|
| 102 |
+
}
|
| 103 |
+
for result in results.values():
|
| 104 |
+
result["accuracy"]["deterministic_replay_latency_ms"] = result["accuracy"].pop(
|
| 105 |
+
"warm_latency_ms"
|
| 106 |
+
)
|
| 107 |
+
report["experiments"][name] = results
|
| 108 |
+
baseline = report["experiments"]["gate03d_literal_control"]
|
| 109 |
+
report["selection_criteria"] = {}
|
| 110 |
+
for name, suites_result in report["experiments"].items():
|
| 111 |
+
if name == "gate03d_literal_control":
|
| 112 |
+
continue
|
| 113 |
+
okay, improvements = True, 0
|
| 114 |
+
for suite, result in suites_result.items():
|
| 115 |
+
before = baseline[suite]
|
| 116 |
+
okay &= result["candidate_return_rate"] >= before["candidate_return_rate"]
|
| 117 |
+
okay &= (
|
| 118 |
+
result["accuracy"]["document_exact_match_rate"]
|
| 119 |
+
>= before["accuracy"]["document_exact_match_rate"]
|
| 120 |
+
)
|
| 121 |
+
for metric in ("candidate_partial_canaries", "candidate_fully_unmasked_canaries"):
|
| 122 |
+
okay &= all(result[metric][k] <= before[metric][k] for k in result[metric])
|
| 123 |
+
improvements += sum(before[metric].values()) - sum(result[metric].values())
|
| 124 |
+
okay &= all(
|
| 125 |
+
result["accuracy"]["combined_entity_metrics"][k]["fp"] <= v["fp"]
|
| 126 |
+
for k, v in before["accuracy"]["combined_entity_metrics"].items()
|
| 127 |
+
)
|
| 128 |
+
report["selection_criteria"][name] = dict(
|
| 129 |
+
passed=bool(okay and improvements > 0), net_canary_improvements=improvements
|
| 130 |
+
)
|
| 131 |
+
report["peak_rss_mib"] = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
| 132 |
+
report["denied_network_calls"] = denied
|
| 133 |
+
write_candidate(output, (json.dumps(report, ensure_ascii=False, indent=2) + "\n").encode())
|
| 134 |
+
return report
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
if __name__ == "__main__":
|
| 138 |
+
import argparse
|
| 139 |
+
|
| 140 |
+
parser = argparse.ArgumentParser(description="Pinned development comparison, not a holdout")
|
| 141 |
+
parser.add_argument("--cache", required=True)
|
| 142 |
+
parser.add_argument("--output", required=True)
|
| 143 |
+
args = parser.parse_args()
|
| 144 |
+
result = run(args.cache, args.output)
|
| 145 |
+
print(json.dumps(result["selection_criteria"]))
|
ocr/anonymization/engine.py
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Trusted local recognition and all-or-nothing OCR handoff."""
|
| 2 |
+
|
| 3 |
+
import uuid
|
| 4 |
+
|
| 5 |
+
from ocr.schemas import OCRResult, SafeError
|
| 6 |
+
|
| 7 |
+
from .detectors import detect
|
| 8 |
+
from .model import MODEL_REVISION
|
| 9 |
+
from .provenance import provenance
|
| 10 |
+
from .replacement import replace, validate_entities
|
| 11 |
+
from .schemas import LABELS, CandidatePage, ReleaseCandidate
|
| 12 |
+
from .status import StatusPolicy
|
| 13 |
+
from .terminology import DEFAULT_TERMS, ReligiousTerminology
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class AnonymizationEngine:
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
recognizer,
|
| 20 |
+
*,
|
| 21 |
+
placeholders=None,
|
| 22 |
+
religion_terms=DEFAULT_TERMS,
|
| 23 |
+
status_policy=StatusPolicy(),
|
| 24 |
+
):
|
| 25 |
+
self.recognizer = recognizer
|
| 26 |
+
self.placeholders = placeholders
|
| 27 |
+
self.terminology = ReligiousTerminology(religion_terms)
|
| 28 |
+
if type(status_policy) is not StatusPolicy:
|
| 29 |
+
raise SafeError("invalid_status_configuration")
|
| 30 |
+
self.status_policy = status_policy
|
| 31 |
+
|
| 32 |
+
def observe(self, text):
|
| 33 |
+
try:
|
| 34 |
+
if set(self.recognizer.categories) != set(LABELS):
|
| 35 |
+
raise SafeError("unsupported_required_categories")
|
| 36 |
+
# Only local implementations are accepted. No remote recognizer transport exists.
|
| 37 |
+
if self.recognizer.local is not True:
|
| 38 |
+
raise SafeError("no_authorized_destination")
|
| 39 |
+
if self.recognizer.revision != MODEL_REVISION:
|
| 40 |
+
raise SafeError("anonymization_model_unavailable")
|
| 41 |
+
if not isinstance(text, str) or len(text) > 2_000_000:
|
| 42 |
+
raise SafeError("anonymization_text_limit")
|
| 43 |
+
predictions = self.recognizer.recognize(text)
|
| 44 |
+
if not isinstance(predictions, (list, tuple)):
|
| 45 |
+
raise SafeError("invalid_entity_spans")
|
| 46 |
+
predictions = validate_entities(text, predictions)
|
| 47 |
+
if any(
|
| 48 |
+
p.source != "multilingual_ner" or p.model_revision != MODEL_REVISION
|
| 49 |
+
for p in predictions
|
| 50 |
+
):
|
| 51 |
+
raise SafeError("invalid_entity_provenance")
|
| 52 |
+
return predictions + detect(
|
| 53 |
+
text, terminology=self.terminology, status=self.status_policy
|
| 54 |
+
)
|
| 55 |
+
except SafeError as exc:
|
| 56 |
+
allowed = {
|
| 57 |
+
"unsupported_required_categories",
|
| 58 |
+
"no_authorized_destination",
|
| 59 |
+
"invalid_entity_spans",
|
| 60 |
+
"invalid_entity_provenance",
|
| 61 |
+
"ambiguous_entity_overlap",
|
| 62 |
+
"anonymization_model_unavailable",
|
| 63 |
+
"anonymization_text_limit",
|
| 64 |
+
"anonymization_token_limit",
|
| 65 |
+
"anonymization_inference_failed",
|
| 66 |
+
}
|
| 67 |
+
raise SafeError(exc.code if exc.code in allowed else "anonymization_failed") from None
|
| 68 |
+
except Exception:
|
| 69 |
+
raise SafeError("anonymization_failed") from None
|
| 70 |
+
|
| 71 |
+
def transform(self, text):
|
| 72 |
+
predictions = self.observe(text)
|
| 73 |
+
try:
|
| 74 |
+
output, tags = replace(text, predictions, self.placeholders)
|
| 75 |
+
return output, tags, not any(p.source == "multilingual_ner" for p in predictions)
|
| 76 |
+
except SafeError as exc:
|
| 77 |
+
if exc.code not in (
|
| 78 |
+
"unsupported_required_categories",
|
| 79 |
+
"invalid_entity_spans",
|
| 80 |
+
"ambiguous_entity_overlap",
|
| 81 |
+
):
|
| 82 |
+
raise SafeError("anonymization_failed") from None
|
| 83 |
+
raise
|
| 84 |
+
except Exception:
|
| 85 |
+
raise SafeError("anonymization_failed") from None
|
| 86 |
+
|
| 87 |
+
@staticmethod
|
| 88 |
+
def validate_document(result):
|
| 89 |
+
if not isinstance(result, OCRResult):
|
| 90 |
+
raise SafeError("invalid_ocr_evidence")
|
| 91 |
+
if (
|
| 92 |
+
result.status != "completed"
|
| 93 |
+
or result.failures
|
| 94 |
+
or not result.pages
|
| 95 |
+
or result.page_count != len(result.pages)
|
| 96 |
+
or result.page_count > 100
|
| 97 |
+
or sum(len(p.text) for p in result.pages) > 2_000_000
|
| 98 |
+
):
|
| 99 |
+
raise SafeError("incomplete_ocr_document")
|
| 100 |
+
indexes = [p.page_index for p in result.pages]
|
| 101 |
+
if indexes not in (
|
| 102 |
+
list(range(result.page_count)),
|
| 103 |
+
list(range(1, result.page_count + 1)),
|
| 104 |
+
):
|
| 105 |
+
raise SafeError("incomplete_ocr_document")
|
| 106 |
+
# Sensitivity mismatches are rejected, never silently downgraded.
|
| 107 |
+
if any(p.sensitivity != result.sensitivity for p in result.pages):
|
| 108 |
+
raise SafeError("classification_mismatch")
|
| 109 |
+
|
| 110 |
+
def handoff(self, result: OCRResult) -> ReleaseCandidate:
|
| 111 |
+
try:
|
| 112 |
+
self.validate_document(result)
|
| 113 |
+
commitment = provenance(
|
| 114 |
+
self.recognizer, self.terminology, self.status_policy, self.placeholders
|
| 115 |
+
)
|
| 116 |
+
pages, empty = [], False
|
| 117 |
+
for p in result.pages:
|
| 118 |
+
text, tags, no_predictions = self.transform(p.text)
|
| 119 |
+
empty |= no_predictions
|
| 120 |
+
pages.append(
|
| 121 |
+
CandidatePage(
|
| 122 |
+
page_index=p.page_index,
|
| 123 |
+
anonymized_text=text,
|
| 124 |
+
tags=tags,
|
| 125 |
+
source=p.source,
|
| 126 |
+
original_sensitivity=p.sensitivity,
|
| 127 |
+
)
|
| 128 |
+
)
|
| 129 |
+
if commitment != provenance(
|
| 130 |
+
self.recognizer, self.terminology, self.status_policy, self.placeholders
|
| 131 |
+
):
|
| 132 |
+
raise SafeError("recognition_configuration_changed")
|
| 133 |
+
reasons = ("recognition_incomplete_possible",) + (
|
| 134 |
+
("empty_prediction",) if empty else ()
|
| 135 |
+
)
|
| 136 |
+
# Rebuild exclusively from allowlisted fields; Markdown, blocks, alternatives,
|
| 137 |
+
# engine/provider strings, page IDs and job IDs stay with classified evidence.
|
| 138 |
+
return ReleaseCandidate(
|
| 139 |
+
candidate_id=uuid.uuid4().hex,
|
| 140 |
+
sensitivity=result.sensitivity,
|
| 141 |
+
pages=tuple(pages),
|
| 142 |
+
model_revision=self.recognizer.revision,
|
| 143 |
+
review_reasons=reasons,
|
| 144 |
+
provenance=commitment,
|
| 145 |
+
)
|
| 146 |
+
except SafeError:
|
| 147 |
+
raise
|
| 148 |
+
except Exception:
|
| 149 |
+
raise SafeError("anonymization_failed") from None
|
ocr/anonymization/evaluate.py
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Opt-in synthetic evaluation; records aggregate metrics, never document payloads."""
|
| 2 |
+
|
| 3 |
+
import importlib.metadata
|
| 4 |
+
import json
|
| 5 |
+
import resource
|
| 6 |
+
import statistics
|
| 7 |
+
import time
|
| 8 |
+
from collections import Counter
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
from ocr.schemas import SafeError
|
| 12 |
+
|
| 13 |
+
from .detectors import detect
|
| 14 |
+
from .model import MODEL_ID, MODEL_REVISION
|
| 15 |
+
from .replacement import replace, resolve_overlaps, validate_entities
|
| 16 |
+
from .schemas import LABELS
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def metrics(counts):
|
| 20 |
+
output = {}
|
| 21 |
+
for label in LABELS:
|
| 22 |
+
tp, fp, fn = (counts[(label, name)] for name in ("tp", "fp", "fn"))
|
| 23 |
+
p = tp / (tp + fp) if tp + fp else 0.0
|
| 24 |
+
r = tp / (tp + fn) if tp + fn else 0.0
|
| 25 |
+
output[label] = dict(
|
| 26 |
+
tp=tp, fp=fp, fn=fn, precision=p, recall=r, f1=2 * p * r / (p + r) if p + r else 0.0
|
| 27 |
+
)
|
| 28 |
+
return output
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
def score_cases(recognizer, cases, *, detector=detect, case_reports=None):
|
| 32 |
+
ner_counts, combined_counts, leaks, failures = Counter(), Counter(), Counter(), Counter()
|
| 33 |
+
literal_leaks = Counter()
|
| 34 |
+
partial_canaries, uncovered_canaries, coverage = Counter(), Counter(), Counter()
|
| 35 |
+
durations, exact, blocked = [], 0, 0
|
| 36 |
+
groups = {}
|
| 37 |
+
for case in cases:
|
| 38 |
+
text = case["text"]
|
| 39 |
+
gold = {(s["start"], s["end"], s["label"]) for s in case["spans"]}
|
| 40 |
+
for s in case["spans"]:
|
| 41 |
+
if text[s["start"] : s["end"]] != s["text"]:
|
| 42 |
+
raise SafeError("invalid_evaluation_fixture")
|
| 43 |
+
start = time.perf_counter()
|
| 44 |
+
actual, predicted, ner = None, set(), set()
|
| 45 |
+
raw, combined, error_code = [], [], None
|
| 46 |
+
try:
|
| 47 |
+
raw = validate_entities(text, recognizer.recognize(text))
|
| 48 |
+
ner = {(s.start, s.end, s.label) for s in raw}
|
| 49 |
+
combined = resolve_overlaps(validate_entities(text, raw + detector(text)))
|
| 50 |
+
predicted = {(s.start, s.end, s.label) for s in combined}
|
| 51 |
+
actual, _ = replace(text, combined)
|
| 52 |
+
except SafeError as e:
|
| 53 |
+
error_code = e.code
|
| 54 |
+
failures[e.code] += 1
|
| 55 |
+
blocked += 1
|
| 56 |
+
durations.append((time.perf_counter() - start) * 1000)
|
| 57 |
+
for predicted_set, counts in ((ner, ner_counts), (predicted, combined_counts)):
|
| 58 |
+
for _, _, label in predicted_set & gold:
|
| 59 |
+
counts[(label, "tp")] += 1
|
| 60 |
+
for _, _, label in predicted_set - gold:
|
| 61 |
+
counts[(label, "fp")] += 1
|
| 62 |
+
for _, _, label in gold - predicted_set:
|
| 63 |
+
counts[(label, "fn")] += 1
|
| 64 |
+
exact += actual == case["expected"]
|
| 65 |
+
for s in case["spans"]:
|
| 66 |
+
if actual is None or s["text"].casefold() in actual.casefold():
|
| 67 |
+
literal_leaks[s["label"]] += 1
|
| 68 |
+
sensitive_positions = [i for i in range(s["start"], s["end"]) if text[i].isalnum()]
|
| 69 |
+
covered = sum(
|
| 70 |
+
actual is not None and any(left <= i < right for left, right, _ in predicted)
|
| 71 |
+
for i in sensitive_positions
|
| 72 |
+
)
|
| 73 |
+
coverage[(s["label"], "total")] += len(sensitive_positions)
|
| 74 |
+
coverage[(s["label"], "covered")] += covered
|
| 75 |
+
if covered < len(sensitive_positions):
|
| 76 |
+
leaks[s["label"]] += 1
|
| 77 |
+
if covered:
|
| 78 |
+
partial_canaries[s["label"]] += 1
|
| 79 |
+
else:
|
| 80 |
+
uncovered_canaries[s["label"]] += 1
|
| 81 |
+
if case_reports is not None:
|
| 82 |
+
case_reports.append(
|
| 83 |
+
dict(
|
| 84 |
+
case_id=case["id"],
|
| 85 |
+
scenario=case.get("scenario"),
|
| 86 |
+
predictions=[s.model_dump() for s in raw],
|
| 87 |
+
combined_predictions=[s.model_dump() for s in combined],
|
| 88 |
+
false_positives=sorted(predicted - gold),
|
| 89 |
+
false_negatives=sorted(gold - predicted),
|
| 90 |
+
exact_match=actual == case["expected"],
|
| 91 |
+
error_code=error_code,
|
| 92 |
+
canaries=[
|
| 93 |
+
dict(
|
| 94 |
+
**s,
|
| 95 |
+
uncovered_positions=[
|
| 96 |
+
i
|
| 97 |
+
for i in range(s["start"], s["end"])
|
| 98 |
+
if text[i].isalnum()
|
| 99 |
+
and (
|
| 100 |
+
actual is None
|
| 101 |
+
or not any(left <= i < right for left, right, _ in predicted)
|
| 102 |
+
)
|
| 103 |
+
],
|
| 104 |
+
)
|
| 105 |
+
for s in case["spans"]
|
| 106 |
+
],
|
| 107 |
+
)
|
| 108 |
+
)
|
| 109 |
+
group = case.get("language", "unspecified")
|
| 110 |
+
stats = groups.setdefault(group, dict(documents=0, exact_matches=0, false_negatives=0))
|
| 111 |
+
stats["documents"] += 1
|
| 112 |
+
stats["exact_matches"] += actual == case["expected"]
|
| 113 |
+
stats["false_negatives"] += len(gold - predicted)
|
| 114 |
+
if case.get("ocr_noise"):
|
| 115 |
+
stats = groups.setdefault(
|
| 116 |
+
"ocr_noise", dict(documents=0, exact_matches=0, false_negatives=0)
|
| 117 |
+
)
|
| 118 |
+
stats["documents"] += 1
|
| 119 |
+
stats["exact_matches"] += actual == case["expected"]
|
| 120 |
+
stats["false_negatives"] += len(gold - predicted)
|
| 121 |
+
return dict(
|
| 122 |
+
documents=len(cases),
|
| 123 |
+
ner_entity_metrics=metrics(ner_counts),
|
| 124 |
+
combined_entity_metrics=metrics(combined_counts),
|
| 125 |
+
unredacted_canaries={k: leaks[k] for k in LABELS},
|
| 126 |
+
literal_unredacted_canaries={k: literal_leaks[k] for k in LABELS},
|
| 127 |
+
partial_canaries={k: partial_canaries[k] for k in LABELS},
|
| 128 |
+
fully_uncovered_canaries={k: uncovered_canaries[k] for k in LABELS},
|
| 129 |
+
alphanumeric_canary_coverage={
|
| 130 |
+
k: dict(covered=coverage[(k, "covered")], total=coverage[(k, "total")]) for k in LABELS
|
| 131 |
+
},
|
| 132 |
+
document_exact_match_rate=exact / len(cases),
|
| 133 |
+
groups=groups,
|
| 134 |
+
blocked_documents=blocked,
|
| 135 |
+
failure_codes=dict(failures),
|
| 136 |
+
warm_latency_ms=dict(median=statistics.median(durations), max=max(durations)),
|
| 137 |
+
automated_release_eligible=False,
|
| 138 |
+
qualification_passed=not any(leaks.values()) and blocked == 0,
|
| 139 |
+
)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
def run(cache, output):
|
| 143 |
+
from .recognizer import GLiNERRecognizer
|
| 144 |
+
|
| 145 |
+
start = time.perf_counter()
|
| 146 |
+
recognizer = GLiNERRecognizer(cache)
|
| 147 |
+
load = (time.perf_counter() - start) * 1000
|
| 148 |
+
# Explicit warmup is synthetic public text, independently reported.
|
| 149 |
+
recognizer.recognize("The cat sat on the mat.")
|
| 150 |
+
report = dict(
|
| 151 |
+
model=MODEL_ID,
|
| 152 |
+
revision=MODEL_REVISION,
|
| 153 |
+
threshold=recognizer.threshold,
|
| 154 |
+
prompts=recognizer.prompts,
|
| 155 |
+
runtime_versions={
|
| 156 |
+
k: importlib.metadata.version(k)
|
| 157 |
+
for k in ("gliner", "torch", "transformers", "numpy", "pydantic")
|
| 158 |
+
},
|
| 159 |
+
device="cpu",
|
| 160 |
+
threads=2,
|
| 161 |
+
loading_ms=load,
|
| 162 |
+
suites={},
|
| 163 |
+
)
|
| 164 |
+
root = Path(__file__).parent / "fixtures"
|
| 165 |
+
for name in ("qualification", "validation"):
|
| 166 |
+
report["suites"][name] = score_cases(
|
| 167 |
+
recognizer, json.loads((root / f"{name}.json").read_text())
|
| 168 |
+
)
|
| 169 |
+
report["peak_rss_mib"] = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
| 170 |
+
report["automated_release_eligible"] = False
|
| 171 |
+
report["recognition_qualification_passed"] = all(
|
| 172 |
+
s["qualification_passed"] for s in report["suites"].values()
|
| 173 |
+
)
|
| 174 |
+
Path(output).write_text(json.dumps(report, indent=2) + "\n")
|
| 175 |
+
return report
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
if __name__ == "__main__":
|
| 179 |
+
import argparse
|
| 180 |
+
|
| 181 |
+
parser = argparse.ArgumentParser(description="Pinned local synthetic model evaluation")
|
| 182 |
+
parser.add_argument("--cache", required=True)
|
| 183 |
+
parser.add_argument("--output", required=True)
|
| 184 |
+
args = parser.parse_args()
|
| 185 |
+
try:
|
| 186 |
+
report = run(args.cache, args.output)
|
| 187 |
+
except Exception:
|
| 188 |
+
raise SystemExit("anonymization_evaluation_failed") from None
|
| 189 |
+
print(
|
| 190 |
+
json.dumps(
|
| 191 |
+
{
|
| 192 |
+
"recognition_qualification_passed": report["recognition_qualification_passed"],
|
| 193 |
+
"automated_release_eligible": False,
|
| 194 |
+
}
|
| 195 |
+
)
|
| 196 |
+
)
|
ocr/anonymization/fixtures/holdout03c/cases.json
ADDED
|
@@ -0,0 +1,3854 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "holdout03c-000",
|
| 4 |
+
"language": "de",
|
| 5 |
+
"ocr_noise": false,
|
| 6 |
+
"features": [
|
| 7 |
+
"organization",
|
| 8 |
+
"geography"
|
| 9 |
+
],
|
| 10 |
+
"text": "Auskunft: Saskia Dornfeld arbeitet für Aster Research Institute in Osnabrück.",
|
| 11 |
+
"spans": [
|
| 12 |
+
{
|
| 13 |
+
"start": 10,
|
| 14 |
+
"end": 25,
|
| 15 |
+
"label": "PERSON",
|
| 16 |
+
"text": "Saskia Dornfeld"
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"start": 39,
|
| 20 |
+
"end": 63,
|
| 21 |
+
"label": "ORGANIZATION",
|
| 22 |
+
"text": "Aster Research Institute"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"start": 67,
|
| 26 |
+
"end": 76,
|
| 27 |
+
"label": "CITY",
|
| 28 |
+
"text": "Osnabrück"
|
| 29 |
+
}
|
| 30 |
+
],
|
| 31 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 32 |
+
},
|
| 33 |
+
{
|
| 34 |
+
"id": "holdout03c-001",
|
| 35 |
+
"language": "en",
|
| 36 |
+
"ocr_noise": false,
|
| 37 |
+
"features": [
|
| 38 |
+
"organization",
|
| 39 |
+
"geography"
|
| 40 |
+
],
|
| 41 |
+
"text": "The contact Saskia Dornfeld represents Aster Research Institute near Osnabrück.",
|
| 42 |
+
"spans": [
|
| 43 |
+
{
|
| 44 |
+
"start": 12,
|
| 45 |
+
"end": 27,
|
| 46 |
+
"label": "PERSON",
|
| 47 |
+
"text": "Saskia Dornfeld"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"start": 39,
|
| 51 |
+
"end": 63,
|
| 52 |
+
"label": "ORGANIZATION",
|
| 53 |
+
"text": "Aster Research Institute"
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"start": 69,
|
| 57 |
+
"end": 78,
|
| 58 |
+
"label": "CITY",
|
| 59 |
+
"text": "Osnabrück"
|
| 60 |
+
}
|
| 61 |
+
],
|
| 62 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 63 |
+
},
|
| 64 |
+
{
|
| 65 |
+
"id": "holdout03c-002",
|
| 66 |
+
"language": "de",
|
| 67 |
+
"ocr_noise": false,
|
| 68 |
+
"features": [
|
| 69 |
+
"affiliation",
|
| 70 |
+
"contacts"
|
| 71 |
+
],
|
| 72 |
+
"text": "Saskia Dornfeld nennt sikhistisch als Zugehörigkeit. E-Mail: case101.review@example.invalid; Telefon: +49 541 730101.",
|
| 73 |
+
"spans": [
|
| 74 |
+
{
|
| 75 |
+
"start": 0,
|
| 76 |
+
"end": 15,
|
| 77 |
+
"label": "PERSON",
|
| 78 |
+
"text": "Saskia Dornfeld"
|
| 79 |
+
},
|
| 80 |
+
{
|
| 81 |
+
"start": 22,
|
| 82 |
+
"end": 33,
|
| 83 |
+
"label": "RELIGION",
|
| 84 |
+
"text": "sikhistisch"
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
"start": 61,
|
| 88 |
+
"end": 91,
|
| 89 |
+
"label": "EMAIL",
|
| 90 |
+
"text": "case101.review@example.invalid"
|
| 91 |
+
},
|
| 92 |
+
{
|
| 93 |
+
"start": 102,
|
| 94 |
+
"end": 116,
|
| 95 |
+
"label": "PHONE",
|
| 96 |
+
"text": "+49 541 730101"
|
| 97 |
+
}
|
| 98 |
+
],
|
| 99 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 100 |
+
},
|
| 101 |
+
{
|
| 102 |
+
"id": "holdout03c-003",
|
| 103 |
+
"language": "en",
|
| 104 |
+
"ocr_noise": false,
|
| 105 |
+
"features": [
|
| 106 |
+
"affiliation",
|
| 107 |
+
"contacts"
|
| 108 |
+
],
|
| 109 |
+
"text": "Saskia Dornfeld explicitly names Sikhism as their faith. Write to case101.review@example.invalid or call +49 541 730101.",
|
| 110 |
+
"spans": [
|
| 111 |
+
{
|
| 112 |
+
"start": 0,
|
| 113 |
+
"end": 15,
|
| 114 |
+
"label": "PERSON",
|
| 115 |
+
"text": "Saskia Dornfeld"
|
| 116 |
+
},
|
| 117 |
+
{
|
| 118 |
+
"start": 33,
|
| 119 |
+
"end": 40,
|
| 120 |
+
"label": "RELIGION",
|
| 121 |
+
"text": "Sikhism"
|
| 122 |
+
},
|
| 123 |
+
{
|
| 124 |
+
"start": 66,
|
| 125 |
+
"end": 96,
|
| 126 |
+
"label": "EMAIL",
|
| 127 |
+
"text": "case101.review@example.invalid"
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"start": 105,
|
| 131 |
+
"end": 119,
|
| 132 |
+
"label": "PHONE",
|
| 133 |
+
"text": "+49 541 730101"
|
| 134 |
+
}
|
| 135 |
+
],
|
| 136 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 137 |
+
},
|
| 138 |
+
{
|
| 139 |
+
"id": "holdout03c-004",
|
| 140 |
+
"language": "de",
|
| 141 |
+
"ocr_noise": false,
|
| 142 |
+
"features": [
|
| 143 |
+
"repeats",
|
| 144 |
+
"paragraphs"
|
| 145 |
+
],
|
| 146 |
+
"text": "Saskia Dornfeld besucht Osnabrück.\n\nSaskia Dornfeld bestätigt den Termin bei Aster Research Institute.",
|
| 147 |
+
"spans": [
|
| 148 |
+
{
|
| 149 |
+
"start": 0,
|
| 150 |
+
"end": 15,
|
| 151 |
+
"label": "PERSON",
|
| 152 |
+
"text": "Saskia Dornfeld"
|
| 153 |
+
},
|
| 154 |
+
{
|
| 155 |
+
"start": 24,
|
| 156 |
+
"end": 33,
|
| 157 |
+
"label": "CITY",
|
| 158 |
+
"text": "Osnabrück"
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"start": 36,
|
| 162 |
+
"end": 51,
|
| 163 |
+
"label": "PERSON",
|
| 164 |
+
"text": "Saskia Dornfeld"
|
| 165 |
+
},
|
| 166 |
+
{
|
| 167 |
+
"start": 77,
|
| 168 |
+
"end": 101,
|
| 169 |
+
"label": "ORGANIZATION",
|
| 170 |
+
"text": "Aster Research Institute"
|
| 171 |
+
}
|
| 172 |
+
],
|
| 173 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 174 |
+
},
|
| 175 |
+
{
|
| 176 |
+
"id": "holdout03c-005",
|
| 177 |
+
"language": "en",
|
| 178 |
+
"ocr_noise": false,
|
| 179 |
+
"features": [
|
| 180 |
+
"repeats",
|
| 181 |
+
"ambiguous_geography"
|
| 182 |
+
],
|
| 183 |
+
"text": "In Osnabrück, Saskia Dornfeld signed for Aster Research Institute. The representative Saskia Dornfeld returned to Osnabrück.",
|
| 184 |
+
"spans": [
|
| 185 |
+
{
|
| 186 |
+
"start": 3,
|
| 187 |
+
"end": 12,
|
| 188 |
+
"label": "CITY",
|
| 189 |
+
"text": "Osnabrück"
|
| 190 |
+
},
|
| 191 |
+
{
|
| 192 |
+
"start": 14,
|
| 193 |
+
"end": 29,
|
| 194 |
+
"label": "PERSON",
|
| 195 |
+
"text": "Saskia Dornfeld"
|
| 196 |
+
},
|
| 197 |
+
{
|
| 198 |
+
"start": 41,
|
| 199 |
+
"end": 65,
|
| 200 |
+
"label": "ORGANIZATION",
|
| 201 |
+
"text": "Aster Research Institute"
|
| 202 |
+
},
|
| 203 |
+
{
|
| 204 |
+
"start": 86,
|
| 205 |
+
"end": 101,
|
| 206 |
+
"label": "PERSON",
|
| 207 |
+
"text": "Saskia Dornfeld"
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"start": 114,
|
| 211 |
+
"end": 123,
|
| 212 |
+
"label": "CITY",
|
| 213 |
+
"text": "Osnabrück"
|
| 214 |
+
}
|
| 215 |
+
],
|
| 216 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 217 |
+
},
|
| 218 |
+
{
|
| 219 |
+
"id": "holdout03c-006",
|
| 220 |
+
"language": "de",
|
| 221 |
+
"ocr_noise": true,
|
| 222 |
+
"features": [
|
| 223 |
+
"line_wrap",
|
| 224 |
+
"organization"
|
| 225 |
+
],
|
| 226 |
+
"text": "Name: Saskia-\nDornfeld\nOrt: Osna-\nbrück\nArbeitgeber: Aster Research Institute.",
|
| 227 |
+
"spans": [
|
| 228 |
+
{
|
| 229 |
+
"start": 6,
|
| 230 |
+
"end": 22,
|
| 231 |
+
"label": "PERSON",
|
| 232 |
+
"text": "Saskia-\nDornfeld"
|
| 233 |
+
},
|
| 234 |
+
{
|
| 235 |
+
"start": 28,
|
| 236 |
+
"end": 39,
|
| 237 |
+
"label": "CITY",
|
| 238 |
+
"text": "Osna-\nbrück"
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"start": 53,
|
| 242 |
+
"end": 77,
|
| 243 |
+
"label": "ORGANIZATION",
|
| 244 |
+
"text": "Aster Research Institute"
|
| 245 |
+
}
|
| 246 |
+
],
|
| 247 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"id": "holdout03c-007",
|
| 251 |
+
"language": "en",
|
| 252 |
+
"ocr_noise": true,
|
| 253 |
+
"features": [
|
| 254 |
+
"ocr_spelling",
|
| 255 |
+
"organization"
|
| 256 |
+
],
|
| 257 |
+
"text": "Scanned record: Sask1a D0rnfeld resides in Osn4brück and works at Aster Research Institute.",
|
| 258 |
+
"spans": [
|
| 259 |
+
{
|
| 260 |
+
"start": 16,
|
| 261 |
+
"end": 31,
|
| 262 |
+
"label": "PERSON",
|
| 263 |
+
"text": "Sask1a D0rnfeld"
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"start": 43,
|
| 267 |
+
"end": 52,
|
| 268 |
+
"label": "CITY",
|
| 269 |
+
"text": "Osn4brück"
|
| 270 |
+
},
|
| 271 |
+
{
|
| 272 |
+
"start": 66,
|
| 273 |
+
"end": 90,
|
| 274 |
+
"label": "ORGANIZATION",
|
| 275 |
+
"text": "Aster Research Institute"
|
| 276 |
+
}
|
| 277 |
+
],
|
| 278 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 279 |
+
},
|
| 280 |
+
{
|
| 281 |
+
"id": "holdout03c-008",
|
| 282 |
+
"language": "de",
|
| 283 |
+
"ocr_noise": false,
|
| 284 |
+
"features": [
|
| 285 |
+
"religious_reference",
|
| 286 |
+
"no_inferred_affiliation"
|
| 287 |
+
],
|
| 288 |
+
"text": "Das Thema sikhistisch wird in einem Bericht von Aster Research Institute erwähnt. Für Saskia Dornfeld wird keine Religionszugehörigkeit behauptet.",
|
| 289 |
+
"spans": [
|
| 290 |
+
{
|
| 291 |
+
"start": 10,
|
| 292 |
+
"end": 21,
|
| 293 |
+
"label": "RELIGION",
|
| 294 |
+
"text": "sikhistisch"
|
| 295 |
+
},
|
| 296 |
+
{
|
| 297 |
+
"start": 48,
|
| 298 |
+
"end": 72,
|
| 299 |
+
"label": "ORGANIZATION",
|
| 300 |
+
"text": "Aster Research Institute"
|
| 301 |
+
},
|
| 302 |
+
{
|
| 303 |
+
"start": 86,
|
| 304 |
+
"end": 101,
|
| 305 |
+
"label": "PERSON",
|
| 306 |
+
"text": "Saskia Dornfeld"
|
| 307 |
+
}
|
| 308 |
+
],
|
| 309 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 310 |
+
},
|
| 311 |
+
{
|
| 312 |
+
"id": "holdout03c-009",
|
| 313 |
+
"language": "en",
|
| 314 |
+
"ocr_noise": false,
|
| 315 |
+
"features": [
|
| 316 |
+
"negative",
|
| 317 |
+
"religious_reference"
|
| 318 |
+
],
|
| 319 |
+
"text": "Worksheet 101: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 320 |
+
"spans": [],
|
| 321 |
+
"expected": "Worksheet 101: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 322 |
+
},
|
| 323 |
+
{
|
| 324 |
+
"id": "holdout03c-010",
|
| 325 |
+
"language": "de",
|
| 326 |
+
"ocr_noise": false,
|
| 327 |
+
"features": [
|
| 328 |
+
"organization",
|
| 329 |
+
"geography"
|
| 330 |
+
],
|
| 331 |
+
"text": "Auskunft: Emil Farber arbeitet für Northbridge Analytics in Potsdam.",
|
| 332 |
+
"spans": [
|
| 333 |
+
{
|
| 334 |
+
"start": 10,
|
| 335 |
+
"end": 21,
|
| 336 |
+
"label": "PERSON",
|
| 337 |
+
"text": "Emil Farber"
|
| 338 |
+
},
|
| 339 |
+
{
|
| 340 |
+
"start": 35,
|
| 341 |
+
"end": 56,
|
| 342 |
+
"label": "ORGANIZATION",
|
| 343 |
+
"text": "Northbridge Analytics"
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
"start": 60,
|
| 347 |
+
"end": 67,
|
| 348 |
+
"label": "CITY",
|
| 349 |
+
"text": "Potsdam"
|
| 350 |
+
}
|
| 351 |
+
],
|
| 352 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 353 |
+
},
|
| 354 |
+
{
|
| 355 |
+
"id": "holdout03c-011",
|
| 356 |
+
"language": "en",
|
| 357 |
+
"ocr_noise": false,
|
| 358 |
+
"features": [
|
| 359 |
+
"organization",
|
| 360 |
+
"geography"
|
| 361 |
+
],
|
| 362 |
+
"text": "The contact Emil Farber represents Northbridge Analytics near Potsdam.",
|
| 363 |
+
"spans": [
|
| 364 |
+
{
|
| 365 |
+
"start": 12,
|
| 366 |
+
"end": 23,
|
| 367 |
+
"label": "PERSON",
|
| 368 |
+
"text": "Emil Farber"
|
| 369 |
+
},
|
| 370 |
+
{
|
| 371 |
+
"start": 35,
|
| 372 |
+
"end": 56,
|
| 373 |
+
"label": "ORGANIZATION",
|
| 374 |
+
"text": "Northbridge Analytics"
|
| 375 |
+
},
|
| 376 |
+
{
|
| 377 |
+
"start": 62,
|
| 378 |
+
"end": 69,
|
| 379 |
+
"label": "CITY",
|
| 380 |
+
"text": "Potsdam"
|
| 381 |
+
}
|
| 382 |
+
],
|
| 383 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 384 |
+
},
|
| 385 |
+
{
|
| 386 |
+
"id": "holdout03c-012",
|
| 387 |
+
"language": "de",
|
| 388 |
+
"ocr_noise": false,
|
| 389 |
+
"features": [
|
| 390 |
+
"affiliation",
|
| 391 |
+
"contacts"
|
| 392 |
+
],
|
| 393 |
+
"text": "Emil Farber nennt orthodox als Zugehörigkeit. E-Mail: case102.review@example.invalid; Telefon: +49 541 730102.",
|
| 394 |
+
"spans": [
|
| 395 |
+
{
|
| 396 |
+
"start": 0,
|
| 397 |
+
"end": 11,
|
| 398 |
+
"label": "PERSON",
|
| 399 |
+
"text": "Emil Farber"
|
| 400 |
+
},
|
| 401 |
+
{
|
| 402 |
+
"start": 18,
|
| 403 |
+
"end": 26,
|
| 404 |
+
"label": "RELIGION",
|
| 405 |
+
"text": "orthodox"
|
| 406 |
+
},
|
| 407 |
+
{
|
| 408 |
+
"start": 54,
|
| 409 |
+
"end": 84,
|
| 410 |
+
"label": "EMAIL",
|
| 411 |
+
"text": "case102.review@example.invalid"
|
| 412 |
+
},
|
| 413 |
+
{
|
| 414 |
+
"start": 95,
|
| 415 |
+
"end": 109,
|
| 416 |
+
"label": "PHONE",
|
| 417 |
+
"text": "+49 541 730102"
|
| 418 |
+
}
|
| 419 |
+
],
|
| 420 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 421 |
+
},
|
| 422 |
+
{
|
| 423 |
+
"id": "holdout03c-013",
|
| 424 |
+
"language": "en",
|
| 425 |
+
"ocr_noise": false,
|
| 426 |
+
"features": [
|
| 427 |
+
"affiliation",
|
| 428 |
+
"contacts"
|
| 429 |
+
],
|
| 430 |
+
"text": "Emil Farber explicitly names Orthodox Christianity as their faith. Write to case102.review@example.invalid or call +49 541 730102.",
|
| 431 |
+
"spans": [
|
| 432 |
+
{
|
| 433 |
+
"start": 0,
|
| 434 |
+
"end": 11,
|
| 435 |
+
"label": "PERSON",
|
| 436 |
+
"text": "Emil Farber"
|
| 437 |
+
},
|
| 438 |
+
{
|
| 439 |
+
"start": 29,
|
| 440 |
+
"end": 50,
|
| 441 |
+
"label": "RELIGION",
|
| 442 |
+
"text": "Orthodox Christianity"
|
| 443 |
+
},
|
| 444 |
+
{
|
| 445 |
+
"start": 76,
|
| 446 |
+
"end": 106,
|
| 447 |
+
"label": "EMAIL",
|
| 448 |
+
"text": "case102.review@example.invalid"
|
| 449 |
+
},
|
| 450 |
+
{
|
| 451 |
+
"start": 115,
|
| 452 |
+
"end": 129,
|
| 453 |
+
"label": "PHONE",
|
| 454 |
+
"text": "+49 541 730102"
|
| 455 |
+
}
|
| 456 |
+
],
|
| 457 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 458 |
+
},
|
| 459 |
+
{
|
| 460 |
+
"id": "holdout03c-014",
|
| 461 |
+
"language": "de",
|
| 462 |
+
"ocr_noise": false,
|
| 463 |
+
"features": [
|
| 464 |
+
"repeats",
|
| 465 |
+
"paragraphs"
|
| 466 |
+
],
|
| 467 |
+
"text": "Emil Farber besucht Potsdam.\n\nEmil Farber bestätigt den Termin bei Northbridge Analytics.",
|
| 468 |
+
"spans": [
|
| 469 |
+
{
|
| 470 |
+
"start": 0,
|
| 471 |
+
"end": 11,
|
| 472 |
+
"label": "PERSON",
|
| 473 |
+
"text": "Emil Farber"
|
| 474 |
+
},
|
| 475 |
+
{
|
| 476 |
+
"start": 20,
|
| 477 |
+
"end": 27,
|
| 478 |
+
"label": "CITY",
|
| 479 |
+
"text": "Potsdam"
|
| 480 |
+
},
|
| 481 |
+
{
|
| 482 |
+
"start": 30,
|
| 483 |
+
"end": 41,
|
| 484 |
+
"label": "PERSON",
|
| 485 |
+
"text": "Emil Farber"
|
| 486 |
+
},
|
| 487 |
+
{
|
| 488 |
+
"start": 67,
|
| 489 |
+
"end": 88,
|
| 490 |
+
"label": "ORGANIZATION",
|
| 491 |
+
"text": "Northbridge Analytics"
|
| 492 |
+
}
|
| 493 |
+
],
|
| 494 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 495 |
+
},
|
| 496 |
+
{
|
| 497 |
+
"id": "holdout03c-015",
|
| 498 |
+
"language": "en",
|
| 499 |
+
"ocr_noise": false,
|
| 500 |
+
"features": [
|
| 501 |
+
"repeats",
|
| 502 |
+
"ambiguous_geography"
|
| 503 |
+
],
|
| 504 |
+
"text": "In Potsdam, Emil Farber signed for Northbridge Analytics. The representative Emil Farber returned to Potsdam.",
|
| 505 |
+
"spans": [
|
| 506 |
+
{
|
| 507 |
+
"start": 3,
|
| 508 |
+
"end": 10,
|
| 509 |
+
"label": "CITY",
|
| 510 |
+
"text": "Potsdam"
|
| 511 |
+
},
|
| 512 |
+
{
|
| 513 |
+
"start": 12,
|
| 514 |
+
"end": 23,
|
| 515 |
+
"label": "PERSON",
|
| 516 |
+
"text": "Emil Farber"
|
| 517 |
+
},
|
| 518 |
+
{
|
| 519 |
+
"start": 35,
|
| 520 |
+
"end": 56,
|
| 521 |
+
"label": "ORGANIZATION",
|
| 522 |
+
"text": "Northbridge Analytics"
|
| 523 |
+
},
|
| 524 |
+
{
|
| 525 |
+
"start": 77,
|
| 526 |
+
"end": 88,
|
| 527 |
+
"label": "PERSON",
|
| 528 |
+
"text": "Emil Farber"
|
| 529 |
+
},
|
| 530 |
+
{
|
| 531 |
+
"start": 101,
|
| 532 |
+
"end": 108,
|
| 533 |
+
"label": "CITY",
|
| 534 |
+
"text": "Potsdam"
|
| 535 |
+
}
|
| 536 |
+
],
|
| 537 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 538 |
+
},
|
| 539 |
+
{
|
| 540 |
+
"id": "holdout03c-016",
|
| 541 |
+
"language": "de",
|
| 542 |
+
"ocr_noise": true,
|
| 543 |
+
"features": [
|
| 544 |
+
"line_wrap",
|
| 545 |
+
"organization"
|
| 546 |
+
],
|
| 547 |
+
"text": "Name: Emil-\nFarber\nOrt: Pot-\nsdam\nArbeitgeber: Northbridge Analytics.",
|
| 548 |
+
"spans": [
|
| 549 |
+
{
|
| 550 |
+
"start": 6,
|
| 551 |
+
"end": 18,
|
| 552 |
+
"label": "PERSON",
|
| 553 |
+
"text": "Emil-\nFarber"
|
| 554 |
+
},
|
| 555 |
+
{
|
| 556 |
+
"start": 24,
|
| 557 |
+
"end": 33,
|
| 558 |
+
"label": "CITY",
|
| 559 |
+
"text": "Pot-\nsdam"
|
| 560 |
+
},
|
| 561 |
+
{
|
| 562 |
+
"start": 47,
|
| 563 |
+
"end": 68,
|
| 564 |
+
"label": "ORGANIZATION",
|
| 565 |
+
"text": "Northbridge Analytics"
|
| 566 |
+
}
|
| 567 |
+
],
|
| 568 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 569 |
+
},
|
| 570 |
+
{
|
| 571 |
+
"id": "holdout03c-017",
|
| 572 |
+
"language": "en",
|
| 573 |
+
"ocr_noise": true,
|
| 574 |
+
"features": [
|
| 575 |
+
"ocr_spelling",
|
| 576 |
+
"organization"
|
| 577 |
+
],
|
| 578 |
+
"text": "Scanned record: Em1l Farber resides in P0tsd4m and works at Northbridge Analytics.",
|
| 579 |
+
"spans": [
|
| 580 |
+
{
|
| 581 |
+
"start": 16,
|
| 582 |
+
"end": 27,
|
| 583 |
+
"label": "PERSON",
|
| 584 |
+
"text": "Em1l Farber"
|
| 585 |
+
},
|
| 586 |
+
{
|
| 587 |
+
"start": 39,
|
| 588 |
+
"end": 46,
|
| 589 |
+
"label": "CITY",
|
| 590 |
+
"text": "P0tsd4m"
|
| 591 |
+
},
|
| 592 |
+
{
|
| 593 |
+
"start": 60,
|
| 594 |
+
"end": 81,
|
| 595 |
+
"label": "ORGANIZATION",
|
| 596 |
+
"text": "Northbridge Analytics"
|
| 597 |
+
}
|
| 598 |
+
],
|
| 599 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 600 |
+
},
|
| 601 |
+
{
|
| 602 |
+
"id": "holdout03c-018",
|
| 603 |
+
"language": "de",
|
| 604 |
+
"ocr_noise": false,
|
| 605 |
+
"features": [
|
| 606 |
+
"religious_reference",
|
| 607 |
+
"no_inferred_affiliation"
|
| 608 |
+
],
|
| 609 |
+
"text": "Das Thema orthodox wird in einem Bericht von Northbridge Analytics erwähnt. Für Emil Farber wird keine Religionszugehörigkeit behauptet.",
|
| 610 |
+
"spans": [
|
| 611 |
+
{
|
| 612 |
+
"start": 10,
|
| 613 |
+
"end": 18,
|
| 614 |
+
"label": "RELIGION",
|
| 615 |
+
"text": "orthodox"
|
| 616 |
+
},
|
| 617 |
+
{
|
| 618 |
+
"start": 45,
|
| 619 |
+
"end": 66,
|
| 620 |
+
"label": "ORGANIZATION",
|
| 621 |
+
"text": "Northbridge Analytics"
|
| 622 |
+
},
|
| 623 |
+
{
|
| 624 |
+
"start": 80,
|
| 625 |
+
"end": 91,
|
| 626 |
+
"label": "PERSON",
|
| 627 |
+
"text": "Emil Farber"
|
| 628 |
+
}
|
| 629 |
+
],
|
| 630 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 631 |
+
},
|
| 632 |
+
{
|
| 633 |
+
"id": "holdout03c-019",
|
| 634 |
+
"language": "en",
|
| 635 |
+
"ocr_noise": false,
|
| 636 |
+
"features": [
|
| 637 |
+
"negative",
|
| 638 |
+
"religious_reference"
|
| 639 |
+
],
|
| 640 |
+
"text": "Worksheet 102: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 641 |
+
"spans": [],
|
| 642 |
+
"expected": "Worksheet 102: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 643 |
+
},
|
| 644 |
+
{
|
| 645 |
+
"id": "holdout03c-020",
|
| 646 |
+
"language": "de",
|
| 647 |
+
"ocr_noise": false,
|
| 648 |
+
"features": [
|
| 649 |
+
"organization",
|
| 650 |
+
"geography"
|
| 651 |
+
],
|
| 652 |
+
"text": "Auskunft: Cora Lindenfels arbeitet für Amber Health Foundation in Dundee.",
|
| 653 |
+
"spans": [
|
| 654 |
+
{
|
| 655 |
+
"start": 10,
|
| 656 |
+
"end": 25,
|
| 657 |
+
"label": "PERSON",
|
| 658 |
+
"text": "Cora Lindenfels"
|
| 659 |
+
},
|
| 660 |
+
{
|
| 661 |
+
"start": 39,
|
| 662 |
+
"end": 62,
|
| 663 |
+
"label": "ORGANIZATION",
|
| 664 |
+
"text": "Amber Health Foundation"
|
| 665 |
+
},
|
| 666 |
+
{
|
| 667 |
+
"start": 66,
|
| 668 |
+
"end": 72,
|
| 669 |
+
"label": "CITY",
|
| 670 |
+
"text": "Dundee"
|
| 671 |
+
}
|
| 672 |
+
],
|
| 673 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 674 |
+
},
|
| 675 |
+
{
|
| 676 |
+
"id": "holdout03c-021",
|
| 677 |
+
"language": "en",
|
| 678 |
+
"ocr_noise": false,
|
| 679 |
+
"features": [
|
| 680 |
+
"organization",
|
| 681 |
+
"geography"
|
| 682 |
+
],
|
| 683 |
+
"text": "The contact Cora Lindenfels represents Amber Health Foundation near Dundee.",
|
| 684 |
+
"spans": [
|
| 685 |
+
{
|
| 686 |
+
"start": 12,
|
| 687 |
+
"end": 27,
|
| 688 |
+
"label": "PERSON",
|
| 689 |
+
"text": "Cora Lindenfels"
|
| 690 |
+
},
|
| 691 |
+
{
|
| 692 |
+
"start": 39,
|
| 693 |
+
"end": 62,
|
| 694 |
+
"label": "ORGANIZATION",
|
| 695 |
+
"text": "Amber Health Foundation"
|
| 696 |
+
},
|
| 697 |
+
{
|
| 698 |
+
"start": 68,
|
| 699 |
+
"end": 74,
|
| 700 |
+
"label": "CITY",
|
| 701 |
+
"text": "Dundee"
|
| 702 |
+
}
|
| 703 |
+
],
|
| 704 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 705 |
+
},
|
| 706 |
+
{
|
| 707 |
+
"id": "holdout03c-022",
|
| 708 |
+
"language": "de",
|
| 709 |
+
"ocr_noise": false,
|
| 710 |
+
"features": [
|
| 711 |
+
"affiliation",
|
| 712 |
+
"contacts"
|
| 713 |
+
],
|
| 714 |
+
"text": "Cora Lindenfels nennt jainistisch als Zugehörigkeit. E-Mail: case103.review@example.invalid; Telefon: +49 541 730103.",
|
| 715 |
+
"spans": [
|
| 716 |
+
{
|
| 717 |
+
"start": 0,
|
| 718 |
+
"end": 15,
|
| 719 |
+
"label": "PERSON",
|
| 720 |
+
"text": "Cora Lindenfels"
|
| 721 |
+
},
|
| 722 |
+
{
|
| 723 |
+
"start": 22,
|
| 724 |
+
"end": 33,
|
| 725 |
+
"label": "RELIGION",
|
| 726 |
+
"text": "jainistisch"
|
| 727 |
+
},
|
| 728 |
+
{
|
| 729 |
+
"start": 61,
|
| 730 |
+
"end": 91,
|
| 731 |
+
"label": "EMAIL",
|
| 732 |
+
"text": "case103.review@example.invalid"
|
| 733 |
+
},
|
| 734 |
+
{
|
| 735 |
+
"start": 102,
|
| 736 |
+
"end": 116,
|
| 737 |
+
"label": "PHONE",
|
| 738 |
+
"text": "+49 541 730103"
|
| 739 |
+
}
|
| 740 |
+
],
|
| 741 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 742 |
+
},
|
| 743 |
+
{
|
| 744 |
+
"id": "holdout03c-023",
|
| 745 |
+
"language": "en",
|
| 746 |
+
"ocr_noise": false,
|
| 747 |
+
"features": [
|
| 748 |
+
"affiliation",
|
| 749 |
+
"contacts"
|
| 750 |
+
],
|
| 751 |
+
"text": "Cora Lindenfels explicitly names Jainism as their faith. Write to case103.review@example.invalid or call +49 541 730103.",
|
| 752 |
+
"spans": [
|
| 753 |
+
{
|
| 754 |
+
"start": 0,
|
| 755 |
+
"end": 15,
|
| 756 |
+
"label": "PERSON",
|
| 757 |
+
"text": "Cora Lindenfels"
|
| 758 |
+
},
|
| 759 |
+
{
|
| 760 |
+
"start": 33,
|
| 761 |
+
"end": 40,
|
| 762 |
+
"label": "RELIGION",
|
| 763 |
+
"text": "Jainism"
|
| 764 |
+
},
|
| 765 |
+
{
|
| 766 |
+
"start": 66,
|
| 767 |
+
"end": 96,
|
| 768 |
+
"label": "EMAIL",
|
| 769 |
+
"text": "case103.review@example.invalid"
|
| 770 |
+
},
|
| 771 |
+
{
|
| 772 |
+
"start": 105,
|
| 773 |
+
"end": 119,
|
| 774 |
+
"label": "PHONE",
|
| 775 |
+
"text": "+49 541 730103"
|
| 776 |
+
}
|
| 777 |
+
],
|
| 778 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 779 |
+
},
|
| 780 |
+
{
|
| 781 |
+
"id": "holdout03c-024",
|
| 782 |
+
"language": "de",
|
| 783 |
+
"ocr_noise": false,
|
| 784 |
+
"features": [
|
| 785 |
+
"repeats",
|
| 786 |
+
"paragraphs"
|
| 787 |
+
],
|
| 788 |
+
"text": "Cora Lindenfels besucht Dundee.\n\nCora Lindenfels bestätigt den Termin bei Amber Health Foundation.",
|
| 789 |
+
"spans": [
|
| 790 |
+
{
|
| 791 |
+
"start": 0,
|
| 792 |
+
"end": 15,
|
| 793 |
+
"label": "PERSON",
|
| 794 |
+
"text": "Cora Lindenfels"
|
| 795 |
+
},
|
| 796 |
+
{
|
| 797 |
+
"start": 24,
|
| 798 |
+
"end": 30,
|
| 799 |
+
"label": "CITY",
|
| 800 |
+
"text": "Dundee"
|
| 801 |
+
},
|
| 802 |
+
{
|
| 803 |
+
"start": 33,
|
| 804 |
+
"end": 48,
|
| 805 |
+
"label": "PERSON",
|
| 806 |
+
"text": "Cora Lindenfels"
|
| 807 |
+
},
|
| 808 |
+
{
|
| 809 |
+
"start": 74,
|
| 810 |
+
"end": 97,
|
| 811 |
+
"label": "ORGANIZATION",
|
| 812 |
+
"text": "Amber Health Foundation"
|
| 813 |
+
}
|
| 814 |
+
],
|
| 815 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 816 |
+
},
|
| 817 |
+
{
|
| 818 |
+
"id": "holdout03c-025",
|
| 819 |
+
"language": "en",
|
| 820 |
+
"ocr_noise": false,
|
| 821 |
+
"features": [
|
| 822 |
+
"repeats",
|
| 823 |
+
"ambiguous_geography"
|
| 824 |
+
],
|
| 825 |
+
"text": "In Dundee, Cora Lindenfels signed for Amber Health Foundation. The representative Cora Lindenfels returned to Dundee.",
|
| 826 |
+
"spans": [
|
| 827 |
+
{
|
| 828 |
+
"start": 3,
|
| 829 |
+
"end": 9,
|
| 830 |
+
"label": "CITY",
|
| 831 |
+
"text": "Dundee"
|
| 832 |
+
},
|
| 833 |
+
{
|
| 834 |
+
"start": 11,
|
| 835 |
+
"end": 26,
|
| 836 |
+
"label": "PERSON",
|
| 837 |
+
"text": "Cora Lindenfels"
|
| 838 |
+
},
|
| 839 |
+
{
|
| 840 |
+
"start": 38,
|
| 841 |
+
"end": 61,
|
| 842 |
+
"label": "ORGANIZATION",
|
| 843 |
+
"text": "Amber Health Foundation"
|
| 844 |
+
},
|
| 845 |
+
{
|
| 846 |
+
"start": 82,
|
| 847 |
+
"end": 97,
|
| 848 |
+
"label": "PERSON",
|
| 849 |
+
"text": "Cora Lindenfels"
|
| 850 |
+
},
|
| 851 |
+
{
|
| 852 |
+
"start": 110,
|
| 853 |
+
"end": 116,
|
| 854 |
+
"label": "CITY",
|
| 855 |
+
"text": "Dundee"
|
| 856 |
+
}
|
| 857 |
+
],
|
| 858 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 859 |
+
},
|
| 860 |
+
{
|
| 861 |
+
"id": "holdout03c-026",
|
| 862 |
+
"language": "de",
|
| 863 |
+
"ocr_noise": true,
|
| 864 |
+
"features": [
|
| 865 |
+
"line_wrap",
|
| 866 |
+
"organization"
|
| 867 |
+
],
|
| 868 |
+
"text": "Name: Cora-\nLindenfels\nOrt: Dun-\ndee\nArbeitgeber: Amber Health Foundation.",
|
| 869 |
+
"spans": [
|
| 870 |
+
{
|
| 871 |
+
"start": 6,
|
| 872 |
+
"end": 22,
|
| 873 |
+
"label": "PERSON",
|
| 874 |
+
"text": "Cora-\nLindenfels"
|
| 875 |
+
},
|
| 876 |
+
{
|
| 877 |
+
"start": 28,
|
| 878 |
+
"end": 36,
|
| 879 |
+
"label": "CITY",
|
| 880 |
+
"text": "Dun-\ndee"
|
| 881 |
+
},
|
| 882 |
+
{
|
| 883 |
+
"start": 50,
|
| 884 |
+
"end": 73,
|
| 885 |
+
"label": "ORGANIZATION",
|
| 886 |
+
"text": "Amber Health Foundation"
|
| 887 |
+
}
|
| 888 |
+
],
|
| 889 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 890 |
+
},
|
| 891 |
+
{
|
| 892 |
+
"id": "holdout03c-027",
|
| 893 |
+
"language": "en",
|
| 894 |
+
"ocr_noise": true,
|
| 895 |
+
"features": [
|
| 896 |
+
"ocr_spelling",
|
| 897 |
+
"organization"
|
| 898 |
+
],
|
| 899 |
+
"text": "Scanned record: C0ra L1ndenfels resides in Dundee and works at Amber Health Foundation.",
|
| 900 |
+
"spans": [
|
| 901 |
+
{
|
| 902 |
+
"start": 16,
|
| 903 |
+
"end": 31,
|
| 904 |
+
"label": "PERSON",
|
| 905 |
+
"text": "C0ra L1ndenfels"
|
| 906 |
+
},
|
| 907 |
+
{
|
| 908 |
+
"start": 43,
|
| 909 |
+
"end": 49,
|
| 910 |
+
"label": "CITY",
|
| 911 |
+
"text": "Dundee"
|
| 912 |
+
},
|
| 913 |
+
{
|
| 914 |
+
"start": 63,
|
| 915 |
+
"end": 86,
|
| 916 |
+
"label": "ORGANIZATION",
|
| 917 |
+
"text": "Amber Health Foundation"
|
| 918 |
+
}
|
| 919 |
+
],
|
| 920 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 921 |
+
},
|
| 922 |
+
{
|
| 923 |
+
"id": "holdout03c-028",
|
| 924 |
+
"language": "de",
|
| 925 |
+
"ocr_noise": false,
|
| 926 |
+
"features": [
|
| 927 |
+
"religious_reference",
|
| 928 |
+
"no_inferred_affiliation"
|
| 929 |
+
],
|
| 930 |
+
"text": "Das Thema jainistisch wird in einem Bericht von Amber Health Foundation erwähnt. Für Cora Lindenfels wird keine Religionszugehörigkeit behauptet.",
|
| 931 |
+
"spans": [
|
| 932 |
+
{
|
| 933 |
+
"start": 10,
|
| 934 |
+
"end": 21,
|
| 935 |
+
"label": "RELIGION",
|
| 936 |
+
"text": "jainistisch"
|
| 937 |
+
},
|
| 938 |
+
{
|
| 939 |
+
"start": 48,
|
| 940 |
+
"end": 71,
|
| 941 |
+
"label": "ORGANIZATION",
|
| 942 |
+
"text": "Amber Health Foundation"
|
| 943 |
+
},
|
| 944 |
+
{
|
| 945 |
+
"start": 85,
|
| 946 |
+
"end": 100,
|
| 947 |
+
"label": "PERSON",
|
| 948 |
+
"text": "Cora Lindenfels"
|
| 949 |
+
}
|
| 950 |
+
],
|
| 951 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 952 |
+
},
|
| 953 |
+
{
|
| 954 |
+
"id": "holdout03c-029",
|
| 955 |
+
"language": "en",
|
| 956 |
+
"ocr_noise": false,
|
| 957 |
+
"features": [
|
| 958 |
+
"negative",
|
| 959 |
+
"religious_reference"
|
| 960 |
+
],
|
| 961 |
+
"text": "Worksheet 103: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 962 |
+
"spans": [],
|
| 963 |
+
"expected": "Worksheet 103: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 964 |
+
},
|
| 965 |
+
{
|
| 966 |
+
"id": "holdout03c-030",
|
| 967 |
+
"language": "de",
|
| 968 |
+
"ocr_noise": false,
|
| 969 |
+
"features": [
|
| 970 |
+
"organization",
|
| 971 |
+
"geography"
|
| 972 |
+
],
|
| 973 |
+
"text": "Auskunft: Jasper Keene arbeitet für Silver Finch Robotics in Aberdeen.",
|
| 974 |
+
"spans": [
|
| 975 |
+
{
|
| 976 |
+
"start": 10,
|
| 977 |
+
"end": 22,
|
| 978 |
+
"label": "PERSON",
|
| 979 |
+
"text": "Jasper Keene"
|
| 980 |
+
},
|
| 981 |
+
{
|
| 982 |
+
"start": 36,
|
| 983 |
+
"end": 57,
|
| 984 |
+
"label": "ORGANIZATION",
|
| 985 |
+
"text": "Silver Finch Robotics"
|
| 986 |
+
},
|
| 987 |
+
{
|
| 988 |
+
"start": 61,
|
| 989 |
+
"end": 69,
|
| 990 |
+
"label": "CITY",
|
| 991 |
+
"text": "Aberdeen"
|
| 992 |
+
}
|
| 993 |
+
],
|
| 994 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 995 |
+
},
|
| 996 |
+
{
|
| 997 |
+
"id": "holdout03c-031",
|
| 998 |
+
"language": "en",
|
| 999 |
+
"ocr_noise": false,
|
| 1000 |
+
"features": [
|
| 1001 |
+
"organization",
|
| 1002 |
+
"geography"
|
| 1003 |
+
],
|
| 1004 |
+
"text": "The contact Jasper Keene represents Silver Finch Robotics near Aberdeen.",
|
| 1005 |
+
"spans": [
|
| 1006 |
+
{
|
| 1007 |
+
"start": 12,
|
| 1008 |
+
"end": 24,
|
| 1009 |
+
"label": "PERSON",
|
| 1010 |
+
"text": "Jasper Keene"
|
| 1011 |
+
},
|
| 1012 |
+
{
|
| 1013 |
+
"start": 36,
|
| 1014 |
+
"end": 57,
|
| 1015 |
+
"label": "ORGANIZATION",
|
| 1016 |
+
"text": "Silver Finch Robotics"
|
| 1017 |
+
},
|
| 1018 |
+
{
|
| 1019 |
+
"start": 63,
|
| 1020 |
+
"end": 71,
|
| 1021 |
+
"label": "CITY",
|
| 1022 |
+
"text": "Aberdeen"
|
| 1023 |
+
}
|
| 1024 |
+
],
|
| 1025 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 1026 |
+
},
|
| 1027 |
+
{
|
| 1028 |
+
"id": "holdout03c-032",
|
| 1029 |
+
"language": "de",
|
| 1030 |
+
"ocr_noise": false,
|
| 1031 |
+
"features": [
|
| 1032 |
+
"affiliation",
|
| 1033 |
+
"contacts"
|
| 1034 |
+
],
|
| 1035 |
+
"text": "Jasper Keene nennt hinduistisch als Zugehörigkeit. E-Mail: case104.review@example.invalid; Telefon: +49 541 730104.",
|
| 1036 |
+
"spans": [
|
| 1037 |
+
{
|
| 1038 |
+
"start": 0,
|
| 1039 |
+
"end": 12,
|
| 1040 |
+
"label": "PERSON",
|
| 1041 |
+
"text": "Jasper Keene"
|
| 1042 |
+
},
|
| 1043 |
+
{
|
| 1044 |
+
"start": 19,
|
| 1045 |
+
"end": 31,
|
| 1046 |
+
"label": "RELIGION",
|
| 1047 |
+
"text": "hinduistisch"
|
| 1048 |
+
},
|
| 1049 |
+
{
|
| 1050 |
+
"start": 59,
|
| 1051 |
+
"end": 89,
|
| 1052 |
+
"label": "EMAIL",
|
| 1053 |
+
"text": "case104.review@example.invalid"
|
| 1054 |
+
},
|
| 1055 |
+
{
|
| 1056 |
+
"start": 100,
|
| 1057 |
+
"end": 114,
|
| 1058 |
+
"label": "PHONE",
|
| 1059 |
+
"text": "+49 541 730104"
|
| 1060 |
+
}
|
| 1061 |
+
],
|
| 1062 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 1063 |
+
},
|
| 1064 |
+
{
|
| 1065 |
+
"id": "holdout03c-033",
|
| 1066 |
+
"language": "en",
|
| 1067 |
+
"ocr_noise": false,
|
| 1068 |
+
"features": [
|
| 1069 |
+
"affiliation",
|
| 1070 |
+
"contacts"
|
| 1071 |
+
],
|
| 1072 |
+
"text": "Jasper Keene explicitly names Hinduism as their faith. Write to case104.review@example.invalid or call +49 541 730104.",
|
| 1073 |
+
"spans": [
|
| 1074 |
+
{
|
| 1075 |
+
"start": 0,
|
| 1076 |
+
"end": 12,
|
| 1077 |
+
"label": "PERSON",
|
| 1078 |
+
"text": "Jasper Keene"
|
| 1079 |
+
},
|
| 1080 |
+
{
|
| 1081 |
+
"start": 30,
|
| 1082 |
+
"end": 38,
|
| 1083 |
+
"label": "RELIGION",
|
| 1084 |
+
"text": "Hinduism"
|
| 1085 |
+
},
|
| 1086 |
+
{
|
| 1087 |
+
"start": 64,
|
| 1088 |
+
"end": 94,
|
| 1089 |
+
"label": "EMAIL",
|
| 1090 |
+
"text": "case104.review@example.invalid"
|
| 1091 |
+
},
|
| 1092 |
+
{
|
| 1093 |
+
"start": 103,
|
| 1094 |
+
"end": 117,
|
| 1095 |
+
"label": "PHONE",
|
| 1096 |
+
"text": "+49 541 730104"
|
| 1097 |
+
}
|
| 1098 |
+
],
|
| 1099 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 1100 |
+
},
|
| 1101 |
+
{
|
| 1102 |
+
"id": "holdout03c-034",
|
| 1103 |
+
"language": "de",
|
| 1104 |
+
"ocr_noise": false,
|
| 1105 |
+
"features": [
|
| 1106 |
+
"repeats",
|
| 1107 |
+
"paragraphs"
|
| 1108 |
+
],
|
| 1109 |
+
"text": "Jasper Keene besucht Aberdeen.\n\nJasper Keene bestätigt den Termin bei Silver Finch Robotics.",
|
| 1110 |
+
"spans": [
|
| 1111 |
+
{
|
| 1112 |
+
"start": 0,
|
| 1113 |
+
"end": 12,
|
| 1114 |
+
"label": "PERSON",
|
| 1115 |
+
"text": "Jasper Keene"
|
| 1116 |
+
},
|
| 1117 |
+
{
|
| 1118 |
+
"start": 21,
|
| 1119 |
+
"end": 29,
|
| 1120 |
+
"label": "CITY",
|
| 1121 |
+
"text": "Aberdeen"
|
| 1122 |
+
},
|
| 1123 |
+
{
|
| 1124 |
+
"start": 32,
|
| 1125 |
+
"end": 44,
|
| 1126 |
+
"label": "PERSON",
|
| 1127 |
+
"text": "Jasper Keene"
|
| 1128 |
+
},
|
| 1129 |
+
{
|
| 1130 |
+
"start": 70,
|
| 1131 |
+
"end": 91,
|
| 1132 |
+
"label": "ORGANIZATION",
|
| 1133 |
+
"text": "Silver Finch Robotics"
|
| 1134 |
+
}
|
| 1135 |
+
],
|
| 1136 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 1137 |
+
},
|
| 1138 |
+
{
|
| 1139 |
+
"id": "holdout03c-035",
|
| 1140 |
+
"language": "en",
|
| 1141 |
+
"ocr_noise": false,
|
| 1142 |
+
"features": [
|
| 1143 |
+
"repeats",
|
| 1144 |
+
"ambiguous_geography"
|
| 1145 |
+
],
|
| 1146 |
+
"text": "In Aberdeen, Jasper Keene signed for Silver Finch Robotics. The representative Jasper Keene returned to Aberdeen.",
|
| 1147 |
+
"spans": [
|
| 1148 |
+
{
|
| 1149 |
+
"start": 3,
|
| 1150 |
+
"end": 11,
|
| 1151 |
+
"label": "CITY",
|
| 1152 |
+
"text": "Aberdeen"
|
| 1153 |
+
},
|
| 1154 |
+
{
|
| 1155 |
+
"start": 13,
|
| 1156 |
+
"end": 25,
|
| 1157 |
+
"label": "PERSON",
|
| 1158 |
+
"text": "Jasper Keene"
|
| 1159 |
+
},
|
| 1160 |
+
{
|
| 1161 |
+
"start": 37,
|
| 1162 |
+
"end": 58,
|
| 1163 |
+
"label": "ORGANIZATION",
|
| 1164 |
+
"text": "Silver Finch Robotics"
|
| 1165 |
+
},
|
| 1166 |
+
{
|
| 1167 |
+
"start": 79,
|
| 1168 |
+
"end": 91,
|
| 1169 |
+
"label": "PERSON",
|
| 1170 |
+
"text": "Jasper Keene"
|
| 1171 |
+
},
|
| 1172 |
+
{
|
| 1173 |
+
"start": 104,
|
| 1174 |
+
"end": 112,
|
| 1175 |
+
"label": "CITY",
|
| 1176 |
+
"text": "Aberdeen"
|
| 1177 |
+
}
|
| 1178 |
+
],
|
| 1179 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 1180 |
+
},
|
| 1181 |
+
{
|
| 1182 |
+
"id": "holdout03c-036",
|
| 1183 |
+
"language": "de",
|
| 1184 |
+
"ocr_noise": true,
|
| 1185 |
+
"features": [
|
| 1186 |
+
"line_wrap",
|
| 1187 |
+
"organization"
|
| 1188 |
+
],
|
| 1189 |
+
"text": "Name: Jasper-\nKeene\nOrt: Aber-\ndeen\nArbeitgeber: Silver Finch Robotics.",
|
| 1190 |
+
"spans": [
|
| 1191 |
+
{
|
| 1192 |
+
"start": 6,
|
| 1193 |
+
"end": 19,
|
| 1194 |
+
"label": "PERSON",
|
| 1195 |
+
"text": "Jasper-\nKeene"
|
| 1196 |
+
},
|
| 1197 |
+
{
|
| 1198 |
+
"start": 25,
|
| 1199 |
+
"end": 35,
|
| 1200 |
+
"label": "CITY",
|
| 1201 |
+
"text": "Aber-\ndeen"
|
| 1202 |
+
},
|
| 1203 |
+
{
|
| 1204 |
+
"start": 49,
|
| 1205 |
+
"end": 70,
|
| 1206 |
+
"label": "ORGANIZATION",
|
| 1207 |
+
"text": "Silver Finch Robotics"
|
| 1208 |
+
}
|
| 1209 |
+
],
|
| 1210 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 1211 |
+
},
|
| 1212 |
+
{
|
| 1213 |
+
"id": "holdout03c-037",
|
| 1214 |
+
"language": "en",
|
| 1215 |
+
"ocr_noise": true,
|
| 1216 |
+
"features": [
|
| 1217 |
+
"ocr_spelling",
|
| 1218 |
+
"organization"
|
| 1219 |
+
],
|
| 1220 |
+
"text": "Scanned record: Jasper Keene resides in Aberdeen and works at Silver Finch Robotics.",
|
| 1221 |
+
"spans": [
|
| 1222 |
+
{
|
| 1223 |
+
"start": 16,
|
| 1224 |
+
"end": 28,
|
| 1225 |
+
"label": "PERSON",
|
| 1226 |
+
"text": "Jasper Keene"
|
| 1227 |
+
},
|
| 1228 |
+
{
|
| 1229 |
+
"start": 40,
|
| 1230 |
+
"end": 48,
|
| 1231 |
+
"label": "CITY",
|
| 1232 |
+
"text": "Aberdeen"
|
| 1233 |
+
},
|
| 1234 |
+
{
|
| 1235 |
+
"start": 62,
|
| 1236 |
+
"end": 83,
|
| 1237 |
+
"label": "ORGANIZATION",
|
| 1238 |
+
"text": "Silver Finch Robotics"
|
| 1239 |
+
}
|
| 1240 |
+
],
|
| 1241 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 1242 |
+
},
|
| 1243 |
+
{
|
| 1244 |
+
"id": "holdout03c-038",
|
| 1245 |
+
"language": "de",
|
| 1246 |
+
"ocr_noise": false,
|
| 1247 |
+
"features": [
|
| 1248 |
+
"religious_reference",
|
| 1249 |
+
"no_inferred_affiliation"
|
| 1250 |
+
],
|
| 1251 |
+
"text": "Das Thema hinduistisch wird in einem Bericht von Silver Finch Robotics erwähnt. Für Jasper Keene wird keine Religionszugehörigkeit behauptet.",
|
| 1252 |
+
"spans": [
|
| 1253 |
+
{
|
| 1254 |
+
"start": 10,
|
| 1255 |
+
"end": 22,
|
| 1256 |
+
"label": "RELIGION",
|
| 1257 |
+
"text": "hinduistisch"
|
| 1258 |
+
},
|
| 1259 |
+
{
|
| 1260 |
+
"start": 49,
|
| 1261 |
+
"end": 70,
|
| 1262 |
+
"label": "ORGANIZATION",
|
| 1263 |
+
"text": "Silver Finch Robotics"
|
| 1264 |
+
},
|
| 1265 |
+
{
|
| 1266 |
+
"start": 84,
|
| 1267 |
+
"end": 96,
|
| 1268 |
+
"label": "PERSON",
|
| 1269 |
+
"text": "Jasper Keene"
|
| 1270 |
+
}
|
| 1271 |
+
],
|
| 1272 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 1273 |
+
},
|
| 1274 |
+
{
|
| 1275 |
+
"id": "holdout03c-039",
|
| 1276 |
+
"language": "en",
|
| 1277 |
+
"ocr_noise": false,
|
| 1278 |
+
"features": [
|
| 1279 |
+
"negative",
|
| 1280 |
+
"religious_reference"
|
| 1281 |
+
],
|
| 1282 |
+
"text": "Worksheet 104: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 1283 |
+
"spans": [],
|
| 1284 |
+
"expected": "Worksheet 104: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 1285 |
+
},
|
| 1286 |
+
{
|
| 1287 |
+
"id": "holdout03c-040",
|
| 1288 |
+
"language": "de",
|
| 1289 |
+
"ocr_noise": false,
|
| 1290 |
+
"features": [
|
| 1291 |
+
"organization",
|
| 1292 |
+
"geography"
|
| 1293 |
+
],
|
| 1294 |
+
"text": "Auskunft: Frida Östberg arbeitet für Blue Cedar Cooperative in Uppsala.",
|
| 1295 |
+
"spans": [
|
| 1296 |
+
{
|
| 1297 |
+
"start": 10,
|
| 1298 |
+
"end": 23,
|
| 1299 |
+
"label": "PERSON",
|
| 1300 |
+
"text": "Frida Östberg"
|
| 1301 |
+
},
|
| 1302 |
+
{
|
| 1303 |
+
"start": 37,
|
| 1304 |
+
"end": 59,
|
| 1305 |
+
"label": "ORGANIZATION",
|
| 1306 |
+
"text": "Blue Cedar Cooperative"
|
| 1307 |
+
},
|
| 1308 |
+
{
|
| 1309 |
+
"start": 63,
|
| 1310 |
+
"end": 70,
|
| 1311 |
+
"label": "CITY",
|
| 1312 |
+
"text": "Uppsala"
|
| 1313 |
+
}
|
| 1314 |
+
],
|
| 1315 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 1316 |
+
},
|
| 1317 |
+
{
|
| 1318 |
+
"id": "holdout03c-041",
|
| 1319 |
+
"language": "en",
|
| 1320 |
+
"ocr_noise": false,
|
| 1321 |
+
"features": [
|
| 1322 |
+
"organization",
|
| 1323 |
+
"geography"
|
| 1324 |
+
],
|
| 1325 |
+
"text": "The contact Frida Östberg represents Blue Cedar Cooperative near Uppsala.",
|
| 1326 |
+
"spans": [
|
| 1327 |
+
{
|
| 1328 |
+
"start": 12,
|
| 1329 |
+
"end": 25,
|
| 1330 |
+
"label": "PERSON",
|
| 1331 |
+
"text": "Frida Östberg"
|
| 1332 |
+
},
|
| 1333 |
+
{
|
| 1334 |
+
"start": 37,
|
| 1335 |
+
"end": 59,
|
| 1336 |
+
"label": "ORGANIZATION",
|
| 1337 |
+
"text": "Blue Cedar Cooperative"
|
| 1338 |
+
},
|
| 1339 |
+
{
|
| 1340 |
+
"start": 65,
|
| 1341 |
+
"end": 72,
|
| 1342 |
+
"label": "CITY",
|
| 1343 |
+
"text": "Uppsala"
|
| 1344 |
+
}
|
| 1345 |
+
],
|
| 1346 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 1347 |
+
},
|
| 1348 |
+
{
|
| 1349 |
+
"id": "holdout03c-042",
|
| 1350 |
+
"language": "de",
|
| 1351 |
+
"ocr_noise": false,
|
| 1352 |
+
"features": [
|
| 1353 |
+
"affiliation",
|
| 1354 |
+
"contacts"
|
| 1355 |
+
],
|
| 1356 |
+
"text": "Frida Östberg nennt bahai als Zugehörigkeit. E-Mail: case105.review@example.invalid; Telefon: +49 541 730105.",
|
| 1357 |
+
"spans": [
|
| 1358 |
+
{
|
| 1359 |
+
"start": 0,
|
| 1360 |
+
"end": 13,
|
| 1361 |
+
"label": "PERSON",
|
| 1362 |
+
"text": "Frida Östberg"
|
| 1363 |
+
},
|
| 1364 |
+
{
|
| 1365 |
+
"start": 20,
|
| 1366 |
+
"end": 25,
|
| 1367 |
+
"label": "RELIGION",
|
| 1368 |
+
"text": "bahai"
|
| 1369 |
+
},
|
| 1370 |
+
{
|
| 1371 |
+
"start": 53,
|
| 1372 |
+
"end": 83,
|
| 1373 |
+
"label": "EMAIL",
|
| 1374 |
+
"text": "case105.review@example.invalid"
|
| 1375 |
+
},
|
| 1376 |
+
{
|
| 1377 |
+
"start": 94,
|
| 1378 |
+
"end": 108,
|
| 1379 |
+
"label": "PHONE",
|
| 1380 |
+
"text": "+49 541 730105"
|
| 1381 |
+
}
|
| 1382 |
+
],
|
| 1383 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 1384 |
+
},
|
| 1385 |
+
{
|
| 1386 |
+
"id": "holdout03c-043",
|
| 1387 |
+
"language": "en",
|
| 1388 |
+
"ocr_noise": false,
|
| 1389 |
+
"features": [
|
| 1390 |
+
"affiliation",
|
| 1391 |
+
"contacts"
|
| 1392 |
+
],
|
| 1393 |
+
"text": "Frida Östberg explicitly names Baháʼí Faith as their faith. Write to case105.review@example.invalid or call +49 541 730105.",
|
| 1394 |
+
"spans": [
|
| 1395 |
+
{
|
| 1396 |
+
"start": 0,
|
| 1397 |
+
"end": 13,
|
| 1398 |
+
"label": "PERSON",
|
| 1399 |
+
"text": "Frida Östberg"
|
| 1400 |
+
},
|
| 1401 |
+
{
|
| 1402 |
+
"start": 31,
|
| 1403 |
+
"end": 43,
|
| 1404 |
+
"label": "RELIGION",
|
| 1405 |
+
"text": "Baháʼí Faith"
|
| 1406 |
+
},
|
| 1407 |
+
{
|
| 1408 |
+
"start": 69,
|
| 1409 |
+
"end": 99,
|
| 1410 |
+
"label": "EMAIL",
|
| 1411 |
+
"text": "case105.review@example.invalid"
|
| 1412 |
+
},
|
| 1413 |
+
{
|
| 1414 |
+
"start": 108,
|
| 1415 |
+
"end": 122,
|
| 1416 |
+
"label": "PHONE",
|
| 1417 |
+
"text": "+49 541 730105"
|
| 1418 |
+
}
|
| 1419 |
+
],
|
| 1420 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 1421 |
+
},
|
| 1422 |
+
{
|
| 1423 |
+
"id": "holdout03c-044",
|
| 1424 |
+
"language": "de",
|
| 1425 |
+
"ocr_noise": false,
|
| 1426 |
+
"features": [
|
| 1427 |
+
"repeats",
|
| 1428 |
+
"paragraphs"
|
| 1429 |
+
],
|
| 1430 |
+
"text": "Frida Östberg besucht Uppsala.\n\nFrida Östberg bestätigt den Termin bei Blue Cedar Cooperative.",
|
| 1431 |
+
"spans": [
|
| 1432 |
+
{
|
| 1433 |
+
"start": 0,
|
| 1434 |
+
"end": 13,
|
| 1435 |
+
"label": "PERSON",
|
| 1436 |
+
"text": "Frida Östberg"
|
| 1437 |
+
},
|
| 1438 |
+
{
|
| 1439 |
+
"start": 22,
|
| 1440 |
+
"end": 29,
|
| 1441 |
+
"label": "CITY",
|
| 1442 |
+
"text": "Uppsala"
|
| 1443 |
+
},
|
| 1444 |
+
{
|
| 1445 |
+
"start": 32,
|
| 1446 |
+
"end": 45,
|
| 1447 |
+
"label": "PERSON",
|
| 1448 |
+
"text": "Frida Östberg"
|
| 1449 |
+
},
|
| 1450 |
+
{
|
| 1451 |
+
"start": 71,
|
| 1452 |
+
"end": 93,
|
| 1453 |
+
"label": "ORGANIZATION",
|
| 1454 |
+
"text": "Blue Cedar Cooperative"
|
| 1455 |
+
}
|
| 1456 |
+
],
|
| 1457 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 1458 |
+
},
|
| 1459 |
+
{
|
| 1460 |
+
"id": "holdout03c-045",
|
| 1461 |
+
"language": "en",
|
| 1462 |
+
"ocr_noise": false,
|
| 1463 |
+
"features": [
|
| 1464 |
+
"repeats",
|
| 1465 |
+
"ambiguous_geography"
|
| 1466 |
+
],
|
| 1467 |
+
"text": "In Uppsala, Frida Östberg signed for Blue Cedar Cooperative. The representative Frida Östberg returned to Uppsala.",
|
| 1468 |
+
"spans": [
|
| 1469 |
+
{
|
| 1470 |
+
"start": 3,
|
| 1471 |
+
"end": 10,
|
| 1472 |
+
"label": "CITY",
|
| 1473 |
+
"text": "Uppsala"
|
| 1474 |
+
},
|
| 1475 |
+
{
|
| 1476 |
+
"start": 12,
|
| 1477 |
+
"end": 25,
|
| 1478 |
+
"label": "PERSON",
|
| 1479 |
+
"text": "Frida Östberg"
|
| 1480 |
+
},
|
| 1481 |
+
{
|
| 1482 |
+
"start": 37,
|
| 1483 |
+
"end": 59,
|
| 1484 |
+
"label": "ORGANIZATION",
|
| 1485 |
+
"text": "Blue Cedar Cooperative"
|
| 1486 |
+
},
|
| 1487 |
+
{
|
| 1488 |
+
"start": 80,
|
| 1489 |
+
"end": 93,
|
| 1490 |
+
"label": "PERSON",
|
| 1491 |
+
"text": "Frida Östberg"
|
| 1492 |
+
},
|
| 1493 |
+
{
|
| 1494 |
+
"start": 106,
|
| 1495 |
+
"end": 113,
|
| 1496 |
+
"label": "CITY",
|
| 1497 |
+
"text": "Uppsala"
|
| 1498 |
+
}
|
| 1499 |
+
],
|
| 1500 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 1501 |
+
},
|
| 1502 |
+
{
|
| 1503 |
+
"id": "holdout03c-046",
|
| 1504 |
+
"language": "de",
|
| 1505 |
+
"ocr_noise": true,
|
| 1506 |
+
"features": [
|
| 1507 |
+
"line_wrap",
|
| 1508 |
+
"organization"
|
| 1509 |
+
],
|
| 1510 |
+
"text": "Name: Frida-\nÖstberg\nOrt: Upp-\nsala\nArbeitgeber: Blue Cedar Cooperative.",
|
| 1511 |
+
"spans": [
|
| 1512 |
+
{
|
| 1513 |
+
"start": 6,
|
| 1514 |
+
"end": 20,
|
| 1515 |
+
"label": "PERSON",
|
| 1516 |
+
"text": "Frida-\nÖstberg"
|
| 1517 |
+
},
|
| 1518 |
+
{
|
| 1519 |
+
"start": 26,
|
| 1520 |
+
"end": 35,
|
| 1521 |
+
"label": "CITY",
|
| 1522 |
+
"text": "Upp-\nsala"
|
| 1523 |
+
},
|
| 1524 |
+
{
|
| 1525 |
+
"start": 49,
|
| 1526 |
+
"end": 71,
|
| 1527 |
+
"label": "ORGANIZATION",
|
| 1528 |
+
"text": "Blue Cedar Cooperative"
|
| 1529 |
+
}
|
| 1530 |
+
],
|
| 1531 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 1532 |
+
},
|
| 1533 |
+
{
|
| 1534 |
+
"id": "holdout03c-047",
|
| 1535 |
+
"language": "en",
|
| 1536 |
+
"ocr_noise": true,
|
| 1537 |
+
"features": [
|
| 1538 |
+
"ocr_spelling",
|
| 1539 |
+
"organization"
|
| 1540 |
+
],
|
| 1541 |
+
"text": "Scanned record: Fr1da Östberg resides in Upps4l4 and works at Blue Cedar Cooperative.",
|
| 1542 |
+
"spans": [
|
| 1543 |
+
{
|
| 1544 |
+
"start": 16,
|
| 1545 |
+
"end": 29,
|
| 1546 |
+
"label": "PERSON",
|
| 1547 |
+
"text": "Fr1da Östberg"
|
| 1548 |
+
},
|
| 1549 |
+
{
|
| 1550 |
+
"start": 41,
|
| 1551 |
+
"end": 48,
|
| 1552 |
+
"label": "CITY",
|
| 1553 |
+
"text": "Upps4l4"
|
| 1554 |
+
},
|
| 1555 |
+
{
|
| 1556 |
+
"start": 62,
|
| 1557 |
+
"end": 84,
|
| 1558 |
+
"label": "ORGANIZATION",
|
| 1559 |
+
"text": "Blue Cedar Cooperative"
|
| 1560 |
+
}
|
| 1561 |
+
],
|
| 1562 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 1563 |
+
},
|
| 1564 |
+
{
|
| 1565 |
+
"id": "holdout03c-048",
|
| 1566 |
+
"language": "de",
|
| 1567 |
+
"ocr_noise": false,
|
| 1568 |
+
"features": [
|
| 1569 |
+
"religious_reference",
|
| 1570 |
+
"no_inferred_affiliation"
|
| 1571 |
+
],
|
| 1572 |
+
"text": "Das Thema bahai wird in einem Bericht von Blue Cedar Cooperative erwähnt. Für Frida Östberg wird keine Religionszugehörigkeit behauptet.",
|
| 1573 |
+
"spans": [
|
| 1574 |
+
{
|
| 1575 |
+
"start": 10,
|
| 1576 |
+
"end": 15,
|
| 1577 |
+
"label": "RELIGION",
|
| 1578 |
+
"text": "bahai"
|
| 1579 |
+
},
|
| 1580 |
+
{
|
| 1581 |
+
"start": 42,
|
| 1582 |
+
"end": 64,
|
| 1583 |
+
"label": "ORGANIZATION",
|
| 1584 |
+
"text": "Blue Cedar Cooperative"
|
| 1585 |
+
},
|
| 1586 |
+
{
|
| 1587 |
+
"start": 78,
|
| 1588 |
+
"end": 91,
|
| 1589 |
+
"label": "PERSON",
|
| 1590 |
+
"text": "Frida Östberg"
|
| 1591 |
+
}
|
| 1592 |
+
],
|
| 1593 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 1594 |
+
},
|
| 1595 |
+
{
|
| 1596 |
+
"id": "holdout03c-049",
|
| 1597 |
+
"language": "en",
|
| 1598 |
+
"ocr_noise": false,
|
| 1599 |
+
"features": [
|
| 1600 |
+
"negative",
|
| 1601 |
+
"religious_reference"
|
| 1602 |
+
],
|
| 1603 |
+
"text": "Worksheet 105: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 1604 |
+
"spans": [],
|
| 1605 |
+
"expected": "Worksheet 105: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 1606 |
+
},
|
| 1607 |
+
{
|
| 1608 |
+
"id": "holdout03c-050",
|
| 1609 |
+
"language": "de",
|
| 1610 |
+
"ocr_noise": false,
|
| 1611 |
+
"features": [
|
| 1612 |
+
"organization",
|
| 1613 |
+
"geography"
|
| 1614 |
+
],
|
| 1615 |
+
"text": "Auskunft: Owen Fairchild arbeitet für Oakwell Public Library in Leicester.",
|
| 1616 |
+
"spans": [
|
| 1617 |
+
{
|
| 1618 |
+
"start": 10,
|
| 1619 |
+
"end": 24,
|
| 1620 |
+
"label": "PERSON",
|
| 1621 |
+
"text": "Owen Fairchild"
|
| 1622 |
+
},
|
| 1623 |
+
{
|
| 1624 |
+
"start": 38,
|
| 1625 |
+
"end": 60,
|
| 1626 |
+
"label": "ORGANIZATION",
|
| 1627 |
+
"text": "Oakwell Public Library"
|
| 1628 |
+
},
|
| 1629 |
+
{
|
| 1630 |
+
"start": 64,
|
| 1631 |
+
"end": 73,
|
| 1632 |
+
"label": "CITY",
|
| 1633 |
+
"text": "Leicester"
|
| 1634 |
+
}
|
| 1635 |
+
],
|
| 1636 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 1637 |
+
},
|
| 1638 |
+
{
|
| 1639 |
+
"id": "holdout03c-051",
|
| 1640 |
+
"language": "en",
|
| 1641 |
+
"ocr_noise": false,
|
| 1642 |
+
"features": [
|
| 1643 |
+
"organization",
|
| 1644 |
+
"geography"
|
| 1645 |
+
],
|
| 1646 |
+
"text": "The contact Owen Fairchild represents Oakwell Public Library near Leicester.",
|
| 1647 |
+
"spans": [
|
| 1648 |
+
{
|
| 1649 |
+
"start": 12,
|
| 1650 |
+
"end": 26,
|
| 1651 |
+
"label": "PERSON",
|
| 1652 |
+
"text": "Owen Fairchild"
|
| 1653 |
+
},
|
| 1654 |
+
{
|
| 1655 |
+
"start": 38,
|
| 1656 |
+
"end": 60,
|
| 1657 |
+
"label": "ORGANIZATION",
|
| 1658 |
+
"text": "Oakwell Public Library"
|
| 1659 |
+
},
|
| 1660 |
+
{
|
| 1661 |
+
"start": 66,
|
| 1662 |
+
"end": 75,
|
| 1663 |
+
"label": "CITY",
|
| 1664 |
+
"text": "Leicester"
|
| 1665 |
+
}
|
| 1666 |
+
],
|
| 1667 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 1668 |
+
},
|
| 1669 |
+
{
|
| 1670 |
+
"id": "holdout03c-052",
|
| 1671 |
+
"language": "de",
|
| 1672 |
+
"ocr_noise": false,
|
| 1673 |
+
"features": [
|
| 1674 |
+
"affiliation",
|
| 1675 |
+
"contacts"
|
| 1676 |
+
],
|
| 1677 |
+
"text": "Owen Fairchild nennt buddhistisch als Zugehörigkeit. E-Mail: case106.review@example.invalid; Telefon: +49 541 730106.",
|
| 1678 |
+
"spans": [
|
| 1679 |
+
{
|
| 1680 |
+
"start": 0,
|
| 1681 |
+
"end": 14,
|
| 1682 |
+
"label": "PERSON",
|
| 1683 |
+
"text": "Owen Fairchild"
|
| 1684 |
+
},
|
| 1685 |
+
{
|
| 1686 |
+
"start": 21,
|
| 1687 |
+
"end": 33,
|
| 1688 |
+
"label": "RELIGION",
|
| 1689 |
+
"text": "buddhistisch"
|
| 1690 |
+
},
|
| 1691 |
+
{
|
| 1692 |
+
"start": 61,
|
| 1693 |
+
"end": 91,
|
| 1694 |
+
"label": "EMAIL",
|
| 1695 |
+
"text": "case106.review@example.invalid"
|
| 1696 |
+
},
|
| 1697 |
+
{
|
| 1698 |
+
"start": 102,
|
| 1699 |
+
"end": 116,
|
| 1700 |
+
"label": "PHONE",
|
| 1701 |
+
"text": "+49 541 730106"
|
| 1702 |
+
}
|
| 1703 |
+
],
|
| 1704 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 1705 |
+
},
|
| 1706 |
+
{
|
| 1707 |
+
"id": "holdout03c-053",
|
| 1708 |
+
"language": "en",
|
| 1709 |
+
"ocr_noise": false,
|
| 1710 |
+
"features": [
|
| 1711 |
+
"affiliation",
|
| 1712 |
+
"contacts"
|
| 1713 |
+
],
|
| 1714 |
+
"text": "Owen Fairchild explicitly names Buddhism as their faith. Write to case106.review@example.invalid or call +49 541 730106.",
|
| 1715 |
+
"spans": [
|
| 1716 |
+
{
|
| 1717 |
+
"start": 0,
|
| 1718 |
+
"end": 14,
|
| 1719 |
+
"label": "PERSON",
|
| 1720 |
+
"text": "Owen Fairchild"
|
| 1721 |
+
},
|
| 1722 |
+
{
|
| 1723 |
+
"start": 32,
|
| 1724 |
+
"end": 40,
|
| 1725 |
+
"label": "RELIGION",
|
| 1726 |
+
"text": "Buddhism"
|
| 1727 |
+
},
|
| 1728 |
+
{
|
| 1729 |
+
"start": 66,
|
| 1730 |
+
"end": 96,
|
| 1731 |
+
"label": "EMAIL",
|
| 1732 |
+
"text": "case106.review@example.invalid"
|
| 1733 |
+
},
|
| 1734 |
+
{
|
| 1735 |
+
"start": 105,
|
| 1736 |
+
"end": 119,
|
| 1737 |
+
"label": "PHONE",
|
| 1738 |
+
"text": "+49 541 730106"
|
| 1739 |
+
}
|
| 1740 |
+
],
|
| 1741 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 1742 |
+
},
|
| 1743 |
+
{
|
| 1744 |
+
"id": "holdout03c-054",
|
| 1745 |
+
"language": "de",
|
| 1746 |
+
"ocr_noise": false,
|
| 1747 |
+
"features": [
|
| 1748 |
+
"repeats",
|
| 1749 |
+
"paragraphs"
|
| 1750 |
+
],
|
| 1751 |
+
"text": "Owen Fairchild besucht Leicester.\n\nOwen Fairchild bestätigt den Termin bei Oakwell Public Library.",
|
| 1752 |
+
"spans": [
|
| 1753 |
+
{
|
| 1754 |
+
"start": 0,
|
| 1755 |
+
"end": 14,
|
| 1756 |
+
"label": "PERSON",
|
| 1757 |
+
"text": "Owen Fairchild"
|
| 1758 |
+
},
|
| 1759 |
+
{
|
| 1760 |
+
"start": 23,
|
| 1761 |
+
"end": 32,
|
| 1762 |
+
"label": "CITY",
|
| 1763 |
+
"text": "Leicester"
|
| 1764 |
+
},
|
| 1765 |
+
{
|
| 1766 |
+
"start": 35,
|
| 1767 |
+
"end": 49,
|
| 1768 |
+
"label": "PERSON",
|
| 1769 |
+
"text": "Owen Fairchild"
|
| 1770 |
+
},
|
| 1771 |
+
{
|
| 1772 |
+
"start": 75,
|
| 1773 |
+
"end": 97,
|
| 1774 |
+
"label": "ORGANIZATION",
|
| 1775 |
+
"text": "Oakwell Public Library"
|
| 1776 |
+
}
|
| 1777 |
+
],
|
| 1778 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 1779 |
+
},
|
| 1780 |
+
{
|
| 1781 |
+
"id": "holdout03c-055",
|
| 1782 |
+
"language": "en",
|
| 1783 |
+
"ocr_noise": false,
|
| 1784 |
+
"features": [
|
| 1785 |
+
"repeats",
|
| 1786 |
+
"ambiguous_geography"
|
| 1787 |
+
],
|
| 1788 |
+
"text": "In Leicester, Owen Fairchild signed for Oakwell Public Library. The representative Owen Fairchild returned to Leicester.",
|
| 1789 |
+
"spans": [
|
| 1790 |
+
{
|
| 1791 |
+
"start": 3,
|
| 1792 |
+
"end": 12,
|
| 1793 |
+
"label": "CITY",
|
| 1794 |
+
"text": "Leicester"
|
| 1795 |
+
},
|
| 1796 |
+
{
|
| 1797 |
+
"start": 14,
|
| 1798 |
+
"end": 28,
|
| 1799 |
+
"label": "PERSON",
|
| 1800 |
+
"text": "Owen Fairchild"
|
| 1801 |
+
},
|
| 1802 |
+
{
|
| 1803 |
+
"start": 40,
|
| 1804 |
+
"end": 62,
|
| 1805 |
+
"label": "ORGANIZATION",
|
| 1806 |
+
"text": "Oakwell Public Library"
|
| 1807 |
+
},
|
| 1808 |
+
{
|
| 1809 |
+
"start": 83,
|
| 1810 |
+
"end": 97,
|
| 1811 |
+
"label": "PERSON",
|
| 1812 |
+
"text": "Owen Fairchild"
|
| 1813 |
+
},
|
| 1814 |
+
{
|
| 1815 |
+
"start": 110,
|
| 1816 |
+
"end": 119,
|
| 1817 |
+
"label": "CITY",
|
| 1818 |
+
"text": "Leicester"
|
| 1819 |
+
}
|
| 1820 |
+
],
|
| 1821 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 1822 |
+
},
|
| 1823 |
+
{
|
| 1824 |
+
"id": "holdout03c-056",
|
| 1825 |
+
"language": "de",
|
| 1826 |
+
"ocr_noise": true,
|
| 1827 |
+
"features": [
|
| 1828 |
+
"line_wrap",
|
| 1829 |
+
"organization"
|
| 1830 |
+
],
|
| 1831 |
+
"text": "Name: Owen-\nFairchild\nOrt: Leic-\nester\nArbeitgeber: Oakwell Public Library.",
|
| 1832 |
+
"spans": [
|
| 1833 |
+
{
|
| 1834 |
+
"start": 6,
|
| 1835 |
+
"end": 21,
|
| 1836 |
+
"label": "PERSON",
|
| 1837 |
+
"text": "Owen-\nFairchild"
|
| 1838 |
+
},
|
| 1839 |
+
{
|
| 1840 |
+
"start": 27,
|
| 1841 |
+
"end": 38,
|
| 1842 |
+
"label": "CITY",
|
| 1843 |
+
"text": "Leic-\nester"
|
| 1844 |
+
},
|
| 1845 |
+
{
|
| 1846 |
+
"start": 52,
|
| 1847 |
+
"end": 74,
|
| 1848 |
+
"label": "ORGANIZATION",
|
| 1849 |
+
"text": "Oakwell Public Library"
|
| 1850 |
+
}
|
| 1851 |
+
],
|
| 1852 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 1853 |
+
},
|
| 1854 |
+
{
|
| 1855 |
+
"id": "holdout03c-057",
|
| 1856 |
+
"language": "en",
|
| 1857 |
+
"ocr_noise": true,
|
| 1858 |
+
"features": [
|
| 1859 |
+
"ocr_spelling",
|
| 1860 |
+
"organization"
|
| 1861 |
+
],
|
| 1862 |
+
"text": "Scanned record: Owen Fa1rch1ld resides in Leicester and works at Oakwell Public Library.",
|
| 1863 |
+
"spans": [
|
| 1864 |
+
{
|
| 1865 |
+
"start": 16,
|
| 1866 |
+
"end": 30,
|
| 1867 |
+
"label": "PERSON",
|
| 1868 |
+
"text": "Owen Fa1rch1ld"
|
| 1869 |
+
},
|
| 1870 |
+
{
|
| 1871 |
+
"start": 42,
|
| 1872 |
+
"end": 51,
|
| 1873 |
+
"label": "CITY",
|
| 1874 |
+
"text": "Leicester"
|
| 1875 |
+
},
|
| 1876 |
+
{
|
| 1877 |
+
"start": 65,
|
| 1878 |
+
"end": 87,
|
| 1879 |
+
"label": "ORGANIZATION",
|
| 1880 |
+
"text": "Oakwell Public Library"
|
| 1881 |
+
}
|
| 1882 |
+
],
|
| 1883 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 1884 |
+
},
|
| 1885 |
+
{
|
| 1886 |
+
"id": "holdout03c-058",
|
| 1887 |
+
"language": "de",
|
| 1888 |
+
"ocr_noise": false,
|
| 1889 |
+
"features": [
|
| 1890 |
+
"religious_reference",
|
| 1891 |
+
"no_inferred_affiliation"
|
| 1892 |
+
],
|
| 1893 |
+
"text": "Das Thema buddhistisch wird in einem Bericht von Oakwell Public Library erwähnt. Für Owen Fairchild wird keine Religionszugehörigkeit behauptet.",
|
| 1894 |
+
"spans": [
|
| 1895 |
+
{
|
| 1896 |
+
"start": 10,
|
| 1897 |
+
"end": 22,
|
| 1898 |
+
"label": "RELIGION",
|
| 1899 |
+
"text": "buddhistisch"
|
| 1900 |
+
},
|
| 1901 |
+
{
|
| 1902 |
+
"start": 49,
|
| 1903 |
+
"end": 71,
|
| 1904 |
+
"label": "ORGANIZATION",
|
| 1905 |
+
"text": "Oakwell Public Library"
|
| 1906 |
+
},
|
| 1907 |
+
{
|
| 1908 |
+
"start": 85,
|
| 1909 |
+
"end": 99,
|
| 1910 |
+
"label": "PERSON",
|
| 1911 |
+
"text": "Owen Fairchild"
|
| 1912 |
+
}
|
| 1913 |
+
],
|
| 1914 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 1915 |
+
},
|
| 1916 |
+
{
|
| 1917 |
+
"id": "holdout03c-059",
|
| 1918 |
+
"language": "en",
|
| 1919 |
+
"ocr_noise": false,
|
| 1920 |
+
"features": [
|
| 1921 |
+
"negative",
|
| 1922 |
+
"religious_reference"
|
| 1923 |
+
],
|
| 1924 |
+
"text": "Worksheet 106: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 1925 |
+
"spans": [],
|
| 1926 |
+
"expected": "Worksheet 106: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 1927 |
+
},
|
| 1928 |
+
{
|
| 1929 |
+
"id": "holdout03c-060",
|
| 1930 |
+
"language": "de",
|
| 1931 |
+
"ocr_noise": false,
|
| 1932 |
+
"features": [
|
| 1933 |
+
"organization",
|
| 1934 |
+
"geography"
|
| 1935 |
+
],
|
| 1936 |
+
"text": "Auskunft: Nele Brückner arbeitet für Westhaven Water Authority in Würzburg.",
|
| 1937 |
+
"spans": [
|
| 1938 |
+
{
|
| 1939 |
+
"start": 10,
|
| 1940 |
+
"end": 23,
|
| 1941 |
+
"label": "PERSON",
|
| 1942 |
+
"text": "Nele Brückner"
|
| 1943 |
+
},
|
| 1944 |
+
{
|
| 1945 |
+
"start": 37,
|
| 1946 |
+
"end": 62,
|
| 1947 |
+
"label": "ORGANIZATION",
|
| 1948 |
+
"text": "Westhaven Water Authority"
|
| 1949 |
+
},
|
| 1950 |
+
{
|
| 1951 |
+
"start": 66,
|
| 1952 |
+
"end": 74,
|
| 1953 |
+
"label": "CITY",
|
| 1954 |
+
"text": "Würzburg"
|
| 1955 |
+
}
|
| 1956 |
+
],
|
| 1957 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 1958 |
+
},
|
| 1959 |
+
{
|
| 1960 |
+
"id": "holdout03c-061",
|
| 1961 |
+
"language": "en",
|
| 1962 |
+
"ocr_noise": false,
|
| 1963 |
+
"features": [
|
| 1964 |
+
"organization",
|
| 1965 |
+
"geography"
|
| 1966 |
+
],
|
| 1967 |
+
"text": "The contact Nele Brückner represents Westhaven Water Authority near Würzburg.",
|
| 1968 |
+
"spans": [
|
| 1969 |
+
{
|
| 1970 |
+
"start": 12,
|
| 1971 |
+
"end": 25,
|
| 1972 |
+
"label": "PERSON",
|
| 1973 |
+
"text": "Nele Brückner"
|
| 1974 |
+
},
|
| 1975 |
+
{
|
| 1976 |
+
"start": 37,
|
| 1977 |
+
"end": 62,
|
| 1978 |
+
"label": "ORGANIZATION",
|
| 1979 |
+
"text": "Westhaven Water Authority"
|
| 1980 |
+
},
|
| 1981 |
+
{
|
| 1982 |
+
"start": 68,
|
| 1983 |
+
"end": 76,
|
| 1984 |
+
"label": "CITY",
|
| 1985 |
+
"text": "Würzburg"
|
| 1986 |
+
}
|
| 1987 |
+
],
|
| 1988 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 1989 |
+
},
|
| 1990 |
+
{
|
| 1991 |
+
"id": "holdout03c-062",
|
| 1992 |
+
"language": "de",
|
| 1993 |
+
"ocr_noise": false,
|
| 1994 |
+
"features": [
|
| 1995 |
+
"affiliation",
|
| 1996 |
+
"contacts"
|
| 1997 |
+
],
|
| 1998 |
+
"text": "Nele Brückner nennt katholisch als Zugehörigkeit. E-Mail: case107.review@example.invalid; Telefon: +49 541 730107.",
|
| 1999 |
+
"spans": [
|
| 2000 |
+
{
|
| 2001 |
+
"start": 0,
|
| 2002 |
+
"end": 13,
|
| 2003 |
+
"label": "PERSON",
|
| 2004 |
+
"text": "Nele Brückner"
|
| 2005 |
+
},
|
| 2006 |
+
{
|
| 2007 |
+
"start": 20,
|
| 2008 |
+
"end": 30,
|
| 2009 |
+
"label": "RELIGION",
|
| 2010 |
+
"text": "katholisch"
|
| 2011 |
+
},
|
| 2012 |
+
{
|
| 2013 |
+
"start": 58,
|
| 2014 |
+
"end": 88,
|
| 2015 |
+
"label": "EMAIL",
|
| 2016 |
+
"text": "case107.review@example.invalid"
|
| 2017 |
+
},
|
| 2018 |
+
{
|
| 2019 |
+
"start": 99,
|
| 2020 |
+
"end": 113,
|
| 2021 |
+
"label": "PHONE",
|
| 2022 |
+
"text": "+49 541 730107"
|
| 2023 |
+
}
|
| 2024 |
+
],
|
| 2025 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 2026 |
+
},
|
| 2027 |
+
{
|
| 2028 |
+
"id": "holdout03c-063",
|
| 2029 |
+
"language": "en",
|
| 2030 |
+
"ocr_noise": false,
|
| 2031 |
+
"features": [
|
| 2032 |
+
"affiliation",
|
| 2033 |
+
"contacts"
|
| 2034 |
+
],
|
| 2035 |
+
"text": "Nele Brückner explicitly names Catholicism as their faith. Write to case107.review@example.invalid or call +49 541 730107.",
|
| 2036 |
+
"spans": [
|
| 2037 |
+
{
|
| 2038 |
+
"start": 0,
|
| 2039 |
+
"end": 13,
|
| 2040 |
+
"label": "PERSON",
|
| 2041 |
+
"text": "Nele Brückner"
|
| 2042 |
+
},
|
| 2043 |
+
{
|
| 2044 |
+
"start": 31,
|
| 2045 |
+
"end": 42,
|
| 2046 |
+
"label": "RELIGION",
|
| 2047 |
+
"text": "Catholicism"
|
| 2048 |
+
},
|
| 2049 |
+
{
|
| 2050 |
+
"start": 68,
|
| 2051 |
+
"end": 98,
|
| 2052 |
+
"label": "EMAIL",
|
| 2053 |
+
"text": "case107.review@example.invalid"
|
| 2054 |
+
},
|
| 2055 |
+
{
|
| 2056 |
+
"start": 107,
|
| 2057 |
+
"end": 121,
|
| 2058 |
+
"label": "PHONE",
|
| 2059 |
+
"text": "+49 541 730107"
|
| 2060 |
+
}
|
| 2061 |
+
],
|
| 2062 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 2063 |
+
},
|
| 2064 |
+
{
|
| 2065 |
+
"id": "holdout03c-064",
|
| 2066 |
+
"language": "de",
|
| 2067 |
+
"ocr_noise": false,
|
| 2068 |
+
"features": [
|
| 2069 |
+
"repeats",
|
| 2070 |
+
"paragraphs"
|
| 2071 |
+
],
|
| 2072 |
+
"text": "Nele Brückner besucht Würzburg.\n\nNele Brückner bestätigt den Termin bei Westhaven Water Authority.",
|
| 2073 |
+
"spans": [
|
| 2074 |
+
{
|
| 2075 |
+
"start": 0,
|
| 2076 |
+
"end": 13,
|
| 2077 |
+
"label": "PERSON",
|
| 2078 |
+
"text": "Nele Brückner"
|
| 2079 |
+
},
|
| 2080 |
+
{
|
| 2081 |
+
"start": 22,
|
| 2082 |
+
"end": 30,
|
| 2083 |
+
"label": "CITY",
|
| 2084 |
+
"text": "Würzburg"
|
| 2085 |
+
},
|
| 2086 |
+
{
|
| 2087 |
+
"start": 33,
|
| 2088 |
+
"end": 46,
|
| 2089 |
+
"label": "PERSON",
|
| 2090 |
+
"text": "Nele Brückner"
|
| 2091 |
+
},
|
| 2092 |
+
{
|
| 2093 |
+
"start": 72,
|
| 2094 |
+
"end": 97,
|
| 2095 |
+
"label": "ORGANIZATION",
|
| 2096 |
+
"text": "Westhaven Water Authority"
|
| 2097 |
+
}
|
| 2098 |
+
],
|
| 2099 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 2100 |
+
},
|
| 2101 |
+
{
|
| 2102 |
+
"id": "holdout03c-065",
|
| 2103 |
+
"language": "en",
|
| 2104 |
+
"ocr_noise": false,
|
| 2105 |
+
"features": [
|
| 2106 |
+
"repeats",
|
| 2107 |
+
"ambiguous_geography"
|
| 2108 |
+
],
|
| 2109 |
+
"text": "In Würzburg, Nele Brückner signed for Westhaven Water Authority. The representative Nele Brückner returned to Würzburg.",
|
| 2110 |
+
"spans": [
|
| 2111 |
+
{
|
| 2112 |
+
"start": 3,
|
| 2113 |
+
"end": 11,
|
| 2114 |
+
"label": "CITY",
|
| 2115 |
+
"text": "Würzburg"
|
| 2116 |
+
},
|
| 2117 |
+
{
|
| 2118 |
+
"start": 13,
|
| 2119 |
+
"end": 26,
|
| 2120 |
+
"label": "PERSON",
|
| 2121 |
+
"text": "Nele Brückner"
|
| 2122 |
+
},
|
| 2123 |
+
{
|
| 2124 |
+
"start": 38,
|
| 2125 |
+
"end": 63,
|
| 2126 |
+
"label": "ORGANIZATION",
|
| 2127 |
+
"text": "Westhaven Water Authority"
|
| 2128 |
+
},
|
| 2129 |
+
{
|
| 2130 |
+
"start": 84,
|
| 2131 |
+
"end": 97,
|
| 2132 |
+
"label": "PERSON",
|
| 2133 |
+
"text": "Nele Brückner"
|
| 2134 |
+
},
|
| 2135 |
+
{
|
| 2136 |
+
"start": 110,
|
| 2137 |
+
"end": 118,
|
| 2138 |
+
"label": "CITY",
|
| 2139 |
+
"text": "Würzburg"
|
| 2140 |
+
}
|
| 2141 |
+
],
|
| 2142 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 2143 |
+
},
|
| 2144 |
+
{
|
| 2145 |
+
"id": "holdout03c-066",
|
| 2146 |
+
"language": "de",
|
| 2147 |
+
"ocr_noise": true,
|
| 2148 |
+
"features": [
|
| 2149 |
+
"line_wrap",
|
| 2150 |
+
"organization"
|
| 2151 |
+
],
|
| 2152 |
+
"text": "Name: Nele-\nBrückner\nOrt: Würz-\nburg\nArbeitgeber: Westhaven Water Authority.",
|
| 2153 |
+
"spans": [
|
| 2154 |
+
{
|
| 2155 |
+
"start": 6,
|
| 2156 |
+
"end": 20,
|
| 2157 |
+
"label": "PERSON",
|
| 2158 |
+
"text": "Nele-\nBrückner"
|
| 2159 |
+
},
|
| 2160 |
+
{
|
| 2161 |
+
"start": 26,
|
| 2162 |
+
"end": 36,
|
| 2163 |
+
"label": "CITY",
|
| 2164 |
+
"text": "Würz-\nburg"
|
| 2165 |
+
},
|
| 2166 |
+
{
|
| 2167 |
+
"start": 50,
|
| 2168 |
+
"end": 75,
|
| 2169 |
+
"label": "ORGANIZATION",
|
| 2170 |
+
"text": "Westhaven Water Authority"
|
| 2171 |
+
}
|
| 2172 |
+
],
|
| 2173 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 2174 |
+
},
|
| 2175 |
+
{
|
| 2176 |
+
"id": "holdout03c-067",
|
| 2177 |
+
"language": "en",
|
| 2178 |
+
"ocr_noise": true,
|
| 2179 |
+
"features": [
|
| 2180 |
+
"ocr_spelling",
|
| 2181 |
+
"organization"
|
| 2182 |
+
],
|
| 2183 |
+
"text": "Scanned record: Nele Brückner resides in Würzburg and works at Westhaven Water Authority.",
|
| 2184 |
+
"spans": [
|
| 2185 |
+
{
|
| 2186 |
+
"start": 16,
|
| 2187 |
+
"end": 29,
|
| 2188 |
+
"label": "PERSON",
|
| 2189 |
+
"text": "Nele Brückner"
|
| 2190 |
+
},
|
| 2191 |
+
{
|
| 2192 |
+
"start": 41,
|
| 2193 |
+
"end": 49,
|
| 2194 |
+
"label": "CITY",
|
| 2195 |
+
"text": "Würzburg"
|
| 2196 |
+
},
|
| 2197 |
+
{
|
| 2198 |
+
"start": 63,
|
| 2199 |
+
"end": 88,
|
| 2200 |
+
"label": "ORGANIZATION",
|
| 2201 |
+
"text": "Westhaven Water Authority"
|
| 2202 |
+
}
|
| 2203 |
+
],
|
| 2204 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 2205 |
+
},
|
| 2206 |
+
{
|
| 2207 |
+
"id": "holdout03c-068",
|
| 2208 |
+
"language": "de",
|
| 2209 |
+
"ocr_noise": false,
|
| 2210 |
+
"features": [
|
| 2211 |
+
"religious_reference",
|
| 2212 |
+
"no_inferred_affiliation"
|
| 2213 |
+
],
|
| 2214 |
+
"text": "Das Thema katholisch wird in einem Bericht von Westhaven Water Authority erwähnt. Für Nele Brückner wird keine Religionszugehörigkeit behauptet.",
|
| 2215 |
+
"spans": [
|
| 2216 |
+
{
|
| 2217 |
+
"start": 10,
|
| 2218 |
+
"end": 20,
|
| 2219 |
+
"label": "RELIGION",
|
| 2220 |
+
"text": "katholisch"
|
| 2221 |
+
},
|
| 2222 |
+
{
|
| 2223 |
+
"start": 47,
|
| 2224 |
+
"end": 72,
|
| 2225 |
+
"label": "ORGANIZATION",
|
| 2226 |
+
"text": "Westhaven Water Authority"
|
| 2227 |
+
},
|
| 2228 |
+
{
|
| 2229 |
+
"start": 86,
|
| 2230 |
+
"end": 99,
|
| 2231 |
+
"label": "PERSON",
|
| 2232 |
+
"text": "Nele Brückner"
|
| 2233 |
+
}
|
| 2234 |
+
],
|
| 2235 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 2236 |
+
},
|
| 2237 |
+
{
|
| 2238 |
+
"id": "holdout03c-069",
|
| 2239 |
+
"language": "en",
|
| 2240 |
+
"ocr_noise": false,
|
| 2241 |
+
"features": [
|
| 2242 |
+
"negative",
|
| 2243 |
+
"religious_reference"
|
| 2244 |
+
],
|
| 2245 |
+
"text": "Worksheet 107: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 2246 |
+
"spans": [],
|
| 2247 |
+
"expected": "Worksheet 107: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 2248 |
+
},
|
| 2249 |
+
{
|
| 2250 |
+
"id": "holdout03c-070",
|
| 2251 |
+
"language": "de",
|
| 2252 |
+
"ocr_noise": false,
|
| 2253 |
+
"features": [
|
| 2254 |
+
"organization",
|
| 2255 |
+
"geography"
|
| 2256 |
+
],
|
| 2257 |
+
"text": "Auskunft: Silas Hawthorne arbeitet für Juniper Legal Services in Portland.",
|
| 2258 |
+
"spans": [
|
| 2259 |
+
{
|
| 2260 |
+
"start": 10,
|
| 2261 |
+
"end": 25,
|
| 2262 |
+
"label": "PERSON",
|
| 2263 |
+
"text": "Silas Hawthorne"
|
| 2264 |
+
},
|
| 2265 |
+
{
|
| 2266 |
+
"start": 39,
|
| 2267 |
+
"end": 61,
|
| 2268 |
+
"label": "ORGANIZATION",
|
| 2269 |
+
"text": "Juniper Legal Services"
|
| 2270 |
+
},
|
| 2271 |
+
{
|
| 2272 |
+
"start": 65,
|
| 2273 |
+
"end": 73,
|
| 2274 |
+
"label": "CITY",
|
| 2275 |
+
"text": "Portland"
|
| 2276 |
+
}
|
| 2277 |
+
],
|
| 2278 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 2279 |
+
},
|
| 2280 |
+
{
|
| 2281 |
+
"id": "holdout03c-071",
|
| 2282 |
+
"language": "en",
|
| 2283 |
+
"ocr_noise": false,
|
| 2284 |
+
"features": [
|
| 2285 |
+
"organization",
|
| 2286 |
+
"geography"
|
| 2287 |
+
],
|
| 2288 |
+
"text": "The contact Silas Hawthorne represents Juniper Legal Services near Portland.",
|
| 2289 |
+
"spans": [
|
| 2290 |
+
{
|
| 2291 |
+
"start": 12,
|
| 2292 |
+
"end": 27,
|
| 2293 |
+
"label": "PERSON",
|
| 2294 |
+
"text": "Silas Hawthorne"
|
| 2295 |
+
},
|
| 2296 |
+
{
|
| 2297 |
+
"start": 39,
|
| 2298 |
+
"end": 61,
|
| 2299 |
+
"label": "ORGANIZATION",
|
| 2300 |
+
"text": "Juniper Legal Services"
|
| 2301 |
+
},
|
| 2302 |
+
{
|
| 2303 |
+
"start": 67,
|
| 2304 |
+
"end": 75,
|
| 2305 |
+
"label": "CITY",
|
| 2306 |
+
"text": "Portland"
|
| 2307 |
+
}
|
| 2308 |
+
],
|
| 2309 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 2310 |
+
},
|
| 2311 |
+
{
|
| 2312 |
+
"id": "holdout03c-072",
|
| 2313 |
+
"language": "de",
|
| 2314 |
+
"ocr_noise": false,
|
| 2315 |
+
"features": [
|
| 2316 |
+
"affiliation",
|
| 2317 |
+
"contacts"
|
| 2318 |
+
],
|
| 2319 |
+
"text": "Silas Hawthorne nennt evangelisch als Zugehörigkeit. E-Mail: case108.review@example.invalid; Telefon: +49 541 730108.",
|
| 2320 |
+
"spans": [
|
| 2321 |
+
{
|
| 2322 |
+
"start": 0,
|
| 2323 |
+
"end": 15,
|
| 2324 |
+
"label": "PERSON",
|
| 2325 |
+
"text": "Silas Hawthorne"
|
| 2326 |
+
},
|
| 2327 |
+
{
|
| 2328 |
+
"start": 22,
|
| 2329 |
+
"end": 33,
|
| 2330 |
+
"label": "RELIGION",
|
| 2331 |
+
"text": "evangelisch"
|
| 2332 |
+
},
|
| 2333 |
+
{
|
| 2334 |
+
"start": 61,
|
| 2335 |
+
"end": 91,
|
| 2336 |
+
"label": "EMAIL",
|
| 2337 |
+
"text": "case108.review@example.invalid"
|
| 2338 |
+
},
|
| 2339 |
+
{
|
| 2340 |
+
"start": 102,
|
| 2341 |
+
"end": 116,
|
| 2342 |
+
"label": "PHONE",
|
| 2343 |
+
"text": "+49 541 730108"
|
| 2344 |
+
}
|
| 2345 |
+
],
|
| 2346 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 2347 |
+
},
|
| 2348 |
+
{
|
| 2349 |
+
"id": "holdout03c-073",
|
| 2350 |
+
"language": "en",
|
| 2351 |
+
"ocr_noise": false,
|
| 2352 |
+
"features": [
|
| 2353 |
+
"affiliation",
|
| 2354 |
+
"contacts"
|
| 2355 |
+
],
|
| 2356 |
+
"text": "Silas Hawthorne explicitly names Protestantism as their faith. Write to case108.review@example.invalid or call +49 541 730108.",
|
| 2357 |
+
"spans": [
|
| 2358 |
+
{
|
| 2359 |
+
"start": 0,
|
| 2360 |
+
"end": 15,
|
| 2361 |
+
"label": "PERSON",
|
| 2362 |
+
"text": "Silas Hawthorne"
|
| 2363 |
+
},
|
| 2364 |
+
{
|
| 2365 |
+
"start": 33,
|
| 2366 |
+
"end": 46,
|
| 2367 |
+
"label": "RELIGION",
|
| 2368 |
+
"text": "Protestantism"
|
| 2369 |
+
},
|
| 2370 |
+
{
|
| 2371 |
+
"start": 72,
|
| 2372 |
+
"end": 102,
|
| 2373 |
+
"label": "EMAIL",
|
| 2374 |
+
"text": "case108.review@example.invalid"
|
| 2375 |
+
},
|
| 2376 |
+
{
|
| 2377 |
+
"start": 111,
|
| 2378 |
+
"end": 125,
|
| 2379 |
+
"label": "PHONE",
|
| 2380 |
+
"text": "+49 541 730108"
|
| 2381 |
+
}
|
| 2382 |
+
],
|
| 2383 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 2384 |
+
},
|
| 2385 |
+
{
|
| 2386 |
+
"id": "holdout03c-074",
|
| 2387 |
+
"language": "de",
|
| 2388 |
+
"ocr_noise": false,
|
| 2389 |
+
"features": [
|
| 2390 |
+
"repeats",
|
| 2391 |
+
"paragraphs"
|
| 2392 |
+
],
|
| 2393 |
+
"text": "Silas Hawthorne besucht Portland.\n\nSilas Hawthorne bestätigt den Termin bei Juniper Legal Services.",
|
| 2394 |
+
"spans": [
|
| 2395 |
+
{
|
| 2396 |
+
"start": 0,
|
| 2397 |
+
"end": 15,
|
| 2398 |
+
"label": "PERSON",
|
| 2399 |
+
"text": "Silas Hawthorne"
|
| 2400 |
+
},
|
| 2401 |
+
{
|
| 2402 |
+
"start": 24,
|
| 2403 |
+
"end": 32,
|
| 2404 |
+
"label": "CITY",
|
| 2405 |
+
"text": "Portland"
|
| 2406 |
+
},
|
| 2407 |
+
{
|
| 2408 |
+
"start": 35,
|
| 2409 |
+
"end": 50,
|
| 2410 |
+
"label": "PERSON",
|
| 2411 |
+
"text": "Silas Hawthorne"
|
| 2412 |
+
},
|
| 2413 |
+
{
|
| 2414 |
+
"start": 76,
|
| 2415 |
+
"end": 98,
|
| 2416 |
+
"label": "ORGANIZATION",
|
| 2417 |
+
"text": "Juniper Legal Services"
|
| 2418 |
+
}
|
| 2419 |
+
],
|
| 2420 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 2421 |
+
},
|
| 2422 |
+
{
|
| 2423 |
+
"id": "holdout03c-075",
|
| 2424 |
+
"language": "en",
|
| 2425 |
+
"ocr_noise": false,
|
| 2426 |
+
"features": [
|
| 2427 |
+
"repeats",
|
| 2428 |
+
"ambiguous_geography"
|
| 2429 |
+
],
|
| 2430 |
+
"text": "In Portland, Silas Hawthorne signed for Juniper Legal Services. The representative Silas Hawthorne returned to Portland.",
|
| 2431 |
+
"spans": [
|
| 2432 |
+
{
|
| 2433 |
+
"start": 3,
|
| 2434 |
+
"end": 11,
|
| 2435 |
+
"label": "CITY",
|
| 2436 |
+
"text": "Portland"
|
| 2437 |
+
},
|
| 2438 |
+
{
|
| 2439 |
+
"start": 13,
|
| 2440 |
+
"end": 28,
|
| 2441 |
+
"label": "PERSON",
|
| 2442 |
+
"text": "Silas Hawthorne"
|
| 2443 |
+
},
|
| 2444 |
+
{
|
| 2445 |
+
"start": 40,
|
| 2446 |
+
"end": 62,
|
| 2447 |
+
"label": "ORGANIZATION",
|
| 2448 |
+
"text": "Juniper Legal Services"
|
| 2449 |
+
},
|
| 2450 |
+
{
|
| 2451 |
+
"start": 83,
|
| 2452 |
+
"end": 98,
|
| 2453 |
+
"label": "PERSON",
|
| 2454 |
+
"text": "Silas Hawthorne"
|
| 2455 |
+
},
|
| 2456 |
+
{
|
| 2457 |
+
"start": 111,
|
| 2458 |
+
"end": 119,
|
| 2459 |
+
"label": "CITY",
|
| 2460 |
+
"text": "Portland"
|
| 2461 |
+
}
|
| 2462 |
+
],
|
| 2463 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 2464 |
+
},
|
| 2465 |
+
{
|
| 2466 |
+
"id": "holdout03c-076",
|
| 2467 |
+
"language": "de",
|
| 2468 |
+
"ocr_noise": true,
|
| 2469 |
+
"features": [
|
| 2470 |
+
"line_wrap",
|
| 2471 |
+
"organization"
|
| 2472 |
+
],
|
| 2473 |
+
"text": "Name: Silas-\nHawthorne\nOrt: Port-\nland\nArbeitgeber: Juniper Legal Services.",
|
| 2474 |
+
"spans": [
|
| 2475 |
+
{
|
| 2476 |
+
"start": 6,
|
| 2477 |
+
"end": 22,
|
| 2478 |
+
"label": "PERSON",
|
| 2479 |
+
"text": "Silas-\nHawthorne"
|
| 2480 |
+
},
|
| 2481 |
+
{
|
| 2482 |
+
"start": 28,
|
| 2483 |
+
"end": 38,
|
| 2484 |
+
"label": "CITY",
|
| 2485 |
+
"text": "Port-\nland"
|
| 2486 |
+
},
|
| 2487 |
+
{
|
| 2488 |
+
"start": 52,
|
| 2489 |
+
"end": 74,
|
| 2490 |
+
"label": "ORGANIZATION",
|
| 2491 |
+
"text": "Juniper Legal Services"
|
| 2492 |
+
}
|
| 2493 |
+
],
|
| 2494 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 2495 |
+
},
|
| 2496 |
+
{
|
| 2497 |
+
"id": "holdout03c-077",
|
| 2498 |
+
"language": "en",
|
| 2499 |
+
"ocr_noise": true,
|
| 2500 |
+
"features": [
|
| 2501 |
+
"ocr_spelling",
|
| 2502 |
+
"organization"
|
| 2503 |
+
],
|
| 2504 |
+
"text": "Scanned record: S1las Hawth0rne resides in P0rtl4nd and works at Juniper Legal Services.",
|
| 2505 |
+
"spans": [
|
| 2506 |
+
{
|
| 2507 |
+
"start": 16,
|
| 2508 |
+
"end": 31,
|
| 2509 |
+
"label": "PERSON",
|
| 2510 |
+
"text": "S1las Hawth0rne"
|
| 2511 |
+
},
|
| 2512 |
+
{
|
| 2513 |
+
"start": 43,
|
| 2514 |
+
"end": 51,
|
| 2515 |
+
"label": "CITY",
|
| 2516 |
+
"text": "P0rtl4nd"
|
| 2517 |
+
},
|
| 2518 |
+
{
|
| 2519 |
+
"start": 65,
|
| 2520 |
+
"end": 87,
|
| 2521 |
+
"label": "ORGANIZATION",
|
| 2522 |
+
"text": "Juniper Legal Services"
|
| 2523 |
+
}
|
| 2524 |
+
],
|
| 2525 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 2526 |
+
},
|
| 2527 |
+
{
|
| 2528 |
+
"id": "holdout03c-078",
|
| 2529 |
+
"language": "de",
|
| 2530 |
+
"ocr_noise": false,
|
| 2531 |
+
"features": [
|
| 2532 |
+
"religious_reference",
|
| 2533 |
+
"no_inferred_affiliation"
|
| 2534 |
+
],
|
| 2535 |
+
"text": "Das Thema evangelisch wird in einem Bericht von Juniper Legal Services erwähnt. Für Silas Hawthorne wird keine Religionszugehörigkeit behauptet.",
|
| 2536 |
+
"spans": [
|
| 2537 |
+
{
|
| 2538 |
+
"start": 10,
|
| 2539 |
+
"end": 21,
|
| 2540 |
+
"label": "RELIGION",
|
| 2541 |
+
"text": "evangelisch"
|
| 2542 |
+
},
|
| 2543 |
+
{
|
| 2544 |
+
"start": 48,
|
| 2545 |
+
"end": 70,
|
| 2546 |
+
"label": "ORGANIZATION",
|
| 2547 |
+
"text": "Juniper Legal Services"
|
| 2548 |
+
},
|
| 2549 |
+
{
|
| 2550 |
+
"start": 84,
|
| 2551 |
+
"end": 99,
|
| 2552 |
+
"label": "PERSON",
|
| 2553 |
+
"text": "Silas Hawthorne"
|
| 2554 |
+
}
|
| 2555 |
+
],
|
| 2556 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 2557 |
+
},
|
| 2558 |
+
{
|
| 2559 |
+
"id": "holdout03c-079",
|
| 2560 |
+
"language": "en",
|
| 2561 |
+
"ocr_noise": false,
|
| 2562 |
+
"features": [
|
| 2563 |
+
"negative",
|
| 2564 |
+
"religious_reference"
|
| 2565 |
+
],
|
| 2566 |
+
"text": "Worksheet 108: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 2567 |
+
"spans": [],
|
| 2568 |
+
"expected": "Worksheet 108: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 2569 |
+
},
|
| 2570 |
+
{
|
| 2571 |
+
"id": "holdout03c-080",
|
| 2572 |
+
"language": "de",
|
| 2573 |
+
"ocr_noise": false,
|
| 2574 |
+
"features": [
|
| 2575 |
+
"organization",
|
| 2576 |
+
"geography"
|
| 2577 |
+
],
|
| 2578 |
+
"text": "Auskunft: Alva Rosenqvist arbeitet für Maple Ridge University in Bergen.",
|
| 2579 |
+
"spans": [
|
| 2580 |
+
{
|
| 2581 |
+
"start": 10,
|
| 2582 |
+
"end": 25,
|
| 2583 |
+
"label": "PERSON",
|
| 2584 |
+
"text": "Alva Rosenqvist"
|
| 2585 |
+
},
|
| 2586 |
+
{
|
| 2587 |
+
"start": 39,
|
| 2588 |
+
"end": 61,
|
| 2589 |
+
"label": "ORGANIZATION",
|
| 2590 |
+
"text": "Maple Ridge University"
|
| 2591 |
+
},
|
| 2592 |
+
{
|
| 2593 |
+
"start": 65,
|
| 2594 |
+
"end": 71,
|
| 2595 |
+
"label": "CITY",
|
| 2596 |
+
"text": "Bergen"
|
| 2597 |
+
}
|
| 2598 |
+
],
|
| 2599 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 2600 |
+
},
|
| 2601 |
+
{
|
| 2602 |
+
"id": "holdout03c-081",
|
| 2603 |
+
"language": "en",
|
| 2604 |
+
"ocr_noise": false,
|
| 2605 |
+
"features": [
|
| 2606 |
+
"organization",
|
| 2607 |
+
"geography"
|
| 2608 |
+
],
|
| 2609 |
+
"text": "The contact Alva Rosenqvist represents Maple Ridge University near Bergen.",
|
| 2610 |
+
"spans": [
|
| 2611 |
+
{
|
| 2612 |
+
"start": 12,
|
| 2613 |
+
"end": 27,
|
| 2614 |
+
"label": "PERSON",
|
| 2615 |
+
"text": "Alva Rosenqvist"
|
| 2616 |
+
},
|
| 2617 |
+
{
|
| 2618 |
+
"start": 39,
|
| 2619 |
+
"end": 61,
|
| 2620 |
+
"label": "ORGANIZATION",
|
| 2621 |
+
"text": "Maple Ridge University"
|
| 2622 |
+
},
|
| 2623 |
+
{
|
| 2624 |
+
"start": 67,
|
| 2625 |
+
"end": 73,
|
| 2626 |
+
"label": "CITY",
|
| 2627 |
+
"text": "Bergen"
|
| 2628 |
+
}
|
| 2629 |
+
],
|
| 2630 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 2631 |
+
},
|
| 2632 |
+
{
|
| 2633 |
+
"id": "holdout03c-082",
|
| 2634 |
+
"language": "de",
|
| 2635 |
+
"ocr_noise": false,
|
| 2636 |
+
"features": [
|
| 2637 |
+
"affiliation",
|
| 2638 |
+
"contacts"
|
| 2639 |
+
],
|
| 2640 |
+
"text": "Alva Rosenqvist nennt jüdisch als Zugehörigkeit. E-Mail: case109.review@example.invalid; Telefon: +49 541 730109.",
|
| 2641 |
+
"spans": [
|
| 2642 |
+
{
|
| 2643 |
+
"start": 0,
|
| 2644 |
+
"end": 15,
|
| 2645 |
+
"label": "PERSON",
|
| 2646 |
+
"text": "Alva Rosenqvist"
|
| 2647 |
+
},
|
| 2648 |
+
{
|
| 2649 |
+
"start": 22,
|
| 2650 |
+
"end": 29,
|
| 2651 |
+
"label": "RELIGION",
|
| 2652 |
+
"text": "jüdisch"
|
| 2653 |
+
},
|
| 2654 |
+
{
|
| 2655 |
+
"start": 57,
|
| 2656 |
+
"end": 87,
|
| 2657 |
+
"label": "EMAIL",
|
| 2658 |
+
"text": "case109.review@example.invalid"
|
| 2659 |
+
},
|
| 2660 |
+
{
|
| 2661 |
+
"start": 98,
|
| 2662 |
+
"end": 112,
|
| 2663 |
+
"label": "PHONE",
|
| 2664 |
+
"text": "+49 541 730109"
|
| 2665 |
+
}
|
| 2666 |
+
],
|
| 2667 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 2668 |
+
},
|
| 2669 |
+
{
|
| 2670 |
+
"id": "holdout03c-083",
|
| 2671 |
+
"language": "en",
|
| 2672 |
+
"ocr_noise": false,
|
| 2673 |
+
"features": [
|
| 2674 |
+
"affiliation",
|
| 2675 |
+
"contacts"
|
| 2676 |
+
],
|
| 2677 |
+
"text": "Alva Rosenqvist explicitly names Judaism as their faith. Write to case109.review@example.invalid or call +49 541 730109.",
|
| 2678 |
+
"spans": [
|
| 2679 |
+
{
|
| 2680 |
+
"start": 0,
|
| 2681 |
+
"end": 15,
|
| 2682 |
+
"label": "PERSON",
|
| 2683 |
+
"text": "Alva Rosenqvist"
|
| 2684 |
+
},
|
| 2685 |
+
{
|
| 2686 |
+
"start": 33,
|
| 2687 |
+
"end": 40,
|
| 2688 |
+
"label": "RELIGION",
|
| 2689 |
+
"text": "Judaism"
|
| 2690 |
+
},
|
| 2691 |
+
{
|
| 2692 |
+
"start": 66,
|
| 2693 |
+
"end": 96,
|
| 2694 |
+
"label": "EMAIL",
|
| 2695 |
+
"text": "case109.review@example.invalid"
|
| 2696 |
+
},
|
| 2697 |
+
{
|
| 2698 |
+
"start": 105,
|
| 2699 |
+
"end": 119,
|
| 2700 |
+
"label": "PHONE",
|
| 2701 |
+
"text": "+49 541 730109"
|
| 2702 |
+
}
|
| 2703 |
+
],
|
| 2704 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 2705 |
+
},
|
| 2706 |
+
{
|
| 2707 |
+
"id": "holdout03c-084",
|
| 2708 |
+
"language": "de",
|
| 2709 |
+
"ocr_noise": false,
|
| 2710 |
+
"features": [
|
| 2711 |
+
"repeats",
|
| 2712 |
+
"paragraphs"
|
| 2713 |
+
],
|
| 2714 |
+
"text": "Alva Rosenqvist besucht Bergen.\n\nAlva Rosenqvist bestätigt den Termin bei Maple Ridge University.",
|
| 2715 |
+
"spans": [
|
| 2716 |
+
{
|
| 2717 |
+
"start": 0,
|
| 2718 |
+
"end": 15,
|
| 2719 |
+
"label": "PERSON",
|
| 2720 |
+
"text": "Alva Rosenqvist"
|
| 2721 |
+
},
|
| 2722 |
+
{
|
| 2723 |
+
"start": 24,
|
| 2724 |
+
"end": 30,
|
| 2725 |
+
"label": "CITY",
|
| 2726 |
+
"text": "Bergen"
|
| 2727 |
+
},
|
| 2728 |
+
{
|
| 2729 |
+
"start": 33,
|
| 2730 |
+
"end": 48,
|
| 2731 |
+
"label": "PERSON",
|
| 2732 |
+
"text": "Alva Rosenqvist"
|
| 2733 |
+
},
|
| 2734 |
+
{
|
| 2735 |
+
"start": 74,
|
| 2736 |
+
"end": 96,
|
| 2737 |
+
"label": "ORGANIZATION",
|
| 2738 |
+
"text": "Maple Ridge University"
|
| 2739 |
+
}
|
| 2740 |
+
],
|
| 2741 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 2742 |
+
},
|
| 2743 |
+
{
|
| 2744 |
+
"id": "holdout03c-085",
|
| 2745 |
+
"language": "en",
|
| 2746 |
+
"ocr_noise": false,
|
| 2747 |
+
"features": [
|
| 2748 |
+
"repeats",
|
| 2749 |
+
"ambiguous_geography"
|
| 2750 |
+
],
|
| 2751 |
+
"text": "In Bergen, Alva Rosenqvist signed for Maple Ridge University. The representative Alva Rosenqvist returned to Bergen.",
|
| 2752 |
+
"spans": [
|
| 2753 |
+
{
|
| 2754 |
+
"start": 3,
|
| 2755 |
+
"end": 9,
|
| 2756 |
+
"label": "CITY",
|
| 2757 |
+
"text": "Bergen"
|
| 2758 |
+
},
|
| 2759 |
+
{
|
| 2760 |
+
"start": 11,
|
| 2761 |
+
"end": 26,
|
| 2762 |
+
"label": "PERSON",
|
| 2763 |
+
"text": "Alva Rosenqvist"
|
| 2764 |
+
},
|
| 2765 |
+
{
|
| 2766 |
+
"start": 38,
|
| 2767 |
+
"end": 60,
|
| 2768 |
+
"label": "ORGANIZATION",
|
| 2769 |
+
"text": "Maple Ridge University"
|
| 2770 |
+
},
|
| 2771 |
+
{
|
| 2772 |
+
"start": 81,
|
| 2773 |
+
"end": 96,
|
| 2774 |
+
"label": "PERSON",
|
| 2775 |
+
"text": "Alva Rosenqvist"
|
| 2776 |
+
},
|
| 2777 |
+
{
|
| 2778 |
+
"start": 109,
|
| 2779 |
+
"end": 115,
|
| 2780 |
+
"label": "CITY",
|
| 2781 |
+
"text": "Bergen"
|
| 2782 |
+
}
|
| 2783 |
+
],
|
| 2784 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 2785 |
+
},
|
| 2786 |
+
{
|
| 2787 |
+
"id": "holdout03c-086",
|
| 2788 |
+
"language": "de",
|
| 2789 |
+
"ocr_noise": true,
|
| 2790 |
+
"features": [
|
| 2791 |
+
"line_wrap",
|
| 2792 |
+
"organization"
|
| 2793 |
+
],
|
| 2794 |
+
"text": "Name: Alva-\nRosenqvist\nOrt: Ber-\ngen\nArbeitgeber: Maple Ridge University.",
|
| 2795 |
+
"spans": [
|
| 2796 |
+
{
|
| 2797 |
+
"start": 6,
|
| 2798 |
+
"end": 22,
|
| 2799 |
+
"label": "PERSON",
|
| 2800 |
+
"text": "Alva-\nRosenqvist"
|
| 2801 |
+
},
|
| 2802 |
+
{
|
| 2803 |
+
"start": 28,
|
| 2804 |
+
"end": 36,
|
| 2805 |
+
"label": "CITY",
|
| 2806 |
+
"text": "Ber-\ngen"
|
| 2807 |
+
},
|
| 2808 |
+
{
|
| 2809 |
+
"start": 50,
|
| 2810 |
+
"end": 72,
|
| 2811 |
+
"label": "ORGANIZATION",
|
| 2812 |
+
"text": "Maple Ridge University"
|
| 2813 |
+
}
|
| 2814 |
+
],
|
| 2815 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 2816 |
+
},
|
| 2817 |
+
{
|
| 2818 |
+
"id": "holdout03c-087",
|
| 2819 |
+
"language": "en",
|
| 2820 |
+
"ocr_noise": true,
|
| 2821 |
+
"features": [
|
| 2822 |
+
"ocr_spelling",
|
| 2823 |
+
"organization"
|
| 2824 |
+
],
|
| 2825 |
+
"text": "Scanned record: Alva R0senqv1st resides in Bergen and works at Maple Ridge University.",
|
| 2826 |
+
"spans": [
|
| 2827 |
+
{
|
| 2828 |
+
"start": 16,
|
| 2829 |
+
"end": 31,
|
| 2830 |
+
"label": "PERSON",
|
| 2831 |
+
"text": "Alva R0senqv1st"
|
| 2832 |
+
},
|
| 2833 |
+
{
|
| 2834 |
+
"start": 43,
|
| 2835 |
+
"end": 49,
|
| 2836 |
+
"label": "CITY",
|
| 2837 |
+
"text": "Bergen"
|
| 2838 |
+
},
|
| 2839 |
+
{
|
| 2840 |
+
"start": 63,
|
| 2841 |
+
"end": 85,
|
| 2842 |
+
"label": "ORGANIZATION",
|
| 2843 |
+
"text": "Maple Ridge University"
|
| 2844 |
+
}
|
| 2845 |
+
],
|
| 2846 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 2847 |
+
},
|
| 2848 |
+
{
|
| 2849 |
+
"id": "holdout03c-088",
|
| 2850 |
+
"language": "de",
|
| 2851 |
+
"ocr_noise": false,
|
| 2852 |
+
"features": [
|
| 2853 |
+
"religious_reference",
|
| 2854 |
+
"no_inferred_affiliation"
|
| 2855 |
+
],
|
| 2856 |
+
"text": "Das Thema jüdisch wird in einem Bericht von Maple Ridge University erwähnt. Für Alva Rosenqvist wird keine Religionszugehörigkeit behauptet.",
|
| 2857 |
+
"spans": [
|
| 2858 |
+
{
|
| 2859 |
+
"start": 10,
|
| 2860 |
+
"end": 17,
|
| 2861 |
+
"label": "RELIGION",
|
| 2862 |
+
"text": "jüdisch"
|
| 2863 |
+
},
|
| 2864 |
+
{
|
| 2865 |
+
"start": 44,
|
| 2866 |
+
"end": 66,
|
| 2867 |
+
"label": "ORGANIZATION",
|
| 2868 |
+
"text": "Maple Ridge University"
|
| 2869 |
+
},
|
| 2870 |
+
{
|
| 2871 |
+
"start": 80,
|
| 2872 |
+
"end": 95,
|
| 2873 |
+
"label": "PERSON",
|
| 2874 |
+
"text": "Alva Rosenqvist"
|
| 2875 |
+
}
|
| 2876 |
+
],
|
| 2877 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 2878 |
+
},
|
| 2879 |
+
{
|
| 2880 |
+
"id": "holdout03c-089",
|
| 2881 |
+
"language": "en",
|
| 2882 |
+
"ocr_noise": false,
|
| 2883 |
+
"features": [
|
| 2884 |
+
"negative",
|
| 2885 |
+
"religious_reference"
|
| 2886 |
+
],
|
| 2887 |
+
"text": "Worksheet 109: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 2888 |
+
"spans": [],
|
| 2889 |
+
"expected": "Worksheet 109: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 2890 |
+
},
|
| 2891 |
+
{
|
| 2892 |
+
"id": "holdout03c-090",
|
| 2893 |
+
"language": "de",
|
| 2894 |
+
"ocr_noise": false,
|
| 2895 |
+
"features": [
|
| 2896 |
+
"organization",
|
| 2897 |
+
"geography"
|
| 2898 |
+
],
|
| 2899 |
+
"text": "Auskunft: Theo Merriweather arbeitet für Polar Systems Ltd. in York.",
|
| 2900 |
+
"spans": [
|
| 2901 |
+
{
|
| 2902 |
+
"start": 10,
|
| 2903 |
+
"end": 27,
|
| 2904 |
+
"label": "PERSON",
|
| 2905 |
+
"text": "Theo Merriweather"
|
| 2906 |
+
},
|
| 2907 |
+
{
|
| 2908 |
+
"start": 41,
|
| 2909 |
+
"end": 59,
|
| 2910 |
+
"label": "ORGANIZATION",
|
| 2911 |
+
"text": "Polar Systems Ltd."
|
| 2912 |
+
},
|
| 2913 |
+
{
|
| 2914 |
+
"start": 63,
|
| 2915 |
+
"end": 67,
|
| 2916 |
+
"label": "CITY",
|
| 2917 |
+
"text": "York"
|
| 2918 |
+
}
|
| 2919 |
+
],
|
| 2920 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 2921 |
+
},
|
| 2922 |
+
{
|
| 2923 |
+
"id": "holdout03c-091",
|
| 2924 |
+
"language": "en",
|
| 2925 |
+
"ocr_noise": false,
|
| 2926 |
+
"features": [
|
| 2927 |
+
"organization",
|
| 2928 |
+
"geography"
|
| 2929 |
+
],
|
| 2930 |
+
"text": "The contact Theo Merriweather represents Polar Systems Ltd. near York.",
|
| 2931 |
+
"spans": [
|
| 2932 |
+
{
|
| 2933 |
+
"start": 12,
|
| 2934 |
+
"end": 29,
|
| 2935 |
+
"label": "PERSON",
|
| 2936 |
+
"text": "Theo Merriweather"
|
| 2937 |
+
},
|
| 2938 |
+
{
|
| 2939 |
+
"start": 41,
|
| 2940 |
+
"end": 59,
|
| 2941 |
+
"label": "ORGANIZATION",
|
| 2942 |
+
"text": "Polar Systems Ltd."
|
| 2943 |
+
},
|
| 2944 |
+
{
|
| 2945 |
+
"start": 65,
|
| 2946 |
+
"end": 69,
|
| 2947 |
+
"label": "CITY",
|
| 2948 |
+
"text": "York"
|
| 2949 |
+
}
|
| 2950 |
+
],
|
| 2951 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 2952 |
+
},
|
| 2953 |
+
{
|
| 2954 |
+
"id": "holdout03c-092",
|
| 2955 |
+
"language": "de",
|
| 2956 |
+
"ocr_noise": false,
|
| 2957 |
+
"features": [
|
| 2958 |
+
"affiliation",
|
| 2959 |
+
"contacts"
|
| 2960 |
+
],
|
| 2961 |
+
"text": "Theo Merriweather nennt muslimisch als Zugehörigkeit. E-Mail: case110.review@example.invalid; Telefon: +49 541 730110.",
|
| 2962 |
+
"spans": [
|
| 2963 |
+
{
|
| 2964 |
+
"start": 0,
|
| 2965 |
+
"end": 17,
|
| 2966 |
+
"label": "PERSON",
|
| 2967 |
+
"text": "Theo Merriweather"
|
| 2968 |
+
},
|
| 2969 |
+
{
|
| 2970 |
+
"start": 24,
|
| 2971 |
+
"end": 34,
|
| 2972 |
+
"label": "RELIGION",
|
| 2973 |
+
"text": "muslimisch"
|
| 2974 |
+
},
|
| 2975 |
+
{
|
| 2976 |
+
"start": 62,
|
| 2977 |
+
"end": 92,
|
| 2978 |
+
"label": "EMAIL",
|
| 2979 |
+
"text": "case110.review@example.invalid"
|
| 2980 |
+
},
|
| 2981 |
+
{
|
| 2982 |
+
"start": 103,
|
| 2983 |
+
"end": 117,
|
| 2984 |
+
"label": "PHONE",
|
| 2985 |
+
"text": "+49 541 730110"
|
| 2986 |
+
}
|
| 2987 |
+
],
|
| 2988 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 2989 |
+
},
|
| 2990 |
+
{
|
| 2991 |
+
"id": "holdout03c-093",
|
| 2992 |
+
"language": "en",
|
| 2993 |
+
"ocr_noise": false,
|
| 2994 |
+
"features": [
|
| 2995 |
+
"affiliation",
|
| 2996 |
+
"contacts"
|
| 2997 |
+
],
|
| 2998 |
+
"text": "Theo Merriweather explicitly names Islam as their faith. Write to case110.review@example.invalid or call +49 541 730110.",
|
| 2999 |
+
"spans": [
|
| 3000 |
+
{
|
| 3001 |
+
"start": 0,
|
| 3002 |
+
"end": 17,
|
| 3003 |
+
"label": "PERSON",
|
| 3004 |
+
"text": "Theo Merriweather"
|
| 3005 |
+
},
|
| 3006 |
+
{
|
| 3007 |
+
"start": 35,
|
| 3008 |
+
"end": 40,
|
| 3009 |
+
"label": "RELIGION",
|
| 3010 |
+
"text": "Islam"
|
| 3011 |
+
},
|
| 3012 |
+
{
|
| 3013 |
+
"start": 66,
|
| 3014 |
+
"end": 96,
|
| 3015 |
+
"label": "EMAIL",
|
| 3016 |
+
"text": "case110.review@example.invalid"
|
| 3017 |
+
},
|
| 3018 |
+
{
|
| 3019 |
+
"start": 105,
|
| 3020 |
+
"end": 119,
|
| 3021 |
+
"label": "PHONE",
|
| 3022 |
+
"text": "+49 541 730110"
|
| 3023 |
+
}
|
| 3024 |
+
],
|
| 3025 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 3026 |
+
},
|
| 3027 |
+
{
|
| 3028 |
+
"id": "holdout03c-094",
|
| 3029 |
+
"language": "de",
|
| 3030 |
+
"ocr_noise": false,
|
| 3031 |
+
"features": [
|
| 3032 |
+
"repeats",
|
| 3033 |
+
"paragraphs"
|
| 3034 |
+
],
|
| 3035 |
+
"text": "Theo Merriweather besucht York.\n\nTheo Merriweather bestätigt den Termin bei Polar Systems Ltd..",
|
| 3036 |
+
"spans": [
|
| 3037 |
+
{
|
| 3038 |
+
"start": 0,
|
| 3039 |
+
"end": 17,
|
| 3040 |
+
"label": "PERSON",
|
| 3041 |
+
"text": "Theo Merriweather"
|
| 3042 |
+
},
|
| 3043 |
+
{
|
| 3044 |
+
"start": 26,
|
| 3045 |
+
"end": 30,
|
| 3046 |
+
"label": "CITY",
|
| 3047 |
+
"text": "York"
|
| 3048 |
+
},
|
| 3049 |
+
{
|
| 3050 |
+
"start": 33,
|
| 3051 |
+
"end": 50,
|
| 3052 |
+
"label": "PERSON",
|
| 3053 |
+
"text": "Theo Merriweather"
|
| 3054 |
+
},
|
| 3055 |
+
{
|
| 3056 |
+
"start": 76,
|
| 3057 |
+
"end": 94,
|
| 3058 |
+
"label": "ORGANIZATION",
|
| 3059 |
+
"text": "Polar Systems Ltd."
|
| 3060 |
+
}
|
| 3061 |
+
],
|
| 3062 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 3063 |
+
},
|
| 3064 |
+
{
|
| 3065 |
+
"id": "holdout03c-095",
|
| 3066 |
+
"language": "en",
|
| 3067 |
+
"ocr_noise": false,
|
| 3068 |
+
"features": [
|
| 3069 |
+
"repeats",
|
| 3070 |
+
"ambiguous_geography"
|
| 3071 |
+
],
|
| 3072 |
+
"text": "In York, Theo Merriweather signed for Polar Systems Ltd.. The representative Theo Merriweather returned to York.",
|
| 3073 |
+
"spans": [
|
| 3074 |
+
{
|
| 3075 |
+
"start": 3,
|
| 3076 |
+
"end": 7,
|
| 3077 |
+
"label": "CITY",
|
| 3078 |
+
"text": "York"
|
| 3079 |
+
},
|
| 3080 |
+
{
|
| 3081 |
+
"start": 9,
|
| 3082 |
+
"end": 26,
|
| 3083 |
+
"label": "PERSON",
|
| 3084 |
+
"text": "Theo Merriweather"
|
| 3085 |
+
},
|
| 3086 |
+
{
|
| 3087 |
+
"start": 38,
|
| 3088 |
+
"end": 56,
|
| 3089 |
+
"label": "ORGANIZATION",
|
| 3090 |
+
"text": "Polar Systems Ltd."
|
| 3091 |
+
},
|
| 3092 |
+
{
|
| 3093 |
+
"start": 77,
|
| 3094 |
+
"end": 94,
|
| 3095 |
+
"label": "PERSON",
|
| 3096 |
+
"text": "Theo Merriweather"
|
| 3097 |
+
},
|
| 3098 |
+
{
|
| 3099 |
+
"start": 107,
|
| 3100 |
+
"end": 111,
|
| 3101 |
+
"label": "CITY",
|
| 3102 |
+
"text": "York"
|
| 3103 |
+
}
|
| 3104 |
+
],
|
| 3105 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 3106 |
+
},
|
| 3107 |
+
{
|
| 3108 |
+
"id": "holdout03c-096",
|
| 3109 |
+
"language": "de",
|
| 3110 |
+
"ocr_noise": true,
|
| 3111 |
+
"features": [
|
| 3112 |
+
"line_wrap",
|
| 3113 |
+
"organization"
|
| 3114 |
+
],
|
| 3115 |
+
"text": "Name: Theo-\nMerriweather\nOrt: Yo-\nrk\nArbeitgeber: Polar Systems Ltd..",
|
| 3116 |
+
"spans": [
|
| 3117 |
+
{
|
| 3118 |
+
"start": 6,
|
| 3119 |
+
"end": 24,
|
| 3120 |
+
"label": "PERSON",
|
| 3121 |
+
"text": "Theo-\nMerriweather"
|
| 3122 |
+
},
|
| 3123 |
+
{
|
| 3124 |
+
"start": 30,
|
| 3125 |
+
"end": 36,
|
| 3126 |
+
"label": "CITY",
|
| 3127 |
+
"text": "Yo-\nrk"
|
| 3128 |
+
},
|
| 3129 |
+
{
|
| 3130 |
+
"start": 50,
|
| 3131 |
+
"end": 68,
|
| 3132 |
+
"label": "ORGANIZATION",
|
| 3133 |
+
"text": "Polar Systems Ltd."
|
| 3134 |
+
}
|
| 3135 |
+
],
|
| 3136 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 3137 |
+
},
|
| 3138 |
+
{
|
| 3139 |
+
"id": "holdout03c-097",
|
| 3140 |
+
"language": "en",
|
| 3141 |
+
"ocr_noise": true,
|
| 3142 |
+
"features": [
|
| 3143 |
+
"ocr_spelling",
|
| 3144 |
+
"organization"
|
| 3145 |
+
],
|
| 3146 |
+
"text": "Scanned record: The0 Merr1weather resides in Y0rk and works at Polar Systems Ltd..",
|
| 3147 |
+
"spans": [
|
| 3148 |
+
{
|
| 3149 |
+
"start": 16,
|
| 3150 |
+
"end": 33,
|
| 3151 |
+
"label": "PERSON",
|
| 3152 |
+
"text": "The0 Merr1weather"
|
| 3153 |
+
},
|
| 3154 |
+
{
|
| 3155 |
+
"start": 45,
|
| 3156 |
+
"end": 49,
|
| 3157 |
+
"label": "CITY",
|
| 3158 |
+
"text": "Y0rk"
|
| 3159 |
+
},
|
| 3160 |
+
{
|
| 3161 |
+
"start": 63,
|
| 3162 |
+
"end": 81,
|
| 3163 |
+
"label": "ORGANIZATION",
|
| 3164 |
+
"text": "Polar Systems Ltd."
|
| 3165 |
+
}
|
| 3166 |
+
],
|
| 3167 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 3168 |
+
},
|
| 3169 |
+
{
|
| 3170 |
+
"id": "holdout03c-098",
|
| 3171 |
+
"language": "de",
|
| 3172 |
+
"ocr_noise": false,
|
| 3173 |
+
"features": [
|
| 3174 |
+
"religious_reference",
|
| 3175 |
+
"no_inferred_affiliation"
|
| 3176 |
+
],
|
| 3177 |
+
"text": "Das Thema muslimisch wird in einem Bericht von Polar Systems Ltd. erwähnt. Für Theo Merriweather wird keine Religionszugehörigkeit behauptet.",
|
| 3178 |
+
"spans": [
|
| 3179 |
+
{
|
| 3180 |
+
"start": 10,
|
| 3181 |
+
"end": 20,
|
| 3182 |
+
"label": "RELIGION",
|
| 3183 |
+
"text": "muslimisch"
|
| 3184 |
+
},
|
| 3185 |
+
{
|
| 3186 |
+
"start": 47,
|
| 3187 |
+
"end": 65,
|
| 3188 |
+
"label": "ORGANIZATION",
|
| 3189 |
+
"text": "Polar Systems Ltd."
|
| 3190 |
+
},
|
| 3191 |
+
{
|
| 3192 |
+
"start": 79,
|
| 3193 |
+
"end": 96,
|
| 3194 |
+
"label": "PERSON",
|
| 3195 |
+
"text": "Theo Merriweather"
|
| 3196 |
+
}
|
| 3197 |
+
],
|
| 3198 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 3199 |
+
},
|
| 3200 |
+
{
|
| 3201 |
+
"id": "holdout03c-099",
|
| 3202 |
+
"language": "en",
|
| 3203 |
+
"ocr_noise": false,
|
| 3204 |
+
"features": [
|
| 3205 |
+
"negative",
|
| 3206 |
+
"religious_reference"
|
| 3207 |
+
],
|
| 3208 |
+
"text": "Worksheet 110: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 3209 |
+
"spans": [],
|
| 3210 |
+
"expected": "Worksheet 110: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 3211 |
+
},
|
| 3212 |
+
{
|
| 3213 |
+
"id": "holdout03c-100",
|
| 3214 |
+
"language": "de",
|
| 3215 |
+
"ocr_noise": false,
|
| 3216 |
+
"features": [
|
| 3217 |
+
"organization",
|
| 3218 |
+
"geography"
|
| 3219 |
+
],
|
| 3220 |
+
"text": "Auskunft: Maja Kirschbaum arbeitet für Sonnenbogen Verein in Lüneburg.",
|
| 3221 |
+
"spans": [
|
| 3222 |
+
{
|
| 3223 |
+
"start": 10,
|
| 3224 |
+
"end": 25,
|
| 3225 |
+
"label": "PERSON",
|
| 3226 |
+
"text": "Maja Kirschbaum"
|
| 3227 |
+
},
|
| 3228 |
+
{
|
| 3229 |
+
"start": 39,
|
| 3230 |
+
"end": 57,
|
| 3231 |
+
"label": "ORGANIZATION",
|
| 3232 |
+
"text": "Sonnenbogen Verein"
|
| 3233 |
+
},
|
| 3234 |
+
{
|
| 3235 |
+
"start": 61,
|
| 3236 |
+
"end": 69,
|
| 3237 |
+
"label": "CITY",
|
| 3238 |
+
"text": "Lüneburg"
|
| 3239 |
+
}
|
| 3240 |
+
],
|
| 3241 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 3242 |
+
},
|
| 3243 |
+
{
|
| 3244 |
+
"id": "holdout03c-101",
|
| 3245 |
+
"language": "en",
|
| 3246 |
+
"ocr_noise": false,
|
| 3247 |
+
"features": [
|
| 3248 |
+
"organization",
|
| 3249 |
+
"geography"
|
| 3250 |
+
],
|
| 3251 |
+
"text": "The contact Maja Kirschbaum represents Sonnenbogen Verein near Lüneburg.",
|
| 3252 |
+
"spans": [
|
| 3253 |
+
{
|
| 3254 |
+
"start": 12,
|
| 3255 |
+
"end": 27,
|
| 3256 |
+
"label": "PERSON",
|
| 3257 |
+
"text": "Maja Kirschbaum"
|
| 3258 |
+
},
|
| 3259 |
+
{
|
| 3260 |
+
"start": 39,
|
| 3261 |
+
"end": 57,
|
| 3262 |
+
"label": "ORGANIZATION",
|
| 3263 |
+
"text": "Sonnenbogen Verein"
|
| 3264 |
+
},
|
| 3265 |
+
{
|
| 3266 |
+
"start": 63,
|
| 3267 |
+
"end": 71,
|
| 3268 |
+
"label": "CITY",
|
| 3269 |
+
"text": "Lüneburg"
|
| 3270 |
+
}
|
| 3271 |
+
],
|
| 3272 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 3273 |
+
},
|
| 3274 |
+
{
|
| 3275 |
+
"id": "holdout03c-102",
|
| 3276 |
+
"language": "de",
|
| 3277 |
+
"ocr_noise": false,
|
| 3278 |
+
"features": [
|
| 3279 |
+
"affiliation",
|
| 3280 |
+
"contacts"
|
| 3281 |
+
],
|
| 3282 |
+
"text": "Maja Kirschbaum nennt konfessionslos als Zugehörigkeit. E-Mail: case111.review@example.invalid; Telefon: +49 541 730111.",
|
| 3283 |
+
"spans": [
|
| 3284 |
+
{
|
| 3285 |
+
"start": 0,
|
| 3286 |
+
"end": 15,
|
| 3287 |
+
"label": "PERSON",
|
| 3288 |
+
"text": "Maja Kirschbaum"
|
| 3289 |
+
},
|
| 3290 |
+
{
|
| 3291 |
+
"start": 22,
|
| 3292 |
+
"end": 36,
|
| 3293 |
+
"label": "RELIGION",
|
| 3294 |
+
"text": "konfessionslos"
|
| 3295 |
+
},
|
| 3296 |
+
{
|
| 3297 |
+
"start": 64,
|
| 3298 |
+
"end": 94,
|
| 3299 |
+
"label": "EMAIL",
|
| 3300 |
+
"text": "case111.review@example.invalid"
|
| 3301 |
+
},
|
| 3302 |
+
{
|
| 3303 |
+
"start": 105,
|
| 3304 |
+
"end": 119,
|
| 3305 |
+
"label": "PHONE",
|
| 3306 |
+
"text": "+49 541 730111"
|
| 3307 |
+
}
|
| 3308 |
+
],
|
| 3309 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 3310 |
+
},
|
| 3311 |
+
{
|
| 3312 |
+
"id": "holdout03c-103",
|
| 3313 |
+
"language": "en",
|
| 3314 |
+
"ocr_noise": false,
|
| 3315 |
+
"features": [
|
| 3316 |
+
"affiliation",
|
| 3317 |
+
"contacts"
|
| 3318 |
+
],
|
| 3319 |
+
"text": "Maja Kirschbaum explicitly names atheism as their faith. Write to case111.review@example.invalid or call +49 541 730111.",
|
| 3320 |
+
"spans": [
|
| 3321 |
+
{
|
| 3322 |
+
"start": 0,
|
| 3323 |
+
"end": 15,
|
| 3324 |
+
"label": "PERSON",
|
| 3325 |
+
"text": "Maja Kirschbaum"
|
| 3326 |
+
},
|
| 3327 |
+
{
|
| 3328 |
+
"start": 33,
|
| 3329 |
+
"end": 40,
|
| 3330 |
+
"label": "RELIGION",
|
| 3331 |
+
"text": "atheism"
|
| 3332 |
+
},
|
| 3333 |
+
{
|
| 3334 |
+
"start": 66,
|
| 3335 |
+
"end": 96,
|
| 3336 |
+
"label": "EMAIL",
|
| 3337 |
+
"text": "case111.review@example.invalid"
|
| 3338 |
+
},
|
| 3339 |
+
{
|
| 3340 |
+
"start": 105,
|
| 3341 |
+
"end": 119,
|
| 3342 |
+
"label": "PHONE",
|
| 3343 |
+
"text": "+49 541 730111"
|
| 3344 |
+
}
|
| 3345 |
+
],
|
| 3346 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 3347 |
+
},
|
| 3348 |
+
{
|
| 3349 |
+
"id": "holdout03c-104",
|
| 3350 |
+
"language": "de",
|
| 3351 |
+
"ocr_noise": false,
|
| 3352 |
+
"features": [
|
| 3353 |
+
"repeats",
|
| 3354 |
+
"paragraphs"
|
| 3355 |
+
],
|
| 3356 |
+
"text": "Maja Kirschbaum besucht Lüneburg.\n\nMaja Kirschbaum bestätigt den Termin bei Sonnenbogen Verein.",
|
| 3357 |
+
"spans": [
|
| 3358 |
+
{
|
| 3359 |
+
"start": 0,
|
| 3360 |
+
"end": 15,
|
| 3361 |
+
"label": "PERSON",
|
| 3362 |
+
"text": "Maja Kirschbaum"
|
| 3363 |
+
},
|
| 3364 |
+
{
|
| 3365 |
+
"start": 24,
|
| 3366 |
+
"end": 32,
|
| 3367 |
+
"label": "CITY",
|
| 3368 |
+
"text": "Lüneburg"
|
| 3369 |
+
},
|
| 3370 |
+
{
|
| 3371 |
+
"start": 35,
|
| 3372 |
+
"end": 50,
|
| 3373 |
+
"label": "PERSON",
|
| 3374 |
+
"text": "Maja Kirschbaum"
|
| 3375 |
+
},
|
| 3376 |
+
{
|
| 3377 |
+
"start": 76,
|
| 3378 |
+
"end": 94,
|
| 3379 |
+
"label": "ORGANIZATION",
|
| 3380 |
+
"text": "Sonnenbogen Verein"
|
| 3381 |
+
}
|
| 3382 |
+
],
|
| 3383 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 3384 |
+
},
|
| 3385 |
+
{
|
| 3386 |
+
"id": "holdout03c-105",
|
| 3387 |
+
"language": "en",
|
| 3388 |
+
"ocr_noise": false,
|
| 3389 |
+
"features": [
|
| 3390 |
+
"repeats",
|
| 3391 |
+
"ambiguous_geography"
|
| 3392 |
+
],
|
| 3393 |
+
"text": "In Lüneburg, Maja Kirschbaum signed for Sonnenbogen Verein. The representative Maja Kirschbaum returned to Lüneburg.",
|
| 3394 |
+
"spans": [
|
| 3395 |
+
{
|
| 3396 |
+
"start": 3,
|
| 3397 |
+
"end": 11,
|
| 3398 |
+
"label": "CITY",
|
| 3399 |
+
"text": "Lüneburg"
|
| 3400 |
+
},
|
| 3401 |
+
{
|
| 3402 |
+
"start": 13,
|
| 3403 |
+
"end": 28,
|
| 3404 |
+
"label": "PERSON",
|
| 3405 |
+
"text": "Maja Kirschbaum"
|
| 3406 |
+
},
|
| 3407 |
+
{
|
| 3408 |
+
"start": 40,
|
| 3409 |
+
"end": 58,
|
| 3410 |
+
"label": "ORGANIZATION",
|
| 3411 |
+
"text": "Sonnenbogen Verein"
|
| 3412 |
+
},
|
| 3413 |
+
{
|
| 3414 |
+
"start": 79,
|
| 3415 |
+
"end": 94,
|
| 3416 |
+
"label": "PERSON",
|
| 3417 |
+
"text": "Maja Kirschbaum"
|
| 3418 |
+
},
|
| 3419 |
+
{
|
| 3420 |
+
"start": 107,
|
| 3421 |
+
"end": 115,
|
| 3422 |
+
"label": "CITY",
|
| 3423 |
+
"text": "Lüneburg"
|
| 3424 |
+
}
|
| 3425 |
+
],
|
| 3426 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 3427 |
+
},
|
| 3428 |
+
{
|
| 3429 |
+
"id": "holdout03c-106",
|
| 3430 |
+
"language": "de",
|
| 3431 |
+
"ocr_noise": true,
|
| 3432 |
+
"features": [
|
| 3433 |
+
"line_wrap",
|
| 3434 |
+
"organization"
|
| 3435 |
+
],
|
| 3436 |
+
"text": "Name: Maja-\nKirschbaum\nOrt: Lüne-\nburg\nArbeitgeber: Sonnenbogen Verein.",
|
| 3437 |
+
"spans": [
|
| 3438 |
+
{
|
| 3439 |
+
"start": 6,
|
| 3440 |
+
"end": 22,
|
| 3441 |
+
"label": "PERSON",
|
| 3442 |
+
"text": "Maja-\nKirschbaum"
|
| 3443 |
+
},
|
| 3444 |
+
{
|
| 3445 |
+
"start": 28,
|
| 3446 |
+
"end": 38,
|
| 3447 |
+
"label": "CITY",
|
| 3448 |
+
"text": "Lüne-\nburg"
|
| 3449 |
+
},
|
| 3450 |
+
{
|
| 3451 |
+
"start": 52,
|
| 3452 |
+
"end": 70,
|
| 3453 |
+
"label": "ORGANIZATION",
|
| 3454 |
+
"text": "Sonnenbogen Verein"
|
| 3455 |
+
}
|
| 3456 |
+
],
|
| 3457 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 3458 |
+
},
|
| 3459 |
+
{
|
| 3460 |
+
"id": "holdout03c-107",
|
| 3461 |
+
"language": "en",
|
| 3462 |
+
"ocr_noise": true,
|
| 3463 |
+
"features": [
|
| 3464 |
+
"ocr_spelling",
|
| 3465 |
+
"organization"
|
| 3466 |
+
],
|
| 3467 |
+
"text": "Scanned record: Maja K1rschbaum resides in Lüneburg and works at Sonnenbogen Verein.",
|
| 3468 |
+
"spans": [
|
| 3469 |
+
{
|
| 3470 |
+
"start": 16,
|
| 3471 |
+
"end": 31,
|
| 3472 |
+
"label": "PERSON",
|
| 3473 |
+
"text": "Maja K1rschbaum"
|
| 3474 |
+
},
|
| 3475 |
+
{
|
| 3476 |
+
"start": 43,
|
| 3477 |
+
"end": 51,
|
| 3478 |
+
"label": "CITY",
|
| 3479 |
+
"text": "Lüneburg"
|
| 3480 |
+
},
|
| 3481 |
+
{
|
| 3482 |
+
"start": 65,
|
| 3483 |
+
"end": 83,
|
| 3484 |
+
"label": "ORGANIZATION",
|
| 3485 |
+
"text": "Sonnenbogen Verein"
|
| 3486 |
+
}
|
| 3487 |
+
],
|
| 3488 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 3489 |
+
},
|
| 3490 |
+
{
|
| 3491 |
+
"id": "holdout03c-108",
|
| 3492 |
+
"language": "de",
|
| 3493 |
+
"ocr_noise": false,
|
| 3494 |
+
"features": [
|
| 3495 |
+
"religious_reference",
|
| 3496 |
+
"no_inferred_affiliation"
|
| 3497 |
+
],
|
| 3498 |
+
"text": "Das Thema konfessionslos wird in einem Bericht von Sonnenbogen Verein erwähnt. Für Maja Kirschbaum wird keine Religionszugehörigkeit behauptet.",
|
| 3499 |
+
"spans": [
|
| 3500 |
+
{
|
| 3501 |
+
"start": 10,
|
| 3502 |
+
"end": 24,
|
| 3503 |
+
"label": "RELIGION",
|
| 3504 |
+
"text": "konfessionslos"
|
| 3505 |
+
},
|
| 3506 |
+
{
|
| 3507 |
+
"start": 51,
|
| 3508 |
+
"end": 69,
|
| 3509 |
+
"label": "ORGANIZATION",
|
| 3510 |
+
"text": "Sonnenbogen Verein"
|
| 3511 |
+
},
|
| 3512 |
+
{
|
| 3513 |
+
"start": 83,
|
| 3514 |
+
"end": 98,
|
| 3515 |
+
"label": "PERSON",
|
| 3516 |
+
"text": "Maja Kirschbaum"
|
| 3517 |
+
}
|
| 3518 |
+
],
|
| 3519 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 3520 |
+
},
|
| 3521 |
+
{
|
| 3522 |
+
"id": "holdout03c-109",
|
| 3523 |
+
"language": "en",
|
| 3524 |
+
"ocr_noise": false,
|
| 3525 |
+
"features": [
|
| 3526 |
+
"negative",
|
| 3527 |
+
"religious_reference"
|
| 3528 |
+
],
|
| 3529 |
+
"text": "Worksheet 111: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 3530 |
+
"spans": [],
|
| 3531 |
+
"expected": "Worksheet 111: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 3532 |
+
},
|
| 3533 |
+
{
|
| 3534 |
+
"id": "holdout03c-110",
|
| 3535 |
+
"language": "de",
|
| 3536 |
+
"ocr_noise": false,
|
| 3537 |
+
"features": [
|
| 3538 |
+
"organization",
|
| 3539 |
+
"geography"
|
| 3540 |
+
],
|
| 3541 |
+
"text": "Auskunft: Ronan Whitmore arbeitet für Beacon Valley Labs in Bath.",
|
| 3542 |
+
"spans": [
|
| 3543 |
+
{
|
| 3544 |
+
"start": 10,
|
| 3545 |
+
"end": 24,
|
| 3546 |
+
"label": "PERSON",
|
| 3547 |
+
"text": "Ronan Whitmore"
|
| 3548 |
+
},
|
| 3549 |
+
{
|
| 3550 |
+
"start": 38,
|
| 3551 |
+
"end": 56,
|
| 3552 |
+
"label": "ORGANIZATION",
|
| 3553 |
+
"text": "Beacon Valley Labs"
|
| 3554 |
+
},
|
| 3555 |
+
{
|
| 3556 |
+
"start": 60,
|
| 3557 |
+
"end": 64,
|
| 3558 |
+
"label": "CITY",
|
| 3559 |
+
"text": "Bath"
|
| 3560 |
+
}
|
| 3561 |
+
],
|
| 3562 |
+
"expected": "Auskunft: [PERSON] arbeitet für [ORGANIZATION] in [CITY]."
|
| 3563 |
+
},
|
| 3564 |
+
{
|
| 3565 |
+
"id": "holdout03c-111",
|
| 3566 |
+
"language": "en",
|
| 3567 |
+
"ocr_noise": false,
|
| 3568 |
+
"features": [
|
| 3569 |
+
"organization",
|
| 3570 |
+
"geography"
|
| 3571 |
+
],
|
| 3572 |
+
"text": "The contact Ronan Whitmore represents Beacon Valley Labs near Bath.",
|
| 3573 |
+
"spans": [
|
| 3574 |
+
{
|
| 3575 |
+
"start": 12,
|
| 3576 |
+
"end": 26,
|
| 3577 |
+
"label": "PERSON",
|
| 3578 |
+
"text": "Ronan Whitmore"
|
| 3579 |
+
},
|
| 3580 |
+
{
|
| 3581 |
+
"start": 38,
|
| 3582 |
+
"end": 56,
|
| 3583 |
+
"label": "ORGANIZATION",
|
| 3584 |
+
"text": "Beacon Valley Labs"
|
| 3585 |
+
},
|
| 3586 |
+
{
|
| 3587 |
+
"start": 62,
|
| 3588 |
+
"end": 66,
|
| 3589 |
+
"label": "CITY",
|
| 3590 |
+
"text": "Bath"
|
| 3591 |
+
}
|
| 3592 |
+
],
|
| 3593 |
+
"expected": "The contact [PERSON] represents [ORGANIZATION] near [CITY]."
|
| 3594 |
+
},
|
| 3595 |
+
{
|
| 3596 |
+
"id": "holdout03c-112",
|
| 3597 |
+
"language": "de",
|
| 3598 |
+
"ocr_noise": false,
|
| 3599 |
+
"features": [
|
| 3600 |
+
"affiliation",
|
| 3601 |
+
"contacts"
|
| 3602 |
+
],
|
| 3603 |
+
"text": "Ronan Whitmore nennt christlich als Zugehörigkeit. E-Mail: case112.review@example.invalid; Telefon: +49 541 730112.",
|
| 3604 |
+
"spans": [
|
| 3605 |
+
{
|
| 3606 |
+
"start": 0,
|
| 3607 |
+
"end": 14,
|
| 3608 |
+
"label": "PERSON",
|
| 3609 |
+
"text": "Ronan Whitmore"
|
| 3610 |
+
},
|
| 3611 |
+
{
|
| 3612 |
+
"start": 21,
|
| 3613 |
+
"end": 31,
|
| 3614 |
+
"label": "RELIGION",
|
| 3615 |
+
"text": "christlich"
|
| 3616 |
+
},
|
| 3617 |
+
{
|
| 3618 |
+
"start": 59,
|
| 3619 |
+
"end": 89,
|
| 3620 |
+
"label": "EMAIL",
|
| 3621 |
+
"text": "case112.review@example.invalid"
|
| 3622 |
+
},
|
| 3623 |
+
{
|
| 3624 |
+
"start": 100,
|
| 3625 |
+
"end": 114,
|
| 3626 |
+
"label": "PHONE",
|
| 3627 |
+
"text": "+49 541 730112"
|
| 3628 |
+
}
|
| 3629 |
+
],
|
| 3630 |
+
"expected": "[PERSON] nennt [RELIGION] als Zugehörigkeit. E-Mail: [EMAIL]; Telefon: [PHONE]."
|
| 3631 |
+
},
|
| 3632 |
+
{
|
| 3633 |
+
"id": "holdout03c-113",
|
| 3634 |
+
"language": "en",
|
| 3635 |
+
"ocr_noise": false,
|
| 3636 |
+
"features": [
|
| 3637 |
+
"affiliation",
|
| 3638 |
+
"contacts"
|
| 3639 |
+
],
|
| 3640 |
+
"text": "Ronan Whitmore explicitly names Christianity as their faith. Write to case112.review@example.invalid or call +49 541 730112.",
|
| 3641 |
+
"spans": [
|
| 3642 |
+
{
|
| 3643 |
+
"start": 0,
|
| 3644 |
+
"end": 14,
|
| 3645 |
+
"label": "PERSON",
|
| 3646 |
+
"text": "Ronan Whitmore"
|
| 3647 |
+
},
|
| 3648 |
+
{
|
| 3649 |
+
"start": 32,
|
| 3650 |
+
"end": 44,
|
| 3651 |
+
"label": "RELIGION",
|
| 3652 |
+
"text": "Christianity"
|
| 3653 |
+
},
|
| 3654 |
+
{
|
| 3655 |
+
"start": 70,
|
| 3656 |
+
"end": 100,
|
| 3657 |
+
"label": "EMAIL",
|
| 3658 |
+
"text": "case112.review@example.invalid"
|
| 3659 |
+
},
|
| 3660 |
+
{
|
| 3661 |
+
"start": 109,
|
| 3662 |
+
"end": 123,
|
| 3663 |
+
"label": "PHONE",
|
| 3664 |
+
"text": "+49 541 730112"
|
| 3665 |
+
}
|
| 3666 |
+
],
|
| 3667 |
+
"expected": "[PERSON] explicitly names [RELIGION] as their faith. Write to [EMAIL] or call [PHONE]."
|
| 3668 |
+
},
|
| 3669 |
+
{
|
| 3670 |
+
"id": "holdout03c-114",
|
| 3671 |
+
"language": "de",
|
| 3672 |
+
"ocr_noise": false,
|
| 3673 |
+
"features": [
|
| 3674 |
+
"repeats",
|
| 3675 |
+
"paragraphs"
|
| 3676 |
+
],
|
| 3677 |
+
"text": "Ronan Whitmore besucht Bath.\n\nRonan Whitmore bestätigt den Termin bei Beacon Valley Labs.",
|
| 3678 |
+
"spans": [
|
| 3679 |
+
{
|
| 3680 |
+
"start": 0,
|
| 3681 |
+
"end": 14,
|
| 3682 |
+
"label": "PERSON",
|
| 3683 |
+
"text": "Ronan Whitmore"
|
| 3684 |
+
},
|
| 3685 |
+
{
|
| 3686 |
+
"start": 23,
|
| 3687 |
+
"end": 27,
|
| 3688 |
+
"label": "CITY",
|
| 3689 |
+
"text": "Bath"
|
| 3690 |
+
},
|
| 3691 |
+
{
|
| 3692 |
+
"start": 30,
|
| 3693 |
+
"end": 44,
|
| 3694 |
+
"label": "PERSON",
|
| 3695 |
+
"text": "Ronan Whitmore"
|
| 3696 |
+
},
|
| 3697 |
+
{
|
| 3698 |
+
"start": 70,
|
| 3699 |
+
"end": 88,
|
| 3700 |
+
"label": "ORGANIZATION",
|
| 3701 |
+
"text": "Beacon Valley Labs"
|
| 3702 |
+
}
|
| 3703 |
+
],
|
| 3704 |
+
"expected": "[PERSON] besucht [CITY].\n\n[PERSON] bestätigt den Termin bei [ORGANIZATION]."
|
| 3705 |
+
},
|
| 3706 |
+
{
|
| 3707 |
+
"id": "holdout03c-115",
|
| 3708 |
+
"language": "en",
|
| 3709 |
+
"ocr_noise": false,
|
| 3710 |
+
"features": [
|
| 3711 |
+
"repeats",
|
| 3712 |
+
"ambiguous_geography"
|
| 3713 |
+
],
|
| 3714 |
+
"text": "In Bath, Ronan Whitmore signed for Beacon Valley Labs. The representative Ronan Whitmore returned to Bath.",
|
| 3715 |
+
"spans": [
|
| 3716 |
+
{
|
| 3717 |
+
"start": 3,
|
| 3718 |
+
"end": 7,
|
| 3719 |
+
"label": "CITY",
|
| 3720 |
+
"text": "Bath"
|
| 3721 |
+
},
|
| 3722 |
+
{
|
| 3723 |
+
"start": 9,
|
| 3724 |
+
"end": 23,
|
| 3725 |
+
"label": "PERSON",
|
| 3726 |
+
"text": "Ronan Whitmore"
|
| 3727 |
+
},
|
| 3728 |
+
{
|
| 3729 |
+
"start": 35,
|
| 3730 |
+
"end": 53,
|
| 3731 |
+
"label": "ORGANIZATION",
|
| 3732 |
+
"text": "Beacon Valley Labs"
|
| 3733 |
+
},
|
| 3734 |
+
{
|
| 3735 |
+
"start": 74,
|
| 3736 |
+
"end": 88,
|
| 3737 |
+
"label": "PERSON",
|
| 3738 |
+
"text": "Ronan Whitmore"
|
| 3739 |
+
},
|
| 3740 |
+
{
|
| 3741 |
+
"start": 101,
|
| 3742 |
+
"end": 105,
|
| 3743 |
+
"label": "CITY",
|
| 3744 |
+
"text": "Bath"
|
| 3745 |
+
}
|
| 3746 |
+
],
|
| 3747 |
+
"expected": "In [CITY], [PERSON] signed for [ORGANIZATION]. The representative [PERSON] returned to [CITY]."
|
| 3748 |
+
},
|
| 3749 |
+
{
|
| 3750 |
+
"id": "holdout03c-116",
|
| 3751 |
+
"language": "de",
|
| 3752 |
+
"ocr_noise": true,
|
| 3753 |
+
"features": [
|
| 3754 |
+
"line_wrap",
|
| 3755 |
+
"organization"
|
| 3756 |
+
],
|
| 3757 |
+
"text": "Name: Ronan-\nWhitmore\nOrt: Ba-\nth\nArbeitgeber: Beacon Valley Labs.",
|
| 3758 |
+
"spans": [
|
| 3759 |
+
{
|
| 3760 |
+
"start": 6,
|
| 3761 |
+
"end": 21,
|
| 3762 |
+
"label": "PERSON",
|
| 3763 |
+
"text": "Ronan-\nWhitmore"
|
| 3764 |
+
},
|
| 3765 |
+
{
|
| 3766 |
+
"start": 27,
|
| 3767 |
+
"end": 33,
|
| 3768 |
+
"label": "CITY",
|
| 3769 |
+
"text": "Ba-\nth"
|
| 3770 |
+
},
|
| 3771 |
+
{
|
| 3772 |
+
"start": 47,
|
| 3773 |
+
"end": 65,
|
| 3774 |
+
"label": "ORGANIZATION",
|
| 3775 |
+
"text": "Beacon Valley Labs"
|
| 3776 |
+
}
|
| 3777 |
+
],
|
| 3778 |
+
"expected": "Name: [PERSON]\nOrt: [CITY]\nArbeitgeber: [ORGANIZATION]."
|
| 3779 |
+
},
|
| 3780 |
+
{
|
| 3781 |
+
"id": "holdout03c-117",
|
| 3782 |
+
"language": "en",
|
| 3783 |
+
"ocr_noise": true,
|
| 3784 |
+
"features": [
|
| 3785 |
+
"ocr_spelling",
|
| 3786 |
+
"organization"
|
| 3787 |
+
],
|
| 3788 |
+
"text": "Scanned record: R0nan Wh1tm0re resides in B4th and works at Beacon Valley Labs.",
|
| 3789 |
+
"spans": [
|
| 3790 |
+
{
|
| 3791 |
+
"start": 16,
|
| 3792 |
+
"end": 30,
|
| 3793 |
+
"label": "PERSON",
|
| 3794 |
+
"text": "R0nan Wh1tm0re"
|
| 3795 |
+
},
|
| 3796 |
+
{
|
| 3797 |
+
"start": 42,
|
| 3798 |
+
"end": 46,
|
| 3799 |
+
"label": "CITY",
|
| 3800 |
+
"text": "B4th"
|
| 3801 |
+
},
|
| 3802 |
+
{
|
| 3803 |
+
"start": 60,
|
| 3804 |
+
"end": 78,
|
| 3805 |
+
"label": "ORGANIZATION",
|
| 3806 |
+
"text": "Beacon Valley Labs"
|
| 3807 |
+
}
|
| 3808 |
+
],
|
| 3809 |
+
"expected": "Scanned record: [PERSON] resides in [CITY] and works at [ORGANIZATION]."
|
| 3810 |
+
},
|
| 3811 |
+
{
|
| 3812 |
+
"id": "holdout03c-118",
|
| 3813 |
+
"language": "de",
|
| 3814 |
+
"ocr_noise": false,
|
| 3815 |
+
"features": [
|
| 3816 |
+
"religious_reference",
|
| 3817 |
+
"no_inferred_affiliation"
|
| 3818 |
+
],
|
| 3819 |
+
"text": "Das Thema christlich wird in einem Bericht von Beacon Valley Labs erwähnt. Für Ronan Whitmore wird keine Religionszugehörigkeit behauptet.",
|
| 3820 |
+
"spans": [
|
| 3821 |
+
{
|
| 3822 |
+
"start": 10,
|
| 3823 |
+
"end": 20,
|
| 3824 |
+
"label": "RELIGION",
|
| 3825 |
+
"text": "christlich"
|
| 3826 |
+
},
|
| 3827 |
+
{
|
| 3828 |
+
"start": 47,
|
| 3829 |
+
"end": 65,
|
| 3830 |
+
"label": "ORGANIZATION",
|
| 3831 |
+
"text": "Beacon Valley Labs"
|
| 3832 |
+
},
|
| 3833 |
+
{
|
| 3834 |
+
"start": 79,
|
| 3835 |
+
"end": 93,
|
| 3836 |
+
"label": "PERSON",
|
| 3837 |
+
"text": "Ronan Whitmore"
|
| 3838 |
+
}
|
| 3839 |
+
],
|
| 3840 |
+
"expected": "Das Thema [RELIGION] wird in einem Bericht von [ORGANIZATION] erwähnt. Für [PERSON] wird keine Religionszugehörigkeit behauptet."
|
| 3841 |
+
},
|
| 3842 |
+
{
|
| 3843 |
+
"id": "holdout03c-119",
|
| 3844 |
+
"language": "en",
|
| 3845 |
+
"ocr_noise": false,
|
| 3846 |
+
"features": [
|
| 3847 |
+
"negative",
|
| 3848 |
+
"religious_reference"
|
| 3849 |
+
],
|
| 3850 |
+
"text": "Worksheet 112: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details.",
|
| 3851 |
+
"spans": [],
|
| 3852 |
+
"expected": "Worksheet 112: a name alone establishes no religious affiliation. The plain record contains no named person, place, organization or contact details."
|
| 3853 |
+
}
|
| 3854 |
+
]
|
ocr/anonymization/fixtures/holdout03c/freeze.json
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": "frozen-holdout/v1",
|
| 3 |
+
"cases": 120,
|
| 4 |
+
"sha256": "0174fb41d34d0a4db18877c68fb70149eaa67c05da86608d5da4ff8765a52c15",
|
| 5 |
+
"source_head": "176176a6e13dfb1deb1a6f197b7fa46515b0e1f6",
|
| 6 |
+
"model_revision": "ba967ee030b7e679dd5422cb16fcd1b379da38e1",
|
| 7 |
+
"control_prompts": {
|
| 8 |
+
"PERSON": "person",
|
| 9 |
+
"CITY": "location",
|
| 10 |
+
"RELIGION": "religion",
|
| 11 |
+
"EMAIL": "email address",
|
| 12 |
+
"PHONE": "phone number",
|
| 13 |
+
"ORGANIZATION": "organization"
|
| 14 |
+
},
|
| 15 |
+
"threshold": 0.5,
|
| 16 |
+
"evaluation_run_before_freeze": false,
|
| 17 |
+
"design": "12 novel entity sets x 10 bilingual contexts; 60 de / 60 en; 24 OCR-noise cases. Synthetic/template-based, not a population sample. No holdout-driven tuning permitted before first recorded result."
|
| 18 |
+
}
|
ocr/anonymization/fixtures/holdout03d/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Gate 03d independent qualification corpus
|
| 2 |
+
|
| 3 |
+
Frozen before first model evaluation. 160 individually authored synthetic cases,
|
| 4 |
+
80 German and 80 English, in ten strata of sixteen: explicit affiliation,
|
| 5 |
+
non-affiliation, belief names, general topics, negation/quotation, contextual
|
| 6 |
+
ambiguity, OCR corruption, multiple entities, varied formats/repetition and
|
| 7 |
+
negative controls. No real personal data. These are authored by the implementing
|
| 8 |
+
agent, not an externally blinded or population-representative sample.
|
| 9 |
+
|
| 10 |
+
Gold spans protect religious terminology and explicit status phrases as text;
|
| 11 |
+
they do not assert any person's religion. Religion names in general topics,
|
| 12 |
+
quotations and negations are protected. Status phrases include their negation
|
| 13 |
+
where the phrase expresses non-affiliation. Ambiguous adjectives in a clearly
|
| 14 |
+
religious context are protected; mathematical/marketing metaphors are negatives.
|
| 15 |
+
Deliberately corrupted OCR spellings retain their exact source spans. Person,
|
| 16 |
+
geography, organization and contact spans use the existing category meanings.
|
| 17 |
+
|
| 18 |
+
`cases.json` and `freeze.json` are the immutable first-evaluation inputs. No model
|
| 19 |
+
inference was used to create or annotate them. The new configuration was selected
|
| 20 |
+
using only the previous 40+10+120 development cases. The first result must be kept
|
| 21 |
+
regardless of leakage, false positives or blocked documents. Once inspected,
|
| 22 |
+
this corpus is development evidence for any future tuning. A zero-canary finite
|
| 23 |
+
sample cannot establish universal privacy safety. Automatic release stays off.
|
| 24 |
+
|
| 25 |
+
Reproduce in a checkout of the recorded freeze commit, using the pinned locked
|
| 26 |
+
CPU dependencies and independently integrity-verified local model cache:
|
| 27 |
+
|
| 28 |
+
```sh
|
| 29 |
+
HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 HF_HUB_DISABLE_TELEMETRY=1 \
|
| 30 |
+
python -m ocr.anonymization.qualify03d \
|
| 31 |
+
--cache /private/model-cache --output /private/new-qualification-result.json
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
The command refuses to overwrite an existing result and returns nonzero on a
|
| 35 |
+
failed recognition gate, after saving the full first result and per-case evidence.
|
ocr/anonymization/fixtures/holdout03d/cases.json
ADDED
|
@@ -0,0 +1,2730 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "holdout03d-001",
|
| 4 |
+
"language": "de",
|
| 5 |
+
"scenario": "affiliation",
|
| 6 |
+
"ocr_noise": false,
|
| 7 |
+
"text": "Im Aufnahmebogen steht: Bekenntnis römisch-katholisch.",
|
| 8 |
+
"spans": [
|
| 9 |
+
{
|
| 10 |
+
"start": 35,
|
| 11 |
+
"end": 53,
|
| 12 |
+
"label": "RELIGION",
|
| 13 |
+
"text": "römisch-katholisch"
|
| 14 |
+
}
|
| 15 |
+
],
|
| 16 |
+
"expected": "Im Aufnahmebogen steht: Bekenntnis [RELIGION]."
|
| 17 |
+
},
|
| 18 |
+
{
|
| 19 |
+
"id": "holdout03d-002",
|
| 20 |
+
"language": "en",
|
| 21 |
+
"scenario": "affiliation",
|
| 22 |
+
"ocr_noise": false,
|
| 23 |
+
"text": "Faith field on the admission card: Roman Catholic.",
|
| 24 |
+
"spans": [
|
| 25 |
+
{
|
| 26 |
+
"start": 35,
|
| 27 |
+
"end": 49,
|
| 28 |
+
"label": "RELIGION",
|
| 29 |
+
"text": "Roman Catholic"
|
| 30 |
+
}
|
| 31 |
+
],
|
| 32 |
+
"expected": "Faith field on the admission card: [RELIGION]."
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "holdout03d-003",
|
| 36 |
+
"language": "de",
|
| 37 |
+
"scenario": "affiliation",
|
| 38 |
+
"ocr_noise": false,
|
| 39 |
+
"text": "Tabea Hornfels beschreibt ihre Tradition als sunnitisch.",
|
| 40 |
+
"spans": [
|
| 41 |
+
{
|
| 42 |
+
"start": 0,
|
| 43 |
+
"end": 14,
|
| 44 |
+
"label": "PERSON",
|
| 45 |
+
"text": "Tabea Hornfels"
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"start": 45,
|
| 49 |
+
"end": 55,
|
| 50 |
+
"label": "RELIGION",
|
| 51 |
+
"text": "sunnitisch"
|
| 52 |
+
}
|
| 53 |
+
],
|
| 54 |
+
"expected": "[PERSON] beschreibt ihre Tradition als [RELIGION]."
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": "holdout03d-004",
|
| 58 |
+
"language": "en",
|
| 59 |
+
"scenario": "affiliation",
|
| 60 |
+
"ocr_noise": false,
|
| 61 |
+
"text": "Ewan Fairbrook identifies as Shia Muslim in the consent note.",
|
| 62 |
+
"spans": [
|
| 63 |
+
{
|
| 64 |
+
"start": 0,
|
| 65 |
+
"end": 14,
|
| 66 |
+
"label": "PERSON",
|
| 67 |
+
"text": "Ewan Fairbrook"
|
| 68 |
+
},
|
| 69 |
+
{
|
| 70 |
+
"start": 29,
|
| 71 |
+
"end": 40,
|
| 72 |
+
"label": "RELIGION",
|
| 73 |
+
"text": "Shia Muslim"
|
| 74 |
+
}
|
| 75 |
+
],
|
| 76 |
+
"expected": "[PERSON] identifies as [RELIGION] in the consent note."
|
| 77 |
+
},
|
| 78 |
+
{
|
| 79 |
+
"id": "holdout03d-005",
|
| 80 |
+
"language": "de",
|
| 81 |
+
"scenario": "affiliation",
|
| 82 |
+
"ocr_noise": false,
|
| 83 |
+
"text": "Als Glaubensrichtung wurde Jesidentum eingetragen.",
|
| 84 |
+
"spans": [
|
| 85 |
+
{
|
| 86 |
+
"start": 27,
|
| 87 |
+
"end": 37,
|
| 88 |
+
"label": "RELIGION",
|
| 89 |
+
"text": "Jesidentum"
|
| 90 |
+
}
|
| 91 |
+
],
|
| 92 |
+
"expected": "Als Glaubensrichtung wurde [RELIGION] eingetragen."
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"id": "holdout03d-006",
|
| 96 |
+
"language": "en",
|
| 97 |
+
"scenario": "affiliation",
|
| 98 |
+
"ocr_noise": false,
|
| 99 |
+
"text": "The registrant selected Quaker under faith community.",
|
| 100 |
+
"spans": [
|
| 101 |
+
{
|
| 102 |
+
"start": 24,
|
| 103 |
+
"end": 30,
|
| 104 |
+
"label": "RELIGION",
|
| 105 |
+
"text": "Quaker"
|
| 106 |
+
}
|
| 107 |
+
],
|
| 108 |
+
"expected": "The registrant selected [RELIGION] under faith community."
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"id": "holdout03d-007",
|
| 112 |
+
"language": "de",
|
| 113 |
+
"scenario": "affiliation",
|
| 114 |
+
"ocr_noise": false,
|
| 115 |
+
"text": "Religiöse Selbstauskunft: Sikhismus; Veröffentlichung gesperrt.",
|
| 116 |
+
"spans": [
|
| 117 |
+
{
|
| 118 |
+
"start": 26,
|
| 119 |
+
"end": 35,
|
| 120 |
+
"label": "RELIGION",
|
| 121 |
+
"text": "Sikhismus"
|
| 122 |
+
}
|
| 123 |
+
],
|
| 124 |
+
"expected": "Religiöse Selbstauskunft: [RELIGION]; Veröffentlichung gesperrt."
|
| 125 |
+
},
|
| 126 |
+
{
|
| 127 |
+
"id": "holdout03d-008",
|
| 128 |
+
"language": "en",
|
| 129 |
+
"scenario": "affiliation",
|
| 130 |
+
"ocr_noise": false,
|
| 131 |
+
"text": "The private survey records Hinduism as the participant's belief.",
|
| 132 |
+
"spans": [
|
| 133 |
+
{
|
| 134 |
+
"start": 27,
|
| 135 |
+
"end": 35,
|
| 136 |
+
"label": "RELIGION",
|
| 137 |
+
"text": "Hinduism"
|
| 138 |
+
}
|
| 139 |
+
],
|
| 140 |
+
"expected": "The private survey records [RELIGION] as the participant's belief."
|
| 141 |
+
},
|
| 142 |
+
{
|
| 143 |
+
"id": "holdout03d-009",
|
| 144 |
+
"language": "de",
|
| 145 |
+
"scenario": "affiliation",
|
| 146 |
+
"ocr_noise": false,
|
| 147 |
+
"text": "Die Person gehört nach eigener Auskunft dem Judentum an.",
|
| 148 |
+
"spans": [
|
| 149 |
+
{
|
| 150 |
+
"start": 44,
|
| 151 |
+
"end": 52,
|
| 152 |
+
"label": "RELIGION",
|
| 153 |
+
"text": "Judentum"
|
| 154 |
+
}
|
| 155 |
+
],
|
| 156 |
+
"expected": "Die Person gehört nach eigener Auskunft dem [RELIGION] an."
|
| 157 |
+
},
|
| 158 |
+
{
|
| 159 |
+
"id": "holdout03d-010",
|
| 160 |
+
"language": "en",
|
| 161 |
+
"scenario": "affiliation",
|
| 162 |
+
"ocr_noise": false,
|
| 163 |
+
"text": "The speaker described her religion as Buddhism.",
|
| 164 |
+
"spans": [
|
| 165 |
+
{
|
| 166 |
+
"start": 38,
|
| 167 |
+
"end": 46,
|
| 168 |
+
"label": "RELIGION",
|
| 169 |
+
"text": "Buddhism"
|
| 170 |
+
}
|
| 171 |
+
],
|
| 172 |
+
"expected": "The speaker described her religion as [RELIGION]."
|
| 173 |
+
},
|
| 174 |
+
{
|
| 175 |
+
"id": "holdout03d-011",
|
| 176 |
+
"language": "de",
|
| 177 |
+
"scenario": "affiliation",
|
| 178 |
+
"ocr_noise": false,
|
| 179 |
+
"text": "Auf Seite zwei lautet die Angabe zur Religion Bahai.",
|
| 180 |
+
"spans": [
|
| 181 |
+
{
|
| 182 |
+
"start": 46,
|
| 183 |
+
"end": 51,
|
| 184 |
+
"label": "RELIGION",
|
| 185 |
+
"text": "Bahai"
|
| 186 |
+
}
|
| 187 |
+
],
|
| 188 |
+
"expected": "Auf Seite zwei lautet die Angabe zur Religion [RELIGION]."
|
| 189 |
+
},
|
| 190 |
+
{
|
| 191 |
+
"id": "holdout03d-012",
|
| 192 |
+
"language": "en",
|
| 193 |
+
"scenario": "affiliation",
|
| 194 |
+
"ocr_noise": false,
|
| 195 |
+
"text": "In a handwritten footnote the applicant wrote Jainism.",
|
| 196 |
+
"spans": [
|
| 197 |
+
{
|
| 198 |
+
"start": 46,
|
| 199 |
+
"end": 53,
|
| 200 |
+
"label": "RELIGION",
|
| 201 |
+
"text": "Jainism"
|
| 202 |
+
}
|
| 203 |
+
],
|
| 204 |
+
"expected": "In a handwritten footnote the applicant wrote [RELIGION]."
|
| 205 |
+
},
|
| 206 |
+
{
|
| 207 |
+
"id": "holdout03d-013",
|
| 208 |
+
"language": "de",
|
| 209 |
+
"scenario": "affiliation",
|
| 210 |
+
"ocr_noise": false,
|
| 211 |
+
"text": "Konfession des Antragstellers: evangelisch-lutherisch.",
|
| 212 |
+
"spans": [
|
| 213 |
+
{
|
| 214 |
+
"start": 31,
|
| 215 |
+
"end": 53,
|
| 216 |
+
"label": "RELIGION",
|
| 217 |
+
"text": "evangelisch-lutherisch"
|
| 218 |
+
}
|
| 219 |
+
],
|
| 220 |
+
"expected": "Konfession des Antragstellers: [RELIGION]."
|
| 221 |
+
},
|
| 222 |
+
{
|
| 223 |
+
"id": "holdout03d-014",
|
| 224 |
+
"language": "en",
|
| 225 |
+
"scenario": "affiliation",
|
| 226 |
+
"ocr_noise": false,
|
| 227 |
+
"text": "The confidential intake identifies the resident as Seventh-day Adventist.",
|
| 228 |
+
"spans": [
|
| 229 |
+
{
|
| 230 |
+
"start": 51,
|
| 231 |
+
"end": 72,
|
| 232 |
+
"label": "RELIGION",
|
| 233 |
+
"text": "Seventh-day Adventist"
|
| 234 |
+
}
|
| 235 |
+
],
|
| 236 |
+
"expected": "The confidential intake identifies the resident as [RELIGION]."
|
| 237 |
+
},
|
| 238 |
+
{
|
| 239 |
+
"id": "holdout03d-015",
|
| 240 |
+
"language": "de",
|
| 241 |
+
"scenario": "affiliation",
|
| 242 |
+
"ocr_noise": false,
|
| 243 |
+
"text": "Raban Moosfeld bekennt sich zum Christentum.",
|
| 244 |
+
"spans": [
|
| 245 |
+
{
|
| 246 |
+
"start": 0,
|
| 247 |
+
"end": 14,
|
| 248 |
+
"label": "PERSON",
|
| 249 |
+
"text": "Raban Moosfeld"
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
"start": 32,
|
| 253 |
+
"end": 43,
|
| 254 |
+
"label": "RELIGION",
|
| 255 |
+
"text": "Christentum"
|
| 256 |
+
}
|
| 257 |
+
],
|
| 258 |
+
"expected": "[PERSON] bekennt sich zum [RELIGION]."
|
| 259 |
+
},
|
| 260 |
+
{
|
| 261 |
+
"id": "holdout03d-016",
|
| 262 |
+
"language": "en",
|
| 263 |
+
"scenario": "affiliation",
|
| 264 |
+
"ocr_noise": false,
|
| 265 |
+
"text": "Iona Crestwell states that she practices Zoroastrianism.",
|
| 266 |
+
"spans": [
|
| 267 |
+
{
|
| 268 |
+
"start": 0,
|
| 269 |
+
"end": 14,
|
| 270 |
+
"label": "PERSON",
|
| 271 |
+
"text": "Iona Crestwell"
|
| 272 |
+
},
|
| 273 |
+
{
|
| 274 |
+
"start": 41,
|
| 275 |
+
"end": 55,
|
| 276 |
+
"label": "RELIGION",
|
| 277 |
+
"text": "Zoroastrianism"
|
| 278 |
+
}
|
| 279 |
+
],
|
| 280 |
+
"expected": "[PERSON] states that she practices [RELIGION]."
|
| 281 |
+
},
|
| 282 |
+
{
|
| 283 |
+
"id": "holdout03d-017",
|
| 284 |
+
"language": "de",
|
| 285 |
+
"scenario": "nonaffiliation",
|
| 286 |
+
"ocr_noise": false,
|
| 287 |
+
"text": "Die Antwort im Konfessionsfeld lautet konfessionsfrei.",
|
| 288 |
+
"spans": [
|
| 289 |
+
{
|
| 290 |
+
"start": 38,
|
| 291 |
+
"end": 53,
|
| 292 |
+
"label": "RELIGION",
|
| 293 |
+
"text": "konfessionsfrei"
|
| 294 |
+
}
|
| 295 |
+
],
|
| 296 |
+
"expected": "Die Antwort im Konfessionsfeld lautet [RELIGION]."
|
| 297 |
+
},
|
| 298 |
+
{
|
| 299 |
+
"id": "holdout03d-018",
|
| 300 |
+
"language": "en",
|
| 301 |
+
"scenario": "nonaffiliation",
|
| 302 |
+
"ocr_noise": false,
|
| 303 |
+
"text": "Religious status: no religious affiliation.",
|
| 304 |
+
"spans": [
|
| 305 |
+
{
|
| 306 |
+
"start": 18,
|
| 307 |
+
"end": 42,
|
| 308 |
+
"label": "RELIGION",
|
| 309 |
+
"text": "no religious affiliation"
|
| 310 |
+
}
|
| 311 |
+
],
|
| 312 |
+
"expected": "Religious status: [RELIGION]."
|
| 313 |
+
},
|
| 314 |
+
{
|
| 315 |
+
"id": "holdout03d-019",
|
| 316 |
+
"language": "de",
|
| 317 |
+
"scenario": "nonaffiliation",
|
| 318 |
+
"ocr_noise": false,
|
| 319 |
+
"text": "Antrag: Die Bewohnerin ist religionslos.",
|
| 320 |
+
"spans": [
|
| 321 |
+
{
|
| 322 |
+
"start": 27,
|
| 323 |
+
"end": 39,
|
| 324 |
+
"label": "RELIGION",
|
| 325 |
+
"text": "religionslos"
|
| 326 |
+
}
|
| 327 |
+
],
|
| 328 |
+
"expected": "Antrag: Die Bewohnerin ist [RELIGION]."
|
| 329 |
+
},
|
| 330 |
+
{
|
| 331 |
+
"id": "holdout03d-020",
|
| 332 |
+
"language": "en",
|
| 333 |
+
"scenario": "nonaffiliation",
|
| 334 |
+
"ocr_noise": false,
|
| 335 |
+
"text": "On the registration slip the visitor wrote atheist.",
|
| 336 |
+
"spans": [
|
| 337 |
+
{
|
| 338 |
+
"start": 43,
|
| 339 |
+
"end": 50,
|
| 340 |
+
"label": "RELIGION",
|
| 341 |
+
"text": "atheist"
|
| 342 |
+
}
|
| 343 |
+
],
|
| 344 |
+
"expected": "On the registration slip the visitor wrote [RELIGION]."
|
| 345 |
+
},
|
| 346 |
+
{
|
| 347 |
+
"id": "holdout03d-021",
|
| 348 |
+
"language": "de",
|
| 349 |
+
"scenario": "nonaffiliation",
|
| 350 |
+
"ocr_noise": false,
|
| 351 |
+
"text": "Selbstbeschreibung im versiegelten Fragebogen: agnostisch.",
|
| 352 |
+
"spans": [
|
| 353 |
+
{
|
| 354 |
+
"start": 47,
|
| 355 |
+
"end": 57,
|
| 356 |
+
"label": "RELIGION",
|
| 357 |
+
"text": "agnostisch"
|
| 358 |
+
}
|
| 359 |
+
],
|
| 360 |
+
"expected": "Selbstbeschreibung im versiegelten Fragebogen: [RELIGION]."
|
| 361 |
+
},
|
| 362 |
+
{
|
| 363 |
+
"id": "holdout03d-022",
|
| 364 |
+
"language": "en",
|
| 365 |
+
"scenario": "nonaffiliation",
|
| 366 |
+
"ocr_noise": false,
|
| 367 |
+
"text": "The individual describes himself as agnostic.",
|
| 368 |
+
"spans": [
|
| 369 |
+
{
|
| 370 |
+
"start": 36,
|
| 371 |
+
"end": 44,
|
| 372 |
+
"label": "RELIGION",
|
| 373 |
+
"text": "agnostic"
|
| 374 |
+
}
|
| 375 |
+
],
|
| 376 |
+
"expected": "The individual describes himself as [RELIGION]."
|
| 377 |
+
},
|
| 378 |
+
{
|
| 379 |
+
"id": "holdout03d-023",
|
| 380 |
+
"language": "de",
|
| 381 |
+
"scenario": "nonaffiliation",
|
| 382 |
+
"ocr_noise": false,
|
| 383 |
+
"text": "Zur Frage nach der Religion: ohne Konfession.",
|
| 384 |
+
"spans": [
|
| 385 |
+
{
|
| 386 |
+
"start": 29,
|
| 387 |
+
"end": 44,
|
| 388 |
+
"label": "RELIGION",
|
| 389 |
+
"text": "ohne Konfession"
|
| 390 |
+
}
|
| 391 |
+
],
|
| 392 |
+
"expected": "Zur Frage nach der Religion: [RELIGION]."
|
| 393 |
+
},
|
| 394 |
+
{
|
| 395 |
+
"id": "holdout03d-024",
|
| 396 |
+
"language": "en",
|
| 397 |
+
"scenario": "nonaffiliation",
|
| 398 |
+
"ocr_noise": false,
|
| 399 |
+
"text": "The recorded answer was nonreligious, with no additional details.",
|
| 400 |
+
"spans": [
|
| 401 |
+
{
|
| 402 |
+
"start": 24,
|
| 403 |
+
"end": 36,
|
| 404 |
+
"label": "RELIGION",
|
| 405 |
+
"text": "nonreligious"
|
| 406 |
+
}
|
| 407 |
+
],
|
| 408 |
+
"expected": "The recorded answer was [RELIGION], with no additional details."
|
| 409 |
+
},
|
| 410 |
+
{
|
| 411 |
+
"id": "holdout03d-025",
|
| 412 |
+
"language": "de",
|
| 413 |
+
"scenario": "nonaffiliation",
|
| 414 |
+
"ocr_noise": false,
|
| 415 |
+
"text": "Für den Eintrag gilt keine Religion als wörtliche Antwort.",
|
| 416 |
+
"spans": [
|
| 417 |
+
{
|
| 418 |
+
"start": 21,
|
| 419 |
+
"end": 35,
|
| 420 |
+
"label": "RELIGION",
|
| 421 |
+
"text": "keine Religion"
|
| 422 |
+
}
|
| 423 |
+
],
|
| 424 |
+
"expected": "Für den Eintrag gilt [RELIGION] als wörtliche Antwort."
|
| 425 |
+
},
|
| 426 |
+
{
|
| 427 |
+
"id": "holdout03d-026",
|
| 428 |
+
"language": "en",
|
| 429 |
+
"scenario": "nonaffiliation",
|
| 430 |
+
"ocr_noise": false,
|
| 431 |
+
"text": "Affiliation was entered as religiously unaffiliated.",
|
| 432 |
+
"spans": [
|
| 433 |
+
{
|
| 434 |
+
"start": 27,
|
| 435 |
+
"end": 51,
|
| 436 |
+
"label": "RELIGION",
|
| 437 |
+
"text": "religiously unaffiliated"
|
| 438 |
+
}
|
| 439 |
+
],
|
| 440 |
+
"expected": "Affiliation was entered as [RELIGION]."
|
| 441 |
+
},
|
| 442 |
+
{
|
| 443 |
+
"id": "holdout03d-027",
|
| 444 |
+
"language": "de",
|
| 445 |
+
"scenario": "nonaffiliation",
|
| 446 |
+
"ocr_noise": false,
|
| 447 |
+
"text": "Die vertrauliche Karte vermerkt konfessionslos neben dem Geburtsjahr.",
|
| 448 |
+
"spans": [
|
| 449 |
+
{
|
| 450 |
+
"start": 32,
|
| 451 |
+
"end": 46,
|
| 452 |
+
"label": "RELIGION",
|
| 453 |
+
"text": "konfessionslos"
|
| 454 |
+
}
|
| 455 |
+
],
|
| 456 |
+
"expected": "Die vertrauliche Karte vermerkt [RELIGION] neben dem Geburtsjahr."
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"id": "holdout03d-028",
|
| 460 |
+
"language": "en",
|
| 461 |
+
"scenario": "nonaffiliation",
|
| 462 |
+
"ocr_noise": false,
|
| 463 |
+
"text": "Personal belief: atheism; this field must remain private.",
|
| 464 |
+
"spans": [
|
| 465 |
+
{
|
| 466 |
+
"start": 17,
|
| 467 |
+
"end": 24,
|
| 468 |
+
"label": "RELIGION",
|
| 469 |
+
"text": "atheism"
|
| 470 |
+
}
|
| 471 |
+
],
|
| 472 |
+
"expected": "Personal belief: [RELIGION]; this field must remain private."
|
| 473 |
+
},
|
| 474 |
+
{
|
| 475 |
+
"id": "holdout03d-029",
|
| 476 |
+
"language": "de",
|
| 477 |
+
"scenario": "nonaffiliation",
|
| 478 |
+
"ocr_noise": false,
|
| 479 |
+
"text": "Beim Interview wurde Atheismus als eigene Haltung genannt.",
|
| 480 |
+
"spans": [
|
| 481 |
+
{
|
| 482 |
+
"start": 21,
|
| 483 |
+
"end": 30,
|
| 484 |
+
"label": "RELIGION",
|
| 485 |
+
"text": "Atheismus"
|
| 486 |
+
}
|
| 487 |
+
],
|
| 488 |
+
"expected": "Beim Interview wurde [RELIGION] als eigene Haltung genannt."
|
| 489 |
+
},
|
| 490 |
+
{
|
| 491 |
+
"id": "holdout03d-030",
|
| 492 |
+
"language": "en",
|
| 493 |
+
"scenario": "nonaffiliation",
|
| 494 |
+
"ocr_noise": false,
|
| 495 |
+
"text": "The resident's own answer is no religion.",
|
| 496 |
+
"spans": [
|
| 497 |
+
{
|
| 498 |
+
"start": 29,
|
| 499 |
+
"end": 40,
|
| 500 |
+
"label": "RELIGION",
|
| 501 |
+
"text": "no religion"
|
| 502 |
+
}
|
| 503 |
+
],
|
| 504 |
+
"expected": "The resident's own answer is [RELIGION]."
|
| 505 |
+
},
|
| 506 |
+
{
|
| 507 |
+
"id": "holdout03d-031",
|
| 508 |
+
"language": "de",
|
| 509 |
+
"scenario": "nonaffiliation",
|
| 510 |
+
"ocr_noise": false,
|
| 511 |
+
"text": "Als Weltanschauung wird Agnostizismus angegeben.",
|
| 512 |
+
"spans": [
|
| 513 |
+
{
|
| 514 |
+
"start": 24,
|
| 515 |
+
"end": 37,
|
| 516 |
+
"label": "RELIGION",
|
| 517 |
+
"text": "Agnostizismus"
|
| 518 |
+
}
|
| 519 |
+
],
|
| 520 |
+
"expected": "Als Weltanschauung wird [RELIGION] angegeben."
|
| 521 |
+
},
|
| 522 |
+
{
|
| 523 |
+
"id": "holdout03d-032",
|
| 524 |
+
"language": "en",
|
| 525 |
+
"scenario": "nonaffiliation",
|
| 526 |
+
"ocr_noise": false,
|
| 527 |
+
"text": "The form marks the applicant non-religious.",
|
| 528 |
+
"spans": [
|
| 529 |
+
{
|
| 530 |
+
"start": 29,
|
| 531 |
+
"end": 42,
|
| 532 |
+
"label": "RELIGION",
|
| 533 |
+
"text": "non-religious"
|
| 534 |
+
}
|
| 535 |
+
],
|
| 536 |
+
"expected": "The form marks the applicant [RELIGION]."
|
| 537 |
+
},
|
| 538 |
+
{
|
| 539 |
+
"id": "holdout03d-033",
|
| 540 |
+
"language": "de",
|
| 541 |
+
"scenario": "belief_names",
|
| 542 |
+
"ocr_noise": false,
|
| 543 |
+
"text": "Ein Glossar definiert Daoismus auf der ersten Seite.",
|
| 544 |
+
"spans": [
|
| 545 |
+
{
|
| 546 |
+
"start": 22,
|
| 547 |
+
"end": 30,
|
| 548 |
+
"label": "RELIGION",
|
| 549 |
+
"text": "Daoismus"
|
| 550 |
+
}
|
| 551 |
+
],
|
| 552 |
+
"expected": "Ein Glossar definiert [RELIGION] auf der ersten Seite."
|
| 553 |
+
},
|
| 554 |
+
{
|
| 555 |
+
"id": "holdout03d-034",
|
| 556 |
+
"language": "en",
|
| 557 |
+
"scenario": "belief_names",
|
| 558 |
+
"ocr_noise": false,
|
| 559 |
+
"text": "The index contains an entry for Shintoism.",
|
| 560 |
+
"spans": [
|
| 561 |
+
{
|
| 562 |
+
"start": 32,
|
| 563 |
+
"end": 41,
|
| 564 |
+
"label": "RELIGION",
|
| 565 |
+
"text": "Shintoism"
|
| 566 |
+
}
|
| 567 |
+
],
|
| 568 |
+
"expected": "The index contains an entry for [RELIGION]."
|
| 569 |
+
},
|
| 570 |
+
{
|
| 571 |
+
"id": "holdout03d-035",
|
| 572 |
+
"language": "de",
|
| 573 |
+
"scenario": "belief_names",
|
| 574 |
+
"ocr_noise": false,
|
| 575 |
+
"text": "Die Karteikarte trägt nur das Stichwort Islam.",
|
| 576 |
+
"spans": [
|
| 577 |
+
{
|
| 578 |
+
"start": 40,
|
| 579 |
+
"end": 45,
|
| 580 |
+
"label": "RELIGION",
|
| 581 |
+
"text": "Islam"
|
| 582 |
+
}
|
| 583 |
+
],
|
| 584 |
+
"expected": "Die Karteikarte trägt nur das Stichwort [RELIGION]."
|
| 585 |
+
},
|
| 586 |
+
{
|
| 587 |
+
"id": "holdout03d-036",
|
| 588 |
+
"language": "en",
|
| 589 |
+
"scenario": "belief_names",
|
| 590 |
+
"ocr_noise": false,
|
| 591 |
+
"text": "A dictionary heading reads Judaism.",
|
| 592 |
+
"spans": [
|
| 593 |
+
{
|
| 594 |
+
"start": 27,
|
| 595 |
+
"end": 34,
|
| 596 |
+
"label": "RELIGION",
|
| 597 |
+
"text": "Judaism"
|
| 598 |
+
}
|
| 599 |
+
],
|
| 600 |
+
"expected": "A dictionary heading reads [RELIGION]."
|
| 601 |
+
},
|
| 602 |
+
{
|
| 603 |
+
"id": "holdout03d-037",
|
| 604 |
+
"language": "de",
|
| 605 |
+
"scenario": "belief_names",
|
| 606 |
+
"ocr_noise": false,
|
| 607 |
+
"text": "Im Inhaltsverzeichnis stehen Hinduismus und Buddhismus.",
|
| 608 |
+
"spans": [
|
| 609 |
+
{
|
| 610 |
+
"start": 29,
|
| 611 |
+
"end": 39,
|
| 612 |
+
"label": "RELIGION",
|
| 613 |
+
"text": "Hinduismus"
|
| 614 |
+
},
|
| 615 |
+
{
|
| 616 |
+
"start": 44,
|
| 617 |
+
"end": 54,
|
| 618 |
+
"label": "RELIGION",
|
| 619 |
+
"text": "Buddhismus"
|
| 620 |
+
}
|
| 621 |
+
],
|
| 622 |
+
"expected": "Im Inhaltsverzeichnis stehen [RELIGION] und [RELIGION]."
|
| 623 |
+
},
|
| 624 |
+
{
|
| 625 |
+
"id": "holdout03d-038",
|
| 626 |
+
"language": "en",
|
| 627 |
+
"scenario": "belief_names",
|
| 628 |
+
"ocr_noise": false,
|
| 629 |
+
"text": "The glossary compares Christianity with Taoism.",
|
| 630 |
+
"spans": [
|
| 631 |
+
{
|
| 632 |
+
"start": 22,
|
| 633 |
+
"end": 34,
|
| 634 |
+
"label": "RELIGION",
|
| 635 |
+
"text": "Christianity"
|
| 636 |
+
},
|
| 637 |
+
{
|
| 638 |
+
"start": 40,
|
| 639 |
+
"end": 46,
|
| 640 |
+
"label": "RELIGION",
|
| 641 |
+
"text": "Taoism"
|
| 642 |
+
}
|
| 643 |
+
],
|
| 644 |
+
"expected": "The glossary compares [RELIGION] with [RELIGION]."
|
| 645 |
+
},
|
| 646 |
+
{
|
| 647 |
+
"id": "holdout03d-039",
|
| 648 |
+
"language": "de",
|
| 649 |
+
"scenario": "belief_names",
|
| 650 |
+
"ocr_noise": false,
|
| 651 |
+
"text": "Das Museum beschriftet einen Abschnitt mit Jainismus.",
|
| 652 |
+
"spans": [
|
| 653 |
+
{
|
| 654 |
+
"start": 43,
|
| 655 |
+
"end": 52,
|
| 656 |
+
"label": "RELIGION",
|
| 657 |
+
"text": "Jainismus"
|
| 658 |
+
}
|
| 659 |
+
],
|
| 660 |
+
"expected": "Das Museum beschriftet einen Abschnitt mit [RELIGION]."
|
| 661 |
+
},
|
| 662 |
+
{
|
| 663 |
+
"id": "holdout03d-040",
|
| 664 |
+
"language": "en",
|
| 665 |
+
"scenario": "belief_names",
|
| 666 |
+
"ocr_noise": false,
|
| 667 |
+
"text": "The catalog classifies the manuscript under Sikhism.",
|
| 668 |
+
"spans": [
|
| 669 |
+
{
|
| 670 |
+
"start": 44,
|
| 671 |
+
"end": 51,
|
| 672 |
+
"label": "RELIGION",
|
| 673 |
+
"text": "Sikhism"
|
| 674 |
+
}
|
| 675 |
+
],
|
| 676 |
+
"expected": "The catalog classifies the manuscript under [RELIGION]."
|
| 677 |
+
},
|
| 678 |
+
{
|
| 679 |
+
"id": "holdout03d-041",
|
| 680 |
+
"language": "de",
|
| 681 |
+
"scenario": "belief_names",
|
| 682 |
+
"ocr_noise": false,
|
| 683 |
+
"text": "Der Buchtitel erwähnt Zoroastrismus, ohne Personen zu benennen.",
|
| 684 |
+
"spans": [
|
| 685 |
+
{
|
| 686 |
+
"start": 22,
|
| 687 |
+
"end": 35,
|
| 688 |
+
"label": "RELIGION",
|
| 689 |
+
"text": "Zoroastrismus"
|
| 690 |
+
}
|
| 691 |
+
],
|
| 692 |
+
"expected": "Der Buchtitel erwähnt [RELIGION], ohne Personen zu benennen."
|
| 693 |
+
},
|
| 694 |
+
{
|
| 695 |
+
"id": "holdout03d-042",
|
| 696 |
+
"language": "en",
|
| 697 |
+
"scenario": "belief_names",
|
| 698 |
+
"ocr_noise": false,
|
| 699 |
+
"text": "A subject heading names Baháʼí Faith.",
|
| 700 |
+
"spans": [
|
| 701 |
+
{
|
| 702 |
+
"start": 24,
|
| 703 |
+
"end": 36,
|
| 704 |
+
"label": "RELIGION",
|
| 705 |
+
"text": "Baháʼí Faith"
|
| 706 |
+
}
|
| 707 |
+
],
|
| 708 |
+
"expected": "A subject heading names [RELIGION]."
|
| 709 |
+
},
|
| 710 |
+
{
|
| 711 |
+
"id": "holdout03d-043",
|
| 712 |
+
"language": "de",
|
| 713 |
+
"scenario": "belief_names",
|
| 714 |
+
"ocr_noise": false,
|
| 715 |
+
"text": "Die Übersicht behandelt Katholizismus in einem historischen Kapitel.",
|
| 716 |
+
"spans": [
|
| 717 |
+
{
|
| 718 |
+
"start": 24,
|
| 719 |
+
"end": 37,
|
| 720 |
+
"label": "RELIGION",
|
| 721 |
+
"text": "Katholizismus"
|
| 722 |
+
}
|
| 723 |
+
],
|
| 724 |
+
"expected": "Die Übersicht behandelt [RELIGION] in einem historischen Kapitel."
|
| 725 |
+
},
|
| 726 |
+
{
|
| 727 |
+
"id": "holdout03d-044",
|
| 728 |
+
"language": "en",
|
| 729 |
+
"scenario": "belief_names",
|
| 730 |
+
"ocr_noise": false,
|
| 731 |
+
"text": "The bibliography has a subsection titled Protestantism.",
|
| 732 |
+
"spans": [
|
| 733 |
+
{
|
| 734 |
+
"start": 41,
|
| 735 |
+
"end": 54,
|
| 736 |
+
"label": "RELIGION",
|
| 737 |
+
"text": "Protestantism"
|
| 738 |
+
}
|
| 739 |
+
],
|
| 740 |
+
"expected": "The bibliography has a subsection titled [RELIGION]."
|
| 741 |
+
},
|
| 742 |
+
{
|
| 743 |
+
"id": "holdout03d-045",
|
| 744 |
+
"language": "de",
|
| 745 |
+
"scenario": "belief_names",
|
| 746 |
+
"ocr_noise": false,
|
| 747 |
+
"text": "Auf dem Deckblatt steht Orthodoxie.",
|
| 748 |
+
"spans": [
|
| 749 |
+
{
|
| 750 |
+
"start": 24,
|
| 751 |
+
"end": 34,
|
| 752 |
+
"label": "RELIGION",
|
| 753 |
+
"text": "Orthodoxie"
|
| 754 |
+
}
|
| 755 |
+
],
|
| 756 |
+
"expected": "Auf dem Deckblatt steht [RELIGION]."
|
| 757 |
+
},
|
| 758 |
+
{
|
| 759 |
+
"id": "holdout03d-046",
|
| 760 |
+
"language": "en",
|
| 761 |
+
"scenario": "belief_names",
|
| 762 |
+
"ocr_noise": false,
|
| 763 |
+
"text": "A lecture outline begins with Orthodox Christianity.",
|
| 764 |
+
"spans": [
|
| 765 |
+
{
|
| 766 |
+
"start": 30,
|
| 767 |
+
"end": 51,
|
| 768 |
+
"label": "RELIGION",
|
| 769 |
+
"text": "Orthodox Christianity"
|
| 770 |
+
}
|
| 771 |
+
],
|
| 772 |
+
"expected": "A lecture outline begins with [RELIGION]."
|
| 773 |
+
},
|
| 774 |
+
{
|
| 775 |
+
"id": "holdout03d-047",
|
| 776 |
+
"language": "de",
|
| 777 |
+
"scenario": "belief_names",
|
| 778 |
+
"ocr_noise": false,
|
| 779 |
+
"text": "Das Handbuch unterscheidet Sikhismus von Jainismus.",
|
| 780 |
+
"spans": [
|
| 781 |
+
{
|
| 782 |
+
"start": 27,
|
| 783 |
+
"end": 36,
|
| 784 |
+
"label": "RELIGION",
|
| 785 |
+
"text": "Sikhismus"
|
| 786 |
+
},
|
| 787 |
+
{
|
| 788 |
+
"start": 41,
|
| 789 |
+
"end": 50,
|
| 790 |
+
"label": "RELIGION",
|
| 791 |
+
"text": "Jainismus"
|
| 792 |
+
}
|
| 793 |
+
],
|
| 794 |
+
"expected": "Das Handbuch unterscheidet [RELIGION] von [RELIGION]."
|
| 795 |
+
},
|
| 796 |
+
{
|
| 797 |
+
"id": "holdout03d-048",
|
| 798 |
+
"language": "en",
|
| 799 |
+
"scenario": "belief_names",
|
| 800 |
+
"ocr_noise": false,
|
| 801 |
+
"text": "The encyclopedia provides a chapter about Confucianism.",
|
| 802 |
+
"spans": [
|
| 803 |
+
{
|
| 804 |
+
"start": 42,
|
| 805 |
+
"end": 54,
|
| 806 |
+
"label": "RELIGION",
|
| 807 |
+
"text": "Confucianism"
|
| 808 |
+
}
|
| 809 |
+
],
|
| 810 |
+
"expected": "The encyclopedia provides a chapter about [RELIGION]."
|
| 811 |
+
},
|
| 812 |
+
{
|
| 813 |
+
"id": "holdout03d-049",
|
| 814 |
+
"language": "de",
|
| 815 |
+
"scenario": "general_topics",
|
| 816 |
+
"ocr_noise": false,
|
| 817 |
+
"text": "Die Ausstellung erklärt das Fasten im Islam; Besucherangaben fehlen.",
|
| 818 |
+
"spans": [
|
| 819 |
+
{
|
| 820 |
+
"start": 38,
|
| 821 |
+
"end": 43,
|
| 822 |
+
"label": "RELIGION",
|
| 823 |
+
"text": "Islam"
|
| 824 |
+
}
|
| 825 |
+
],
|
| 826 |
+
"expected": "Die Ausstellung erklärt das Fasten im [RELIGION]; Besucherangaben fehlen."
|
| 827 |
+
},
|
| 828 |
+
{
|
| 829 |
+
"id": "holdout03d-050",
|
| 830 |
+
"language": "en",
|
| 831 |
+
"scenario": "general_topics",
|
| 832 |
+
"ocr_noise": false,
|
| 833 |
+
"text": "The essay examines pilgrimage in Hinduism without naming a follower.",
|
| 834 |
+
"spans": [
|
| 835 |
+
{
|
| 836 |
+
"start": 33,
|
| 837 |
+
"end": 41,
|
| 838 |
+
"label": "RELIGION",
|
| 839 |
+
"text": "Hinduism"
|
| 840 |
+
}
|
| 841 |
+
],
|
| 842 |
+
"expected": "The essay examines pilgrimage in [RELIGION] without naming a follower."
|
| 843 |
+
},
|
| 844 |
+
{
|
| 845 |
+
"id": "holdout03d-051",
|
| 846 |
+
"language": "de",
|
| 847 |
+
"scenario": "general_topics",
|
| 848 |
+
"ocr_noise": false,
|
| 849 |
+
"text": "Historische Randnotiz: Christentum im frühen Mittelalter.",
|
| 850 |
+
"spans": [
|
| 851 |
+
{
|
| 852 |
+
"start": 23,
|
| 853 |
+
"end": 34,
|
| 854 |
+
"label": "RELIGION",
|
| 855 |
+
"text": "Christentum"
|
| 856 |
+
}
|
| 857 |
+
],
|
| 858 |
+
"expected": "Historische Randnotiz: [RELIGION] im frühen Mittelalter."
|
| 859 |
+
},
|
| 860 |
+
{
|
| 861 |
+
"id": "holdout03d-052",
|
| 862 |
+
"language": "en",
|
| 863 |
+
"scenario": "general_topics",
|
| 864 |
+
"ocr_noise": false,
|
| 865 |
+
"text": "The radio program discusses Buddhism as a topic, not the presenter's identity.",
|
| 866 |
+
"spans": [
|
| 867 |
+
{
|
| 868 |
+
"start": 28,
|
| 869 |
+
"end": 36,
|
| 870 |
+
"label": "RELIGION",
|
| 871 |
+
"text": "Buddhism"
|
| 872 |
+
}
|
| 873 |
+
],
|
| 874 |
+
"expected": "The radio program discusses [RELIGION] as a topic, not the presenter's identity."
|
| 875 |
+
},
|
| 876 |
+
{
|
| 877 |
+
"id": "holdout03d-053",
|
| 878 |
+
"language": "de",
|
| 879 |
+
"scenario": "general_topics",
|
| 880 |
+
"ocr_noise": false,
|
| 881 |
+
"text": "Auf der Leseliste steht ein Werk zum Judentum.",
|
| 882 |
+
"spans": [
|
| 883 |
+
{
|
| 884 |
+
"start": 37,
|
| 885 |
+
"end": 45,
|
| 886 |
+
"label": "RELIGION",
|
| 887 |
+
"text": "Judentum"
|
| 888 |
+
}
|
| 889 |
+
],
|
| 890 |
+
"expected": "Auf der Leseliste steht ein Werk zum [RELIGION]."
|
| 891 |
+
},
|
| 892 |
+
{
|
| 893 |
+
"id": "holdout03d-054",
|
| 894 |
+
"language": "en",
|
| 895 |
+
"scenario": "general_topics",
|
| 896 |
+
"ocr_noise": false,
|
| 897 |
+
"text": "A public seminar on Jainism does not establish anyone's affiliation.",
|
| 898 |
+
"spans": [
|
| 899 |
+
{
|
| 900 |
+
"start": 20,
|
| 901 |
+
"end": 27,
|
| 902 |
+
"label": "RELIGION",
|
| 903 |
+
"text": "Jainism"
|
| 904 |
+
}
|
| 905 |
+
],
|
| 906 |
+
"expected": "A public seminar on [RELIGION] does not establish anyone's affiliation."
|
| 907 |
+
},
|
| 908 |
+
{
|
| 909 |
+
"id": "holdout03d-055",
|
| 910 |
+
"language": "de",
|
| 911 |
+
"scenario": "general_topics",
|
| 912 |
+
"ocr_noise": false,
|
| 913 |
+
"text": "Die Wandtafel erläutert Bahai als historischen Gegenstand.",
|
| 914 |
+
"spans": [
|
| 915 |
+
{
|
| 916 |
+
"start": 24,
|
| 917 |
+
"end": 29,
|
| 918 |
+
"label": "RELIGION",
|
| 919 |
+
"text": "Bahai"
|
| 920 |
+
}
|
| 921 |
+
],
|
| 922 |
+
"expected": "Die Wandtafel erläutert [RELIGION] als historischen Gegenstand."
|
| 923 |
+
},
|
| 924 |
+
{
|
| 925 |
+
"id": "holdout03d-056",
|
| 926 |
+
"language": "en",
|
| 927 |
+
"scenario": "general_topics",
|
| 928 |
+
"ocr_noise": false,
|
| 929 |
+
"text": "The podcast's subject is Sikhism, with no personal disclosures.",
|
| 930 |
+
"spans": [
|
| 931 |
+
{
|
| 932 |
+
"start": 25,
|
| 933 |
+
"end": 32,
|
| 934 |
+
"label": "RELIGION",
|
| 935 |
+
"text": "Sikhism"
|
| 936 |
+
}
|
| 937 |
+
],
|
| 938 |
+
"expected": "The podcast's subject is [RELIGION], with no personal disclosures."
|
| 939 |
+
},
|
| 940 |
+
{
|
| 941 |
+
"id": "holdout03d-057",
|
| 942 |
+
"language": "de",
|
| 943 |
+
"scenario": "general_topics",
|
| 944 |
+
"ocr_noise": false,
|
| 945 |
+
"text": "Kapitelüberschrift: Atheismus und gesellschaftlicher Wandel.",
|
| 946 |
+
"spans": [
|
| 947 |
+
{
|
| 948 |
+
"start": 20,
|
| 949 |
+
"end": 29,
|
| 950 |
+
"label": "RELIGION",
|
| 951 |
+
"text": "Atheismus"
|
| 952 |
+
}
|
| 953 |
+
],
|
| 954 |
+
"expected": "Kapitelüberschrift: [RELIGION] und gesellschaftlicher Wandel."
|
| 955 |
+
},
|
| 956 |
+
{
|
| 957 |
+
"id": "holdout03d-058",
|
| 958 |
+
"language": "en",
|
| 959 |
+
"scenario": "general_topics",
|
| 960 |
+
"ocr_noise": false,
|
| 961 |
+
"text": "The chart compares atheism and agnosticism at a conceptual level.",
|
| 962 |
+
"spans": [
|
| 963 |
+
{
|
| 964 |
+
"start": 19,
|
| 965 |
+
"end": 26,
|
| 966 |
+
"label": "RELIGION",
|
| 967 |
+
"text": "atheism"
|
| 968 |
+
},
|
| 969 |
+
{
|
| 970 |
+
"start": 31,
|
| 971 |
+
"end": 42,
|
| 972 |
+
"label": "RELIGION",
|
| 973 |
+
"text": "agnosticism"
|
| 974 |
+
}
|
| 975 |
+
],
|
| 976 |
+
"expected": "The chart compares [RELIGION] and [RELIGION] at a conceptual level."
|
| 977 |
+
},
|
| 978 |
+
{
|
| 979 |
+
"id": "holdout03d-059",
|
| 980 |
+
"language": "de",
|
| 981 |
+
"scenario": "general_topics",
|
| 982 |
+
"ocr_noise": false,
|
| 983 |
+
"text": "Ein Forschungstext beschreibt Shintoismus ohne individuelle Angaben.",
|
| 984 |
+
"spans": [
|
| 985 |
+
{
|
| 986 |
+
"start": 30,
|
| 987 |
+
"end": 41,
|
| 988 |
+
"label": "RELIGION",
|
| 989 |
+
"text": "Shintoismus"
|
| 990 |
+
}
|
| 991 |
+
],
|
| 992 |
+
"expected": "Ein Forschungstext beschreibt [RELIGION] ohne individuelle Angaben."
|
| 993 |
+
},
|
| 994 |
+
{
|
| 995 |
+
"id": "holdout03d-060",
|
| 996 |
+
"language": "en",
|
| 997 |
+
"scenario": "general_topics",
|
| 998 |
+
"ocr_noise": false,
|
| 999 |
+
"text": "The librarian assigned Taoism as a subject keyword.",
|
| 1000 |
+
"spans": [
|
| 1001 |
+
{
|
| 1002 |
+
"start": 23,
|
| 1003 |
+
"end": 29,
|
| 1004 |
+
"label": "RELIGION",
|
| 1005 |
+
"text": "Taoism"
|
| 1006 |
+
}
|
| 1007 |
+
],
|
| 1008 |
+
"expected": "The librarian assigned [RELIGION] as a subject keyword."
|
| 1009 |
+
},
|
| 1010 |
+
{
|
| 1011 |
+
"id": "holdout03d-061",
|
| 1012 |
+
"language": "de",
|
| 1013 |
+
"scenario": "general_topics",
|
| 1014 |
+
"ocr_noise": false,
|
| 1015 |
+
"text": "Die Zeitleiste beschreibt den Protestantismus als Bewegung.",
|
| 1016 |
+
"spans": [
|
| 1017 |
+
{
|
| 1018 |
+
"start": 30,
|
| 1019 |
+
"end": 45,
|
| 1020 |
+
"label": "RELIGION",
|
| 1021 |
+
"text": "Protestantismus"
|
| 1022 |
+
}
|
| 1023 |
+
],
|
| 1024 |
+
"expected": "Die Zeitleiste beschreibt den [RELIGION] als Bewegung."
|
| 1025 |
+
},
|
| 1026 |
+
{
|
| 1027 |
+
"id": "holdout03d-062",
|
| 1028 |
+
"language": "en",
|
| 1029 |
+
"scenario": "general_topics",
|
| 1030 |
+
"ocr_noise": false,
|
| 1031 |
+
"text": "The audio guide explains Catholicism to an unspecified audience.",
|
| 1032 |
+
"spans": [
|
| 1033 |
+
{
|
| 1034 |
+
"start": 25,
|
| 1035 |
+
"end": 36,
|
| 1036 |
+
"label": "RELIGION",
|
| 1037 |
+
"text": "Catholicism"
|
| 1038 |
+
}
|
| 1039 |
+
],
|
| 1040 |
+
"expected": "The audio guide explains [RELIGION] to an unspecified audience."
|
| 1041 |
+
},
|
| 1042 |
+
{
|
| 1043 |
+
"id": "holdout03d-063",
|
| 1044 |
+
"language": "de",
|
| 1045 |
+
"scenario": "general_topics",
|
| 1046 |
+
"ocr_noise": false,
|
| 1047 |
+
"text": "Ein allgemeiner Artikel nennt konfessionslos als statistische Kategorie.",
|
| 1048 |
+
"spans": [
|
| 1049 |
+
{
|
| 1050 |
+
"start": 30,
|
| 1051 |
+
"end": 44,
|
| 1052 |
+
"label": "RELIGION",
|
| 1053 |
+
"text": "konfessionslos"
|
| 1054 |
+
}
|
| 1055 |
+
],
|
| 1056 |
+
"expected": "Ein allgemeiner Artikel nennt [RELIGION] als statistische Kategorie."
|
| 1057 |
+
},
|
| 1058 |
+
{
|
| 1059 |
+
"id": "holdout03d-064",
|
| 1060 |
+
"language": "en",
|
| 1061 |
+
"scenario": "general_topics",
|
| 1062 |
+
"ocr_noise": false,
|
| 1063 |
+
"text": "A terminology sheet explains non-religious; it contains no respondent records.",
|
| 1064 |
+
"spans": [
|
| 1065 |
+
{
|
| 1066 |
+
"start": 29,
|
| 1067 |
+
"end": 42,
|
| 1068 |
+
"label": "RELIGION",
|
| 1069 |
+
"text": "non-religious"
|
| 1070 |
+
}
|
| 1071 |
+
],
|
| 1072 |
+
"expected": "A terminology sheet explains [RELIGION]; it contains no respondent records."
|
| 1073 |
+
},
|
| 1074 |
+
{
|
| 1075 |
+
"id": "holdout03d-065",
|
| 1076 |
+
"language": "de",
|
| 1077 |
+
"scenario": "negation_quotation",
|
| 1078 |
+
"ocr_noise": false,
|
| 1079 |
+
"text": "Im Protokoll steht: »Ich bin nicht muslimisch.«",
|
| 1080 |
+
"spans": [
|
| 1081 |
+
{
|
| 1082 |
+
"start": 35,
|
| 1083 |
+
"end": 45,
|
| 1084 |
+
"label": "RELIGION",
|
| 1085 |
+
"text": "muslimisch"
|
| 1086 |
+
}
|
| 1087 |
+
],
|
| 1088 |
+
"expected": "Im Protokoll steht: »Ich bin nicht [RELIGION].«"
|
| 1089 |
+
},
|
| 1090 |
+
{
|
| 1091 |
+
"id": "holdout03d-066",
|
| 1092 |
+
"language": "en",
|
| 1093 |
+
"scenario": "negation_quotation",
|
| 1094 |
+
"ocr_noise": false,
|
| 1095 |
+
"text": "The witness said, \"I am not Christian.\"",
|
| 1096 |
+
"spans": [
|
| 1097 |
+
{
|
| 1098 |
+
"start": 28,
|
| 1099 |
+
"end": 37,
|
| 1100 |
+
"label": "RELIGION",
|
| 1101 |
+
"text": "Christian"
|
| 1102 |
+
}
|
| 1103 |
+
],
|
| 1104 |
+
"expected": "The witness said, \"I am not [RELIGION].\""
|
| 1105 |
+
},
|
| 1106 |
+
{
|
| 1107 |
+
"id": "holdout03d-067",
|
| 1108 |
+
"language": "de",
|
| 1109 |
+
"scenario": "negation_quotation",
|
| 1110 |
+
"ocr_noise": false,
|
| 1111 |
+
"text": "Sie widerspricht dem Eintrag katholisch ausdrücklich.",
|
| 1112 |
+
"spans": [
|
| 1113 |
+
{
|
| 1114 |
+
"start": 29,
|
| 1115 |
+
"end": 39,
|
| 1116 |
+
"label": "RELIGION",
|
| 1117 |
+
"text": "katholisch"
|
| 1118 |
+
}
|
| 1119 |
+
],
|
| 1120 |
+
"expected": "Sie widerspricht dem Eintrag [RELIGION] ausdrücklich."
|
| 1121 |
+
},
|
| 1122 |
+
{
|
| 1123 |
+
"id": "holdout03d-068",
|
| 1124 |
+
"language": "en",
|
| 1125 |
+
"scenario": "negation_quotation",
|
| 1126 |
+
"ocr_noise": false,
|
| 1127 |
+
"text": "The interviewee rejected the label atheist.",
|
| 1128 |
+
"spans": [
|
| 1129 |
+
{
|
| 1130 |
+
"start": 35,
|
| 1131 |
+
"end": 42,
|
| 1132 |
+
"label": "RELIGION",
|
| 1133 |
+
"text": "atheist"
|
| 1134 |
+
}
|
| 1135 |
+
],
|
| 1136 |
+
"expected": "The interviewee rejected the label [RELIGION]."
|
| 1137 |
+
},
|
| 1138 |
+
{
|
| 1139 |
+
"id": "holdout03d-069",
|
| 1140 |
+
"language": "de",
|
| 1141 |
+
"scenario": "negation_quotation",
|
| 1142 |
+
"ocr_noise": false,
|
| 1143 |
+
"text": "Das Wort »buddhistisch« ist ein Zitat aus einem Lehrbuch.",
|
| 1144 |
+
"spans": [
|
| 1145 |
+
{
|
| 1146 |
+
"start": 10,
|
| 1147 |
+
"end": 22,
|
| 1148 |
+
"label": "RELIGION",
|
| 1149 |
+
"text": "buddhistisch"
|
| 1150 |
+
}
|
| 1151 |
+
],
|
| 1152 |
+
"expected": "Das Wort »[RELIGION]« ist ein Zitat aus einem Lehrbuch."
|
| 1153 |
+
},
|
| 1154 |
+
{
|
| 1155 |
+
"id": "holdout03d-070",
|
| 1156 |
+
"language": "en",
|
| 1157 |
+
"scenario": "negation_quotation",
|
| 1158 |
+
"ocr_noise": false,
|
| 1159 |
+
"text": "\"Jewish\" appears in a quotation, not a claim about the editor.",
|
| 1160 |
+
"spans": [
|
| 1161 |
+
{
|
| 1162 |
+
"start": 1,
|
| 1163 |
+
"end": 7,
|
| 1164 |
+
"label": "RELIGION",
|
| 1165 |
+
"text": "Jewish"
|
| 1166 |
+
}
|
| 1167 |
+
],
|
| 1168 |
+
"expected": "\"[RELIGION]\" appears in a quotation, not a claim about the editor."
|
| 1169 |
+
},
|
| 1170 |
+
{
|
| 1171 |
+
"id": "holdout03d-071",
|
| 1172 |
+
"language": "de",
|
| 1173 |
+
"scenario": "negation_quotation",
|
| 1174 |
+
"ocr_noise": false,
|
| 1175 |
+
"text": "Niemand behauptet hier, die Autorin sei hinduistisch.",
|
| 1176 |
+
"spans": [
|
| 1177 |
+
{
|
| 1178 |
+
"start": 40,
|
| 1179 |
+
"end": 52,
|
| 1180 |
+
"label": "RELIGION",
|
| 1181 |
+
"text": "hinduistisch"
|
| 1182 |
+
}
|
| 1183 |
+
],
|
| 1184 |
+
"expected": "Niemand behauptet hier, die Autorin sei [RELIGION]."
|
| 1185 |
+
},
|
| 1186 |
+
{
|
| 1187 |
+
"id": "holdout03d-072",
|
| 1188 |
+
"language": "en",
|
| 1189 |
+
"scenario": "negation_quotation",
|
| 1190 |
+
"ocr_noise": false,
|
| 1191 |
+
"text": "No one alleged that the resident was Muslim.",
|
| 1192 |
+
"spans": [
|
| 1193 |
+
{
|
| 1194 |
+
"start": 37,
|
| 1195 |
+
"end": 43,
|
| 1196 |
+
"label": "RELIGION",
|
| 1197 |
+
"text": "Muslim"
|
| 1198 |
+
}
|
| 1199 |
+
],
|
| 1200 |
+
"expected": "No one alleged that the resident was [RELIGION]."
|
| 1201 |
+
},
|
| 1202 |
+
{
|
| 1203 |
+
"id": "holdout03d-073",
|
| 1204 |
+
"language": "de",
|
| 1205 |
+
"scenario": "negation_quotation",
|
| 1206 |
+
"ocr_noise": false,
|
| 1207 |
+
"text": "»Ohne Konfession« wurde als mögliche Antwort vorgelesen.",
|
| 1208 |
+
"spans": [
|
| 1209 |
+
{
|
| 1210 |
+
"start": 1,
|
| 1211 |
+
"end": 16,
|
| 1212 |
+
"label": "RELIGION",
|
| 1213 |
+
"text": "Ohne Konfession"
|
| 1214 |
+
}
|
| 1215 |
+
],
|
| 1216 |
+
"expected": "»[RELIGION]« wurde als mögliche Antwort vorgelesen."
|
| 1217 |
+
},
|
| 1218 |
+
{
|
| 1219 |
+
"id": "holdout03d-074",
|
| 1220 |
+
"language": "en",
|
| 1221 |
+
"scenario": "negation_quotation",
|
| 1222 |
+
"ocr_noise": false,
|
| 1223 |
+
"text": "The clerk read the option \"no religion\" aloud.",
|
| 1224 |
+
"spans": [
|
| 1225 |
+
{
|
| 1226 |
+
"start": 27,
|
| 1227 |
+
"end": 38,
|
| 1228 |
+
"label": "RELIGION",
|
| 1229 |
+
"text": "no religion"
|
| 1230 |
+
}
|
| 1231 |
+
],
|
| 1232 |
+
"expected": "The clerk read the option \"[RELIGION]\" aloud."
|
| 1233 |
+
},
|
| 1234 |
+
{
|
| 1235 |
+
"id": "holdout03d-075",
|
| 1236 |
+
"language": "de",
|
| 1237 |
+
"scenario": "negation_quotation",
|
| 1238 |
+
"ocr_noise": false,
|
| 1239 |
+
"text": "Nicht evangelisch, sondern agnostisch: so lautet die Korrektur.",
|
| 1240 |
+
"spans": [
|
| 1241 |
+
{
|
| 1242 |
+
"start": 6,
|
| 1243 |
+
"end": 17,
|
| 1244 |
+
"label": "RELIGION",
|
| 1245 |
+
"text": "evangelisch"
|
| 1246 |
+
},
|
| 1247 |
+
{
|
| 1248 |
+
"start": 27,
|
| 1249 |
+
"end": 37,
|
| 1250 |
+
"label": "RELIGION",
|
| 1251 |
+
"text": "agnostisch"
|
| 1252 |
+
}
|
| 1253 |
+
],
|
| 1254 |
+
"expected": "Nicht [RELIGION], sondern [RELIGION]: so lautet die Korrektur."
|
| 1255 |
+
},
|
| 1256 |
+
{
|
| 1257 |
+
"id": "holdout03d-076",
|
| 1258 |
+
"language": "en",
|
| 1259 |
+
"scenario": "negation_quotation",
|
| 1260 |
+
"ocr_noise": false,
|
| 1261 |
+
"text": "The note corrects Catholic to nonreligious.",
|
| 1262 |
+
"spans": [
|
| 1263 |
+
{
|
| 1264 |
+
"start": 18,
|
| 1265 |
+
"end": 26,
|
| 1266 |
+
"label": "RELIGION",
|
| 1267 |
+
"text": "Catholic"
|
| 1268 |
+
},
|
| 1269 |
+
{
|
| 1270 |
+
"start": 30,
|
| 1271 |
+
"end": 42,
|
| 1272 |
+
"label": "RELIGION",
|
| 1273 |
+
"text": "nonreligious"
|
| 1274 |
+
}
|
| 1275 |
+
],
|
| 1276 |
+
"expected": "The note corrects [RELIGION] to [RELIGION]."
|
| 1277 |
+
},
|
| 1278 |
+
{
|
| 1279 |
+
"id": "holdout03d-077",
|
| 1280 |
+
"language": "de",
|
| 1281 |
+
"scenario": "negation_quotation",
|
| 1282 |
+
"ocr_noise": false,
|
| 1283 |
+
"text": "Die Rezension zitiert Judentum, ohne einen Glauben zuzuschreiben.",
|
| 1284 |
+
"spans": [
|
| 1285 |
+
{
|
| 1286 |
+
"start": 22,
|
| 1287 |
+
"end": 30,
|
| 1288 |
+
"label": "RELIGION",
|
| 1289 |
+
"text": "Judentum"
|
| 1290 |
+
}
|
| 1291 |
+
],
|
| 1292 |
+
"expected": "Die Rezension zitiert [RELIGION], ohne einen Glauben zuzuschreiben."
|
| 1293 |
+
},
|
| 1294 |
+
{
|
| 1295 |
+
"id": "holdout03d-078",
|
| 1296 |
+
"language": "en",
|
| 1297 |
+
"scenario": "negation_quotation",
|
| 1298 |
+
"ocr_noise": false,
|
| 1299 |
+
"text": "A reviewer underlined \"Baha'i\" in the supplied excerpt.",
|
| 1300 |
+
"spans": [
|
| 1301 |
+
{
|
| 1302 |
+
"start": 23,
|
| 1303 |
+
"end": 29,
|
| 1304 |
+
"label": "RELIGION",
|
| 1305 |
+
"text": "Baha'i"
|
| 1306 |
+
}
|
| 1307 |
+
],
|
| 1308 |
+
"expected": "A reviewer underlined \"[RELIGION]\" in the supplied excerpt."
|
| 1309 |
+
},
|
| 1310 |
+
{
|
| 1311 |
+
"id": "holdout03d-079",
|
| 1312 |
+
"language": "de",
|
| 1313 |
+
"scenario": "negation_quotation",
|
| 1314 |
+
"ocr_noise": false,
|
| 1315 |
+
"text": "Die Behauptung einer islamischen Zugehörigkeit wurde zurückgewiesen.",
|
| 1316 |
+
"spans": [
|
| 1317 |
+
{
|
| 1318 |
+
"start": 21,
|
| 1319 |
+
"end": 32,
|
| 1320 |
+
"label": "RELIGION",
|
| 1321 |
+
"text": "islamischen"
|
| 1322 |
+
}
|
| 1323 |
+
],
|
| 1324 |
+
"expected": "Die Behauptung einer [RELIGION] Zugehörigkeit wurde zurückgewiesen."
|
| 1325 |
+
},
|
| 1326 |
+
{
|
| 1327 |
+
"id": "holdout03d-080",
|
| 1328 |
+
"language": "en",
|
| 1329 |
+
"scenario": "negation_quotation",
|
| 1330 |
+
"ocr_noise": false,
|
| 1331 |
+
"text": "The statement \"Buddhist\" was disputed in the correction sheet.",
|
| 1332 |
+
"spans": [
|
| 1333 |
+
{
|
| 1334 |
+
"start": 15,
|
| 1335 |
+
"end": 23,
|
| 1336 |
+
"label": "RELIGION",
|
| 1337 |
+
"text": "Buddhist"
|
| 1338 |
+
}
|
| 1339 |
+
],
|
| 1340 |
+
"expected": "The statement \"[RELIGION]\" was disputed in the correction sheet."
|
| 1341 |
+
},
|
| 1342 |
+
{
|
| 1343 |
+
"id": "holdout03d-081",
|
| 1344 |
+
"language": "de",
|
| 1345 |
+
"scenario": "ambiguity",
|
| 1346 |
+
"ocr_noise": false,
|
| 1347 |
+
"text": "Ein orthodoxes Sortierverfahren ist hier eine mathematische Metapher.",
|
| 1348 |
+
"spans": [],
|
| 1349 |
+
"expected": "Ein orthodoxes Sortierverfahren ist hier eine mathematische Metapher."
|
| 1350 |
+
},
|
| 1351 |
+
{
|
| 1352 |
+
"id": "holdout03d-082",
|
| 1353 |
+
"language": "en",
|
| 1354 |
+
"scenario": "ambiguity",
|
| 1355 |
+
"ocr_noise": false,
|
| 1356 |
+
"text": "Her catholic tastes include jazz and astronomy.",
|
| 1357 |
+
"spans": [],
|
| 1358 |
+
"expected": "Her catholic tastes include jazz and astronomy."
|
| 1359 |
+
},
|
| 1360 |
+
{
|
| 1361 |
+
"id": "holdout03d-083",
|
| 1362 |
+
"language": "de",
|
| 1363 |
+
"scenario": "ambiguity",
|
| 1364 |
+
"ocr_noise": false,
|
| 1365 |
+
"text": "Die evangelikale Werbemetapher beschreibt keine Religionszugehörigkeit.",
|
| 1366 |
+
"spans": [],
|
| 1367 |
+
"expected": "Die evangelikale Werbemetapher beschreibt keine Religionszugehörigkeit."
|
| 1368 |
+
},
|
| 1369 |
+
{
|
| 1370 |
+
"id": "holdout03d-084",
|
| 1371 |
+
"language": "en",
|
| 1372 |
+
"scenario": "ambiguity",
|
| 1373 |
+
"ocr_noise": false,
|
| 1374 |
+
"text": "An evangelical sales pitch is the example in this marketing exercise.",
|
| 1375 |
+
"spans": [],
|
| 1376 |
+
"expected": "An evangelical sales pitch is the example in this marketing exercise."
|
| 1377 |
+
},
|
| 1378 |
+
{
|
| 1379 |
+
"id": "holdout03d-085",
|
| 1380 |
+
"language": "de",
|
| 1381 |
+
"scenario": "ambiguity",
|
| 1382 |
+
"ocr_noise": false,
|
| 1383 |
+
"text": "Der Patient wurde orthodox getauft, wie der geschützte Eintrag berichtet.",
|
| 1384 |
+
"spans": [
|
| 1385 |
+
{
|
| 1386 |
+
"start": 18,
|
| 1387 |
+
"end": 26,
|
| 1388 |
+
"label": "RELIGION",
|
| 1389 |
+
"text": "orthodox"
|
| 1390 |
+
}
|
| 1391 |
+
],
|
| 1392 |
+
"expected": "Der Patient wurde [RELIGION] getauft, wie der geschützte Eintrag berichtet."
|
| 1393 |
+
},
|
| 1394 |
+
{
|
| 1395 |
+
"id": "holdout03d-086",
|
| 1396 |
+
"language": "en",
|
| 1397 |
+
"scenario": "ambiguity",
|
| 1398 |
+
"ocr_noise": false,
|
| 1399 |
+
"text": "The baptismal record identifies the denomination as Orthodox.",
|
| 1400 |
+
"spans": [
|
| 1401 |
+
{
|
| 1402 |
+
"start": 52,
|
| 1403 |
+
"end": 60,
|
| 1404 |
+
"label": "RELIGION",
|
| 1405 |
+
"text": "Orthodox"
|
| 1406 |
+
}
|
| 1407 |
+
],
|
| 1408 |
+
"expected": "The baptismal record identifies the denomination as [RELIGION]."
|
| 1409 |
+
},
|
| 1410 |
+
{
|
| 1411 |
+
"id": "holdout03d-087",
|
| 1412 |
+
"language": "de",
|
| 1413 |
+
"scenario": "ambiguity",
|
| 1414 |
+
"ocr_noise": false,
|
| 1415 |
+
"text": "Konfession: evangelisch; Algorithmus: konventionell.",
|
| 1416 |
+
"spans": [
|
| 1417 |
+
{
|
| 1418 |
+
"start": 12,
|
| 1419 |
+
"end": 23,
|
| 1420 |
+
"label": "RELIGION",
|
| 1421 |
+
"text": "evangelisch"
|
| 1422 |
+
}
|
| 1423 |
+
],
|
| 1424 |
+
"expected": "Konfession: [RELIGION]; Algorithmus: konventionell."
|
| 1425 |
+
},
|
| 1426 |
+
{
|
| 1427 |
+
"id": "holdout03d-088",
|
| 1428 |
+
"language": "en",
|
| 1429 |
+
"scenario": "ambiguity",
|
| 1430 |
+
"ocr_noise": false,
|
| 1431 |
+
"text": "The faith survey selected Evangelical under denomination.",
|
| 1432 |
+
"spans": [
|
| 1433 |
+
{
|
| 1434 |
+
"start": 26,
|
| 1435 |
+
"end": 37,
|
| 1436 |
+
"label": "RELIGION",
|
| 1437 |
+
"text": "Evangelical"
|
| 1438 |
+
}
|
| 1439 |
+
],
|
| 1440 |
+
"expected": "The faith survey selected [RELIGION] under denomination."
|
| 1441 |
+
},
|
| 1442 |
+
{
|
| 1443 |
+
"id": "holdout03d-089",
|
| 1444 |
+
"language": "de",
|
| 1445 |
+
"scenario": "ambiguity",
|
| 1446 |
+
"ocr_noise": false,
|
| 1447 |
+
"text": "Eine säkulare Rechtsordnung nennt keine persönlichen Bekenntnisse.",
|
| 1448 |
+
"spans": [],
|
| 1449 |
+
"expected": "Eine säkulare Rechtsordnung nennt keine persönlichen Bekenntnisse."
|
| 1450 |
+
},
|
| 1451 |
+
{
|
| 1452 |
+
"id": "holdout03d-090",
|
| 1453 |
+
"language": "en",
|
| 1454 |
+
"scenario": "ambiguity",
|
| 1455 |
+
"ocr_noise": false,
|
| 1456 |
+
"text": "The meeting has an unaffiliated chair and no membership declarations.",
|
| 1457 |
+
"spans": [],
|
| 1458 |
+
"expected": "The meeting has an unaffiliated chair and no membership declarations."
|
| 1459 |
+
},
|
| 1460 |
+
{
|
| 1461 |
+
"id": "holdout03d-091",
|
| 1462 |
+
"language": "de",
|
| 1463 |
+
"scenario": "ambiguity",
|
| 1464 |
+
"ocr_noise": false,
|
| 1465 |
+
"text": "Als Religion ist christlich vermerkt, nicht als Personenname.",
|
| 1466 |
+
"spans": [
|
| 1467 |
+
{
|
| 1468 |
+
"start": 17,
|
| 1469 |
+
"end": 27,
|
| 1470 |
+
"label": "RELIGION",
|
| 1471 |
+
"text": "christlich"
|
| 1472 |
+
}
|
| 1473 |
+
],
|
| 1474 |
+
"expected": "Als Religion ist [RELIGION] vermerkt, nicht als Personenname."
|
| 1475 |
+
},
|
| 1476 |
+
{
|
| 1477 |
+
"id": "holdout03d-092",
|
| 1478 |
+
"language": "en",
|
| 1479 |
+
"scenario": "ambiguity",
|
| 1480 |
+
"ocr_noise": false,
|
| 1481 |
+
"text": "The religion field is Christian, distinct from the given-name column.",
|
| 1482 |
+
"spans": [
|
| 1483 |
+
{
|
| 1484 |
+
"start": 22,
|
| 1485 |
+
"end": 31,
|
| 1486 |
+
"label": "RELIGION",
|
| 1487 |
+
"text": "Christian"
|
| 1488 |
+
}
|
| 1489 |
+
],
|
| 1490 |
+
"expected": "The religion field is [RELIGION], distinct from the given-name column."
|
| 1491 |
+
},
|
| 1492 |
+
{
|
| 1493 |
+
"id": "holdout03d-093",
|
| 1494 |
+
"language": "de",
|
| 1495 |
+
"scenario": "ambiguity",
|
| 1496 |
+
"ocr_noise": false,
|
| 1497 |
+
"text": "Die goldene Regel im Handwerk ist kein personenbezogenes Religionsmerkmal.",
|
| 1498 |
+
"spans": [],
|
| 1499 |
+
"expected": "Die goldene Regel im Handwerk ist kein personenbezogenes Religionsmerkmal."
|
| 1500 |
+
},
|
| 1501 |
+
{
|
| 1502 |
+
"id": "holdout03d-094",
|
| 1503 |
+
"language": "en",
|
| 1504 |
+
"scenario": "ambiguity",
|
| 1505 |
+
"ocr_noise": false,
|
| 1506 |
+
"text": "The orthodox spelling of a variable is a programming concern.",
|
| 1507 |
+
"spans": [],
|
| 1508 |
+
"expected": "The orthodox spelling of a variable is a programming concern."
|
| 1509 |
+
},
|
| 1510 |
+
{
|
| 1511 |
+
"id": "holdout03d-095",
|
| 1512 |
+
"language": "de",
|
| 1513 |
+
"scenario": "ambiguity",
|
| 1514 |
+
"ocr_noise": false,
|
| 1515 |
+
"text": "Im Glaubensregister steht jüdisch als vertrauliche Angabe.",
|
| 1516 |
+
"spans": [
|
| 1517 |
+
{
|
| 1518 |
+
"start": 26,
|
| 1519 |
+
"end": 33,
|
| 1520 |
+
"label": "RELIGION",
|
| 1521 |
+
"text": "jüdisch"
|
| 1522 |
+
}
|
| 1523 |
+
],
|
| 1524 |
+
"expected": "Im Glaubensregister steht [RELIGION] als vertrauliche Angabe."
|
| 1525 |
+
},
|
| 1526 |
+
{
|
| 1527 |
+
"id": "holdout03d-096",
|
| 1528 |
+
"language": "en",
|
| 1529 |
+
"scenario": "ambiguity",
|
| 1530 |
+
"ocr_noise": false,
|
| 1531 |
+
"text": "The theological syllabus labels a tradition Reformed.",
|
| 1532 |
+
"spans": [
|
| 1533 |
+
{
|
| 1534 |
+
"start": 44,
|
| 1535 |
+
"end": 52,
|
| 1536 |
+
"label": "RELIGION",
|
| 1537 |
+
"text": "Reformed"
|
| 1538 |
+
}
|
| 1539 |
+
],
|
| 1540 |
+
"expected": "The theological syllabus labels a tradition [RELIGION]."
|
| 1541 |
+
},
|
| 1542 |
+
{
|
| 1543 |
+
"id": "holdout03d-097",
|
| 1544 |
+
"language": "de",
|
| 1545 |
+
"scenario": "ocr_noise",
|
| 1546 |
+
"ocr_noise": true,
|
| 1547 |
+
"text": "Religion laut Scan: Buddh1smus.",
|
| 1548 |
+
"spans": [
|
| 1549 |
+
{
|
| 1550 |
+
"start": 20,
|
| 1551 |
+
"end": 30,
|
| 1552 |
+
"label": "RELIGION",
|
| 1553 |
+
"text": "Buddh1smus"
|
| 1554 |
+
}
|
| 1555 |
+
],
|
| 1556 |
+
"expected": "Religion laut Scan: [RELIGION]."
|
| 1557 |
+
},
|
| 1558 |
+
{
|
| 1559 |
+
"id": "holdout03d-098",
|
| 1560 |
+
"language": "en",
|
| 1561 |
+
"scenario": "ocr_noise",
|
| 1562 |
+
"ocr_noise": true,
|
| 1563 |
+
"text": "The scanned belief field reads Christlanity.",
|
| 1564 |
+
"spans": [
|
| 1565 |
+
{
|
| 1566 |
+
"start": 31,
|
| 1567 |
+
"end": 43,
|
| 1568 |
+
"label": "RELIGION",
|
| 1569 |
+
"text": "Christlanity"
|
| 1570 |
+
}
|
| 1571 |
+
],
|
| 1572 |
+
"expected": "The scanned belief field reads [RELIGION]."
|
| 1573 |
+
},
|
| 1574 |
+
{
|
| 1575 |
+
"id": "holdout03d-099",
|
| 1576 |
+
"language": "de",
|
| 1577 |
+
"scenario": "ocr_noise",
|
| 1578 |
+
"ocr_noise": true,
|
| 1579 |
+
"text": "Konfession im OCR-Text: konfessions-\nlos.",
|
| 1580 |
+
"spans": [
|
| 1581 |
+
{
|
| 1582 |
+
"start": 24,
|
| 1583 |
+
"end": 40,
|
| 1584 |
+
"label": "RELIGION",
|
| 1585 |
+
"text": "konfessions-\nlos"
|
| 1586 |
+
}
|
| 1587 |
+
],
|
| 1588 |
+
"expected": "Konfession im OCR-Text: [RELIGION]."
|
| 1589 |
+
},
|
| 1590 |
+
{
|
| 1591 |
+
"id": "holdout03d-100",
|
| 1592 |
+
"language": "en",
|
| 1593 |
+
"scenario": "ocr_noise",
|
| 1594 |
+
"ocr_noise": true,
|
| 1595 |
+
"text": "Faith from the scan: non-\nreligious.",
|
| 1596 |
+
"spans": [
|
| 1597 |
+
{
|
| 1598 |
+
"start": 21,
|
| 1599 |
+
"end": 35,
|
| 1600 |
+
"label": "RELIGION",
|
| 1601 |
+
"text": "non-\nreligious"
|
| 1602 |
+
}
|
| 1603 |
+
],
|
| 1604 |
+
"expected": "Faith from the scan: [RELIGION]."
|
| 1605 |
+
},
|
| 1606 |
+
{
|
| 1607 |
+
"id": "holdout03d-101",
|
| 1608 |
+
"language": "de",
|
| 1609 |
+
"scenario": "ocr_noise",
|
| 1610 |
+
"ocr_noise": true,
|
| 1611 |
+
"text": "Die Lesart katho1isch ist im Faksimile markiert.",
|
| 1612 |
+
"spans": [
|
| 1613 |
+
{
|
| 1614 |
+
"start": 11,
|
| 1615 |
+
"end": 21,
|
| 1616 |
+
"label": "RELIGION",
|
| 1617 |
+
"text": "katho1isch"
|
| 1618 |
+
}
|
| 1619 |
+
],
|
| 1620 |
+
"expected": "Die Lesart [RELIGION] ist im Faksimile markiert."
|
| 1621 |
+
},
|
| 1622 |
+
{
|
| 1623 |
+
"id": "holdout03d-102",
|
| 1624 |
+
"language": "en",
|
| 1625 |
+
"scenario": "ocr_noise",
|
| 1626 |
+
"ocr_noise": true,
|
| 1627 |
+
"text": "Belief value after OCR: Hinduisrn.",
|
| 1628 |
+
"spans": [
|
| 1629 |
+
{
|
| 1630 |
+
"start": 24,
|
| 1631 |
+
"end": 33,
|
| 1632 |
+
"label": "RELIGION",
|
| 1633 |
+
"text": "Hinduisrn"
|
| 1634 |
+
}
|
| 1635 |
+
],
|
| 1636 |
+
"expected": "Belief value after OCR: [RELIGION]."
|
| 1637 |
+
},
|
| 1638 |
+
{
|
| 1639 |
+
"id": "holdout03d-103",
|
| 1640 |
+
"language": "de",
|
| 1641 |
+
"scenario": "ocr_noise",
|
| 1642 |
+
"ocr_noise": true,
|
| 1643 |
+
"text": "Im Formular wurde agnos-\ntisch getrennt gesetzt.",
|
| 1644 |
+
"spans": [
|
| 1645 |
+
{
|
| 1646 |
+
"start": 18,
|
| 1647 |
+
"end": 30,
|
| 1648 |
+
"label": "RELIGION",
|
| 1649 |
+
"text": "agnos-\ntisch"
|
| 1650 |
+
}
|
| 1651 |
+
],
|
| 1652 |
+
"expected": "Im Formular wurde [RELIGION] getrennt gesetzt."
|
| 1653 |
+
},
|
| 1654 |
+
{
|
| 1655 |
+
"id": "holdout03d-104",
|
| 1656 |
+
"language": "en",
|
| 1657 |
+
"scenario": "ocr_noise",
|
| 1658 |
+
"ocr_noise": true,
|
| 1659 |
+
"text": "The scanned heading says Juda-\nism.",
|
| 1660 |
+
"spans": [
|
| 1661 |
+
{
|
| 1662 |
+
"start": 25,
|
| 1663 |
+
"end": 34,
|
| 1664 |
+
"label": "RELIGION",
|
| 1665 |
+
"text": "Juda-\nism"
|
| 1666 |
+
}
|
| 1667 |
+
],
|
| 1668 |
+
"expected": "The scanned heading says [RELIGION]."
|
| 1669 |
+
},
|
| 1670 |
+
{
|
| 1671 |
+
"id": "holdout03d-105",
|
| 1672 |
+
"language": "de",
|
| 1673 |
+
"scenario": "ocr_noise",
|
| 1674 |
+
"ocr_noise": true,
|
| 1675 |
+
"text": "Eine Zeile enthält lslam mit verwechseltem Anfangsbuchstaben.",
|
| 1676 |
+
"spans": [
|
| 1677 |
+
{
|
| 1678 |
+
"start": 19,
|
| 1679 |
+
"end": 24,
|
| 1680 |
+
"label": "RELIGION",
|
| 1681 |
+
"text": "lslam"
|
| 1682 |
+
}
|
| 1683 |
+
],
|
| 1684 |
+
"expected": "Eine Zeile enthält [RELIGION] mit verwechseltem Anfangsbuchstaben."
|
| 1685 |
+
},
|
| 1686 |
+
{
|
| 1687 |
+
"id": "holdout03d-106",
|
| 1688 |
+
"language": "en",
|
| 1689 |
+
"scenario": "ocr_noise",
|
| 1690 |
+
"ocr_noise": true,
|
| 1691 |
+
"text": "The OCR extracted S1khism in the private religion column.",
|
| 1692 |
+
"spans": [
|
| 1693 |
+
{
|
| 1694 |
+
"start": 18,
|
| 1695 |
+
"end": 25,
|
| 1696 |
+
"label": "RELIGION",
|
| 1697 |
+
"text": "S1khism"
|
| 1698 |
+
}
|
| 1699 |
+
],
|
| 1700 |
+
"expected": "The OCR extracted [RELIGION] in the private religion column."
|
| 1701 |
+
},
|
| 1702 |
+
{
|
| 1703 |
+
"id": "holdout03d-107",
|
| 1704 |
+
"language": "de",
|
| 1705 |
+
"scenario": "ocr_noise",
|
| 1706 |
+
"ocr_noise": true,
|
| 1707 |
+
"text": "Die handschriftliche Angabe lautet buddhis-\ntisch.",
|
| 1708 |
+
"spans": [
|
| 1709 |
+
{
|
| 1710 |
+
"start": 35,
|
| 1711 |
+
"end": 49,
|
| 1712 |
+
"label": "RELIGION",
|
| 1713 |
+
"text": "buddhis-\ntisch"
|
| 1714 |
+
}
|
| 1715 |
+
],
|
| 1716 |
+
"expected": "Die handschriftliche Angabe lautet [RELIGION]."
|
| 1717 |
+
},
|
| 1718 |
+
{
|
| 1719 |
+
"id": "holdout03d-108",
|
| 1720 |
+
"language": "en",
|
| 1721 |
+
"scenario": "ocr_noise",
|
| 1722 |
+
"ocr_noise": true,
|
| 1723 |
+
"text": "The line-broken subject is Zoroas-\ntrianism.",
|
| 1724 |
+
"spans": [
|
| 1725 |
+
{
|
| 1726 |
+
"start": 27,
|
| 1727 |
+
"end": 43,
|
| 1728 |
+
"label": "RELIGION",
|
| 1729 |
+
"text": "Zoroas-\ntrianism"
|
| 1730 |
+
}
|
| 1731 |
+
],
|
| 1732 |
+
"expected": "The line-broken subject is [RELIGION]."
|
| 1733 |
+
},
|
| 1734 |
+
{
|
| 1735 |
+
"id": "holdout03d-109",
|
| 1736 |
+
"language": "de",
|
| 1737 |
+
"scenario": "ocr_noise",
|
| 1738 |
+
"ocr_noise": true,
|
| 1739 |
+
"text": "Aus dem Scan: Fenja Torwald, Bekenntnis Judenturn.",
|
| 1740 |
+
"spans": [
|
| 1741 |
+
{
|
| 1742 |
+
"start": 14,
|
| 1743 |
+
"end": 27,
|
| 1744 |
+
"label": "PERSON",
|
| 1745 |
+
"text": "Fenja Torwald"
|
| 1746 |
+
},
|
| 1747 |
+
{
|
| 1748 |
+
"start": 40,
|
| 1749 |
+
"end": 49,
|
| 1750 |
+
"label": "RELIGION",
|
| 1751 |
+
"text": "Judenturn"
|
| 1752 |
+
}
|
| 1753 |
+
],
|
| 1754 |
+
"expected": "Aus dem Scan: [PERSON], Bekenntnis [RELIGION]."
|
| 1755 |
+
},
|
| 1756 |
+
{
|
| 1757 |
+
"id": "holdout03d-110",
|
| 1758 |
+
"language": "en",
|
| 1759 |
+
"scenario": "ocr_noise",
|
| 1760 |
+
"ocr_noise": true,
|
| 1761 |
+
"text": "The noisy note names Orin Wexford and Cath0licism.",
|
| 1762 |
+
"spans": [
|
| 1763 |
+
{
|
| 1764 |
+
"start": 21,
|
| 1765 |
+
"end": 33,
|
| 1766 |
+
"label": "PERSON",
|
| 1767 |
+
"text": "Orin Wexford"
|
| 1768 |
+
},
|
| 1769 |
+
{
|
| 1770 |
+
"start": 38,
|
| 1771 |
+
"end": 49,
|
| 1772 |
+
"label": "RELIGION",
|
| 1773 |
+
"text": "Cath0licism"
|
| 1774 |
+
}
|
| 1775 |
+
],
|
| 1776 |
+
"expected": "The noisy note names [PERSON] and [RELIGION]."
|
| 1777 |
+
},
|
| 1778 |
+
{
|
| 1779 |
+
"id": "holdout03d-111",
|
| 1780 |
+
"language": "de",
|
| 1781 |
+
"scenario": "ocr_noise",
|
| 1782 |
+
"ocr_noise": true,
|
| 1783 |
+
"text": "Der OCR-Wert konfesslonsfrei bleibt vertraulich.",
|
| 1784 |
+
"spans": [
|
| 1785 |
+
{
|
| 1786 |
+
"start": 13,
|
| 1787 |
+
"end": 28,
|
| 1788 |
+
"label": "RELIGION",
|
| 1789 |
+
"text": "konfesslonsfrei"
|
| 1790 |
+
}
|
| 1791 |
+
],
|
| 1792 |
+
"expected": "Der OCR-Wert [RELIGION] bleibt vertraulich."
|
| 1793 |
+
},
|
| 1794 |
+
{
|
| 1795 |
+
"id": "holdout03d-112",
|
| 1796 |
+
"language": "en",
|
| 1797 |
+
"scenario": "ocr_noise",
|
| 1798 |
+
"ocr_noise": true,
|
| 1799 |
+
"text": "Scanned status text: agn0stic.",
|
| 1800 |
+
"spans": [
|
| 1801 |
+
{
|
| 1802 |
+
"start": 21,
|
| 1803 |
+
"end": 29,
|
| 1804 |
+
"label": "RELIGION",
|
| 1805 |
+
"text": "agn0stic"
|
| 1806 |
+
}
|
| 1807 |
+
],
|
| 1808 |
+
"expected": "Scanned status text: [RELIGION]."
|
| 1809 |
+
},
|
| 1810 |
+
{
|
| 1811 |
+
"id": "holdout03d-113",
|
| 1812 |
+
"language": "de",
|
| 1813 |
+
"scenario": "multiple_entities",
|
| 1814 |
+
"ocr_noise": false,
|
| 1815 |
+
"text": "Silja Dornbach aus Rostock meldet Judentum beim Morgenkai Institut.",
|
| 1816 |
+
"spans": [
|
| 1817 |
+
{
|
| 1818 |
+
"start": 0,
|
| 1819 |
+
"end": 14,
|
| 1820 |
+
"label": "PERSON",
|
| 1821 |
+
"text": "Silja Dornbach"
|
| 1822 |
+
},
|
| 1823 |
+
{
|
| 1824 |
+
"start": 19,
|
| 1825 |
+
"end": 26,
|
| 1826 |
+
"label": "CITY",
|
| 1827 |
+
"text": "Rostock"
|
| 1828 |
+
},
|
| 1829 |
+
{
|
| 1830 |
+
"start": 34,
|
| 1831 |
+
"end": 42,
|
| 1832 |
+
"label": "RELIGION",
|
| 1833 |
+
"text": "Judentum"
|
| 1834 |
+
},
|
| 1835 |
+
{
|
| 1836 |
+
"start": 48,
|
| 1837 |
+
"end": 66,
|
| 1838 |
+
"label": "ORGANIZATION",
|
| 1839 |
+
"text": "Morgenkai Institut"
|
| 1840 |
+
}
|
| 1841 |
+
],
|
| 1842 |
+
"expected": "[PERSON] aus [CITY] meldet [RELIGION] beim [ORGANIZATION]."
|
| 1843 |
+
},
|
| 1844 |
+
{
|
| 1845 |
+
"id": "holdout03d-114",
|
| 1846 |
+
"language": "en",
|
| 1847 |
+
"scenario": "multiple_entities",
|
| 1848 |
+
"ocr_noise": false,
|
| 1849 |
+
"text": "Nolan Briarwood in York lists Jainism on the Cobalt Quay Trust form.",
|
| 1850 |
+
"spans": [
|
| 1851 |
+
{
|
| 1852 |
+
"start": 0,
|
| 1853 |
+
"end": 15,
|
| 1854 |
+
"label": "PERSON",
|
| 1855 |
+
"text": "Nolan Briarwood"
|
| 1856 |
+
},
|
| 1857 |
+
{
|
| 1858 |
+
"start": 19,
|
| 1859 |
+
"end": 23,
|
| 1860 |
+
"label": "CITY",
|
| 1861 |
+
"text": "York"
|
| 1862 |
+
},
|
| 1863 |
+
{
|
| 1864 |
+
"start": 30,
|
| 1865 |
+
"end": 37,
|
| 1866 |
+
"label": "RELIGION",
|
| 1867 |
+
"text": "Jainism"
|
| 1868 |
+
},
|
| 1869 |
+
{
|
| 1870 |
+
"start": 45,
|
| 1871 |
+
"end": 62,
|
| 1872 |
+
"label": "ORGANIZATION",
|
| 1873 |
+
"text": "Cobalt Quay Trust"
|
| 1874 |
+
}
|
| 1875 |
+
],
|
| 1876 |
+
"expected": "[PERSON] in [CITY] lists [RELIGION] on the [ORGANIZATION] form."
|
| 1877 |
+
},
|
| 1878 |
+
{
|
| 1879 |
+
"id": "holdout03d-115",
|
| 1880 |
+
"language": "de",
|
| 1881 |
+
"scenario": "multiple_entities",
|
| 1882 |
+
"ocr_noise": false,
|
| 1883 |
+
"text": "Bernsteinpfad Stiftung vermerkt Henrik Wiesenau als konfessionslos.",
|
| 1884 |
+
"spans": [
|
| 1885 |
+
{
|
| 1886 |
+
"start": 0,
|
| 1887 |
+
"end": 22,
|
| 1888 |
+
"label": "ORGANIZATION",
|
| 1889 |
+
"text": "Bernsteinpfad Stiftung"
|
| 1890 |
+
},
|
| 1891 |
+
{
|
| 1892 |
+
"start": 32,
|
| 1893 |
+
"end": 47,
|
| 1894 |
+
"label": "PERSON",
|
| 1895 |
+
"text": "Henrik Wiesenau"
|
| 1896 |
+
},
|
| 1897 |
+
{
|
| 1898 |
+
"start": 52,
|
| 1899 |
+
"end": 66,
|
| 1900 |
+
"label": "RELIGION",
|
| 1901 |
+
"text": "konfessionslos"
|
| 1902 |
+
}
|
| 1903 |
+
],
|
| 1904 |
+
"expected": "[ORGANIZATION] vermerkt [PERSON] als [RELIGION]."
|
| 1905 |
+
},
|
| 1906 |
+
{
|
| 1907 |
+
"id": "holdout03d-116",
|
| 1908 |
+
"language": "en",
|
| 1909 |
+
"scenario": "multiple_entities",
|
| 1910 |
+
"ocr_noise": false,
|
| 1911 |
+
"text": "The Silver Estuary Council stores Ada Merefield's atheism response.",
|
| 1912 |
+
"spans": [
|
| 1913 |
+
{
|
| 1914 |
+
"start": 4,
|
| 1915 |
+
"end": 26,
|
| 1916 |
+
"label": "ORGANIZATION",
|
| 1917 |
+
"text": "Silver Estuary Council"
|
| 1918 |
+
},
|
| 1919 |
+
{
|
| 1920 |
+
"start": 34,
|
| 1921 |
+
"end": 47,
|
| 1922 |
+
"label": "PERSON",
|
| 1923 |
+
"text": "Ada Merefield"
|
| 1924 |
+
},
|
| 1925 |
+
{
|
| 1926 |
+
"start": 50,
|
| 1927 |
+
"end": 57,
|
| 1928 |
+
"label": "RELIGION",
|
| 1929 |
+
"text": "atheism"
|
| 1930 |
+
}
|
| 1931 |
+
],
|
| 1932 |
+
"expected": "The [ORGANIZATION] stores [PERSON]'s [RELIGION] response."
|
| 1933 |
+
},
|
| 1934 |
+
{
|
| 1935 |
+
"id": "holdout03d-117",
|
| 1936 |
+
"language": "de",
|
| 1937 |
+
"scenario": "multiple_entities",
|
| 1938 |
+
"ocr_noise": false,
|
| 1939 |
+
"text": "Ida Talbrück nennt Islam; Kontakt: ida.tal@example.invalid.",
|
| 1940 |
+
"spans": [
|
| 1941 |
+
{
|
| 1942 |
+
"start": 0,
|
| 1943 |
+
"end": 12,
|
| 1944 |
+
"label": "PERSON",
|
| 1945 |
+
"text": "Ida Talbrück"
|
| 1946 |
+
},
|
| 1947 |
+
{
|
| 1948 |
+
"start": 19,
|
| 1949 |
+
"end": 24,
|
| 1950 |
+
"label": "RELIGION",
|
| 1951 |
+
"text": "Islam"
|
| 1952 |
+
},
|
| 1953 |
+
{
|
| 1954 |
+
"start": 35,
|
| 1955 |
+
"end": 58,
|
| 1956 |
+
"label": "EMAIL",
|
| 1957 |
+
"text": "ida.tal@example.invalid"
|
| 1958 |
+
}
|
| 1959 |
+
],
|
| 1960 |
+
"expected": "[PERSON] nennt [RELIGION]; Kontakt: [EMAIL]."
|
| 1961 |
+
},
|
| 1962 |
+
{
|
| 1963 |
+
"id": "holdout03d-118",
|
| 1964 |
+
"language": "en",
|
| 1965 |
+
"scenario": "multiple_entities",
|
| 1966 |
+
"ocr_noise": false,
|
| 1967 |
+
"text": "Lyle Ashcombe wrote Buddhism; contact lyle.a@example.invalid.",
|
| 1968 |
+
"spans": [
|
| 1969 |
+
{
|
| 1970 |
+
"start": 0,
|
| 1971 |
+
"end": 13,
|
| 1972 |
+
"label": "PERSON",
|
| 1973 |
+
"text": "Lyle Ashcombe"
|
| 1974 |
+
},
|
| 1975 |
+
{
|
| 1976 |
+
"start": 20,
|
| 1977 |
+
"end": 28,
|
| 1978 |
+
"label": "RELIGION",
|
| 1979 |
+
"text": "Buddhism"
|
| 1980 |
+
},
|
| 1981 |
+
{
|
| 1982 |
+
"start": 38,
|
| 1983 |
+
"end": 60,
|
| 1984 |
+
"label": "EMAIL",
|
| 1985 |
+
"text": "lyle.a@example.invalid"
|
| 1986 |
+
}
|
| 1987 |
+
],
|
| 1988 |
+
"expected": "[PERSON] wrote [RELIGION]; contact [EMAIL]."
|
| 1989 |
+
},
|
| 1990 |
+
{
|
| 1991 |
+
"id": "holdout03d-119",
|
| 1992 |
+
"language": "de",
|
| 1993 |
+
"scenario": "multiple_entities",
|
| 1994 |
+
"ocr_noise": false,
|
| 1995 |
+
"text": "Rückfrage an Arvid Sandborn: +49 381 482701; Status religionslos.",
|
| 1996 |
+
"spans": [
|
| 1997 |
+
{
|
| 1998 |
+
"start": 13,
|
| 1999 |
+
"end": 27,
|
| 2000 |
+
"label": "PERSON",
|
| 2001 |
+
"text": "Arvid Sandborn"
|
| 2002 |
+
},
|
| 2003 |
+
{
|
| 2004 |
+
"start": 29,
|
| 2005 |
+
"end": 43,
|
| 2006 |
+
"label": "PHONE",
|
| 2007 |
+
"text": "+49 381 482701"
|
| 2008 |
+
},
|
| 2009 |
+
{
|
| 2010 |
+
"start": 52,
|
| 2011 |
+
"end": 64,
|
| 2012 |
+
"label": "RELIGION",
|
| 2013 |
+
"text": "religionslos"
|
| 2014 |
+
}
|
| 2015 |
+
],
|
| 2016 |
+
"expected": "Rückfrage an [PERSON]: [PHONE]; Status [RELIGION]."
|
| 2017 |
+
},
|
| 2018 |
+
{
|
| 2019 |
+
"id": "holdout03d-120",
|
| 2020 |
+
"language": "en",
|
| 2021 |
+
"scenario": "multiple_entities",
|
| 2022 |
+
"ocr_noise": false,
|
| 2023 |
+
"text": "Call Mira Fenwick at +44 1904 582701; belief Hinduism.",
|
| 2024 |
+
"spans": [
|
| 2025 |
+
{
|
| 2026 |
+
"start": 5,
|
| 2027 |
+
"end": 17,
|
| 2028 |
+
"label": "PERSON",
|
| 2029 |
+
"text": "Mira Fenwick"
|
| 2030 |
+
},
|
| 2031 |
+
{
|
| 2032 |
+
"start": 21,
|
| 2033 |
+
"end": 36,
|
| 2034 |
+
"label": "PHONE",
|
| 2035 |
+
"text": "+44 1904 582701"
|
| 2036 |
+
},
|
| 2037 |
+
{
|
| 2038 |
+
"start": 45,
|
| 2039 |
+
"end": 53,
|
| 2040 |
+
"label": "RELIGION",
|
| 2041 |
+
"text": "Hinduism"
|
| 2042 |
+
}
|
| 2043 |
+
],
|
| 2044 |
+
"expected": "Call [PERSON] at [PHONE]; belief [RELIGION]."
|
| 2045 |
+
},
|
| 2046 |
+
{
|
| 2047 |
+
"id": "holdout03d-121",
|
| 2048 |
+
"language": "de",
|
| 2049 |
+
"scenario": "multiple_entities",
|
| 2050 |
+
"ocr_noise": false,
|
| 2051 |
+
"text": "Erfurt: Svea Morgenau korrigiert katholisch zu agnostisch.",
|
| 2052 |
+
"spans": [
|
| 2053 |
+
{
|
| 2054 |
+
"start": 0,
|
| 2055 |
+
"end": 6,
|
| 2056 |
+
"label": "CITY",
|
| 2057 |
+
"text": "Erfurt"
|
| 2058 |
+
},
|
| 2059 |
+
{
|
| 2060 |
+
"start": 8,
|
| 2061 |
+
"end": 21,
|
| 2062 |
+
"label": "PERSON",
|
| 2063 |
+
"text": "Svea Morgenau"
|
| 2064 |
+
},
|
| 2065 |
+
{
|
| 2066 |
+
"start": 33,
|
| 2067 |
+
"end": 43,
|
| 2068 |
+
"label": "RELIGION",
|
| 2069 |
+
"text": "katholisch"
|
| 2070 |
+
},
|
| 2071 |
+
{
|
| 2072 |
+
"start": 47,
|
| 2073 |
+
"end": 57,
|
| 2074 |
+
"label": "RELIGION",
|
| 2075 |
+
"text": "agnostisch"
|
| 2076 |
+
}
|
| 2077 |
+
],
|
| 2078 |
+
"expected": "[CITY]: [PERSON] korrigiert [RELIGION] zu [RELIGION]."
|
| 2079 |
+
},
|
| 2080 |
+
{
|
| 2081 |
+
"id": "holdout03d-122",
|
| 2082 |
+
"language": "en",
|
| 2083 |
+
"scenario": "multiple_entities",
|
| 2084 |
+
"ocr_noise": false,
|
| 2085 |
+
"text": "Bristol: Owen Thornley replaced Christianity with no religion.",
|
| 2086 |
+
"spans": [
|
| 2087 |
+
{
|
| 2088 |
+
"start": 0,
|
| 2089 |
+
"end": 7,
|
| 2090 |
+
"label": "CITY",
|
| 2091 |
+
"text": "Bristol"
|
| 2092 |
+
},
|
| 2093 |
+
{
|
| 2094 |
+
"start": 9,
|
| 2095 |
+
"end": 22,
|
| 2096 |
+
"label": "PERSON",
|
| 2097 |
+
"text": "Owen Thornley"
|
| 2098 |
+
},
|
| 2099 |
+
{
|
| 2100 |
+
"start": 32,
|
| 2101 |
+
"end": 44,
|
| 2102 |
+
"label": "RELIGION",
|
| 2103 |
+
"text": "Christianity"
|
| 2104 |
+
},
|
| 2105 |
+
{
|
| 2106 |
+
"start": 50,
|
| 2107 |
+
"end": 61,
|
| 2108 |
+
"label": "RELIGION",
|
| 2109 |
+
"text": "no religion"
|
| 2110 |
+
}
|
| 2111 |
+
],
|
| 2112 |
+
"expected": "[CITY]: [PERSON] replaced [RELIGION] with [RELIGION]."
|
| 2113 |
+
},
|
| 2114 |
+
{
|
| 2115 |
+
"id": "holdout03d-123",
|
| 2116 |
+
"language": "de",
|
| 2117 |
+
"scenario": "multiple_entities",
|
| 2118 |
+
"ocr_noise": false,
|
| 2119 |
+
"text": "Jona Falkenau und Jona Falkenau erscheinen mit Sikhismus zweimal.",
|
| 2120 |
+
"spans": [
|
| 2121 |
+
{
|
| 2122 |
+
"start": 0,
|
| 2123 |
+
"end": 13,
|
| 2124 |
+
"label": "PERSON",
|
| 2125 |
+
"text": "Jona Falkenau"
|
| 2126 |
+
},
|
| 2127 |
+
{
|
| 2128 |
+
"start": 18,
|
| 2129 |
+
"end": 31,
|
| 2130 |
+
"label": "PERSON",
|
| 2131 |
+
"text": "Jona Falkenau"
|
| 2132 |
+
},
|
| 2133 |
+
{
|
| 2134 |
+
"start": 47,
|
| 2135 |
+
"end": 56,
|
| 2136 |
+
"label": "RELIGION",
|
| 2137 |
+
"text": "Sikhismus"
|
| 2138 |
+
}
|
| 2139 |
+
],
|
| 2140 |
+
"expected": "[PERSON] und [PERSON] erscheinen mit [RELIGION] zweimal."
|
| 2141 |
+
},
|
| 2142 |
+
{
|
| 2143 |
+
"id": "holdout03d-124",
|
| 2144 |
+
"language": "en",
|
| 2145 |
+
"scenario": "multiple_entities",
|
| 2146 |
+
"ocr_noise": false,
|
| 2147 |
+
"text": "Esme Rowanby repeated Taoism, then signed Esme Rowanby again.",
|
| 2148 |
+
"spans": [
|
| 2149 |
+
{
|
| 2150 |
+
"start": 0,
|
| 2151 |
+
"end": 12,
|
| 2152 |
+
"label": "PERSON",
|
| 2153 |
+
"text": "Esme Rowanby"
|
| 2154 |
+
},
|
| 2155 |
+
{
|
| 2156 |
+
"start": 22,
|
| 2157 |
+
"end": 28,
|
| 2158 |
+
"label": "RELIGION",
|
| 2159 |
+
"text": "Taoism"
|
| 2160 |
+
},
|
| 2161 |
+
{
|
| 2162 |
+
"start": 42,
|
| 2163 |
+
"end": 54,
|
| 2164 |
+
"label": "PERSON",
|
| 2165 |
+
"text": "Esme Rowanby"
|
| 2166 |
+
}
|
| 2167 |
+
],
|
| 2168 |
+
"expected": "[PERSON] repeated [RELIGION], then signed [PERSON] again."
|
| 2169 |
+
},
|
| 2170 |
+
{
|
| 2171 |
+
"id": "holdout03d-125",
|
| 2172 |
+
"language": "de",
|
| 2173 |
+
"scenario": "multiple_entities",
|
| 2174 |
+
"ocr_noise": false,
|
| 2175 |
+
"text": "Interner Datensatz des Hafenbogen Archivs: Kiel, evangelisch.",
|
| 2176 |
+
"spans": [
|
| 2177 |
+
{
|
| 2178 |
+
"start": 23,
|
| 2179 |
+
"end": 41,
|
| 2180 |
+
"label": "ORGANIZATION",
|
| 2181 |
+
"text": "Hafenbogen Archivs"
|
| 2182 |
+
},
|
| 2183 |
+
{
|
| 2184 |
+
"start": 43,
|
| 2185 |
+
"end": 47,
|
| 2186 |
+
"label": "CITY",
|
| 2187 |
+
"text": "Kiel"
|
| 2188 |
+
},
|
| 2189 |
+
{
|
| 2190 |
+
"start": 49,
|
| 2191 |
+
"end": 60,
|
| 2192 |
+
"label": "RELIGION",
|
| 2193 |
+
"text": "evangelisch"
|
| 2194 |
+
}
|
| 2195 |
+
],
|
| 2196 |
+
"expected": "Interner Datensatz des [ORGANIZATION]: [CITY], [RELIGION]."
|
| 2197 |
+
},
|
| 2198 |
+
{
|
| 2199 |
+
"id": "holdout03d-126",
|
| 2200 |
+
"language": "en",
|
| 2201 |
+
"scenario": "multiple_entities",
|
| 2202 |
+
"ocr_noise": false,
|
| 2203 |
+
"text": "A Maple Tide Institute record from Durham mentions Judaism.",
|
| 2204 |
+
"spans": [
|
| 2205 |
+
{
|
| 2206 |
+
"start": 2,
|
| 2207 |
+
"end": 22,
|
| 2208 |
+
"label": "ORGANIZATION",
|
| 2209 |
+
"text": "Maple Tide Institute"
|
| 2210 |
+
},
|
| 2211 |
+
{
|
| 2212 |
+
"start": 35,
|
| 2213 |
+
"end": 41,
|
| 2214 |
+
"label": "CITY",
|
| 2215 |
+
"text": "Durham"
|
| 2216 |
+
},
|
| 2217 |
+
{
|
| 2218 |
+
"start": 51,
|
| 2219 |
+
"end": 58,
|
| 2220 |
+
"label": "RELIGION",
|
| 2221 |
+
"text": "Judaism"
|
| 2222 |
+
}
|
| 2223 |
+
],
|
| 2224 |
+
"expected": "A [ORGANIZATION] record from [CITY] mentions [RELIGION]."
|
| 2225 |
+
},
|
| 2226 |
+
{
|
| 2227 |
+
"id": "holdout03d-127",
|
| 2228 |
+
"language": "de",
|
| 2229 |
+
"scenario": "multiple_entities",
|
| 2230 |
+
"ocr_noise": false,
|
| 2231 |
+
"text": "Liv Hohenrain nennt Bonn und Bahai; liv.h@example.invalid bleibt intern.",
|
| 2232 |
+
"spans": [
|
| 2233 |
+
{
|
| 2234 |
+
"start": 0,
|
| 2235 |
+
"end": 13,
|
| 2236 |
+
"label": "PERSON",
|
| 2237 |
+
"text": "Liv Hohenrain"
|
| 2238 |
+
},
|
| 2239 |
+
{
|
| 2240 |
+
"start": 20,
|
| 2241 |
+
"end": 24,
|
| 2242 |
+
"label": "CITY",
|
| 2243 |
+
"text": "Bonn"
|
| 2244 |
+
},
|
| 2245 |
+
{
|
| 2246 |
+
"start": 29,
|
| 2247 |
+
"end": 34,
|
| 2248 |
+
"label": "RELIGION",
|
| 2249 |
+
"text": "Bahai"
|
| 2250 |
+
},
|
| 2251 |
+
{
|
| 2252 |
+
"start": 36,
|
| 2253 |
+
"end": 57,
|
| 2254 |
+
"label": "EMAIL",
|
| 2255 |
+
"text": "liv.h@example.invalid"
|
| 2256 |
+
}
|
| 2257 |
+
],
|
| 2258 |
+
"expected": "[PERSON] nennt [CITY] und [RELIGION]; [EMAIL] bleibt intern."
|
| 2259 |
+
},
|
| 2260 |
+
{
|
| 2261 |
+
"id": "holdout03d-128",
|
| 2262 |
+
"language": "en",
|
| 2263 |
+
"scenario": "multiple_entities",
|
| 2264 |
+
"ocr_noise": false,
|
| 2265 |
+
"text": "Reed Alderwick, Bath, agnosticism; reed.a@example.invalid is private.",
|
| 2266 |
+
"spans": [
|
| 2267 |
+
{
|
| 2268 |
+
"start": 0,
|
| 2269 |
+
"end": 14,
|
| 2270 |
+
"label": "PERSON",
|
| 2271 |
+
"text": "Reed Alderwick"
|
| 2272 |
+
},
|
| 2273 |
+
{
|
| 2274 |
+
"start": 16,
|
| 2275 |
+
"end": 20,
|
| 2276 |
+
"label": "CITY",
|
| 2277 |
+
"text": "Bath"
|
| 2278 |
+
},
|
| 2279 |
+
{
|
| 2280 |
+
"start": 22,
|
| 2281 |
+
"end": 33,
|
| 2282 |
+
"label": "RELIGION",
|
| 2283 |
+
"text": "agnosticism"
|
| 2284 |
+
},
|
| 2285 |
+
{
|
| 2286 |
+
"start": 35,
|
| 2287 |
+
"end": 57,
|
| 2288 |
+
"label": "EMAIL",
|
| 2289 |
+
"text": "reed.a@example.invalid"
|
| 2290 |
+
}
|
| 2291 |
+
],
|
| 2292 |
+
"expected": "[PERSON], [CITY], [RELIGION]; [EMAIL] is private."
|
| 2293 |
+
},
|
| 2294 |
+
{
|
| 2295 |
+
"id": "holdout03d-129",
|
| 2296 |
+
"language": "de",
|
| 2297 |
+
"scenario": "formats_repetition",
|
| 2298 |
+
"ocr_noise": false,
|
| 2299 |
+
"text": "| Bekenntnis | Islam | Freigabe | nein |",
|
| 2300 |
+
"spans": [
|
| 2301 |
+
{
|
| 2302 |
+
"start": 15,
|
| 2303 |
+
"end": 20,
|
| 2304 |
+
"label": "RELIGION",
|
| 2305 |
+
"text": "Islam"
|
| 2306 |
+
}
|
| 2307 |
+
],
|
| 2308 |
+
"expected": "| Bekenntnis | [RELIGION] | Freigabe | nein |"
|
| 2309 |
+
},
|
| 2310 |
+
{
|
| 2311 |
+
"id": "holdout03d-130",
|
| 2312 |
+
"language": "en",
|
| 2313 |
+
"scenario": "formats_repetition",
|
| 2314 |
+
"ocr_noise": false,
|
| 2315 |
+
"text": "{\"belief\":\"Buddhism\",\"public\":false}",
|
| 2316 |
+
"spans": [
|
| 2317 |
+
{
|
| 2318 |
+
"start": 11,
|
| 2319 |
+
"end": 19,
|
| 2320 |
+
"label": "RELIGION",
|
| 2321 |
+
"text": "Buddhism"
|
| 2322 |
+
}
|
| 2323 |
+
],
|
| 2324 |
+
"expected": "{\"belief\":\"[RELIGION]\",\"public\":false}"
|
| 2325 |
+
},
|
| 2326 |
+
{
|
| 2327 |
+
"id": "holdout03d-131",
|
| 2328 |
+
"language": "de",
|
| 2329 |
+
"scenario": "formats_repetition",
|
| 2330 |
+
"ocr_noise": false,
|
| 2331 |
+
"text": "Notiz A: konfessionsfrei. Notiz B: konfessionsfrei.",
|
| 2332 |
+
"spans": [
|
| 2333 |
+
{
|
| 2334 |
+
"start": 9,
|
| 2335 |
+
"end": 24,
|
| 2336 |
+
"label": "RELIGION",
|
| 2337 |
+
"text": "konfessionsfrei"
|
| 2338 |
+
},
|
| 2339 |
+
{
|
| 2340 |
+
"start": 35,
|
| 2341 |
+
"end": 50,
|
| 2342 |
+
"label": "RELIGION",
|
| 2343 |
+
"text": "konfessionsfrei"
|
| 2344 |
+
}
|
| 2345 |
+
],
|
| 2346 |
+
"expected": "Notiz A: [RELIGION]. Notiz B: [RELIGION]."
|
| 2347 |
+
},
|
| 2348 |
+
{
|
| 2349 |
+
"id": "holdout03d-132",
|
| 2350 |
+
"language": "en",
|
| 2351 |
+
"scenario": "formats_repetition",
|
| 2352 |
+
"ocr_noise": false,
|
| 2353 |
+
"text": "Row one: atheist; row two: atheist.",
|
| 2354 |
+
"spans": [
|
| 2355 |
+
{
|
| 2356 |
+
"start": 9,
|
| 2357 |
+
"end": 16,
|
| 2358 |
+
"label": "RELIGION",
|
| 2359 |
+
"text": "atheist"
|
| 2360 |
+
},
|
| 2361 |
+
{
|
| 2362 |
+
"start": 27,
|
| 2363 |
+
"end": 34,
|
| 2364 |
+
"label": "RELIGION",
|
| 2365 |
+
"text": "atheist"
|
| 2366 |
+
}
|
| 2367 |
+
],
|
| 2368 |
+
"expected": "Row one: [RELIGION]; row two: [RELIGION]."
|
| 2369 |
+
},
|
| 2370 |
+
{
|
| 2371 |
+
"id": "holdout03d-133",
|
| 2372 |
+
"language": "de",
|
| 2373 |
+
"scenario": "formats_repetition",
|
| 2374 |
+
"ocr_noise": false,
|
| 2375 |
+
"text": "RELIGION=Judentum; QUELLE=Selbstauskunft",
|
| 2376 |
+
"spans": [
|
| 2377 |
+
{
|
| 2378 |
+
"start": 9,
|
| 2379 |
+
"end": 17,
|
| 2380 |
+
"label": "RELIGION",
|
| 2381 |
+
"text": "Judentum"
|
| 2382 |
+
}
|
| 2383 |
+
],
|
| 2384 |
+
"expected": "RELIGION=[RELIGION]; QUELLE=Selbstauskunft"
|
| 2385 |
+
},
|
| 2386 |
+
{
|
| 2387 |
+
"id": "holdout03d-134",
|
| 2388 |
+
"language": "en",
|
| 2389 |
+
"scenario": "formats_repetition",
|
| 2390 |
+
"ocr_noise": false,
|
| 2391 |
+
"text": "BELIEF=Shinto; SOURCE=self-report",
|
| 2392 |
+
"spans": [
|
| 2393 |
+
{
|
| 2394 |
+
"start": 7,
|
| 2395 |
+
"end": 13,
|
| 2396 |
+
"label": "RELIGION",
|
| 2397 |
+
"text": "Shinto"
|
| 2398 |
+
}
|
| 2399 |
+
],
|
| 2400 |
+
"expected": "BELIEF=[RELIGION]; SOURCE=self-report"
|
| 2401 |
+
},
|
| 2402 |
+
{
|
| 2403 |
+
"id": "holdout03d-135",
|
| 2404 |
+
"language": "de",
|
| 2405 |
+
"scenario": "formats_repetition",
|
| 2406 |
+
"ocr_noise": false,
|
| 2407 |
+
"text": "• Weltanschauung: Agnostizismus\n• Weitergabe: gesperrt",
|
| 2408 |
+
"spans": [
|
| 2409 |
+
{
|
| 2410 |
+
"start": 18,
|
| 2411 |
+
"end": 31,
|
| 2412 |
+
"label": "RELIGION",
|
| 2413 |
+
"text": "Agnostizismus"
|
| 2414 |
+
}
|
| 2415 |
+
],
|
| 2416 |
+
"expected": "• Weltanschauung: [RELIGION]\n• Weitergabe: gesperrt"
|
| 2417 |
+
},
|
| 2418 |
+
{
|
| 2419 |
+
"id": "holdout03d-136",
|
| 2420 |
+
"language": "en",
|
| 2421 |
+
"scenario": "formats_repetition",
|
| 2422 |
+
"ocr_noise": false,
|
| 2423 |
+
"text": "1. Faith: Sikhism\n2. Access: restricted",
|
| 2424 |
+
"spans": [
|
| 2425 |
+
{
|
| 2426 |
+
"start": 10,
|
| 2427 |
+
"end": 17,
|
| 2428 |
+
"label": "RELIGION",
|
| 2429 |
+
"text": "Sikhism"
|
| 2430 |
+
}
|
| 2431 |
+
],
|
| 2432 |
+
"expected": "1. Faith: [RELIGION]\n2. Access: restricted"
|
| 2433 |
+
},
|
| 2434 |
+
{
|
| 2435 |
+
"id": "holdout03d-137",
|
| 2436 |
+
"language": "de",
|
| 2437 |
+
"scenario": "formats_repetition",
|
| 2438 |
+
"ocr_noise": false,
|
| 2439 |
+
"text": "Seitenrand Katholizismus / Fußnote Katholizismus.",
|
| 2440 |
+
"spans": [
|
| 2441 |
+
{
|
| 2442 |
+
"start": 11,
|
| 2443 |
+
"end": 24,
|
| 2444 |
+
"label": "RELIGION",
|
| 2445 |
+
"text": "Katholizismus"
|
| 2446 |
+
},
|
| 2447 |
+
{
|
| 2448 |
+
"start": 35,
|
| 2449 |
+
"end": 48,
|
| 2450 |
+
"label": "RELIGION",
|
| 2451 |
+
"text": "Katholizismus"
|
| 2452 |
+
}
|
| 2453 |
+
],
|
| 2454 |
+
"expected": "Seitenrand [RELIGION] / Fußnote [RELIGION]."
|
| 2455 |
+
},
|
| 2456 |
+
{
|
| 2457 |
+
"id": "holdout03d-138",
|
| 2458 |
+
"language": "en",
|
| 2459 |
+
"scenario": "formats_repetition",
|
| 2460 |
+
"ocr_noise": false,
|
| 2461 |
+
"text": "Margin Hinduism; footnote Hinduism.",
|
| 2462 |
+
"spans": [
|
| 2463 |
+
{
|
| 2464 |
+
"start": 7,
|
| 2465 |
+
"end": 15,
|
| 2466 |
+
"label": "RELIGION",
|
| 2467 |
+
"text": "Hinduism"
|
| 2468 |
+
},
|
| 2469 |
+
{
|
| 2470 |
+
"start": 26,
|
| 2471 |
+
"end": 34,
|
| 2472 |
+
"label": "RELIGION",
|
| 2473 |
+
"text": "Hinduism"
|
| 2474 |
+
}
|
| 2475 |
+
],
|
| 2476 |
+
"expected": "Margin [RELIGION]; footnote [RELIGION]."
|
| 2477 |
+
},
|
| 2478 |
+
{
|
| 2479 |
+
"id": "holdout03d-139",
|
| 2480 |
+
"language": "de",
|
| 2481 |
+
"scenario": "formats_repetition",
|
| 2482 |
+
"ocr_noise": false,
|
| 2483 |
+
"text": "Eingabefeld «ohne Konfession» — nur intern.",
|
| 2484 |
+
"spans": [
|
| 2485 |
+
{
|
| 2486 |
+
"start": 13,
|
| 2487 |
+
"end": 28,
|
| 2488 |
+
"label": "RELIGION",
|
| 2489 |
+
"text": "ohne Konfession"
|
| 2490 |
+
}
|
| 2491 |
+
],
|
| 2492 |
+
"expected": "Eingabefeld «[RELIGION]» — nur intern."
|
| 2493 |
+
},
|
| 2494 |
+
{
|
| 2495 |
+
"id": "holdout03d-140",
|
| 2496 |
+
"language": "en",
|
| 2497 |
+
"scenario": "formats_repetition",
|
| 2498 |
+
"ocr_noise": false,
|
| 2499 |
+
"text": "Checkbox ☑ non-religious; do not publish.",
|
| 2500 |
+
"spans": [
|
| 2501 |
+
{
|
| 2502 |
+
"start": 11,
|
| 2503 |
+
"end": 24,
|
| 2504 |
+
"label": "RELIGION",
|
| 2505 |
+
"text": "non-religious"
|
| 2506 |
+
}
|
| 2507 |
+
],
|
| 2508 |
+
"expected": "Checkbox ☑ [RELIGION]; do not publish."
|
| 2509 |
+
},
|
| 2510 |
+
{
|
| 2511 |
+
"id": "holdout03d-141",
|
| 2512 |
+
"language": "de",
|
| 2513 |
+
"scenario": "formats_repetition",
|
| 2514 |
+
"ocr_noise": false,
|
| 2515 |
+
"text": "Christentum → Atheismus ist als Änderung notiert.",
|
| 2516 |
+
"spans": [
|
| 2517 |
+
{
|
| 2518 |
+
"start": 0,
|
| 2519 |
+
"end": 11,
|
| 2520 |
+
"label": "RELIGION",
|
| 2521 |
+
"text": "Christentum"
|
| 2522 |
+
},
|
| 2523 |
+
{
|
| 2524 |
+
"start": 14,
|
| 2525 |
+
"end": 23,
|
| 2526 |
+
"label": "RELIGION",
|
| 2527 |
+
"text": "Atheismus"
|
| 2528 |
+
}
|
| 2529 |
+
],
|
| 2530 |
+
"expected": "[RELIGION] → [RELIGION] ist als Änderung notiert."
|
| 2531 |
+
},
|
| 2532 |
+
{
|
| 2533 |
+
"id": "holdout03d-142",
|
| 2534 |
+
"language": "en",
|
| 2535 |
+
"scenario": "formats_repetition",
|
| 2536 |
+
"ocr_noise": false,
|
| 2537 |
+
"text": "Christianity → agnosticism is the recorded correction.",
|
| 2538 |
+
"spans": [
|
| 2539 |
+
{
|
| 2540 |
+
"start": 0,
|
| 2541 |
+
"end": 12,
|
| 2542 |
+
"label": "RELIGION",
|
| 2543 |
+
"text": "Christianity"
|
| 2544 |
+
},
|
| 2545 |
+
{
|
| 2546 |
+
"start": 15,
|
| 2547 |
+
"end": 26,
|
| 2548 |
+
"label": "RELIGION",
|
| 2549 |
+
"text": "agnosticism"
|
| 2550 |
+
}
|
| 2551 |
+
],
|
| 2552 |
+
"expected": "[RELIGION] → [RELIGION] is the recorded correction."
|
| 2553 |
+
},
|
| 2554 |
+
{
|
| 2555 |
+
"id": "holdout03d-143",
|
| 2556 |
+
"language": "de",
|
| 2557 |
+
"scenario": "formats_repetition",
|
| 2558 |
+
"ocr_noise": false,
|
| 2559 |
+
"text": "Bekenntnis\tBuddhismus\tgeprüft=offen",
|
| 2560 |
+
"spans": [
|
| 2561 |
+
{
|
| 2562 |
+
"start": 11,
|
| 2563 |
+
"end": 21,
|
| 2564 |
+
"label": "RELIGION",
|
| 2565 |
+
"text": "Buddhismus"
|
| 2566 |
+
}
|
| 2567 |
+
],
|
| 2568 |
+
"expected": "Bekenntnis\t[RELIGION]\tgeprüft=offen"
|
| 2569 |
+
},
|
| 2570 |
+
{
|
| 2571 |
+
"id": "holdout03d-144",
|
| 2572 |
+
"language": "en",
|
| 2573 |
+
"scenario": "formats_repetition",
|
| 2574 |
+
"ocr_noise": false,
|
| 2575 |
+
"text": "religion\tJainism\treview=pending",
|
| 2576 |
+
"spans": [
|
| 2577 |
+
{
|
| 2578 |
+
"start": 9,
|
| 2579 |
+
"end": 16,
|
| 2580 |
+
"label": "RELIGION",
|
| 2581 |
+
"text": "Jainism"
|
| 2582 |
+
}
|
| 2583 |
+
],
|
| 2584 |
+
"expected": "religion\t[RELIGION]\treview=pending"
|
| 2585 |
+
},
|
| 2586 |
+
{
|
| 2587 |
+
"id": "holdout03d-145",
|
| 2588 |
+
"language": "de",
|
| 2589 |
+
"scenario": "negative_controls",
|
| 2590 |
+
"ocr_noise": false,
|
| 2591 |
+
"text": "Die Lieferung enthält zwölf Schrauben und einen Lieferschein.",
|
| 2592 |
+
"spans": [],
|
| 2593 |
+
"expected": "Die Lieferung enthält zwölf Schrauben und einen Lieferschein."
|
| 2594 |
+
},
|
| 2595 |
+
{
|
| 2596 |
+
"id": "holdout03d-146",
|
| 2597 |
+
"language": "en",
|
| 2598 |
+
"scenario": "negative_controls",
|
| 2599 |
+
"ocr_noise": false,
|
| 2600 |
+
"text": "A blue folder sits beside the empty filing tray.",
|
| 2601 |
+
"spans": [],
|
| 2602 |
+
"expected": "A blue folder sits beside the empty filing tray."
|
| 2603 |
+
},
|
| 2604 |
+
{
|
| 2605 |
+
"id": "holdout03d-147",
|
| 2606 |
+
"language": "de",
|
| 2607 |
+
"scenario": "negative_controls",
|
| 2608 |
+
"ocr_noise": false,
|
| 2609 |
+
"text": "Das Formular enthält keine ausgefüllten persönlichen Felder.",
|
| 2610 |
+
"spans": [],
|
| 2611 |
+
"expected": "Das Formular enthält keine ausgefüllten persönlichen Felder."
|
| 2612 |
+
},
|
| 2613 |
+
{
|
| 2614 |
+
"id": "holdout03d-148",
|
| 2615 |
+
"language": "en",
|
| 2616 |
+
"scenario": "negative_controls",
|
| 2617 |
+
"ocr_noise": false,
|
| 2618 |
+
"text": "The spelling correction changes a comma to a semicolon.",
|
| 2619 |
+
"spans": [],
|
| 2620 |
+
"expected": "The spelling correction changes a comma to a semicolon."
|
| 2621 |
+
},
|
| 2622 |
+
{
|
| 2623 |
+
"id": "holdout03d-149",
|
| 2624 |
+
"language": "de",
|
| 2625 |
+
"scenario": "negative_controls",
|
| 2626 |
+
"ocr_noise": false,
|
| 2627 |
+
"text": "Der Kalender zeigt nur Wochentage ohne Termine.",
|
| 2628 |
+
"spans": [],
|
| 2629 |
+
"expected": "Der Kalender zeigt nur Wochentage ohne Termine."
|
| 2630 |
+
},
|
| 2631 |
+
{
|
| 2632 |
+
"id": "holdout03d-150",
|
| 2633 |
+
"language": "en",
|
| 2634 |
+
"scenario": "negative_controls",
|
| 2635 |
+
"ocr_noise": false,
|
| 2636 |
+
"text": "The maintenance note records a loose hinge.",
|
| 2637 |
+
"spans": [],
|
| 2638 |
+
"expected": "The maintenance note records a loose hinge."
|
| 2639 |
+
},
|
| 2640 |
+
{
|
| 2641 |
+
"id": "holdout03d-151",
|
| 2642 |
+
"language": "de",
|
| 2643 |
+
"scenario": "negative_controls",
|
| 2644 |
+
"ocr_noise": false,
|
| 2645 |
+
"text": "Eine Überschrift erklärt die Bedienung der Taschenlampe.",
|
| 2646 |
+
"spans": [],
|
| 2647 |
+
"expected": "Eine Überschrift erklärt die Bedienung der Taschenlampe."
|
| 2648 |
+
},
|
| 2649 |
+
{
|
| 2650 |
+
"id": "holdout03d-152",
|
| 2651 |
+
"language": "en",
|
| 2652 |
+
"scenario": "negative_controls",
|
| 2653 |
+
"ocr_noise": false,
|
| 2654 |
+
"text": "A measurement table lists height and width without identifiers.",
|
| 2655 |
+
"spans": [],
|
| 2656 |
+
"expected": "A measurement table lists height and width without identifiers."
|
| 2657 |
+
},
|
| 2658 |
+
{
|
| 2659 |
+
"id": "holdout03d-153",
|
| 2660 |
+
"language": "de",
|
| 2661 |
+
"scenario": "negative_controls",
|
| 2662 |
+
"ocr_noise": false,
|
| 2663 |
+
"text": "Das Prüfprotokoll enthält ausschließlich Materialnummern aus Buchstaben.",
|
| 2664 |
+
"spans": [],
|
| 2665 |
+
"expected": "Das Prüfprotokoll enthält ausschließlich Materialnummern aus Buchstaben."
|
| 2666 |
+
},
|
| 2667 |
+
{
|
| 2668 |
+
"id": "holdout03d-154",
|
| 2669 |
+
"language": "en",
|
| 2670 |
+
"scenario": "negative_controls",
|
| 2671 |
+
"ocr_noise": false,
|
| 2672 |
+
"text": "The anonymous inventory includes pencils and unmarked boxes.",
|
| 2673 |
+
"spans": [],
|
| 2674 |
+
"expected": "The anonymous inventory includes pencils and unmarked boxes."
|
| 2675 |
+
},
|
| 2676 |
+
{
|
| 2677 |
+
"id": "holdout03d-155",
|
| 2678 |
+
"language": "de",
|
| 2679 |
+
"scenario": "negative_controls",
|
| 2680 |
+
"ocr_noise": false,
|
| 2681 |
+
"text": "Die Zutatenliste nennt Mehl, Wasser und Salz.",
|
| 2682 |
+
"spans": [],
|
| 2683 |
+
"expected": "Die Zutatenliste nennt Mehl, Wasser und Salz."
|
| 2684 |
+
},
|
| 2685 |
+
{
|
| 2686 |
+
"id": "holdout03d-156",
|
| 2687 |
+
"language": "en",
|
| 2688 |
+
"scenario": "negative_controls",
|
| 2689 |
+
"ocr_noise": false,
|
| 2690 |
+
"text": "The diagram shows arrows connecting unlabeled circles.",
|
| 2691 |
+
"spans": [],
|
| 2692 |
+
"expected": "The diagram shows arrows connecting unlabeled circles."
|
| 2693 |
+
},
|
| 2694 |
+
{
|
| 2695 |
+
"id": "holdout03d-157",
|
| 2696 |
+
"language": "de",
|
| 2697 |
+
"scenario": "negative_controls",
|
| 2698 |
+
"ocr_noise": false,
|
| 2699 |
+
"text": "Der Absatz behandelt die Grammatik unpersönlicher Sätze.",
|
| 2700 |
+
"spans": [],
|
| 2701 |
+
"expected": "Der Absatz behandelt die Grammatik unpersönlicher Sätze."
|
| 2702 |
+
},
|
| 2703 |
+
{
|
| 2704 |
+
"id": "holdout03d-158",
|
| 2705 |
+
"language": "en",
|
| 2706 |
+
"scenario": "negative_controls",
|
| 2707 |
+
"ocr_noise": false,
|
| 2708 |
+
"text": "A draft contains only instructions for replacing printer paper.",
|
| 2709 |
+
"spans": [],
|
| 2710 |
+
"expected": "A draft contains only instructions for replacing printer paper."
|
| 2711 |
+
},
|
| 2712 |
+
{
|
| 2713 |
+
"id": "holdout03d-159",
|
| 2714 |
+
"language": "de",
|
| 2715 |
+
"scenario": "negative_controls",
|
| 2716 |
+
"ocr_noise": false,
|
| 2717 |
+
"text": "Die leere Tabelle hat Spalten für Farbe und Gewicht.",
|
| 2718 |
+
"spans": [],
|
| 2719 |
+
"expected": "Die leere Tabelle hat Spalten für Farbe und Gewicht."
|
| 2720 |
+
},
|
| 2721 |
+
{
|
| 2722 |
+
"id": "holdout03d-160",
|
| 2723 |
+
"language": "en",
|
| 2724 |
+
"scenario": "negative_controls",
|
| 2725 |
+
"ocr_noise": false,
|
| 2726 |
+
"text": "No religious status, personal name, address or contact is supplied.",
|
| 2727 |
+
"spans": [],
|
| 2728 |
+
"expected": "No religious status, personal name, address or contact is supplied."
|
| 2729 |
+
}
|
| 2730 |
+
]
|
ocr/anonymization/fixtures/holdout03d/freeze.json
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": "frozen-holdout03d/v1",
|
| 3 |
+
"cases": 160,
|
| 4 |
+
"sha256": "7180e176f6d8bd259da56ad729ede6fd2c798aca15a786ba0ff0adcd862f21a1",
|
| 5 |
+
"source_head": "4c5cbbc30801f5911dcf52e1997a70404026d425",
|
| 6 |
+
"model_revision": "ba967ee030b7e679dd5422cb16fcd1b379da38e1",
|
| 7 |
+
"prompts": {
|
| 8 |
+
"PERSON": "person",
|
| 9 |
+
"CITY": "location",
|
| 10 |
+
"RELIGION": "religion",
|
| 11 |
+
"EMAIL": "email address",
|
| 12 |
+
"PHONE": "phone number",
|
| 13 |
+
"ORGANIZATION": "organization"
|
| 14 |
+
},
|
| 15 |
+
"threshold": 0.5,
|
| 16 |
+
"religion_terms": [
|
| 17 |
+
"religionslos",
|
| 18 |
+
"konfessionslos",
|
| 19 |
+
"konfessionsfrei",
|
| 20 |
+
"ohne Konfession",
|
| 21 |
+
"nicht religiös",
|
| 22 |
+
"no religious affiliation",
|
| 23 |
+
"no religion",
|
| 24 |
+
"nonreligious",
|
| 25 |
+
"non-religious",
|
| 26 |
+
"atheist",
|
| 27 |
+
"atheists",
|
| 28 |
+
"atheism",
|
| 29 |
+
"atheistisch",
|
| 30 |
+
"Atheismus",
|
| 31 |
+
"agnostic",
|
| 32 |
+
"agnostics",
|
| 33 |
+
"agnosticism",
|
| 34 |
+
"agnostisch",
|
| 35 |
+
"Agnostizismus",
|
| 36 |
+
"Christentum",
|
| 37 |
+
"Christianity",
|
| 38 |
+
"Christen",
|
| 39 |
+
"Christian",
|
| 40 |
+
"Christians",
|
| 41 |
+
"christlich",
|
| 42 |
+
"christliche",
|
| 43 |
+
"christlichen",
|
| 44 |
+
"Katholizismus",
|
| 45 |
+
"Catholicism",
|
| 46 |
+
"katholisch",
|
| 47 |
+
"katholische",
|
| 48 |
+
"katholischen",
|
| 49 |
+
"Catholic",
|
| 50 |
+
"Catholics",
|
| 51 |
+
"Protestantismus",
|
| 52 |
+
"Protestantism",
|
| 53 |
+
"Protestant",
|
| 54 |
+
"Protestants",
|
| 55 |
+
"evangelisch",
|
| 56 |
+
"evangelische",
|
| 57 |
+
"evangelischen",
|
| 58 |
+
"Judentum",
|
| 59 |
+
"Judaism",
|
| 60 |
+
"jüdisch",
|
| 61 |
+
"jüdische",
|
| 62 |
+
"jüdischen",
|
| 63 |
+
"Jewish",
|
| 64 |
+
"Jude",
|
| 65 |
+
"Jüdin",
|
| 66 |
+
"Juden",
|
| 67 |
+
"Jews",
|
| 68 |
+
"Islam",
|
| 69 |
+
"Muslim",
|
| 70 |
+
"Muslims",
|
| 71 |
+
"muslimisch",
|
| 72 |
+
"muslimische",
|
| 73 |
+
"muslimischen",
|
| 74 |
+
"islamisch",
|
| 75 |
+
"islamische",
|
| 76 |
+
"islamischen",
|
| 77 |
+
"Buddhismus",
|
| 78 |
+
"Buddhism",
|
| 79 |
+
"Buddhist",
|
| 80 |
+
"Buddhists",
|
| 81 |
+
"buddhistisch",
|
| 82 |
+
"buddhistische",
|
| 83 |
+
"buddhistischen",
|
| 84 |
+
"Hinduismus",
|
| 85 |
+
"Hinduism",
|
| 86 |
+
"Hindu",
|
| 87 |
+
"Hindus",
|
| 88 |
+
"hinduistisch",
|
| 89 |
+
"hinduistische",
|
| 90 |
+
"hinduistischen",
|
| 91 |
+
"Sikhismus",
|
| 92 |
+
"Sikhism",
|
| 93 |
+
"Sikh",
|
| 94 |
+
"Sikhs",
|
| 95 |
+
"sikhistisch",
|
| 96 |
+
"Jainismus",
|
| 97 |
+
"Jainism",
|
| 98 |
+
"Jain",
|
| 99 |
+
"Jains",
|
| 100 |
+
"jainistisch",
|
| 101 |
+
"Bahai",
|
| 102 |
+
"Baháʼí Faith",
|
| 103 |
+
"Bahá'í",
|
| 104 |
+
"Baha'i",
|
| 105 |
+
"Shinto",
|
| 106 |
+
"Shintoismus",
|
| 107 |
+
"Shintoism",
|
| 108 |
+
"Taoismus",
|
| 109 |
+
"Taoism",
|
| 110 |
+
"Daoismus",
|
| 111 |
+
"Daoism",
|
| 112 |
+
"Zoroastrismus",
|
| 113 |
+
"Zoroastrianism",
|
| 114 |
+
"Orthodox Christianity",
|
| 115 |
+
"Orthodoxie"
|
| 116 |
+
],
|
| 117 |
+
"implementation_sha256": {
|
| 118 |
+
"ocr/anonymization/recognizer.py": "f695a30f0ff8540ba345d5557f0bf2bfc225635b228010da6ee6f5ccbc71ce45",
|
| 119 |
+
"ocr/anonymization/normalization.py": "3b2598200933a86cedf2dd20affb911c4ad1ac4f7fdc00182c4044b7a2873f10",
|
| 120 |
+
"ocr/anonymization/detectors.py": "c543532c579e8880e4915b3e669063f11e8e39034886f889e9e4e20942bf6a10",
|
| 121 |
+
"ocr/anonymization/terminology.py": "370b61beb1b5339549f2c8143a31b546f4a32dbbabd26c061f009341de16babd",
|
| 122 |
+
"ocr/anonymization/engine.py": "d784774d57e35b25fdac5bef095df4859db9d606e83759625526cbf90cfc8763",
|
| 123 |
+
"ocr/anonymization/replacement.py": "1f048dc237a3355c400a6f085b13848e1c0c49fe2f7f9d5ab8203114ef3e4cd8",
|
| 124 |
+
"ocr/anonymization/evaluate.py": "b9ed1453d6b7c704be3eef18b2a32b9cf8e857075355c42025e5d3bdc863d6b0",
|
| 125 |
+
"ocr/anonymization/qualify03d.py": "ead5767304b8e10e46b855ab0235c2b6f97ad4fe82271d366bfd730de0e1ebb0"
|
| 126 |
+
},
|
| 127 |
+
"evaluation_run_before_freeze": false,
|
| 128 |
+
"design": "160 individually authored synthetic cases, 80 DE / 80 EN, 10 scenario strata. No repeated 03c entity-set templates. Selected configuration based only on 40+10+120 development suites. First evaluation and corpus retained even on failure."
|
| 129 |
+
}
|
ocr/anonymization/fixtures/qualification.json
ADDED
|
@@ -0,0 +1,647 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"text": "Lina Wolkenstein wohnt in Berlin.",
|
| 4 |
+
"expected": "[PERSON] wohnt in [CITY].",
|
| 5 |
+
"spans": [
|
| 6 |
+
{
|
| 7 |
+
"start": 0,
|
| 8 |
+
"end": 16,
|
| 9 |
+
"label": "PERSON",
|
| 10 |
+
"text": "Lina Wolkenstein"
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"start": 26,
|
| 14 |
+
"end": 32,
|
| 15 |
+
"label": "CITY",
|
| 16 |
+
"text": "Berlin"
|
| 17 |
+
}
|
| 18 |
+
],
|
| 19 |
+
"id": "baseline-00",
|
| 20 |
+
"language": "de",
|
| 21 |
+
"ocr_noise": false
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"text": "Hans Müller kauft ein.",
|
| 25 |
+
"expected": "[PERSON] kauft ein.",
|
| 26 |
+
"spans": [
|
| 27 |
+
{
|
| 28 |
+
"start": 0,
|
| 29 |
+
"end": 11,
|
| 30 |
+
"label": "PERSON",
|
| 31 |
+
"text": "Hans Müller"
|
| 32 |
+
}
|
| 33 |
+
],
|
| 34 |
+
"id": "baseline-01",
|
| 35 |
+
"language": "de",
|
| 36 |
+
"ocr_noise": false
|
| 37 |
+
},
|
| 38 |
+
{
|
| 39 |
+
"text": "Das Auto von Dr. Klaus Schmidt.",
|
| 40 |
+
"expected": "Das Auto von Dr. [PERSON].",
|
| 41 |
+
"spans": [
|
| 42 |
+
{
|
| 43 |
+
"start": 17,
|
| 44 |
+
"end": 30,
|
| 45 |
+
"label": "PERSON",
|
| 46 |
+
"text": "Klaus Schmidt"
|
| 47 |
+
}
|
| 48 |
+
],
|
| 49 |
+
"id": "baseline-02",
|
| 50 |
+
"language": "de",
|
| 51 |
+
"ocr_noise": false
|
| 52 |
+
},
|
| 53 |
+
{
|
| 54 |
+
"text": "Friedrich Wilhelm geht nach Hause.",
|
| 55 |
+
"expected": "[PERSON] geht nach Hause.",
|
| 56 |
+
"spans": [
|
| 57 |
+
{
|
| 58 |
+
"start": 0,
|
| 59 |
+
"end": 17,
|
| 60 |
+
"label": "PERSON",
|
| 61 |
+
"text": "Friedrich Wilhelm"
|
| 62 |
+
}
|
| 63 |
+
],
|
| 64 |
+
"id": "baseline-03",
|
| 65 |
+
"language": "de",
|
| 66 |
+
"ocr_noise": false
|
| 67 |
+
},
|
| 68 |
+
{
|
| 69 |
+
"text": "Ich kenne Frau Anne-Marie Schneider nicht.",
|
| 70 |
+
"expected": "Ich kenne Frau [PERSON] nicht.",
|
| 71 |
+
"spans": [
|
| 72 |
+
{
|
| 73 |
+
"start": 15,
|
| 74 |
+
"end": 35,
|
| 75 |
+
"label": "PERSON",
|
| 76 |
+
"text": "Anne-Marie Schneider"
|
| 77 |
+
}
|
| 78 |
+
],
|
| 79 |
+
"id": "baseline-04",
|
| 80 |
+
"language": "de",
|
| 81 |
+
"ocr_noise": false
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"text": "John Doe is a good man.",
|
| 85 |
+
"expected": "[PERSON] is a good man.",
|
| 86 |
+
"spans": [
|
| 87 |
+
{
|
| 88 |
+
"start": 0,
|
| 89 |
+
"end": 8,
|
| 90 |
+
"label": "PERSON",
|
| 91 |
+
"text": "John Doe"
|
| 92 |
+
}
|
| 93 |
+
],
|
| 94 |
+
"id": "baseline-05",
|
| 95 |
+
"language": "en",
|
| 96 |
+
"ocr_noise": false
|
| 97 |
+
},
|
| 98 |
+
{
|
| 99 |
+
"text": "Alice and Bob went to the store.",
|
| 100 |
+
"expected": "[PERSON] and [PERSON] went to the store.",
|
| 101 |
+
"spans": [
|
| 102 |
+
{
|
| 103 |
+
"start": 0,
|
| 104 |
+
"end": 5,
|
| 105 |
+
"label": "PERSON",
|
| 106 |
+
"text": "Alice"
|
| 107 |
+
},
|
| 108 |
+
{
|
| 109 |
+
"start": 10,
|
| 110 |
+
"end": 13,
|
| 111 |
+
"label": "PERSON",
|
| 112 |
+
"text": "Bob"
|
| 113 |
+
}
|
| 114 |
+
],
|
| 115 |
+
"id": "baseline-06",
|
| 116 |
+
"language": "en",
|
| 117 |
+
"ocr_noise": false
|
| 118 |
+
},
|
| 119 |
+
{
|
| 120 |
+
"text": "Is Mary Johnson there?",
|
| 121 |
+
"expected": "Is [PERSON] there?",
|
| 122 |
+
"spans": [
|
| 123 |
+
{
|
| 124 |
+
"start": 3,
|
| 125 |
+
"end": 15,
|
| 126 |
+
"label": "PERSON",
|
| 127 |
+
"text": "Mary Johnson"
|
| 128 |
+
}
|
| 129 |
+
],
|
| 130 |
+
"id": "baseline-07",
|
| 131 |
+
"language": "en",
|
| 132 |
+
"ocr_noise": false
|
| 133 |
+
},
|
| 134 |
+
{
|
| 135 |
+
"text": "Meeting with Mr. Charles Montgomery.",
|
| 136 |
+
"expected": "Meeting with Mr. [PERSON].",
|
| 137 |
+
"spans": [
|
| 138 |
+
{
|
| 139 |
+
"start": 17,
|
| 140 |
+
"end": 35,
|
| 141 |
+
"label": "PERSON",
|
| 142 |
+
"text": "Charles Montgomery"
|
| 143 |
+
}
|
| 144 |
+
],
|
| 145 |
+
"id": "baseline-08",
|
| 146 |
+
"language": "en",
|
| 147 |
+
"ocr_noise": false
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"text": "Elizabeth Taylor was famous.",
|
| 151 |
+
"expected": "[PERSON] was famous.",
|
| 152 |
+
"spans": [
|
| 153 |
+
{
|
| 154 |
+
"start": 0,
|
| 155 |
+
"end": 16,
|
| 156 |
+
"label": "PERSON",
|
| 157 |
+
"text": "Elizabeth Taylor"
|
| 158 |
+
}
|
| 159 |
+
],
|
| 160 |
+
"id": "baseline-09",
|
| 161 |
+
"language": "en",
|
| 162 |
+
"ocr_noise": false
|
| 163 |
+
},
|
| 164 |
+
{
|
| 165 |
+
"text": "I live in New York.",
|
| 166 |
+
"expected": "I live in [CITY].",
|
| 167 |
+
"spans": [
|
| 168 |
+
{
|
| 169 |
+
"start": 10,
|
| 170 |
+
"end": 18,
|
| 171 |
+
"label": "CITY",
|
| 172 |
+
"text": "New York"
|
| 173 |
+
}
|
| 174 |
+
],
|
| 175 |
+
"id": "baseline-10",
|
| 176 |
+
"language": "en",
|
| 177 |
+
"ocr_noise": false
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"text": "Der Zug nach München fährt ab.",
|
| 181 |
+
"expected": "Der Zug nach [CITY] fährt ab.",
|
| 182 |
+
"spans": [
|
| 183 |
+
{
|
| 184 |
+
"start": 13,
|
| 185 |
+
"end": 20,
|
| 186 |
+
"label": "CITY",
|
| 187 |
+
"text": "München"
|
| 188 |
+
}
|
| 189 |
+
],
|
| 190 |
+
"id": "baseline-11",
|
| 191 |
+
"language": "de",
|
| 192 |
+
"ocr_noise": false
|
| 193 |
+
},
|
| 194 |
+
{
|
| 195 |
+
"text": "Paris is beautiful in spring.",
|
| 196 |
+
"expected": "[CITY] is beautiful in spring.",
|
| 197 |
+
"spans": [
|
| 198 |
+
{
|
| 199 |
+
"start": 0,
|
| 200 |
+
"end": 5,
|
| 201 |
+
"label": "CITY",
|
| 202 |
+
"text": "Paris"
|
| 203 |
+
}
|
| 204 |
+
],
|
| 205 |
+
"id": "baseline-12",
|
| 206 |
+
"language": "en",
|
| 207 |
+
"ocr_noise": false
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"text": "Flug von Frankfurt nach Tokyo.",
|
| 211 |
+
"expected": "Flug von [CITY] nach [CITY].",
|
| 212 |
+
"spans": [
|
| 213 |
+
{
|
| 214 |
+
"start": 9,
|
| 215 |
+
"end": 18,
|
| 216 |
+
"label": "CITY",
|
| 217 |
+
"text": "Frankfurt"
|
| 218 |
+
},
|
| 219 |
+
{
|
| 220 |
+
"start": 24,
|
| 221 |
+
"end": 29,
|
| 222 |
+
"label": "CITY",
|
| 223 |
+
"text": "Tokyo"
|
| 224 |
+
}
|
| 225 |
+
],
|
| 226 |
+
"id": "baseline-13",
|
| 227 |
+
"language": "de",
|
| 228 |
+
"ocr_noise": false
|
| 229 |
+
},
|
| 230 |
+
{
|
| 231 |
+
"text": "Welcome to Los Angeles, California.",
|
| 232 |
+
"expected": "Welcome to [CITY], [CITY].",
|
| 233 |
+
"spans": [
|
| 234 |
+
{
|
| 235 |
+
"start": 11,
|
| 236 |
+
"end": 22,
|
| 237 |
+
"label": "CITY",
|
| 238 |
+
"text": "Los Angeles"
|
| 239 |
+
},
|
| 240 |
+
{
|
| 241 |
+
"start": 24,
|
| 242 |
+
"end": 34,
|
| 243 |
+
"label": "CITY",
|
| 244 |
+
"text": "California"
|
| 245 |
+
}
|
| 246 |
+
],
|
| 247 |
+
"id": "baseline-14",
|
| 248 |
+
"language": "en",
|
| 249 |
+
"ocr_noise": false
|
| 250 |
+
},
|
| 251 |
+
{
|
| 252 |
+
"text": "Ihre Religionszugehörigkeit ist evangelisch.",
|
| 253 |
+
"expected": "Ihre Religionszugehörigkeit ist [RELIGION].",
|
| 254 |
+
"spans": [
|
| 255 |
+
{
|
| 256 |
+
"start": 32,
|
| 257 |
+
"end": 43,
|
| 258 |
+
"label": "RELIGION",
|
| 259 |
+
"text": "evangelisch"
|
| 260 |
+
}
|
| 261 |
+
],
|
| 262 |
+
"id": "baseline-15",
|
| 263 |
+
"language": "de",
|
| 264 |
+
"ocr_noise": false
|
| 265 |
+
},
|
| 266 |
+
{
|
| 267 |
+
"text": "Er ist katholisch.",
|
| 268 |
+
"expected": "Er ist [RELIGION].",
|
| 269 |
+
"spans": [
|
| 270 |
+
{
|
| 271 |
+
"start": 7,
|
| 272 |
+
"end": 17,
|
| 273 |
+
"label": "RELIGION",
|
| 274 |
+
"text": "katholisch"
|
| 275 |
+
}
|
| 276 |
+
],
|
| 277 |
+
"id": "baseline-16",
|
| 278 |
+
"language": "de",
|
| 279 |
+
"ocr_noise": false
|
| 280 |
+
},
|
| 281 |
+
{
|
| 282 |
+
"text": "They practice Judaism.",
|
| 283 |
+
"expected": "They practice [RELIGION].",
|
| 284 |
+
"spans": [
|
| 285 |
+
{
|
| 286 |
+
"start": 14,
|
| 287 |
+
"end": 21,
|
| 288 |
+
"label": "RELIGION",
|
| 289 |
+
"text": "Judaism"
|
| 290 |
+
}
|
| 291 |
+
],
|
| 292 |
+
"id": "baseline-17",
|
| 293 |
+
"language": "en",
|
| 294 |
+
"ocr_noise": false
|
| 295 |
+
},
|
| 296 |
+
{
|
| 297 |
+
"text": "He is a devout Muslim.",
|
| 298 |
+
"expected": "He is a devout [RELIGION].",
|
| 299 |
+
"spans": [
|
| 300 |
+
{
|
| 301 |
+
"start": 15,
|
| 302 |
+
"end": 21,
|
| 303 |
+
"label": "RELIGION",
|
| 304 |
+
"text": "Muslim"
|
| 305 |
+
}
|
| 306 |
+
],
|
| 307 |
+
"id": "baseline-18",
|
| 308 |
+
"language": "en",
|
| 309 |
+
"ocr_noise": false
|
| 310 |
+
},
|
| 311 |
+
{
|
| 312 |
+
"text": "Buddhist monks live there.",
|
| 313 |
+
"expected": "[RELIGION] monks live there.",
|
| 314 |
+
"spans": [
|
| 315 |
+
{
|
| 316 |
+
"start": 0,
|
| 317 |
+
"end": 8,
|
| 318 |
+
"label": "RELIGION",
|
| 319 |
+
"text": "Buddhist"
|
| 320 |
+
}
|
| 321 |
+
],
|
| 322 |
+
"id": "baseline-19",
|
| 323 |
+
"language": "en",
|
| 324 |
+
"ocr_noise": false
|
| 325 |
+
},
|
| 326 |
+
{
|
| 327 |
+
"text": "John Doe from New York met Lina Wolkenstein in Berlin.",
|
| 328 |
+
"expected": "[PERSON] from [CITY] met [PERSON] in [CITY].",
|
| 329 |
+
"spans": [
|
| 330 |
+
{
|
| 331 |
+
"start": 0,
|
| 332 |
+
"end": 8,
|
| 333 |
+
"label": "PERSON",
|
| 334 |
+
"text": "John Doe"
|
| 335 |
+
},
|
| 336 |
+
{
|
| 337 |
+
"start": 14,
|
| 338 |
+
"end": 22,
|
| 339 |
+
"label": "CITY",
|
| 340 |
+
"text": "New York"
|
| 341 |
+
},
|
| 342 |
+
{
|
| 343 |
+
"start": 27,
|
| 344 |
+
"end": 43,
|
| 345 |
+
"label": "PERSON",
|
| 346 |
+
"text": "Lina Wolkenstein"
|
| 347 |
+
},
|
| 348 |
+
{
|
| 349 |
+
"start": 47,
|
| 350 |
+
"end": 53,
|
| 351 |
+
"label": "CITY",
|
| 352 |
+
"text": "Berlin"
|
| 353 |
+
}
|
| 354 |
+
],
|
| 355 |
+
"id": "baseline-20",
|
| 356 |
+
"language": "mixed",
|
| 357 |
+
"ocr_noise": false
|
| 358 |
+
},
|
| 359 |
+
{
|
| 360 |
+
"text": "David is Catholic and lives in Rome.",
|
| 361 |
+
"expected": "[PERSON] is [RELIGION] and lives in [CITY].",
|
| 362 |
+
"spans": [
|
| 363 |
+
{
|
| 364 |
+
"start": 0,
|
| 365 |
+
"end": 5,
|
| 366 |
+
"label": "PERSON",
|
| 367 |
+
"text": "David"
|
| 368 |
+
},
|
| 369 |
+
{
|
| 370 |
+
"start": 9,
|
| 371 |
+
"end": 17,
|
| 372 |
+
"label": "RELIGION",
|
| 373 |
+
"text": "Catholic"
|
| 374 |
+
},
|
| 375 |
+
{
|
| 376 |
+
"start": 31,
|
| 377 |
+
"end": 35,
|
| 378 |
+
"label": "CITY",
|
| 379 |
+
"text": "Rome"
|
| 380 |
+
}
|
| 381 |
+
],
|
| 382 |
+
"id": "baseline-21",
|
| 383 |
+
"language": "en",
|
| 384 |
+
"ocr_noise": false
|
| 385 |
+
},
|
| 386 |
+
{
|
| 387 |
+
"text": "John likes John's new car.",
|
| 388 |
+
"expected": "[PERSON] likes [PERSON]'s new car.",
|
| 389 |
+
"spans": [
|
| 390 |
+
{
|
| 391 |
+
"start": 0,
|
| 392 |
+
"end": 4,
|
| 393 |
+
"label": "PERSON",
|
| 394 |
+
"text": "John"
|
| 395 |
+
},
|
| 396 |
+
{
|
| 397 |
+
"start": 11,
|
| 398 |
+
"end": 15,
|
| 399 |
+
"label": "PERSON",
|
| 400 |
+
"text": "John"
|
| 401 |
+
}
|
| 402 |
+
],
|
| 403 |
+
"id": "baseline-22",
|
| 404 |
+
"language": "en",
|
| 405 |
+
"ocr_noise": false
|
| 406 |
+
},
|
| 407 |
+
{
|
| 408 |
+
"text": "Berlin, oh Berlin, my favorite city.",
|
| 409 |
+
"expected": "[CITY], oh [CITY], my favorite city.",
|
| 410 |
+
"spans": [
|
| 411 |
+
{
|
| 412 |
+
"start": 0,
|
| 413 |
+
"end": 6,
|
| 414 |
+
"label": "CITY",
|
| 415 |
+
"text": "Berlin"
|
| 416 |
+
},
|
| 417 |
+
{
|
| 418 |
+
"start": 11,
|
| 419 |
+
"end": 17,
|
| 420 |
+
"label": "CITY",
|
| 421 |
+
"text": "Berlin"
|
| 422 |
+
}
|
| 423 |
+
],
|
| 424 |
+
"id": "baseline-23",
|
| 425 |
+
"language": "en",
|
| 426 |
+
"ocr_noise": false
|
| 427 |
+
},
|
| 428 |
+
{
|
| 429 |
+
"text": "Washington is the capital.",
|
| 430 |
+
"expected": "[CITY] is the capital.",
|
| 431 |
+
"spans": [
|
| 432 |
+
{
|
| 433 |
+
"start": 0,
|
| 434 |
+
"end": 10,
|
| 435 |
+
"label": "CITY",
|
| 436 |
+
"text": "Washington"
|
| 437 |
+
}
|
| 438 |
+
],
|
| 439 |
+
"id": "baseline-24",
|
| 440 |
+
"language": "en",
|
| 441 |
+
"ocr_noise": false
|
| 442 |
+
},
|
| 443 |
+
{
|
| 444 |
+
"text": "George Washington was the president.",
|
| 445 |
+
"expected": "[PERSON] was the president.",
|
| 446 |
+
"spans": [
|
| 447 |
+
{
|
| 448 |
+
"start": 0,
|
| 449 |
+
"end": 17,
|
| 450 |
+
"label": "PERSON",
|
| 451 |
+
"text": "George Washington"
|
| 452 |
+
}
|
| 453 |
+
],
|
| 454 |
+
"id": "baseline-25",
|
| 455 |
+
"language": "en",
|
| 456 |
+
"ocr_noise": false
|
| 457 |
+
},
|
| 458 |
+
{
|
| 459 |
+
"text": "I visited the Jordan river.",
|
| 460 |
+
"expected": "I visited the [CITY] river.",
|
| 461 |
+
"spans": [
|
| 462 |
+
{
|
| 463 |
+
"start": 14,
|
| 464 |
+
"end": 20,
|
| 465 |
+
"label": "CITY",
|
| 466 |
+
"text": "Jordan"
|
| 467 |
+
}
|
| 468 |
+
],
|
| 469 |
+
"id": "baseline-26",
|
| 470 |
+
"language": "en",
|
| 471 |
+
"ocr_noise": false
|
| 472 |
+
},
|
| 473 |
+
{
|
| 474 |
+
"text": "Michael Jordan is a basketball player.",
|
| 475 |
+
"expected": "[PERSON] is a basketball player.",
|
| 476 |
+
"spans": [
|
| 477 |
+
{
|
| 478 |
+
"start": 0,
|
| 479 |
+
"end": 14,
|
| 480 |
+
"label": "PERSON",
|
| 481 |
+
"text": "Michael Jordan"
|
| 482 |
+
}
|
| 483 |
+
],
|
| 484 |
+
"id": "baseline-27",
|
| 485 |
+
"language": "en",
|
| 486 |
+
"ocr_noise": false
|
| 487 |
+
},
|
| 488 |
+
{
|
| 489 |
+
"text": "Mr. Smith-\nson",
|
| 490 |
+
"expected": "Mr. [PERSON]",
|
| 491 |
+
"spans": [
|
| 492 |
+
{
|
| 493 |
+
"start": 4,
|
| 494 |
+
"end": 14,
|
| 495 |
+
"label": "PERSON",
|
| 496 |
+
"text": "Smith-\nson"
|
| 497 |
+
}
|
| 498 |
+
],
|
| 499 |
+
"id": "baseline-28",
|
| 500 |
+
"language": "en",
|
| 501 |
+
"ocr_noise": true
|
| 502 |
+
},
|
| 503 |
+
{
|
| 504 |
+
"text": "Ber-\nlin",
|
| 505 |
+
"expected": "[CITY]",
|
| 506 |
+
"spans": [
|
| 507 |
+
{
|
| 508 |
+
"start": 0,
|
| 509 |
+
"end": 8,
|
| 510 |
+
"label": "CITY",
|
| 511 |
+
"text": "Ber-\nlin"
|
| 512 |
+
}
|
| 513 |
+
],
|
| 514 |
+
"id": "baseline-29",
|
| 515 |
+
"language": "de",
|
| 516 |
+
"ocr_noise": true
|
| 517 |
+
},
|
| 518 |
+
{
|
| 519 |
+
"text": "Jchn D0e went home.",
|
| 520 |
+
"expected": "[PERSON] went home.",
|
| 521 |
+
"spans": [
|
| 522 |
+
{
|
| 523 |
+
"start": 0,
|
| 524 |
+
"end": 8,
|
| 525 |
+
"label": "PERSON",
|
| 526 |
+
"text": "Jchn D0e"
|
| 527 |
+
}
|
| 528 |
+
],
|
| 529 |
+
"id": "baseline-30",
|
| 530 |
+
"language": "en",
|
| 531 |
+
"ocr_noise": true
|
| 532 |
+
},
|
| 533 |
+
{
|
| 534 |
+
"text": "Welcome to Nevv Y0rk.",
|
| 535 |
+
"expected": "Welcome to [CITY].",
|
| 536 |
+
"spans": [
|
| 537 |
+
{
|
| 538 |
+
"start": 11,
|
| 539 |
+
"end": 20,
|
| 540 |
+
"label": "CITY",
|
| 541 |
+
"text": "Nevv Y0rk"
|
| 542 |
+
}
|
| 543 |
+
],
|
| 544 |
+
"id": "baseline-31",
|
| 545 |
+
"language": "en",
|
| 546 |
+
"ocr_noise": true
|
| 547 |
+
},
|
| 548 |
+
{
|
| 549 |
+
"text": "The cat sat on the mat.",
|
| 550 |
+
"expected": "The cat sat on the mat.",
|
| 551 |
+
"spans": [],
|
| 552 |
+
"id": "baseline-32",
|
| 553 |
+
"language": "en",
|
| 554 |
+
"ocr_noise": false
|
| 555 |
+
},
|
| 556 |
+
{
|
| 557 |
+
"text": "12345 is a number.",
|
| 558 |
+
"expected": "12345 is a number.",
|
| 559 |
+
"spans": [],
|
| 560 |
+
"id": "baseline-33",
|
| 561 |
+
"language": "en",
|
| 562 |
+
"ocr_noise": false
|
| 563 |
+
},
|
| 564 |
+
{
|
| 565 |
+
"text": "This is a simple test.",
|
| 566 |
+
"expected": "This is a simple test.",
|
| 567 |
+
"spans": [],
|
| 568 |
+
"id": "baseline-34",
|
| 569 |
+
"language": "en",
|
| 570 |
+
"ocr_noise": false
|
| 571 |
+
},
|
| 572 |
+
{
|
| 573 |
+
"text": "My email is test@example.com.",
|
| 574 |
+
"expected": "My email is [EMAIL].",
|
| 575 |
+
"spans": [
|
| 576 |
+
{
|
| 577 |
+
"start": 12,
|
| 578 |
+
"end": 28,
|
| 579 |
+
"label": "EMAIL",
|
| 580 |
+
"text": "test@example.com"
|
| 581 |
+
}
|
| 582 |
+
],
|
| 583 |
+
"id": "baseline-35",
|
| 584 |
+
"language": "en",
|
| 585 |
+
"ocr_noise": false
|
| 586 |
+
},
|
| 587 |
+
{
|
| 588 |
+
"text": "Call me at 555-0199.",
|
| 589 |
+
"expected": "Call me at [PHONE].",
|
| 590 |
+
"spans": [
|
| 591 |
+
{
|
| 592 |
+
"start": 11,
|
| 593 |
+
"end": 19,
|
| 594 |
+
"label": "PHONE",
|
| 595 |
+
"text": "555-0199"
|
| 596 |
+
}
|
| 597 |
+
],
|
| 598 |
+
"id": "baseline-36",
|
| 599 |
+
"language": "en",
|
| 600 |
+
"ocr_noise": false
|
| 601 |
+
},
|
| 602 |
+
{
|
| 603 |
+
"text": "I work for Google Inc.",
|
| 604 |
+
"expected": "I work for [ORGANIZATION].",
|
| 605 |
+
"spans": [
|
| 606 |
+
{
|
| 607 |
+
"start": 11,
|
| 608 |
+
"end": 22,
|
| 609 |
+
"label": "ORGANIZATION",
|
| 610 |
+
"text": "Google Inc."
|
| 611 |
+
}
|
| 612 |
+
],
|
| 613 |
+
"id": "baseline-37",
|
| 614 |
+
"language": "en",
|
| 615 |
+
"ocr_noise": false
|
| 616 |
+
},
|
| 617 |
+
{
|
| 618 |
+
"text": "Contact info@company.de",
|
| 619 |
+
"expected": "Contact [EMAIL]",
|
| 620 |
+
"spans": [
|
| 621 |
+
{
|
| 622 |
+
"start": 8,
|
| 623 |
+
"end": 23,
|
| 624 |
+
"label": "EMAIL",
|
| 625 |
+
"text": "info@company.de"
|
| 626 |
+
}
|
| 627 |
+
],
|
| 628 |
+
"id": "baseline-38",
|
| 629 |
+
"language": "en",
|
| 630 |
+
"ocr_noise": false
|
| 631 |
+
},
|
| 632 |
+
{
|
| 633 |
+
"text": "The UN is an organization.",
|
| 634 |
+
"expected": "The [ORGANIZATION] is an organization.",
|
| 635 |
+
"spans": [
|
| 636 |
+
{
|
| 637 |
+
"start": 4,
|
| 638 |
+
"end": 6,
|
| 639 |
+
"label": "ORGANIZATION",
|
| 640 |
+
"text": "UN"
|
| 641 |
+
}
|
| 642 |
+
],
|
| 643 |
+
"id": "baseline-39",
|
| 644 |
+
"language": "en",
|
| 645 |
+
"ocr_noise": false
|
| 646 |
+
}
|
| 647 |
+
]
|
ocr/anonymization/fixtures/validation.json
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[
|
| 2 |
+
{
|
| 3 |
+
"id": "validation-00",
|
| 4 |
+
"language": "de",
|
| 5 |
+
"text": "Mara Kühn arbeitet bei Siemens in Dresden.",
|
| 6 |
+
"spans": [
|
| 7 |
+
{
|
| 8 |
+
"start": 0,
|
| 9 |
+
"end": 9,
|
| 10 |
+
"label": "PERSON",
|
| 11 |
+
"text": "Mara Kühn"
|
| 12 |
+
},
|
| 13 |
+
{
|
| 14 |
+
"start": 23,
|
| 15 |
+
"end": 30,
|
| 16 |
+
"label": "ORGANIZATION",
|
| 17 |
+
"text": "Siemens"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"start": 34,
|
| 21 |
+
"end": 41,
|
| 22 |
+
"label": "CITY",
|
| 23 |
+
"text": "Dresden"
|
| 24 |
+
}
|
| 25 |
+
],
|
| 26 |
+
"expected": "[PERSON] arbeitet bei [ORGANIZATION] in [CITY].",
|
| 27 |
+
"ocr_noise": false
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"id": "validation-01",
|
| 31 |
+
"language": "en",
|
| 32 |
+
"text": "Emma Wilson works for Acme Corporation in London.",
|
| 33 |
+
"spans": [
|
| 34 |
+
{
|
| 35 |
+
"start": 0,
|
| 36 |
+
"end": 11,
|
| 37 |
+
"label": "PERSON",
|
| 38 |
+
"text": "Emma Wilson"
|
| 39 |
+
},
|
| 40 |
+
{
|
| 41 |
+
"start": 22,
|
| 42 |
+
"end": 38,
|
| 43 |
+
"label": "ORGANIZATION",
|
| 44 |
+
"text": "Acme Corporation"
|
| 45 |
+
},
|
| 46 |
+
{
|
| 47 |
+
"start": 42,
|
| 48 |
+
"end": 48,
|
| 49 |
+
"label": "CITY",
|
| 50 |
+
"text": "London"
|
| 51 |
+
}
|
| 52 |
+
],
|
| 53 |
+
"expected": "[PERSON] works for [ORGANIZATION] in [CITY].",
|
| 54 |
+
"ocr_noise": false
|
| 55 |
+
},
|
| 56 |
+
{
|
| 57 |
+
"id": "validation-02",
|
| 58 |
+
"language": "de",
|
| 59 |
+
"text": "Sie ist muslimisch. Kontakt: mara.kuehn@example.org, +49 351 7654321.",
|
| 60 |
+
"spans": [
|
| 61 |
+
{
|
| 62 |
+
"start": 8,
|
| 63 |
+
"end": 18,
|
| 64 |
+
"label": "RELIGION",
|
| 65 |
+
"text": "muslimisch"
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"start": 29,
|
| 69 |
+
"end": 51,
|
| 70 |
+
"label": "EMAIL",
|
| 71 |
+
"text": "mara.kuehn@example.org"
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"start": 53,
|
| 75 |
+
"end": 68,
|
| 76 |
+
"label": "PHONE",
|
| 77 |
+
"text": "+49 351 7654321"
|
| 78 |
+
}
|
| 79 |
+
],
|
| 80 |
+
"expected": "Sie ist [RELIGION]. Kontakt: [EMAIL], [PHONE].",
|
| 81 |
+
"ocr_noise": false
|
| 82 |
+
},
|
| 83 |
+
{
|
| 84 |
+
"id": "validation-03",
|
| 85 |
+
"language": "en",
|
| 86 |
+
"text": "His religion is Hinduism. Contact emma@example.net or +1 212 555 0188.",
|
| 87 |
+
"spans": [
|
| 88 |
+
{
|
| 89 |
+
"start": 16,
|
| 90 |
+
"end": 24,
|
| 91 |
+
"label": "RELIGION",
|
| 92 |
+
"text": "Hinduism"
|
| 93 |
+
},
|
| 94 |
+
{
|
| 95 |
+
"start": 34,
|
| 96 |
+
"end": 50,
|
| 97 |
+
"label": "EMAIL",
|
| 98 |
+
"text": "emma@example.net"
|
| 99 |
+
},
|
| 100 |
+
{
|
| 101 |
+
"start": 54,
|
| 102 |
+
"end": 69,
|
| 103 |
+
"label": "PHONE",
|
| 104 |
+
"text": "+1 212 555 0188"
|
| 105 |
+
}
|
| 106 |
+
],
|
| 107 |
+
"expected": "His religion is [RELIGION]. Contact [EMAIL] or [PHONE].",
|
| 108 |
+
"ocr_noise": false
|
| 109 |
+
},
|
| 110 |
+
{
|
| 111 |
+
"id": "validation-04",
|
| 112 |
+
"language": "de",
|
| 113 |
+
"text": "Nora Winter besucht Köln. Nora Winter bleibt dort.",
|
| 114 |
+
"spans": [
|
| 115 |
+
{
|
| 116 |
+
"start": 0,
|
| 117 |
+
"end": 11,
|
| 118 |
+
"label": "PERSON",
|
| 119 |
+
"text": "Nora Winter"
|
| 120 |
+
},
|
| 121 |
+
{
|
| 122 |
+
"start": 20,
|
| 123 |
+
"end": 24,
|
| 124 |
+
"label": "CITY",
|
| 125 |
+
"text": "Köln"
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"start": 26,
|
| 129 |
+
"end": 37,
|
| 130 |
+
"label": "PERSON",
|
| 131 |
+
"text": "Nora Winter"
|
| 132 |
+
}
|
| 133 |
+
],
|
| 134 |
+
"expected": "[PERSON] besucht [CITY]. [PERSON] bleibt dort.",
|
| 135 |
+
"ocr_noise": false
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"id": "validation-05",
|
| 139 |
+
"language": "en",
|
| 140 |
+
"text": "Jordan is a river, and Jordan Miller is a person.",
|
| 141 |
+
"spans": [
|
| 142 |
+
{
|
| 143 |
+
"start": 23,
|
| 144 |
+
"end": 36,
|
| 145 |
+
"label": "PERSON",
|
| 146 |
+
"text": "Jordan Miller"
|
| 147 |
+
}
|
| 148 |
+
],
|
| 149 |
+
"expected": "Jordan is a river, and [PERSON] is a person.",
|
| 150 |
+
"ocr_noise": false
|
| 151 |
+
},
|
| 152 |
+
{
|
| 153 |
+
"id": "validation-06",
|
| 154 |
+
"language": "de",
|
| 155 |
+
"text": "Religion ist ein Gesprächsthema. Keine Zugehörigkeit ist angegeben.",
|
| 156 |
+
"spans": [],
|
| 157 |
+
"expected": "Religion ist ein Gesprächsthema. Keine Zugehörigkeit ist angegeben.",
|
| 158 |
+
"ocr_noise": false
|
| 159 |
+
},
|
| 160 |
+
{
|
| 161 |
+
"id": "validation-07",
|
| 162 |
+
"language": "en",
|
| 163 |
+
"text": "A name alone does not establish religious affiliation.",
|
| 164 |
+
"spans": [],
|
| 165 |
+
"expected": "A name alone does not establish religious affiliation.",
|
| 166 |
+
"ocr_noise": false
|
| 167 |
+
},
|
| 168 |
+
{
|
| 169 |
+
"id": "validation-08",
|
| 170 |
+
"language": "de",
|
| 171 |
+
"text": "Frau Eva-Luise Schröder wohnt in Ham-\nburg.",
|
| 172 |
+
"spans": [
|
| 173 |
+
{
|
| 174 |
+
"start": 5,
|
| 175 |
+
"end": 23,
|
| 176 |
+
"label": "PERSON",
|
| 177 |
+
"text": "Eva-Luise Schröder"
|
| 178 |
+
},
|
| 179 |
+
{
|
| 180 |
+
"start": 33,
|
| 181 |
+
"end": 42,
|
| 182 |
+
"label": "CITY",
|
| 183 |
+
"text": "Ham-\nburg"
|
| 184 |
+
}
|
| 185 |
+
],
|
| 186 |
+
"expected": "Frau [PERSON] wohnt in [CITY].",
|
| 187 |
+
"ocr_noise": true
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"id": "validation-09",
|
| 191 |
+
"language": "en",
|
| 192 |
+
"text": "M4ry Br0wn lives in L0ndon.",
|
| 193 |
+
"spans": [
|
| 194 |
+
{
|
| 195 |
+
"start": 0,
|
| 196 |
+
"end": 10,
|
| 197 |
+
"label": "PERSON",
|
| 198 |
+
"text": "M4ry Br0wn"
|
| 199 |
+
},
|
| 200 |
+
{
|
| 201 |
+
"start": 20,
|
| 202 |
+
"end": 26,
|
| 203 |
+
"label": "CITY",
|
| 204 |
+
"text": "L0ndon"
|
| 205 |
+
}
|
| 206 |
+
],
|
| 207 |
+
"expected": "[PERSON] lives in [CITY].",
|
| 208 |
+
"ocr_noise": true
|
| 209 |
+
}
|
| 210 |
+
]
|
ocr/anonymization/historical.py
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Verify immutable historical artifacts; never evaluate updated code as an old holdout."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
FROZEN_ARTIFACT_SHA256 = {
|
| 8 |
+
"docs/anonymization-evaluation/gate03c-holdout-first.json": "257daa6d43285925c4899471c3f615293293fa0fa8ed17545ebb9d8e99802e83",
|
| 9 |
+
"docs/anonymization-evaluation/gate03c-holdout-first.provenance.json": "bf19f6ae26e146aa6e04aaed9a54705c0f910af308ea33a8c5eacfe4d1724fbb",
|
| 10 |
+
"docs/anonymization-evaluation/gate03d-holdout-first.json": "08f86d2b78871683835c51b627e251c66b83fc33f828d8a6c8e9742c8bd47d9f",
|
| 11 |
+
"docs/anonymization-evaluation/gate03d-holdout-first.provenance.json": "9d5fa15c5427fcfa499262b20453ed4adfe59da86709dffc745ec205e0661bca",
|
| 12 |
+
"ocr/anonymization/fixtures/holdout03c/cases.json": "0174fb41d34d0a4db18877c68fb70149eaa67c05da86608d5da4ff8765a52c15",
|
| 13 |
+
"ocr/anonymization/fixtures/holdout03c/freeze.json": "c707239993099dab4a48b83f6c2aac58674de5423ede75cd1aced17d290708a6",
|
| 14 |
+
"ocr/anonymization/fixtures/holdout03d/cases.json": "7180e176f6d8bd259da56ad729ede6fd2c798aca15a786ba0ff0adcd862f21a1",
|
| 15 |
+
"ocr/anonymization/fixtures/holdout03d/freeze.json": "448dc5d7b0f3ef00f7d0f90084866f7f2e85311aac6af32ed0ad3d2658f8b1f5",
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def verify_historical():
|
| 20 |
+
repo = Path(__file__).parents[2]
|
| 21 |
+
for name, value in FROZEN_ARTIFACT_SHA256.items():
|
| 22 |
+
if hashlib.sha256((repo / name).read_bytes()).hexdigest() != value:
|
| 23 |
+
raise ValueError("historical_evidence_changed")
|
| 24 |
+
root = Path(__file__).parent / "fixtures/holdout03d"
|
| 25 |
+
freeze = json.loads((root / "freeze.json").read_bytes())
|
| 26 |
+
report = json.loads(
|
| 27 |
+
(repo / "docs/anonymization-evaluation/gate03d-holdout-first.json").read_bytes()
|
| 28 |
+
)
|
| 29 |
+
cases = json.loads((root / "cases.json").read_bytes())
|
| 30 |
+
if (
|
| 31 |
+
report["implementation_sha256"] != freeze["implementation_sha256"]
|
| 32 |
+
or report["corpus_sha256"] != freeze["sha256"]
|
| 33 |
+
or {p["case_id"] for p in report["per_case"]} != {c["id"] for c in cases}
|
| 34 |
+
):
|
| 35 |
+
raise ValueError("historical_evidence_changed")
|
| 36 |
+
return root, freeze, cases, report
|
ocr/anonymization/holdout.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""First-result, frozen synthetic holdout; refuses prompt/weight changes."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import importlib.metadata
|
| 5 |
+
import json
|
| 6 |
+
import resource
|
| 7 |
+
import subprocess
|
| 8 |
+
import sys
|
| 9 |
+
import time
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from .atomic_output import write_candidate
|
| 13 |
+
from .detectors import detect_contacts
|
| 14 |
+
from .evaluate import score_cases
|
| 15 |
+
from .model import MODEL_REVISION
|
| 16 |
+
from .recognizer import GLiNERRecognizer
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def run(cache, output):
|
| 20 |
+
root = Path(__file__).parent / "fixtures/holdout03c"
|
| 21 |
+
frozen = json.loads((root / "freeze.json").read_text())
|
| 22 |
+
data = (root / "cases.json").read_bytes()
|
| 23 |
+
if hashlib.sha256(data).hexdigest() != frozen["sha256"]:
|
| 24 |
+
raise ValueError("holdout_integrity_failed")
|
| 25 |
+
denied = []
|
| 26 |
+
|
| 27 |
+
def no_network(event, args):
|
| 28 |
+
if event in ("socket.connect", "socket.bind", "socket.getaddrinfo"):
|
| 29 |
+
denied.append(event)
|
| 30 |
+
raise PermissionError("qualification_network_disabled")
|
| 31 |
+
|
| 32 |
+
sys.addaudithook(no_network)
|
| 33 |
+
started = time.perf_counter()
|
| 34 |
+
recognizer = GLiNERRecognizer(cache)
|
| 35 |
+
loading_ms = (time.perf_counter() - started) * 1000
|
| 36 |
+
if (
|
| 37 |
+
recognizer.prompts != frozen["control_prompts"]
|
| 38 |
+
or recognizer.threshold != frozen["threshold"]
|
| 39 |
+
or MODEL_REVISION != frozen["model_revision"]
|
| 40 |
+
):
|
| 41 |
+
raise ValueError("holdout_control_changed")
|
| 42 |
+
recognizer.recognize("A plain synthetic warmup sentence.")
|
| 43 |
+
report = dict(
|
| 44 |
+
schema_version="holdout03c-first-control/v1",
|
| 45 |
+
corpus_sha256=frozen["sha256"],
|
| 46 |
+
code_sha=subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
|
| 47 |
+
freeze_commit=subprocess.check_output(
|
| 48 |
+
["git", "log", "-1", "--format=%H", "--", str(root / "cases.json")], text=True
|
| 49 |
+
).strip(),
|
| 50 |
+
model_revision=MODEL_REVISION,
|
| 51 |
+
prompts=recognizer.prompts,
|
| 52 |
+
threshold=recognizer.threshold,
|
| 53 |
+
loading_ms=loading_ms,
|
| 54 |
+
runtime_versions={
|
| 55 |
+
k: importlib.metadata.version(k)
|
| 56 |
+
for k in ("gliner", "torch", "transformers", "numpy", "pydantic")
|
| 57 |
+
},
|
| 58 |
+
evaluation=score_cases(recognizer, json.loads(data), detector=detect_contacts),
|
| 59 |
+
peak_rss_mib=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024,
|
| 60 |
+
denied_network_calls=denied,
|
| 61 |
+
automated_release_eligible=False,
|
| 62 |
+
)
|
| 63 |
+
write_candidate(output, (json.dumps(report, indent=2) + "\n").encode())
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
if __name__ == "__main__":
|
| 67 |
+
import argparse
|
| 68 |
+
|
| 69 |
+
parser = argparse.ArgumentParser(
|
| 70 |
+
description="Evaluate frozen holdout once with existing control"
|
| 71 |
+
)
|
| 72 |
+
parser.add_argument("--cache", required=True)
|
| 73 |
+
parser.add_argument("--output", required=True)
|
| 74 |
+
args = parser.parse_args()
|
| 75 |
+
try:
|
| 76 |
+
run(args.cache, args.output)
|
| 77 |
+
except Exception:
|
| 78 |
+
raise SystemExit("holdout_evaluation_failed") from None
|
| 79 |
+
print("holdout_evaluation_recorded_check_qualification_result")
|
ocr/anonymization/host_adapter.py
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Host security contract. No identity provider or request-identity fallback."""
|
| 2 |
+
|
| 3 |
+
import time
|
| 4 |
+
from typing import Protocol
|
| 5 |
+
|
| 6 |
+
from pydantic import Field
|
| 7 |
+
|
| 8 |
+
from ocr.schemas import SafeError
|
| 9 |
+
|
| 10 |
+
from .release_policy import ReleaseAuthority
|
| 11 |
+
from .schemas import Destination, StrictModel, VerifiedReview
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class AuthenticatedIdentity(StrictModel):
|
| 15 |
+
subject: str = Field(pattern=r"^[a-zA-Z0-9:_-]{1,128}$")
|
| 16 |
+
session_id: str = Field(pattern=r"^[a-zA-Z0-9:_-]{1,128}$")
|
| 17 |
+
expires_at: int
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class CompletedReview(StrictModel):
|
| 21 |
+
attestation: VerifiedReview
|
| 22 |
+
completed: bool
|
| 23 |
+
revoked: bool
|
| 24 |
+
expires_at: int
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
class ExportAuthorizationRecord(StrictModel):
|
| 28 |
+
artifact_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 29 |
+
candidate_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 30 |
+
review_id: str
|
| 31 |
+
review_evidence_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 32 |
+
sensitivity: str
|
| 33 |
+
destination_id: str
|
| 34 |
+
expires_at: int
|
| 35 |
+
revoked: bool
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class HostSecurityAdapter(Protocol):
|
| 39 |
+
"""Implement against authenticated server context and authoritative stores.
|
| 40 |
+
|
| 41 |
+
credentials are opaque authentication evidence, never a requested subject.
|
| 42 |
+
Review content/evidence must be immutable; revocation is separate mutable state.
|
| 43 |
+
Host ACLs and destination policy are looked up on EVERY operation.
|
| 44 |
+
"""
|
| 45 |
+
|
| 46 |
+
def authenticate(self, credential: object) -> AuthenticatedIdentity: ...
|
| 47 |
+
def load_review(self, review_id: str) -> CompletedReview: ...
|
| 48 |
+
def candidate_exists(self, candidate_digest: str) -> bool: ...
|
| 49 |
+
def load_export(self, artifact_digest: str) -> ExportAuthorizationRecord: ...
|
| 50 |
+
def current_acl(
|
| 51 |
+
self, identity: AuthenticatedIdentity, scope: str, sensitivity: str, destination_id: str
|
| 52 |
+
) -> bool: ...
|
| 53 |
+
def destination(self, destination_id: str) -> Destination: ...
|
| 54 |
+
def corrected_release_allowed(self, candidate, review: VerifiedReview) -> bool: ...
|
| 55 |
+
def legacy_release_allowed(self, candidate, review: VerifiedReview) -> bool: ...
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
class HostAuthorizationBridge:
|
| 59 |
+
def __init__(self, host: HostSecurityAdapter):
|
| 60 |
+
self.host = host
|
| 61 |
+
|
| 62 |
+
def identity(self, credential):
|
| 63 |
+
identity = AuthenticatedIdentity.model_validate(self.host.authenticate(credential))
|
| 64 |
+
if identity.expires_at <= time.time():
|
| 65 |
+
raise SafeError("host_authorization_denied")
|
| 66 |
+
return identity
|
| 67 |
+
|
| 68 |
+
def review(self, credential, review_id):
|
| 69 |
+
try:
|
| 70 |
+
identity = self.identity(credential)
|
| 71 |
+
record = CompletedReview.model_validate(self.host.load_review(review_id))
|
| 72 |
+
review = record.attestation
|
| 73 |
+
if (
|
| 74 |
+
not record.completed
|
| 75 |
+
or record.revoked
|
| 76 |
+
or record.expires_at <= time.time()
|
| 77 |
+
or review.reviewer_subject != identity.subject
|
| 78 |
+
or review.review_id != review_id
|
| 79 |
+
or self.host.candidate_exists(review.candidate_digest) is not True
|
| 80 |
+
):
|
| 81 |
+
raise ValueError()
|
| 82 |
+
destination = Destination.model_validate(self.host.destination(review.destination_id))
|
| 83 |
+
if (
|
| 84 |
+
self.host.current_acl(
|
| 85 |
+
identity,
|
| 86 |
+
"anonymization:release",
|
| 87 |
+
review.sensitivity,
|
| 88 |
+
destination.destination_id,
|
| 89 |
+
)
|
| 90 |
+
is not True
|
| 91 |
+
):
|
| 92 |
+
raise ValueError()
|
| 93 |
+
return review
|
| 94 |
+
except Exception:
|
| 95 |
+
raise SafeError("host_authorization_denied") from None
|
| 96 |
+
|
| 97 |
+
def acl(self, review, scope, destination):
|
| 98 |
+
# Identity/ACL already checked with the current session by review(). This
|
| 99 |
+
# extra check binds the live operator policy to the approval snapshot.
|
| 100 |
+
return (
|
| 101 |
+
Destination.model_validate(self.host.destination(destination.destination_id))
|
| 102 |
+
== destination
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
def corrected_release(self, candidate, review):
|
| 106 |
+
# Missing production integration denies corrected candidates.
|
| 107 |
+
try:
|
| 108 |
+
return self.host.corrected_release_allowed(candidate, review) is True
|
| 109 |
+
except Exception:
|
| 110 |
+
return False
|
| 111 |
+
|
| 112 |
+
def legacy_release(self, candidate, review):
|
| 113 |
+
# v1/v2 require an explicit authoritative source/schema eligibility policy.
|
| 114 |
+
try:
|
| 115 |
+
return self.host.legacy_release_allowed(candidate, review) is True
|
| 116 |
+
except Exception:
|
| 117 |
+
return False
|
| 118 |
+
|
| 119 |
+
def authority(self, key, ledger, destination_ids, **kwargs):
|
| 120 |
+
return ReleaseAuthority(
|
| 121 |
+
key,
|
| 122 |
+
self.acl,
|
| 123 |
+
verify_review=self.review,
|
| 124 |
+
consume_approval=ledger.consume,
|
| 125 |
+
correction_policy=self.corrected_release,
|
| 126 |
+
legacy_policy=self.legacy_release,
|
| 127 |
+
destinations=tuple(self.host.destination(i) for i in destination_ids),
|
| 128 |
+
**kwargs,
|
| 129 |
+
)
|
| 130 |
+
|
| 131 |
+
def transfer_authorized(self, credential, export_digest, sensitivity, destination_id):
|
| 132 |
+
"""Separate current permission; a release approval alone never calls this."""
|
| 133 |
+
try:
|
| 134 |
+
identity = self.identity(credential)
|
| 135 |
+
destination = Destination.model_validate(self.host.destination(destination_id))
|
| 136 |
+
if (
|
| 137 |
+
destination.destination_class != "private"
|
| 138 |
+
or sensitivity not in destination.allowed_sensitivities
|
| 139 |
+
):
|
| 140 |
+
return False
|
| 141 |
+
record = ExportAuthorizationRecord.model_validate(self.host.load_export(export_digest))
|
| 142 |
+
review = CompletedReview.model_validate(self.host.load_review(record.review_id))
|
| 143 |
+
if (
|
| 144 |
+
record.artifact_digest != export_digest
|
| 145 |
+
or record.sensitivity != sensitivity
|
| 146 |
+
or record.destination_id != destination_id
|
| 147 |
+
or record.revoked
|
| 148 |
+
or record.expires_at <= time.time()
|
| 149 |
+
or not review.completed
|
| 150 |
+
or review.revoked
|
| 151 |
+
or review.expires_at <= time.time()
|
| 152 |
+
or review.attestation.review_id != record.review_id
|
| 153 |
+
or review.attestation.candidate_digest != record.candidate_digest
|
| 154 |
+
or review.attestation.evidence_digest != record.review_evidence_digest
|
| 155 |
+
or review.attestation.sensitivity != sensitivity
|
| 156 |
+
or review.attestation.destination_id != destination_id
|
| 157 |
+
or self.host.candidate_exists(record.candidate_digest) is not True
|
| 158 |
+
):
|
| 159 |
+
return False
|
| 160 |
+
return (
|
| 161 |
+
self.host.current_acl(
|
| 162 |
+
identity, "anonymization:private-transfer", sensitivity, destination_id
|
| 163 |
+
)
|
| 164 |
+
is True
|
| 165 |
+
)
|
| 166 |
+
except Exception:
|
| 167 |
+
raise SafeError("host_authorization_denied") from None
|
ocr/anonymization/investigate.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit opt-in experiment on repository SYNTHETIC fixtures only.
|
| 2 |
+
|
| 3 |
+
Unlike aggregate evaluation, this artifact intentionally contains synthetic gold
|
| 4 |
+
and prediction text for diagnosis. Never use this command with private documents.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import json
|
| 8 |
+
import resource
|
| 9 |
+
import time
|
| 10 |
+
from pathlib import Path
|
| 11 |
+
|
| 12 |
+
from .detectors import detect
|
| 13 |
+
from .evaluate import score_cases
|
| 14 |
+
from .model import MODEL_ID, MODEL_REVISION
|
| 15 |
+
from .normalization import dehyphenate
|
| 16 |
+
from .recognizer import DEFAULT_PROMPTS, GLiNERRecognizer
|
| 17 |
+
from .replacement import resolve_overlaps, validate_entities
|
| 18 |
+
|
| 19 |
+
REVIEWED_PROMPTS = DEFAULT_PROMPTS | {"CITY": "city", "ORGANIZATION": "organization"}
|
| 20 |
+
|
| 21 |
+
EXPERIMENTS = (
|
| 22 |
+
("reviewed_baseline", {}, 0.5),
|
| 23 |
+
("lower_threshold", {}, 0.35),
|
| 24 |
+
("geographic_prompt", {"CITY": "location"}, 0.5),
|
| 25 |
+
(
|
| 26 |
+
"geographic_and_company_prompt",
|
| 27 |
+
{"CITY": "location", "ORGANIZATION": "company or organization"},
|
| 28 |
+
0.5,
|
| 29 |
+
),
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class Recorded:
|
| 34 |
+
def __init__(self, recognizer):
|
| 35 |
+
self.recognizer = recognizer
|
| 36 |
+
self.predictions = {}
|
| 37 |
+
|
| 38 |
+
def recognize(self, text):
|
| 39 |
+
found = self.recognizer.recognize(text)
|
| 40 |
+
self.predictions[text] = found
|
| 41 |
+
return found
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def run(cache, output):
|
| 45 |
+
start = time.perf_counter()
|
| 46 |
+
model = GLiNERRecognizer(cache)
|
| 47 |
+
loading_ms = (time.perf_counter() - start) * 1000
|
| 48 |
+
root = Path(__file__).parent / "fixtures"
|
| 49 |
+
suites = {
|
| 50 |
+
n: json.loads((root / f"{n}.json").read_text()) for n in ("qualification", "validation")
|
| 51 |
+
}
|
| 52 |
+
report = dict(
|
| 53 |
+
model=MODEL_ID,
|
| 54 |
+
revision=MODEL_REVISION,
|
| 55 |
+
loading_ms=loading_ms,
|
| 56 |
+
experiments={},
|
| 57 |
+
baseline_diagnosis=[],
|
| 58 |
+
)
|
| 59 |
+
model.recognize("The cat sat on the mat.")
|
| 60 |
+
for name, changes, threshold in EXPERIMENTS:
|
| 61 |
+
model.prompts = REVIEWED_PROMPTS | changes
|
| 62 |
+
model.threshold = threshold
|
| 63 |
+
recorder = Recorded(model)
|
| 64 |
+
scores = {n: score_cases(recorder, cases) for n, cases in suites.items()}
|
| 65 |
+
report["experiments"][name] = dict(
|
| 66 |
+
prompts=model.prompts.copy(), threshold=threshold, suites=scores
|
| 67 |
+
)
|
| 68 |
+
if name != "reviewed_baseline":
|
| 69 |
+
continue
|
| 70 |
+
for suite, cases in suites.items():
|
| 71 |
+
for case in cases:
|
| 72 |
+
raw = recorder.predictions[case["text"]]
|
| 73 |
+
combined = resolve_overlaps(
|
| 74 |
+
validate_entities(case["text"], raw + detect(case["text"]))
|
| 75 |
+
)
|
| 76 |
+
gold = {(s["start"], s["end"], s["label"]) for s in case["spans"]}
|
| 77 |
+
predicted = {(s.start, s.end, s.label) for s in combined}
|
| 78 |
+
errors = {
|
| 79 |
+
label: dict(
|
| 80 |
+
false_positives=[
|
| 81 |
+
s.model_dump()
|
| 82 |
+
for s in combined
|
| 83 |
+
if s.label == label and (s.start, s.end, s.label) not in gold
|
| 84 |
+
],
|
| 85 |
+
false_negatives=[
|
| 86 |
+
s
|
| 87 |
+
for s in case["spans"]
|
| 88 |
+
if s["label"] == label
|
| 89 |
+
and (s["start"], s["end"], label) not in predicted
|
| 90 |
+
],
|
| 91 |
+
)
|
| 92 |
+
for label in ("CITY", "ORGANIZATION")
|
| 93 |
+
}
|
| 94 |
+
if not any(v["false_positives"] or v["false_negatives"] for v in errors.values()):
|
| 95 |
+
continue
|
| 96 |
+
normalized, _ = dehyphenate(case["text"])
|
| 97 |
+
item = dict(
|
| 98 |
+
suite=suite,
|
| 99 |
+
case_id=case["id"],
|
| 100 |
+
text=case["text"],
|
| 101 |
+
normalized=normalized,
|
| 102 |
+
gold=case["spans"],
|
| 103 |
+
predictions=[s.model_dump() for s in combined],
|
| 104 |
+
errors=errors,
|
| 105 |
+
)
|
| 106 |
+
uncovered_city = [
|
| 107 |
+
s
|
| 108 |
+
for s in case["spans"]
|
| 109 |
+
if s["label"] == "CITY"
|
| 110 |
+
and any(
|
| 111 |
+
case["text"][i].isalnum()
|
| 112 |
+
and not any(p.start <= i < p.end for p in combined)
|
| 113 |
+
for i in range(s["start"], s["end"])
|
| 114 |
+
)
|
| 115 |
+
]
|
| 116 |
+
if uncovered_city:
|
| 117 |
+
tokenizer = model.model.data_processor.transformer_tokenizer
|
| 118 |
+
ids = tokenizer(normalized)["input_ids"]
|
| 119 |
+
item["uncovered_city"] = uncovered_city
|
| 120 |
+
item["tokens"] = tokenizer.convert_ids_to_tokens(ids)
|
| 121 |
+
item["low_threshold_predictions"] = model.model.predict_entities(
|
| 122 |
+
normalized, list(model.prompts.values()), threshold=0.1, flat_ner=True
|
| 123 |
+
)
|
| 124 |
+
report["baseline_diagnosis"].append(item)
|
| 125 |
+
report["peak_rss_mib"] = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024
|
| 126 |
+
Path(output).write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n")
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
if __name__ == "__main__":
|
| 130 |
+
import argparse
|
| 131 |
+
|
| 132 |
+
parser = argparse.ArgumentParser(
|
| 133 |
+
description="Compare fixed experiments on synthetic repository corpora"
|
| 134 |
+
)
|
| 135 |
+
parser.add_argument("--cache", required=True)
|
| 136 |
+
parser.add_argument("--output", required=True)
|
| 137 |
+
args = parser.parse_args()
|
| 138 |
+
try:
|
| 139 |
+
run(args.cache, args.output)
|
| 140 |
+
except Exception:
|
| 141 |
+
raise SystemExit("synthetic_recognition_investigation_failed") from None
|
| 142 |
+
print("synthetic_recognition_investigation_completed")
|
ocr/anonymization/model.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pinned artifacts for native GLiNER; setup is the only network-enabled operation."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
|
| 7 |
+
from ocr.schemas import SafeError
|
| 8 |
+
|
| 9 |
+
MODEL_ID = "VAGOsolutions/SauerkrautLM-GLiNER"
|
| 10 |
+
MODEL_REVISION = "ba967ee030b7e679dd5422cb16fcd1b379da38e1"
|
| 11 |
+
FILES = (
|
| 12 |
+
"gliner_config.json",
|
| 13 |
+
"pytorch_model.bin",
|
| 14 |
+
"special_tokens_map.json",
|
| 15 |
+
"tokenizer.json",
|
| 16 |
+
"tokenizer_config.json",
|
| 17 |
+
)
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def file_hash(path):
|
| 21 |
+
h = hashlib.sha256()
|
| 22 |
+
with path.open("rb") as stream:
|
| 23 |
+
for chunk in iter(lambda: stream.read(1024 * 1024), b""):
|
| 24 |
+
h.update(chunk)
|
| 25 |
+
return h.hexdigest()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def verify_cache(path):
|
| 29 |
+
try:
|
| 30 |
+
path = Path(path)
|
| 31 |
+
manifest = json.loads((path / "manifest.json").read_text())
|
| 32 |
+
expected = json.loads(Path(__file__).with_name("artifacts.json").read_text())
|
| 33 |
+
if manifest != expected:
|
| 34 |
+
raise ValueError()
|
| 35 |
+
if manifest["model"] != MODEL_ID or manifest["revision"] != MODEL_REVISION:
|
| 36 |
+
raise ValueError()
|
| 37 |
+
if set(manifest["files"]) != set(FILES):
|
| 38 |
+
raise ValueError()
|
| 39 |
+
for name in FILES:
|
| 40 |
+
if file_hash(path / name) != manifest["files"][name]:
|
| 41 |
+
raise ValueError()
|
| 42 |
+
return path
|
| 43 |
+
except Exception:
|
| 44 |
+
raise SafeError("anonymization_model_unavailable") from None
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def install(path):
|
| 48 |
+
"""Explicit operator command. Download the fixed snapshot, record file integrity."""
|
| 49 |
+
from huggingface_hub import snapshot_download
|
| 50 |
+
|
| 51 |
+
path = Path(path)
|
| 52 |
+
if (path / "manifest.json").exists():
|
| 53 |
+
return verify_cache(path)
|
| 54 |
+
if path.exists() and any(p.name not in {*FILES, ".cache"} for p in path.iterdir()):
|
| 55 |
+
raise SafeError("anonymization_model_setup_directory_not_empty")
|
| 56 |
+
snapshot_download(
|
| 57 |
+
MODEL_ID,
|
| 58 |
+
revision=MODEL_REVISION,
|
| 59 |
+
local_dir=path,
|
| 60 |
+
allow_patterns=list(FILES),
|
| 61 |
+
)
|
| 62 |
+
manifest = {
|
| 63 |
+
"model": MODEL_ID,
|
| 64 |
+
"revision": MODEL_REVISION,
|
| 65 |
+
"files": {name: file_hash(path / name) for name in FILES},
|
| 66 |
+
}
|
| 67 |
+
(path / "manifest.json").write_text(json.dumps(manifest, indent=2) + "\n")
|
| 68 |
+
return verify_cache(path)
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
if __name__ == "__main__":
|
| 72 |
+
import argparse
|
| 73 |
+
|
| 74 |
+
parser = argparse.ArgumentParser(description="Explicit pinned local anonymization model setup")
|
| 75 |
+
parser.add_argument("--cache", required=True)
|
| 76 |
+
args = parser.parse_args()
|
| 77 |
+
try:
|
| 78 |
+
install(args.cache)
|
| 79 |
+
except Exception:
|
| 80 |
+
raise SystemExit("anonymization_model_setup_failed") from None
|
| 81 |
+
print(f"Cached {MODEL_ID}@{MODEL_REVISION}")
|
ocr/anonymization/normalization.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""One reversible source-coordinate view for hyphenated OCR line wraps."""
|
| 2 |
+
|
| 3 |
+
import re
|
| 4 |
+
|
| 5 |
+
LINE_WRAP = re.compile(r"(?<=[^\W\d_])-\r?\n(?=[^\W\d_])", re.UNICODE)
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
def dehyphenate(text):
|
| 9 |
+
removed = {i for match in LINE_WRAP.finditer(text) for i in range(match.start(), match.end())}
|
| 10 |
+
positions = tuple(i for i in range(len(text)) if i not in removed)
|
| 11 |
+
return "".join(text[i] for i in positions), positions
|
ocr/anonymization/outcomes.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Development accuracy and actual candidate coverage, with explicit abstentions.
|
| 2 |
+
|
| 3 |
+
The historical evaluator and its saved first reports are retained unchanged.
|
| 4 |
+
This reporting interface separates blocked evidence from emitted candidate text.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
from collections import Counter
|
| 8 |
+
|
| 9 |
+
from ocr.schemas import SafeError
|
| 10 |
+
|
| 11 |
+
from .detectors import detect
|
| 12 |
+
from .evaluate import metrics, score_cases
|
| 13 |
+
from .schemas import LABELS
|
| 14 |
+
|
| 15 |
+
OUTCOMES = (
|
| 16 |
+
"complete_no_measured_leakage",
|
| 17 |
+
"candidate_partial_mask",
|
| 18 |
+
"candidate_fully_unmasked",
|
| 19 |
+
"blocked_conflicting_spans",
|
| 20 |
+
"blocked_model_runtime",
|
| 21 |
+
"other_incomplete",
|
| 22 |
+
)
|
| 23 |
+
RUNTIME_FAILURES = frozenset(
|
| 24 |
+
("anonymization_model_unavailable", "anonymization_inference_failed", "anonymization_failed")
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def score_outcomes(recognizer, cases, *, detector=detect):
|
| 29 |
+
class SafeRecognizer:
|
| 30 |
+
def recognize(self, text):
|
| 31 |
+
try:
|
| 32 |
+
return recognizer.recognize(text)
|
| 33 |
+
except SafeError:
|
| 34 |
+
raise
|
| 35 |
+
except Exception:
|
| 36 |
+
raise SafeError("anonymization_inference_failed") from None
|
| 37 |
+
|
| 38 |
+
def safe_detector(text):
|
| 39 |
+
try:
|
| 40 |
+
return detector(text)
|
| 41 |
+
except SafeError:
|
| 42 |
+
raise
|
| 43 |
+
except Exception:
|
| 44 |
+
raise SafeError("anonymization_failed") from None
|
| 45 |
+
|
| 46 |
+
details = []
|
| 47 |
+
accuracy = score_cases(SafeRecognizer(), cases, detector=safe_detector, case_reports=details)
|
| 48 |
+
# Historical aggregate coverage counts incomplete documents as uncovered.
|
| 49 |
+
# Name that conservative scope explicitly; it is not emitted-candidate leakage.
|
| 50 |
+
for old, new in (
|
| 51 |
+
("unredacted_canaries", "uncovered_source_canaries_including_incomplete"),
|
| 52 |
+
("literal_unredacted_canaries", "literal_presence_or_incomplete"),
|
| 53 |
+
("partial_canaries", "partially_covered_source_canaries_including_incomplete"),
|
| 54 |
+
("fully_uncovered_canaries", "fully_uncovered_source_canaries_including_incomplete"),
|
| 55 |
+
("alphanumeric_canary_coverage", "all_document_source_coverage"),
|
| 56 |
+
("qualification_passed", "conservative_accuracy_gate_passed"),
|
| 57 |
+
):
|
| 58 |
+
accuracy[new] = accuracy.pop(old)
|
| 59 |
+
accuracy["scope"] = (
|
| 60 |
+
"all source spans; incomplete processing counts as missing, not leaked output"
|
| 61 |
+
)
|
| 62 |
+
counts, partial, full, candidate_spans = Counter(), Counter(), Counter(), Counter()
|
| 63 |
+
for case, detail in zip(cases, details, strict=True):
|
| 64 |
+
error = detail["error_code"]
|
| 65 |
+
if error:
|
| 66 |
+
outcome = (
|
| 67 |
+
"blocked_conflicting_spans"
|
| 68 |
+
if error == "ambiguous_entity_overlap"
|
| 69 |
+
else "blocked_model_runtime"
|
| 70 |
+
if error in RUNTIME_FAILURES
|
| 71 |
+
else "other_incomplete"
|
| 72 |
+
)
|
| 73 |
+
detail["candidate_returned"] = False
|
| 74 |
+
detail["candidate_canaries"] = []
|
| 75 |
+
else:
|
| 76 |
+
flags = []
|
| 77 |
+
for span in detail["canaries"]:
|
| 78 |
+
missing = span["uncovered_positions"]
|
| 79 |
+
total = sum(c.isalnum() for c in span["text"])
|
| 80 |
+
if missing:
|
| 81 |
+
kind = "fully_unmasked" if len(missing) == total else "partial_mask"
|
| 82 |
+
(full if kind == "fully_unmasked" else partial)[span["label"]] += 1
|
| 83 |
+
flags.append(
|
| 84 |
+
dict(label=span["label"], start=span["start"], end=span["end"], kind=kind)
|
| 85 |
+
)
|
| 86 |
+
detail["candidate_returned"] = True
|
| 87 |
+
detail["candidate_canaries"] = flags
|
| 88 |
+
outcome = (
|
| 89 |
+
"candidate_fully_unmasked"
|
| 90 |
+
if any(f["kind"] == "fully_unmasked" for f in flags)
|
| 91 |
+
else "candidate_partial_mask"
|
| 92 |
+
if flags
|
| 93 |
+
else "complete_no_measured_leakage"
|
| 94 |
+
)
|
| 95 |
+
gold = {(s["start"], s["end"], s["label"]) for s in case["spans"]}
|
| 96 |
+
predicted = {(s["start"], s["end"], s["label"]) for s in detail["combined_predictions"]}
|
| 97 |
+
for labelset, kind in (
|
| 98 |
+
(gold & predicted, "tp"),
|
| 99 |
+
(predicted - gold, "fp"),
|
| 100 |
+
(gold - predicted, "fn"),
|
| 101 |
+
):
|
| 102 |
+
for _, _, label in labelset:
|
| 103 |
+
candidate_spans[(label, kind)] += 1
|
| 104 |
+
detail["outcome"] = outcome
|
| 105 |
+
counts[outcome] += 1
|
| 106 |
+
returned = sum(counts[k] for k in OUTCOMES[:3])
|
| 107 |
+
safe = counts["blocked_conflicting_spans"] + counts["blocked_model_runtime"]
|
| 108 |
+
return dict(
|
| 109 |
+
schema_version="development-candidate-outcomes/v1",
|
| 110 |
+
accuracy=accuracy,
|
| 111 |
+
candidate_outcomes={k: counts[k] for k in OUTCOMES},
|
| 112 |
+
candidate_return_rate=returned / len(cases),
|
| 113 |
+
safe_abstention_rate=safe / len(cases),
|
| 114 |
+
incomplete_rate=(len(cases) - returned) / len(cases),
|
| 115 |
+
no_measured_leakage_rate_among_returned=counts[OUTCOMES[0]] / returned
|
| 116 |
+
if returned
|
| 117 |
+
else None,
|
| 118 |
+
candidate_partial_canaries={k: partial[k] for k in LABELS},
|
| 119 |
+
candidate_fully_unmasked_canaries={k: full[k] for k in LABELS},
|
| 120 |
+
returned_candidate_entity_metrics=metrics(candidate_spans),
|
| 121 |
+
per_case=details,
|
| 122 |
+
automated_release_eligible=False,
|
| 123 |
+
recognition_qualified=False, # development evidence is not independent qualification
|
| 124 |
+
)
|
ocr/anonymization/private_review.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Authorized in-process review views. No HTTP endpoint, exporter or IdP.
|
| 2 |
+
|
| 3 |
+
Only a real host adapter can attest bindings, ACLs and immutable source storage.
|
| 4 |
+
Views remain classified; showing a view or source page never completes review or
|
| 5 |
+
creates release permission. Original pages must be reviewed, not only detected spans.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import hashlib
|
| 9 |
+
import re
|
| 10 |
+
import time
|
| 11 |
+
from dataclasses import dataclass, field
|
| 12 |
+
from typing import Literal, Protocol
|
| 13 |
+
|
| 14 |
+
from pydantic import Field, model_validator
|
| 15 |
+
|
| 16 |
+
from ocr.schemas import SafeError
|
| 17 |
+
|
| 18 |
+
from .host_adapter import HostAuthorizationBridge, HostSecurityAdapter
|
| 19 |
+
from .release_policy import digest
|
| 20 |
+
from .schemas import (
|
| 21 |
+
CorrectedCandidate,
|
| 22 |
+
LegacyReleaseCandidate,
|
| 23 |
+
RecognitionProvenance,
|
| 24 |
+
ReleaseCandidate,
|
| 25 |
+
StrictModel,
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class ReviewBinding(StrictModel):
|
| 30 |
+
review_ref: str = Field(pattern=r"^[a-zA-Z0-9_-]{1,128}$")
|
| 31 |
+
environment_id: str = Field(pattern=r"^[a-zA-Z0-9_-]{1,128}$")
|
| 32 |
+
sensitivity: Literal["D0", "D1", "D2"]
|
| 33 |
+
expires_at: int
|
| 34 |
+
revoked: bool
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class SourcePageReference(StrictModel):
|
| 38 |
+
evidence_ref: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 39 |
+
page_index: int = Field(ge=0)
|
| 40 |
+
sensitivity: Literal["D0", "D1", "D2"]
|
| 41 |
+
sha256: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 42 |
+
characters: int = Field(ge=0, le=2_000_000)
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class PrivateReviewMaterial(StrictModel):
|
| 46 |
+
binding: ReviewBinding
|
| 47 |
+
evidence_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 48 |
+
sources: tuple[SourcePageReference, ...] = Field(min_length=1, max_length=100, repr=False)
|
| 49 |
+
candidate: CorrectedCandidate | ReleaseCandidate | LegacyReleaseCandidate | None = Field(
|
| 50 |
+
repr=False
|
| 51 |
+
)
|
| 52 |
+
candidate_digest: str | None = Field(pattern=r"^[a-f0-9]{64}$")
|
| 53 |
+
provenance: RecognitionProvenance | None
|
| 54 |
+
blocking_reason: (
|
| 55 |
+
Literal["conflicting_spans", "model_runtime_failure", "other_incomplete"] | None
|
| 56 |
+
)
|
| 57 |
+
anonymous: Literal[False] = False
|
| 58 |
+
automated_release_eligible: Literal[False] = False
|
| 59 |
+
release_review_required: Literal[True] = True
|
| 60 |
+
|
| 61 |
+
@model_validator(mode="after")
|
| 62 |
+
def consistent_record(self):
|
| 63 |
+
sources = {p.page_index: p for p in self.sources}
|
| 64 |
+
if (
|
| 65 |
+
len(sources) != len(self.sources)
|
| 66 |
+
or sum(p.characters for p in self.sources) > 2_000_000
|
| 67 |
+
or any(p.sensitivity != self.binding.sensitivity for p in self.sources)
|
| 68 |
+
):
|
| 69 |
+
raise ValueError("invalid_private_review_material")
|
| 70 |
+
if self.candidate is None:
|
| 71 |
+
if (
|
| 72 |
+
self.candidate_digest is not None
|
| 73 |
+
or self.blocking_reason is None
|
| 74 |
+
or self.provenance is None
|
| 75 |
+
):
|
| 76 |
+
raise ValueError("invalid_private_review_material")
|
| 77 |
+
else:
|
| 78 |
+
if (
|
| 79 |
+
self.candidate_digest != digest(self.candidate)
|
| 80 |
+
or self.blocking_reason is not None
|
| 81 |
+
or self.candidate.sensitivity != self.binding.sensitivity
|
| 82 |
+
or len(self.sources) != len(self.candidate.pages)
|
| 83 |
+
or {page.page_index for page in self.candidate.pages} != set(sources)
|
| 84 |
+
):
|
| 85 |
+
raise ValueError("invalid_private_review_material")
|
| 86 |
+
if type(self.candidate) in (ReleaseCandidate, CorrectedCandidate):
|
| 87 |
+
if self.provenance != self.candidate.provenance:
|
| 88 |
+
raise ValueError("invalid_private_review_material")
|
| 89 |
+
elif self.provenance is not None:
|
| 90 |
+
raise ValueError("legacy_provenance_unavailable")
|
| 91 |
+
for page in self.candidate.pages:
|
| 92 |
+
if page.page_index not in sources or any(
|
| 93 |
+
not 0 <= tag.start < tag.end <= sources[page.page_index].characters
|
| 94 |
+
for tag in page.tags
|
| 95 |
+
):
|
| 96 |
+
raise ValueError("invalid_private_review_material")
|
| 97 |
+
return self
|
| 98 |
+
|
| 99 |
+
@property
|
| 100 |
+
def categories(self):
|
| 101 |
+
return (
|
| 102 |
+
tuple(sorted({t.label for p in self.candidate.pages for t in p.tags}))
|
| 103 |
+
if self.candidate
|
| 104 |
+
else ()
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
@property
|
| 108 |
+
def review_flags(self):
|
| 109 |
+
# No fixture/gold knowledge or model success ever removes these flags.
|
| 110 |
+
ocr = self.candidate is not None and any(
|
| 111 |
+
"status-ocr" in t.normalization for p in self.candidate.pages for t in p.tags
|
| 112 |
+
)
|
| 113 |
+
return (
|
| 114 |
+
(
|
| 115 |
+
"recognition_incomplete_possible",
|
| 116 |
+
"model_scores_uncalibrated",
|
| 117 |
+
"review_complete_source_pages",
|
| 118 |
+
)
|
| 119 |
+
+ (("processing_blocked",) if self.candidate is None else self.candidate.review_reasons)
|
| 120 |
+
+ (
|
| 121 |
+
("legacy_configuration_unknown",)
|
| 122 |
+
if type(self.candidate) is LegacyReleaseCandidate
|
| 123 |
+
else ()
|
| 124 |
+
)
|
| 125 |
+
) + (("ocr_status_normalization",) if ocr else ())
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@dataclass(frozen=True)
|
| 129 |
+
class ClassifiedPageView:
|
| 130 |
+
sensitivity: Literal["D0", "D1", "D2"]
|
| 131 |
+
page_index: int
|
| 132 |
+
text: str = field(repr=False)
|
| 133 |
+
|
| 134 |
+
|
| 135 |
+
class PrivateReviewHost(HostSecurityAdapter, Protocol):
|
| 136 |
+
def review_binding(self, review_ref: str) -> ReviewBinding: ...
|
| 137 |
+
def private_environment(self, environment_id: str) -> bool: ...
|
| 138 |
+
def load_review_material(self, review_ref: str) -> PrivateReviewMaterial: ...
|
| 139 |
+
def read_classified_page(self, evidence_ref: str, page_index: int) -> bytes: ...
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class PrivateReviewBridge:
|
| 143 |
+
def __init__(self, host: PrivateReviewHost):
|
| 144 |
+
self.host = host
|
| 145 |
+
self.authorization = HostAuthorizationBridge(host)
|
| 146 |
+
|
| 147 |
+
def _binding(self, credential, review_ref):
|
| 148 |
+
if type(review_ref) is not str or not re.fullmatch(r"[a-zA-Z0-9_-]{1,128}", review_ref):
|
| 149 |
+
raise ValueError()
|
| 150 |
+
identity = self.authorization.identity(credential)
|
| 151 |
+
binding = ReviewBinding.model_validate(self.host.review_binding(review_ref))
|
| 152 |
+
if (
|
| 153 |
+
binding.review_ref != review_ref
|
| 154 |
+
or binding.revoked
|
| 155 |
+
or binding.expires_at <= time.time()
|
| 156 |
+
or self.host.private_environment(binding.environment_id) is not True
|
| 157 |
+
or self.host.current_acl(
|
| 158 |
+
identity, "anonymization:review", binding.sensitivity, binding.environment_id
|
| 159 |
+
)
|
| 160 |
+
is not True
|
| 161 |
+
):
|
| 162 |
+
raise ValueError()
|
| 163 |
+
return binding
|
| 164 |
+
|
| 165 |
+
def open(self, credential, review_ref):
|
| 166 |
+
try:
|
| 167 |
+
binding = self._binding(credential, review_ref)
|
| 168 |
+
material = PrivateReviewMaterial.model_validate(
|
| 169 |
+
self.host.load_review_material(review_ref)
|
| 170 |
+
)
|
| 171 |
+
if (
|
| 172 |
+
material.candidate is not None
|
| 173 |
+
and self.host.candidate_exists(material.candidate_digest) is not True
|
| 174 |
+
):
|
| 175 |
+
raise ValueError()
|
| 176 |
+
if material.binding != binding or self._binding(credential, review_ref) != binding:
|
| 177 |
+
raise ValueError()
|
| 178 |
+
return material
|
| 179 |
+
except Exception:
|
| 180 |
+
raise SafeError("private_review_access_denied") from None
|
| 181 |
+
|
| 182 |
+
def source_page(self, credential, review_ref, page_index):
|
| 183 |
+
try:
|
| 184 |
+
if type(page_index) is not int:
|
| 185 |
+
raise ValueError()
|
| 186 |
+
material = self.open(credential, review_ref)
|
| 187 |
+
source = next(p for p in material.sources if p.page_index == page_index)
|
| 188 |
+
data = self.host.read_classified_page(source.evidence_ref, page_index)
|
| 189 |
+
if (
|
| 190 |
+
not isinstance(data, bytes)
|
| 191 |
+
or len(data) > 8 * 1024 * 1024
|
| 192 |
+
or hashlib.sha256(data).hexdigest() != source.sha256
|
| 193 |
+
):
|
| 194 |
+
raise ValueError()
|
| 195 |
+
text = data.decode("utf-8", errors="strict")
|
| 196 |
+
if len(text) != source.characters or self.open(credential, review_ref) != material:
|
| 197 |
+
raise ValueError()
|
| 198 |
+
return ClassifiedPageView(material.binding.sensitivity, page_index, text)
|
| 199 |
+
except Exception:
|
| 200 |
+
raise SafeError("private_review_access_denied") from None
|
ocr/anonymization/private_transport.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Explicit local private delivery only. No URL client, redirects or fallback."""
|
| 2 |
+
|
| 3 |
+
import fcntl
|
| 4 |
+
import hashlib
|
| 5 |
+
import os
|
| 6 |
+
import stat
|
| 7 |
+
import uuid
|
| 8 |
+
from dataclasses import dataclass
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
from types import MappingProxyType
|
| 11 |
+
from typing import Literal
|
| 12 |
+
|
| 13 |
+
from pydantic import Field
|
| 14 |
+
|
| 15 |
+
from ocr.schemas import SafeError
|
| 16 |
+
|
| 17 |
+
from .schemas import Destination, StrictModel
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
class ReviewedExport(StrictModel):
|
| 21 |
+
schema_version: Literal["ocr-reviewed-export/v2"]
|
| 22 |
+
text: str
|
| 23 |
+
sensitivity: Literal["D0", "D1", "D2"]
|
| 24 |
+
anonymous: Literal[False]
|
| 25 |
+
release_review_required: Literal[False]
|
| 26 |
+
release_approved: Literal[True]
|
| 27 |
+
candidate_digest: str = Field(pattern=r"^[a-f0-9]{64}$")
|
| 28 |
+
destination_id: str = Field(pattern=r"^[a-zA-Z0-9:_-]{1,128}$")
|
| 29 |
+
destination_class: Literal["private"]
|
| 30 |
+
transmission_authorized: Literal[False]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
@dataclass(frozen=True)
|
| 34 |
+
class LocalDestination:
|
| 35 |
+
policy: Destination
|
| 36 |
+
root: Path
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class PrivateFileTransport:
|
| 40 |
+
def __init__(self, destinations, *, authorize_transfer):
|
| 41 |
+
if not callable(authorize_transfer):
|
| 42 |
+
raise SafeError("private_transport_not_configured")
|
| 43 |
+
self._authorize = authorize_transfer
|
| 44 |
+
registry = {}
|
| 45 |
+
for entry in destinations:
|
| 46 |
+
try:
|
| 47 |
+
root = Path(entry.root)
|
| 48 |
+
if (
|
| 49 |
+
not root.is_absolute()
|
| 50 |
+
or root.resolve() != root
|
| 51 |
+
or entry.policy.destination_class != "private"
|
| 52 |
+
):
|
| 53 |
+
raise ValueError()
|
| 54 |
+
fd = os.open(root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
| 55 |
+
try:
|
| 56 |
+
metadata = os.fstat(fd)
|
| 57 |
+
if metadata.st_uid != os.getuid() or metadata.st_mode & 0o077:
|
| 58 |
+
raise ValueError()
|
| 59 |
+
finally:
|
| 60 |
+
os.close(fd)
|
| 61 |
+
if entry.policy.destination_id in registry:
|
| 62 |
+
raise ValueError()
|
| 63 |
+
registry[entry.policy.destination_id] = (entry, metadata.st_dev, metadata.st_ino)
|
| 64 |
+
except Exception:
|
| 65 |
+
raise SafeError("private_destination_unavailable") from None
|
| 66 |
+
self._registry = MappingProxyType(registry)
|
| 67 |
+
|
| 68 |
+
def deliver(self, exported_json, *, credential, destination_id):
|
| 69 |
+
directory, temporary = None, None
|
| 70 |
+
try:
|
| 71 |
+
if (
|
| 72 |
+
destination_id not in self._registry
|
| 73 |
+
or not isinstance(exported_json, str)
|
| 74 |
+
or len(exported_json) > 8 * 1024 * 1024
|
| 75 |
+
):
|
| 76 |
+
raise ValueError()
|
| 77 |
+
payload = ReviewedExport.model_validate_json(exported_json).model_dump()
|
| 78 |
+
entry, device, inode = self._registry[destination_id]
|
| 79 |
+
if (
|
| 80 |
+
payload.get("schema_version") != "ocr-reviewed-export/v2"
|
| 81 |
+
or payload.get("release_approved") is not True
|
| 82 |
+
or payload.get("transmission_authorized") is not False
|
| 83 |
+
or payload.get("destination_id") != destination_id
|
| 84 |
+
or payload.get("destination_class") != "private"
|
| 85 |
+
or payload.get("sensitivity") not in entry.policy.allowed_sensitivities
|
| 86 |
+
):
|
| 87 |
+
raise ValueError()
|
| 88 |
+
data = exported_json.encode()
|
| 89 |
+
artifact_digest = hashlib.sha256(data).hexdigest()
|
| 90 |
+
if (
|
| 91 |
+
self._authorize(credential, artifact_digest, payload["sensitivity"], destination_id)
|
| 92 |
+
is not True
|
| 93 |
+
):
|
| 94 |
+
raise ValueError()
|
| 95 |
+
directory = os.open(entry.root, os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW)
|
| 96 |
+
fcntl.flock(directory, fcntl.LOCK_SH)
|
| 97 |
+
metadata = os.fstat(directory)
|
| 98 |
+
if (
|
| 99 |
+
not stat.S_ISDIR(metadata.st_mode)
|
| 100 |
+
or metadata.st_mode & 0o077
|
| 101 |
+
or metadata.st_uid != os.getuid()
|
| 102 |
+
or (metadata.st_dev, metadata.st_ino) != (device, inode)
|
| 103 |
+
):
|
| 104 |
+
raise ValueError()
|
| 105 |
+
temporary = ".private-transfer-" + uuid.uuid4().hex
|
| 106 |
+
fd = os.open(
|
| 107 |
+
temporary,
|
| 108 |
+
os.O_CREAT | os.O_EXCL | os.O_WRONLY | os.O_NOFOLLOW,
|
| 109 |
+
0o600,
|
| 110 |
+
dir_fd=directory,
|
| 111 |
+
)
|
| 112 |
+
with os.fdopen(fd, "wb") as stream:
|
| 113 |
+
if stream.write(data) != len(data):
|
| 114 |
+
raise ValueError()
|
| 115 |
+
stream.flush()
|
| 116 |
+
os.fsync(stream.fileno())
|
| 117 |
+
# Disk I/O may outlast a session/ACL/review/destination authorization.
|
| 118 |
+
# Recheck immediately before the atomic publication point. A real
|
| 119 |
+
# host must serialize revocation/publication if it needs stronger
|
| 120 |
+
# cross-store atomic semantics; this callback is not a transaction.
|
| 121 |
+
metadata = os.fstat(directory)
|
| 122 |
+
if (
|
| 123 |
+
metadata.st_mode & 0o077
|
| 124 |
+
or metadata.st_uid != os.getuid()
|
| 125 |
+
or self._authorize(
|
| 126 |
+
credential, artifact_digest, payload["sensitivity"], destination_id
|
| 127 |
+
)
|
| 128 |
+
is not True
|
| 129 |
+
):
|
| 130 |
+
raise ValueError()
|
| 131 |
+
os.link(
|
| 132 |
+
temporary,
|
| 133 |
+
artifact_digest + ".json",
|
| 134 |
+
src_dir_fd=directory,
|
| 135 |
+
dst_dir_fd=directory,
|
| 136 |
+
follow_symlinks=False,
|
| 137 |
+
)
|
| 138 |
+
os.fsync(directory)
|
| 139 |
+
return dict(
|
| 140 |
+
destination_id=destination_id,
|
| 141 |
+
artifact_digest=artifact_digest,
|
| 142 |
+
sensitivity=payload["sensitivity"],
|
| 143 |
+
transport="local-private-file",
|
| 144 |
+
automated_release_eligible=False,
|
| 145 |
+
)
|
| 146 |
+
except Exception:
|
| 147 |
+
raise SafeError("private_transfer_denied_or_failed") from None
|
| 148 |
+
finally:
|
| 149 |
+
if directory is not None:
|
| 150 |
+
if temporary is not None:
|
| 151 |
+
try:
|
| 152 |
+
os.unlink(temporary, dir_fd=directory)
|
| 153 |
+
except OSError:
|
| 154 |
+
pass
|
| 155 |
+
os.close(directory)
|
ocr/anonymization/provenance.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Canonical configuration commitment; no raw terms/prompts in candidate metadata."""
|
| 2 |
+
|
| 3 |
+
import hashlib
|
| 4 |
+
import json
|
| 5 |
+
import math
|
| 6 |
+
|
| 7 |
+
from ocr.schemas import SafeError
|
| 8 |
+
|
| 9 |
+
from .normalization import LINE_WRAP
|
| 10 |
+
from .replacement import REPLACEMENT_POLICY_VERSION
|
| 11 |
+
from .schemas import LABELS, RecognitionProvenance
|
| 12 |
+
from .terminology import TERMINOLOGY_REVISION
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def provenance(recognizer, terminology, status_policy, placeholders):
|
| 16 |
+
try:
|
| 17 |
+
prompts = dict(recognizer.prompts)
|
| 18 |
+
threshold = recognizer.threshold
|
| 19 |
+
if (
|
| 20 |
+
set(prompts) != set(LABELS)
|
| 21 |
+
or any(
|
| 22 |
+
not isinstance(v, str) or not 1 <= len(v) <= 80 or not v.strip()
|
| 23 |
+
for v in prompts.values()
|
| 24 |
+
)
|
| 25 |
+
or len(set(prompts.values())) != len(LABELS)
|
| 26 |
+
or type(threshold) not in (float, int)
|
| 27 |
+
or not math.isfinite(threshold)
|
| 28 |
+
or not 0 < threshold < 1
|
| 29 |
+
):
|
| 30 |
+
raise ValueError()
|
| 31 |
+
replacements = {k: f"[{k}]" for k in LABELS} if placeholders is None else dict(placeholders)
|
| 32 |
+
# replace() validates the complete placeholder syntax independently.
|
| 33 |
+
payload = dict(
|
| 34 |
+
schema_version="recognition-configuration/v1",
|
| 35 |
+
model_revision=recognizer.revision,
|
| 36 |
+
threshold=float(threshold),
|
| 37 |
+
prompts=prompts,
|
| 38 |
+
terminology=dict(version=TERMINOLOGY_REVISION, terms=sorted(terminology.terms)),
|
| 39 |
+
status=status_policy.configuration(),
|
| 40 |
+
replacement=dict(version=REPLACEMENT_POLICY_VERSION, placeholders=replacements),
|
| 41 |
+
normalization=dict(
|
| 42 |
+
line_wrap=LINE_WRAP.pattern,
|
| 43 |
+
unicode_casefold=True,
|
| 44 |
+
offset_mapping="source-codepoint-v1",
|
| 45 |
+
),
|
| 46 |
+
contact_policy="contact-patterns-v1",
|
| 47 |
+
)
|
| 48 |
+
encoded = json.dumps(
|
| 49 |
+
payload, sort_keys=True, separators=(",", ":"), ensure_ascii=True, allow_nan=False
|
| 50 |
+
).encode()
|
| 51 |
+
return RecognitionProvenance(
|
| 52 |
+
model_revision=recognizer.revision,
|
| 53 |
+
configuration_digest=hashlib.sha256(encoded).hexdigest(),
|
| 54 |
+
)
|
| 55 |
+
except Exception:
|
| 56 |
+
raise SafeError("invalid_recognition_configuration") from None
|
ocr/anonymization/qualify03d.py
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""First-result evaluation of the frozen Gate03d synthetic holdout.
|
| 2 |
+
|
| 3 |
+
No prompt/threshold search, no remote access, no release authorization. Corpus,
|
| 4 |
+
configuration and implementation hashes are checked before model initialization.
|
| 5 |
+
An exclusive output is mandatory; the report is saved before acceptance is tested.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
import hashlib
|
| 9 |
+
import importlib.metadata
|
| 10 |
+
import json
|
| 11 |
+
import resource
|
| 12 |
+
import statistics
|
| 13 |
+
import subprocess
|
| 14 |
+
import sys
|
| 15 |
+
import time
|
| 16 |
+
from datetime import datetime, timezone
|
| 17 |
+
from pathlib import Path
|
| 18 |
+
|
| 19 |
+
from .atomic_output import write_candidate
|
| 20 |
+
from .detectors import detect
|
| 21 |
+
from .evaluate import score_cases
|
| 22 |
+
from .model import MODEL_REVISION
|
| 23 |
+
from .recognizer import DEFAULT_PROMPTS, GLiNERRecognizer
|
| 24 |
+
from .terminology import DEFAULT_TERMS
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def verify_freeze():
|
| 28 |
+
root = Path(__file__).parent / "fixtures/holdout03d"
|
| 29 |
+
freeze = json.loads((root / "freeze.json").read_bytes())
|
| 30 |
+
data = (root / "cases.json").read_bytes()
|
| 31 |
+
repo = Path(__file__).parents[2]
|
| 32 |
+
if (
|
| 33 |
+
hashlib.sha256(data).hexdigest() != freeze["sha256"]
|
| 34 |
+
or freeze["model_revision"] != MODEL_REVISION
|
| 35 |
+
or freeze["prompts"] != DEFAULT_PROMPTS
|
| 36 |
+
or freeze["threshold"] != 0.5
|
| 37 |
+
or freeze["religion_terms"] != list(DEFAULT_TERMS)
|
| 38 |
+
or any(
|
| 39 |
+
hashlib.sha256((repo / name).read_bytes()).hexdigest() != digest
|
| 40 |
+
for name, digest in freeze["implementation_sha256"].items()
|
| 41 |
+
)
|
| 42 |
+
):
|
| 43 |
+
raise ValueError("holdout_integrity_or_configuration_changed")
|
| 44 |
+
cases = json.loads(data)
|
| 45 |
+
if len(cases) != freeze["cases"] or len({c["id"] for c in cases}) != len(cases):
|
| 46 |
+
raise ValueError("holdout_integrity_failed")
|
| 47 |
+
return root, freeze, cases
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def run(cache, output):
|
| 51 |
+
if Path(output).exists():
|
| 52 |
+
raise FileExistsError("first_result_already_exists")
|
| 53 |
+
root, freeze, cases = verify_freeze()
|
| 54 |
+
denied = []
|
| 55 |
+
|
| 56 |
+
def offline(event, args):
|
| 57 |
+
if event in ("socket.connect", "socket.bind", "socket.getaddrinfo"):
|
| 58 |
+
denied.append(event)
|
| 59 |
+
raise PermissionError("qualification_network_disabled")
|
| 60 |
+
|
| 61 |
+
sys.addaudithook(offline)
|
| 62 |
+
started = datetime.now(timezone.utc).isoformat()
|
| 63 |
+
start = time.perf_counter()
|
| 64 |
+
model = GLiNERRecognizer(cache)
|
| 65 |
+
load_ms = (time.perf_counter() - start) * 1000
|
| 66 |
+
model.recognize("A synthetic warmup sentence.")
|
| 67 |
+
details = []
|
| 68 |
+
evaluation = score_cases(model, cases, case_reports=details)
|
| 69 |
+
# Cost of the deterministic layer separately, with no extra model inference.
|
| 70 |
+
rule_ms = []
|
| 71 |
+
for case in cases:
|
| 72 |
+
start = time.perf_counter()
|
| 73 |
+
detect(case["text"])
|
| 74 |
+
rule_ms.append((time.perf_counter() - start) * 1000)
|
| 75 |
+
report = dict(
|
| 76 |
+
schema_version="gate03d-independent-first-selected/v1",
|
| 77 |
+
started_utc=started,
|
| 78 |
+
finished_utc=datetime.now(timezone.utc).isoformat(),
|
| 79 |
+
code_sha=subprocess.check_output(["git", "rev-parse", "HEAD"], text=True).strip(),
|
| 80 |
+
freeze_commit=subprocess.check_output(
|
| 81 |
+
["git", "log", "-1", "--format=%H", "--", str(root / "cases.json")], text=True
|
| 82 |
+
).strip(),
|
| 83 |
+
corpus_sha256=freeze["sha256"],
|
| 84 |
+
implementation_sha256=freeze["implementation_sha256"],
|
| 85 |
+
model_revision=MODEL_REVISION,
|
| 86 |
+
prompts=model.prompts,
|
| 87 |
+
threshold=model.threshold,
|
| 88 |
+
religion_terms=list(DEFAULT_TERMS),
|
| 89 |
+
runtime_versions={
|
| 90 |
+
k: importlib.metadata.version(k)
|
| 91 |
+
for k in ("gliner", "torch", "transformers", "numpy", "pydantic")
|
| 92 |
+
},
|
| 93 |
+
loading_ms=load_ms,
|
| 94 |
+
evaluation=evaluation,
|
| 95 |
+
per_case=details,
|
| 96 |
+
deterministic_latency_ms=dict(median=statistics.median(rule_ms), max=max(rule_ms)),
|
| 97 |
+
peak_rss_mib=resource.getrusage(resource.RUSAGE_SELF).ru_maxrss / 1024,
|
| 98 |
+
denied_network_calls=denied,
|
| 99 |
+
automated_release_eligible=False,
|
| 100 |
+
)
|
| 101 |
+
write_candidate(output, (json.dumps(report, ensure_ascii=False, indent=2) + "\n").encode())
|
| 102 |
+
return report
|
| 103 |
+
|
| 104 |
+
|
| 105 |
+
if __name__ == "__main__":
|
| 106 |
+
import argparse
|
| 107 |
+
|
| 108 |
+
parser = argparse.ArgumentParser(description="Evaluate frozen Gate03d holdout once")
|
| 109 |
+
parser.add_argument("--cache", required=True)
|
| 110 |
+
parser.add_argument("--output", required=True)
|
| 111 |
+
args = parser.parse_args()
|
| 112 |
+
result = run(args.cache, args.output)
|
| 113 |
+
print(
|
| 114 |
+
json.dumps(
|
| 115 |
+
dict(
|
| 116 |
+
qualification_passed=result["evaluation"]["qualification_passed"],
|
| 117 |
+
automated_release_eligible=False,
|
| 118 |
+
)
|
| 119 |
+
)
|
| 120 |
+
)
|
| 121 |
+
raise SystemExit(0 if result["evaluation"]["qualification_passed"] else 1)
|
ocr/anonymization/recognizer.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Local native GLiNER runtime. Character offsets always refer to untouched text."""
|
| 2 |
+
|
| 3 |
+
import math
|
| 4 |
+
import re
|
| 5 |
+
import threading
|
| 6 |
+
|
| 7 |
+
from ocr.schemas import SafeError
|
| 8 |
+
|
| 9 |
+
from .model import MODEL_REVISION, verify_cache
|
| 10 |
+
from .normalization import dehyphenate
|
| 11 |
+
from .replacement import validate_entities
|
| 12 |
+
from .schemas import LABELS, Entity
|
| 13 |
+
|
| 14 |
+
DEFAULT_PROMPTS = {
|
| 15 |
+
"PERSON": "person",
|
| 16 |
+
# Privacy category includes geopolitical places, matching the existing
|
| 17 |
+
# qualification taxonomy (cities, states and named geographic locations).
|
| 18 |
+
"CITY": "location",
|
| 19 |
+
"RELIGION": "religion",
|
| 20 |
+
"EMAIL": "email address",
|
| 21 |
+
"PHONE": "phone number",
|
| 22 |
+
"ORGANIZATION": "organization",
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
class GLiNERRecognizer:
|
| 27 |
+
local = True
|
| 28 |
+
revision = MODEL_REVISION
|
| 29 |
+
categories = LABELS
|
| 30 |
+
|
| 31 |
+
def __init__(self, cache, *, device="cpu", threshold=0.5, prompts=None, threads=2):
|
| 32 |
+
self._lock = threading.Lock()
|
| 33 |
+
try:
|
| 34 |
+
self.prompts = dict(DEFAULT_PROMPTS if prompts is None else prompts)
|
| 35 |
+
except Exception:
|
| 36 |
+
raise SafeError("unsupported_required_categories") from None
|
| 37 |
+
if set(self.prompts) != set(LABELS) or (
|
| 38 |
+
any(
|
| 39 |
+
not isinstance(v, str) or not v.strip() or len(v) > 80
|
| 40 |
+
for v in self.prompts.values()
|
| 41 |
+
)
|
| 42 |
+
or len(set(self.prompts.values())) != len(LABELS)
|
| 43 |
+
):
|
| 44 |
+
raise SafeError("unsupported_required_categories")
|
| 45 |
+
if (
|
| 46 |
+
type(threshold) not in (int, float)
|
| 47 |
+
or not math.isfinite(threshold)
|
| 48 |
+
or not 0 < threshold < 1
|
| 49 |
+
):
|
| 50 |
+
raise SafeError("invalid_recognizer_configuration")
|
| 51 |
+
if device not in ("cpu", "cuda") or type(threads) is not int or not 1 <= threads <= 16:
|
| 52 |
+
raise SafeError("invalid_recognizer_configuration")
|
| 53 |
+
self.threshold = threshold
|
| 54 |
+
try:
|
| 55 |
+
path = verify_cache(cache)
|
| 56 |
+
import torch
|
| 57 |
+
from gliner import GLiNER
|
| 58 |
+
|
| 59 |
+
torch.set_num_threads(threads)
|
| 60 |
+
self.model = GLiNER.from_pretrained(
|
| 61 |
+
str(path),
|
| 62 |
+
local_files_only=True,
|
| 63 |
+
map_location=device,
|
| 64 |
+
_attn_implementation="eager",
|
| 65 |
+
strict=True,
|
| 66 |
+
)
|
| 67 |
+
self.model.eval()
|
| 68 |
+
except Exception:
|
| 69 |
+
raise SafeError("anonymization_model_unavailable") from None
|
| 70 |
+
|
| 71 |
+
def recognize(self, text):
|
| 72 |
+
if not isinstance(text, str) or len(text) > 2_000_000:
|
| 73 |
+
raise SafeError("anonymization_text_limit")
|
| 74 |
+
normalized, positions = dehyphenate(text)
|
| 75 |
+
predictions = self._recognize_view(normalized)
|
| 76 |
+
if normalized == text:
|
| 77 |
+
return predictions
|
| 78 |
+
mapped = []
|
| 79 |
+
for entity in predictions:
|
| 80 |
+
start = positions[entity.start]
|
| 81 |
+
end = positions[entity.end - 1] + 1
|
| 82 |
+
mapped.append(
|
| 83 |
+
entity.model_copy(
|
| 84 |
+
update={
|
| 85 |
+
"start": start,
|
| 86 |
+
"end": end,
|
| 87 |
+
"text": text[start:end],
|
| 88 |
+
"normalization": "dehyphenation-v1",
|
| 89 |
+
}
|
| 90 |
+
)
|
| 91 |
+
)
|
| 92 |
+
return validate_entities(text, mapped)
|
| 93 |
+
|
| 94 |
+
def _recognize_view(self, text):
|
| 95 |
+
if not isinstance(text, str) or len(text) > 2_000_000:
|
| 96 |
+
raise SafeError("anonymization_text_limit")
|
| 97 |
+
# Overlapping windows cover all original characters without normalizing OCR noise.
|
| 98 |
+
# Hard bounds prevent silent tokenizer/model truncation on unusual long tokens.
|
| 99 |
+
words = list(re.finditer(r"\S+", text))
|
| 100 |
+
if any(len(w[0]) > 256 for w in words):
|
| 101 |
+
raise SafeError("anonymization_token_limit")
|
| 102 |
+
entities = []
|
| 103 |
+
reverse = {v: k for k, v in self.prompts.items()}
|
| 104 |
+
try:
|
| 105 |
+
import torch
|
| 106 |
+
|
| 107 |
+
with self._lock, torch.inference_mode():
|
| 108 |
+
for start in range(0, len(words), 96):
|
| 109 |
+
window = words[start : start + 128]
|
| 110 |
+
left, right = window[0].start(), window[-1].end()
|
| 111 |
+
chunk = text[left:right]
|
| 112 |
+
encoded = self.model.data_processor.transformer_tokenizer(chunk)["input_ids"]
|
| 113 |
+
if len(encoded) > 1536:
|
| 114 |
+
raise SafeError("anonymization_token_limit")
|
| 115 |
+
raw = self.model.predict_entities(
|
| 116 |
+
chunk,
|
| 117 |
+
list(self.prompts.values()),
|
| 118 |
+
threshold=self.threshold,
|
| 119 |
+
flat_ner=True,
|
| 120 |
+
)
|
| 121 |
+
if not isinstance(raw, list):
|
| 122 |
+
raise ValueError()
|
| 123 |
+
for item in raw:
|
| 124 |
+
if not isinstance(item, dict) or set(item) != {
|
| 125 |
+
"start",
|
| 126 |
+
"end",
|
| 127 |
+
"text",
|
| 128 |
+
"label",
|
| 129 |
+
"score",
|
| 130 |
+
}:
|
| 131 |
+
raise ValueError()
|
| 132 |
+
# Validate runtime output before adding the original window offset.
|
| 133 |
+
local = Entity(
|
| 134 |
+
start=item["start"],
|
| 135 |
+
end=item["end"],
|
| 136 |
+
text=item["text"],
|
| 137 |
+
label=reverse[item["label"]],
|
| 138 |
+
score=item["score"],
|
| 139 |
+
source="multilingual_ner",
|
| 140 |
+
model_revision=self.revision,
|
| 141 |
+
)
|
| 142 |
+
validate_entities(chunk, [local])
|
| 143 |
+
entities.append(
|
| 144 |
+
local.model_copy(
|
| 145 |
+
update={"start": local.start + left, "end": local.end + left}
|
| 146 |
+
)
|
| 147 |
+
)
|
| 148 |
+
return validate_entities(text, entities)
|
| 149 |
+
except SafeError:
|
| 150 |
+
raise
|
| 151 |
+
except Exception:
|
| 152 |
+
raise SafeError("anonymization_inference_failed") from None
|