Skip to content
Merged
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
31 changes: 31 additions & 0 deletions .github/workflows/js-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Run JS Tests

on:
push:
branches:
- main
paths:
- '**/*.js'
- '**/*.mjs'
pull_request:
branches:
- main
paths:
- '**/*.js'
- '**/*.mjs'

jobs:
run-js-tests:
runs-on: ubuntu-latest

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

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 'latest'

- name: Run JS tests
run: npm run test:js
1 change: 1 addition & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ services:
- ./detection_service:/tests/tests/detection_service
- ./web_service:/tests/tests/web_service
- ./ocr_service:/tests/tests/ocr_service
- ./file_naming_service:/tests/tests/file_naming_service

redis:
image: redis:latest
Expand Down
33 changes: 29 additions & 4 deletions file_naming_service/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,22 @@
RABBITQUEUE = "file_naming_queue"


def get_latest_file_naming_status(item: ProcessItem):
status_name = execute_query(
"SELECT file_naming_status FROM file_naming_jobs WHERE id = ?",
(item.file_naming_db_id,),
return_scalar=True
)
if status_name:
try:
return FileNamingStatus[status_name]
except KeyError:
logger.warning(f"Unknown file naming status '{status_name}' for item {item.filename}")
return item.file_naming_status


def callback(ch, method, properties, body):
item = None
try:
item: ProcessItem = pickle.loads(body)

Expand All @@ -42,6 +57,7 @@ def callback(ch, method, properties, body):

if not openai_enabled and not ollama_enabled:
logger.error("Neither OpenAI nor Ollama is enabled. Please enable one of them in the settings.")
item.file_naming_status = FileNamingStatus.PROCESSING
execute_query('UPDATE file_naming_jobs SET file_naming_status = ? WHERE id = ?', (FileNamingStatus.PROCESSING.name, item.file_naming_db_id))

method_setting = settings.file_naming.method
Expand All @@ -65,7 +81,12 @@ def callback(ch, method, properties, body):
execute_query("UPDATE file_naming_jobs SET file_naming_status = ?, error_description = ?, finished = DATETIME('now', 'localtime') WHERE id = ?", (FileNamingStatus.FAILED.name, "OCR file does not exist", item.file_naming_db_id))
except TypeError as e:
logger.error(f"Received object is not a ProcessItem: {e}. Skipping.")
execute_query("UPDATE file_naming_jobs SET file_naming_status = ?, error_description = ?, finished = DATETIME('now', 'localtime') WHERE id = ?", (FileNamingStatus.FAILED.name, str(e), item.file_naming_db_id))
item_file_naming_db_id = getattr(item, "file_naming_db_id", None)
if item_file_naming_db_id:
execute_query(
"UPDATE file_naming_jobs SET file_naming_status = ?, error_description = ?, finished = DATETIME('now', 'localtime') WHERE id = ?",
(FileNamingStatus.FAILED.name, str(e), item_file_naming_db_id)
)
return
except Exception as e:
logger.exception(f"Failed processing {item.filename}.")
Expand All @@ -77,7 +98,10 @@ def callback(ch, method, properties, body):
logger.error("Connection lost while acknowledging message. Reconnecting...")
connection, channel = connect_rabbitmq([RABBITQUEUE], heartbeat=120)
channel.basic_ack(delivery_tag=method.delivery_tag)
if item:
if isinstance(item, ProcessItem):
item_file_naming_db_id = getattr(item, "file_naming_db_id", None)
if item_file_naming_db_id:
item.file_naming_status = get_latest_file_naming_status(item)
item.status = ProcessStatus.SYNC_PENDING
update_scanneddata_database(item, {"file_status": item.status.value})
forward_to_rabbitmq("upload_queue", item)
Expand All @@ -101,5 +125,6 @@ def start_consuming_with_reconnect():
time.sleep(5)


# Start the consumer with reconnect logic
start_consuming_with_reconnect()
if __name__ == "__main__":
# Start the consumer with reconnect logic
start_consuming_with_reconnect()
34 changes: 29 additions & 5 deletions ocr_service/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from scansynclib.logging import logger
from scansynclib.ProcessItem import ProcessItem, ProcessStatus, OCRStatus
from scansynclib.sqlite_wrapper import update_scanneddata_database
from scansynclib.sqlite_wrapper import execute_query, update_scanneddata_database
from scansynclib.helpers import connect_rabbitmq, forward_to_rabbitmq, extract_text
import pickle
import ocrmypdf
Expand Down Expand Up @@ -30,53 +30,77 @@ def callback(ch, method, properties, body):

def start_processing(item: ProcessItem):
item.status = ProcessStatus.OCR
item.ocr_status = OCRStatus.PROCESSING
item.ocr_db_id = execute_query(
'INSERT INTO ocr_jobs (scanneddata_id, ocr_status) VALUES (?, ?)',
(item.db_id, OCRStatus.PROCESSING.name),
return_last_id=True
)
update_scanneddata_database(item, {"file_status": item.status.value})
item.time_ocr_started = datetime.now()

logger.info(f"Processing file with OCR: {item.filename}")
ocr_error = None
result = None

try:
result = ocrmypdf.ocr(item.local_file_path, item.ocr_file, output_type='pdfa', skip_text=True, rotate_pages=True, jpg_quality=80, png_quality=80, optimize=2, language=["eng", "deu"], tesseract_timeout=120)
logger.debug(f"OCR exited with code {result}")

if result != 0:
logger.error(f"OCR exited with code {result}")
item.ocr_status = OCRStatus.FAILED
ocr_error = f"OCR exited with code {result}"
else:
logger.info(f"OCR processing completed: {item.filename}")

# Verify that the OCR file actually contains text
if os.path.exists(item.ocr_file):
extracted_text = (extract_text(item.ocr_file, max_pages=2, max_chars=2048) or "").strip()
extracted_text = (extract_text(item.ocr_file, max_pages=5, max_chars=2048) or "").strip()
if extracted_text:
logger.info(f"OCR verification successful: extracted {len(extracted_text)} characters from {item.filename}")
item.ocr_status = OCRStatus.COMPLETED
else:
logger.warning(f"OCR verification failed: no text found in OCR output file {item.ocr_file}")
item.ocr_status = OCRStatus.FAILED
item.ocr_status = OCRStatus.NO_TEXT
else:
logger.error(f"OCR output file not found: {item.ocr_file}")
item.ocr_status = OCRStatus.OUTPUT_ERROR
except ocrmypdf.UnsupportedImageFormatError:
logger.error(f"Unsupported image format: {item.local_file_path}")
item.ocr_status = OCRStatus.UNSUPPORTED
ocr_error = "Unsupported image format"
except ocrmypdf.DpiError as dpiex:
logger.error(f"DPI error: {item.local_file_path} {dpiex}")
item.ocr_status = OCRStatus.DPI_ERROR
ocr_error = str(dpiex)
except ocrmypdf.InputFileError as inex:
logger.error(f"Input error: {item.local_file_path} {inex}")
item.ocr_status = OCRStatus.INPUT_ERROR
ocr_error = str(inex)
except ocrmypdf.OutputFileAccessError as outex:
logger.error(f"Output error: {item.local_file_path} {outex}")
item.ocr_status = OCRStatus.OUTPUT_ERROR
ocr_error = str(outex)
except ocrmypdf.MissingDependencyError:
logger.exception("Cannot process with OCR due to missing dependencies.")
item.ocr_status = OCRStatus.FAILED
ocr_error = "Missing OCR dependency"
except Exception as ex:
logger.exception(f"Failed processing {item.local_file_path} with OCR: {ex}")
item.ocr_status = OCRStatus.FAILED
ocr_error = str(ex)
finally:
item.time_ocr_finished = datetime.now()
if result is not None and result != 0:
item.ocr_status = OCRStatus.FAILED
if not ocr_error:
ocr_error = f"OCR exited with code {result}"
if item.ocr_db_id:
execute_query(
"UPDATE ocr_jobs SET ocr_status = ?, ocr_error = ?, finished = DATETIME('now', 'localtime') WHERE id = ?",
(item.ocr_status.name, ocr_error, item.ocr_db_id)
)
item.status = ProcessStatus.SYNC_PENDING

try:
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"test": "tests"
},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"test:js": "node --test \"tests/js/**/*.test.mjs\""
},
Comment on lines 10 to 13
"repository": {
"type": "git",
Expand Down
3 changes: 3 additions & 0 deletions run-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ set -e
COMPOSE_FILE="docker-compose.test.yml"
TEST_SERVICE_NAME="test_service"

echo "🧪 Running JS tests..."
npm run test:js

echo "🧪 Starting tests with Docker Compose..."

docker compose -f $COMPOSE_FILE up --build --abort-on-container-exit --exit-code-from $TEST_SERVICE_NAME
Expand Down
2 changes: 2 additions & 0 deletions scansynclib/scansynclib/ProcessItem.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ class OCRStatus(Enum):
DPI_ERROR: Image DPI is too low for accurate OCR.
INPUT_ERROR: Error reading input image/PDF.
OUTPUT_ERROR: Error writing OCR output file.
NO_TEXT: OCR completed but the output file contained no extractable text.
"""
UNKNOWN = 0
PENDING = 1
Expand All @@ -80,6 +81,7 @@ class OCRStatus(Enum):
DPI_ERROR = -4
INPUT_ERROR = -5
OUTPUT_ERROR = -6
NO_TEXT = -7


class FileNamingStatus(Enum):
Expand Down
Loading
Loading