Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions .github/workflows/ocr-regression-degraded.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: OCR Regression Test (Degraded)

on:
push:
paths:
- 'app/ai-service/services/ocr.py'
- 'app/ai-service/services/preprocessing.py'
- 'app/ai-service/regression_harness/dataset/degraded/**'
- 'app/ai-service/regression_harness/**'
branches: [ main, develop ]
pull_request:
paths:
- 'app/ai-service/services/ocr.py'
- 'app/ai-service/services/preprocessing.py'
- 'app/ai-service/regression_harness/dataset/degraded/**'
- 'app/ai-service/regression_harness/**'
branches: [ main ]
workflow_dispatch:

jobs:
regression-degraded:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'

- name: Install System Dependencies
run: |
sudo apt-get update
sudo apt-get install -y tesseract-ocr libtesseract-dev

- name: Install Python Dependencies
working-directory: ./app/ai-service
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install Pillow pytesseract

- name: Run OCR Regression Harness (degraded)
working-directory: ./app/ai-service
run: |
set -euo pipefail
export PYTHONPATH=$PYTHONPATH:.
# We require:
# - >= 90% of samples pass the exact-field matching gate
# - >= 60% reported accuracy
python regression_harness/cli.py \
--dataset regression_harness/dataset/degraded/ground_truth.json \
--output ocr_degraded_report.json \
--threshold 0.8

python - <<'PY'
import json
with open('ocr_degraded_report.json','r') as f:
report = json.load(f)
summary = report.get('summary', {})
total = summary.get('total', 0)
passed = summary.get('passed', 0)
accuracy = float(summary.get('accuracy', 0.0))
pass_ratio = (passed/total) if total else 0.0
print('Degraded regression summary:', {'total': total, 'passed': passed, 'pass_ratio': pass_ratio, 'accuracy': accuracy})
if pass_ratio < 0.9:
raise SystemExit(f'FAILED: pass_ratio {pass_ratio:.3f} < 0.9')
if accuracy < 0.6:
raise SystemExit(f'FAILED: accuracy {accuracy:.3f}% < 0.6%')
PY

- name: Upload Regression Report
if: always()
uses: actions/upload-artifact@v4
with:
name: ocr-regression-degraded-report
path: app/ai-service/ocr_degraded_report.json
retention-days: 14

8 changes: 8 additions & 0 deletions app/ai-service/metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@
CIRCUIT_STATE_HALF_OPEN = 1
CIRCUIT_STATE_OPEN = 2

# Human-readable labels for the encoded gauge values. This keeps the metric
# values and the Grafana/operational mapping aligned with the same constants.
CIRCUIT_STATE_LABELS = {
CIRCUIT_STATE_CLOSED: 'CLOSED',
CIRCUIT_STATE_HALF_OPEN: 'HALF_OPEN',
CIRCUIT_STATE_OPEN: 'OPEN',
}


def set_circuit_state(breaker_name: str, state_value: int) -> None:
"""Helper to update the circuit-state gauge from anywhere."""
Expand Down
21 changes: 18 additions & 3 deletions app/ai-service/regression_harness/cli.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import os
import sys
import json
import argparse
from typing import List
# Ensure this script can be executed directly regardless of CWD / PYTHONPATH.
# When running as: python app/ai-service/regression_harness/cli.py
# we want to treat `app/ai-service` as the import root.
import_path_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
if import_path_root not in sys.path:
sys.path.insert(0, import_path_root)

from regression_harness.models import EvaluationSample, BoundingBox
from regression_harness.evaluator import OCREvaluator

Expand Down Expand Up @@ -54,11 +62,13 @@ def main():
parser.add_argument("--dataset", default="regression_harness/dataset/ground_truth.json", help="Path to ground truth JSON")
parser.add_argument("--output", help="Path to save JSON report")
parser.add_argument("--threshold", type=float, default=0.8, help="Confidence threshold")

parser.add_argument("--min_pass_ratio", type=float, default=None, help="If set, CI can enforce minimum pass ratio (0-1).")

args = parser.parse_args()

base_dir = os.path.dirname(os.path.abspath(__file__))
# Adjust base_dir if it's currently inside regression_harness
# Ensure args.dataset paths work regardless of where this script is run from.
# Default expects to be relative to app/ai-service.
if base_dir.endswith("regression_harness"):
base_dir = os.path.dirname(base_dir)
# We want base_dir to be app/ai-service
Expand All @@ -81,7 +91,12 @@ def main():
json.dump(report.to_dict(), f, indent=2)
print(f"Report saved to {args.output}")

if report.failed_samples > 0:
if args.min_pass_ratio is not None:
pass_ratio = (report.passed_samples / report.total_samples) if report.total_samples > 0 else 0
print(f"Min pass ratio requirement: {args.min_pass_ratio:.2f}, actual: {pass_ratio:.2f}")
if pass_ratio < args.min_pass_ratio:
exit(1)
elif report.failed_samples > 0:
exit(1)

if __name__ == "__main__":
Expand Down
12 changes: 12 additions & 0 deletions app/ai-service/regression_harness/dataset/degraded/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Degraded regression dataset

This dataset contains intentionally degraded versions of the golden `sample_001.png` fixture to validate OCR robustness against:
- 90° rotations
- low contrast
- blur
- low resolution
- a faint watermark overlay

Images live in `documents/`.
Ground truth lives in `ground_truth.json`.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading