204000aabc
Resolve 3 merge conflicts and renumber the automation_hooks migration to follow main's migration chain (036_add_document_translation_fields). Conflicts resolved: - app/api/__init__.py: add automation_router alongside main's new routers - app/utils/settings_service.py: add automation_hooks_enabled alongside compliance_enabled - tests/conftest.py: add AutomationHook alongside AuditLog/ComplianceTemplate imports Migration renumbered: - 027_add_automation_hooks → 037_add_automation_hooks - down_revision: 026_add_scheduled_jobs → 036_add_document_translation_fields Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
142 lines
5.2 KiB
Python
142 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Celery task to translate extracted document text into the default target language.
|
|
|
|
This task is triggered after metadata extraction when the detected document
|
|
language differs from the user's (or system) default document language. The
|
|
translated text is persisted in ``FileRecord.default_language_text`` so that
|
|
users can always read a reference copy in their preferred language.
|
|
|
|
Other ad-hoc translations are generated on the fly via the ``/api/files/{id}/translate``
|
|
endpoint and are NOT persisted.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from app.celery_app import celery
|
|
from app.config import settings
|
|
from app.database import SessionLocal
|
|
from app.models import FileRecord, UserProfile
|
|
from app.tasks.retry_config import BaseTaskWithRetry
|
|
from app.utils import log_task_progress
|
|
from app.utils.ai_provider import get_ai_provider
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _resolve_default_language(owner_id: str | None) -> str:
|
|
"""Return the default document language for the given owner.
|
|
|
|
Resolution order:
|
|
1. ``UserProfile.default_document_language`` (per-user override)
|
|
2. ``settings.default_document_language`` (global setting)
|
|
"""
|
|
if owner_id:
|
|
with SessionLocal() as db:
|
|
profile = db.query(UserProfile).filter_by(user_id=owner_id).first()
|
|
if profile and profile.default_document_language:
|
|
return profile.default_document_language
|
|
return settings.default_document_language
|
|
|
|
|
|
@celery.task(base=BaseTaskWithRetry, bind=True)
|
|
def translate_to_default_language(
|
|
self,
|
|
file_id: int,
|
|
extracted_text: str,
|
|
detected_language: str,
|
|
owner_id: str | None = None,
|
|
) -> dict:
|
|
"""Translate *extracted_text* into the default document language and persist the result.
|
|
|
|
Args:
|
|
file_id: Primary key of the :class:`FileRecord`.
|
|
extracted_text: The OCR / refined text in the document's original language.
|
|
detected_language: ISO 639-1 code of the document's detected language.
|
|
owner_id: Owner identifier used to resolve per-user language preference.
|
|
|
|
Returns:
|
|
A dict with ``status``, ``target_language``, and the translated text length.
|
|
"""
|
|
task_id = self.request.id
|
|
target_language = _resolve_default_language(owner_id)
|
|
|
|
# Nothing to do when the document is already in the target language.
|
|
if detected_language == target_language:
|
|
logger.info(
|
|
f"[{task_id}] Document {file_id} already in target language '{target_language}', skipping translation"
|
|
)
|
|
log_task_progress(
|
|
task_id,
|
|
"translate_to_default_language",
|
|
"skipped",
|
|
f"Document already in {target_language}",
|
|
file_id=file_id,
|
|
)
|
|
return {"status": "skipped", "reason": "already_in_target_language"}
|
|
|
|
logger.info(f"[{task_id}] Translating document {file_id} from '{detected_language}' to '{target_language}'")
|
|
log_task_progress(
|
|
task_id,
|
|
"translate_to_default_language",
|
|
"in_progress",
|
|
f"Translating from {detected_language} to {target_language}",
|
|
file_id=file_id,
|
|
)
|
|
|
|
try:
|
|
provider = get_ai_provider()
|
|
model = settings.ai_model or settings.openai_model
|
|
translated_text = provider.chat_completion(
|
|
messages=[
|
|
{
|
|
"role": "system",
|
|
"content": (
|
|
f"You are a professional translator. Translate the following text "
|
|
f"from {detected_language} to {target_language}. "
|
|
f"Preserve the original formatting, paragraph structure, and meaning. "
|
|
f"Do not add any commentary or explanation — output ONLY the translated text."
|
|
),
|
|
},
|
|
{"role": "user", "content": extracted_text},
|
|
],
|
|
model=model,
|
|
temperature=0.3,
|
|
)
|
|
|
|
# Persist the translation.
|
|
with SessionLocal() as db:
|
|
record = db.query(FileRecord).filter_by(id=file_id).first()
|
|
if record:
|
|
record.default_language_text = translated_text
|
|
record.default_language_code = target_language
|
|
record.detected_language = detected_language
|
|
db.commit()
|
|
logger.info(
|
|
f"[{task_id}] Stored default-language translation ({len(translated_text)} chars) for file {file_id}"
|
|
)
|
|
|
|
log_task_progress(
|
|
task_id,
|
|
"translate_to_default_language",
|
|
"success",
|
|
f"Translated {len(extracted_text)} → {len(translated_text)} chars ({detected_language} → {target_language})",
|
|
file_id=file_id,
|
|
)
|
|
|
|
return {
|
|
"status": "success",
|
|
"target_language": target_language,
|
|
"translated_length": len(translated_text),
|
|
}
|
|
|
|
except Exception as exc:
|
|
logger.exception(f"[{task_id}] Translation failed for file {file_id}: {exc}")
|
|
log_task_progress(
|
|
task_id,
|
|
"translate_to_default_language",
|
|
"failure",
|
|
f"Exception: {exc}",
|
|
file_id=file_id,
|
|
)
|
|
raise
|