Download apps/document_recognition/services/recognition_service.py from jun2024/fc: direct link, hf CLI and curl.
- Browser
- Download file 14.2 kB
-
https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/document_recognition/services/recognition_service.py
- Command line
-
hf download hf://spaces/jun2024/fc/apps/document_recognition/services/recognition_service.py
-
curl -L -o recognition_service.py https://huggingface.co/spaces/jun2024/fc/resolve/main/apps/document_recognition/services/recognition_service.py
14.2 kB
| """ | |
| 法院文书智能识别主服务 | |
| 协调各子服务完成文书识别流程。 | |
| Requirements: 4.5, 4.6, 4.7, 6.2, 7.1, 7.2, 7.3, 8.1, 8.2, 8.3, 8.4 | |
| """ | |
| import logging | |
| from datetime import date | |
| from typing import Any | |
| from django.utils.translation import gettext_lazy as _ | |
| from apps.core.exceptions import RecognitionTimeoutError, ServiceUnavailableError, ValidationException | |
| from .data_classes import BindingResult, DocumentType, RecognitionResponse, RecognitionResult | |
| logger = logging.getLogger("apps.document_recognition") | |
| class CourtDocumentRecognitionService: | |
| """ | |
| 法院文书智能识别服务(协调器) | |
| 协调文本提取、文书分类、信息提取、案件绑定等子服务, | |
| 完成完整的文书识别流程。 | |
| Requirements: 4.5, 4.6, 4.7, 6.2, 7.1, 7.2, 7.3, 8.1, 8.2, 8.3, 8.4 | |
| """ | |
| def __init__( | |
| self, | |
| text_extraction: Any = None, | |
| classifier: Any = None, | |
| extractor: Any = None, | |
| binding_service: Any = None, | |
| document_renamer: Any = None, | |
| ) -> None: | |
| """ | |
| 初始化服务 | |
| Args: | |
| text_extraction: 文本提取服务(可选,用于依赖注入) | |
| classifier: 文书分类器(可选,用于依赖注入) | |
| extractor: 信息提取器(可选,用于依赖注入) | |
| binding_service: 案件绑定服务(可选,用于依赖注入) | |
| document_renamer: 文书重命名服务(可选,用于依赖注入) | |
| """ | |
| self._text_extraction = text_extraction | |
| self._classifier = classifier | |
| self._extractor = extractor | |
| self._binding_service = binding_service | |
| self._document_renamer = document_renamer | |
| def text_extraction(self) -> Any: | |
| """延迟加载文本提取服务""" | |
| if self._text_extraction is None: | |
| from .text_extraction_service import TextExtractionService | |
| self._text_extraction = TextExtractionService() | |
| return self._text_extraction | |
| def classifier(self) -> Any: | |
| """延迟加载文书分类器""" | |
| if self._classifier is None: | |
| from .document_classifier import DocumentClassifier | |
| self._classifier = DocumentClassifier() | |
| return self._classifier | |
| def extractor(self) -> Any: | |
| """延迟加载信息提取器""" | |
| if self._extractor is None: | |
| from .info_extractor import InfoExtractor | |
| self._extractor = InfoExtractor() | |
| return self._extractor | |
| def binding_service(self) -> Any: | |
| """延迟加载案件绑定服务""" | |
| if self._binding_service is None: | |
| from .case_binding_service import CaseBindingService | |
| self._binding_service = CaseBindingService() | |
| return self._binding_service | |
| def document_renamer(self) -> Any: | |
| """延迟加载文书重命名服务""" | |
| if self._document_renamer is None: | |
| from apps.automation.services.sms.document_renamer import DocumentRenamer | |
| self._document_renamer = DocumentRenamer() | |
| return self._document_renamer | |
| def _extract_doc_info(self, doc_type: Any, text: str) -> tuple[Any, Any]: | |
| """根据文书类型提取案号和关键时间""" | |
| case_number = None | |
| key_time = None | |
| if doc_type.value == "summons": | |
| info = self.extractor.extract_summons_info(text) | |
| case_number = info.get("case_number") | |
| key_time = info.get("court_time") | |
| elif doc_type.value == "execution_ruling": | |
| info = self.extractor.extract_execution_info(text) | |
| case_number = info.get("case_number") | |
| key_time = info.get("preservation_deadline") | |
| return case_number, key_time | |
| def _build_binding( | |
| self, | |
| doc_type: Any, | |
| case_number: Any, | |
| key_time: Any, | |
| file_path: str, | |
| extraction_text: str, | |
| user: Any, | |
| ) -> tuple[Any, str]: | |
| """绑定案件,返回 (binding_result, renamed_file_path)""" | |
| from .data_classes import DocumentType | |
| renamed_file_path = file_path | |
| if doc_type == DocumentType.SUMMONS and case_number: | |
| case_id = self.binding_service.find_case_by_number(case_number) | |
| case_name = None | |
| if case_id: | |
| case_dto = self.binding_service.case_service.get_case_by_id_internal(case_id) | |
| if case_dto: | |
| case_name = case_dto.name | |
| if case_name: | |
| renamed_file_path = self._rename_document( | |
| file_path=file_path, document_type=doc_type, case_name=case_name | |
| ) | |
| log_content = self.binding_service.format_log_content( | |
| document_type=doc_type, case_number=case_number, key_time=key_time, raw_text=extraction_text | |
| ) | |
| binding = self.binding_service.bind_document_to_case( | |
| case_number=case_number, | |
| document_type=doc_type, | |
| content=log_content, | |
| key_time=key_time, | |
| file_path=renamed_file_path, | |
| user=user, | |
| ) | |
| elif doc_type == DocumentType.OTHER: | |
| binding = BindingResult.failure_result( | |
| message=_("暂时只支持传票识别,其他文书类型敬请期待"), | |
| error_code="UNSUPPORTED_DOCUMENT_TYPE", # type: ignore | |
| ) | |
| elif doc_type == DocumentType.EXECUTION_RULING: | |
| binding = BindingResult.failure_result( | |
| message=_("执行裁定书绑定功能开发中,敬请期待"), | |
| error_code="FEATURE_NOT_IMPLEMENTED", # type: ignore | |
| ) | |
| else: | |
| binding = BindingResult.failure_result( | |
| message=_("未识别到案号,无法绑定案件"), | |
| error_code="CASE_NUMBER_NOT_FOUND", # type: ignore | |
| ) | |
| return binding, renamed_file_path | |
| def recognize_document(self, file_path: str, user: Any | None = None) -> RecognitionResponse: | |
| """识别文书并绑定案件 | |
| Requirements: 4.5, 4.6, 4.7, 6.2, 8.1, 8.2, 8.3, 8.4 | |
| """ | |
| logger.info( | |
| "开始识别文书", | |
| extra={ | |
| "action": "recognize_document", | |
| "file_path": file_path, | |
| "user_id": getattr(user, "id", None) if user else None, | |
| }, | |
| ) | |
| try: | |
| extraction_result = self.text_extraction.extract_text(file_path) | |
| if not extraction_result.success or not extraction_result.text.strip(): | |
| logger.warning("文本提取失败或内容为空", extra={"action": "recognize_document", "file_path": file_path}) | |
| return RecognitionResponse( | |
| recognition=RecognitionResult( | |
| document_type=DocumentType.OTHER, | |
| case_number=None, | |
| key_time=None, | |
| raw_text="", | |
| confidence=0.0, | |
| extraction_method=extraction_result.extraction_method, | |
| ), | |
| binding=BindingResult.failure_result( | |
| message=_("无法从文书中提取文字"), # type: ignore | |
| error_code="TEXT_EXTRACTION_FAILED", | |
| ), | |
| file_path=file_path, | |
| ) | |
| doc_type, confidence = self.classifier.classify(extraction_result.text) | |
| case_number, key_time = self._extract_doc_info(doc_type, extraction_result.text) | |
| recognition = RecognitionResult( | |
| document_type=doc_type, | |
| case_number=case_number, | |
| key_time=key_time, | |
| raw_text=extraction_result.text, | |
| confidence=confidence, | |
| extraction_method=extraction_result.extraction_method, | |
| ) | |
| binding, renamed_file_path = self._build_binding( | |
| doc_type, case_number, key_time, file_path, extraction_result.text, user | |
| ) | |
| logger.info( | |
| "文书识别完成", | |
| extra={ | |
| "action": "recognize_document", | |
| "file_path": file_path, | |
| "renamed_file_path": renamed_file_path, | |
| "document_type": doc_type.value, | |
| "case_number": case_number, | |
| "binding_success": binding.success if binding else None, | |
| }, | |
| ) | |
| return RecognitionResponse(recognition=recognition, binding=binding, file_path=renamed_file_path) | |
| except (ValidationException, ServiceUnavailableError, RecognitionTimeoutError): | |
| raise | |
| except Exception as e: | |
| logger.error( | |
| f"文书识别失败: {e}", | |
| extra={ | |
| "action": "recognize_document", | |
| "file_path": file_path, | |
| "error_type": type(e).__name__, | |
| "error": str(e), | |
| }, | |
| exc_info=True, | |
| ) | |
| raise | |
| def recognize_document_from_text(self, text: str) -> RecognitionResult: | |
| """ | |
| 从已提取的文本识别文书 | |
| Args: | |
| text: 文书文本内容 | |
| Returns: | |
| RecognitionResult 对象 | |
| Raises: | |
| ValidationException: 文本内容为空 | |
| ServiceUnavailableError: AI 服务不可用 | |
| RecognitionTimeoutError: 识别超时 | |
| Requirements: 7.2, 7.3 | |
| """ | |
| if not text or not text.strip(): | |
| logger.warning("文本内容为空", extra={"action": "recognize_document_from_text", "result": "empty_text"}) | |
| raise ValidationException( | |
| message=_("文本内容不能为空"), code="EMPTY_TEXT", errors={"text": "请提供有效的文书文本"} | |
| ) | |
| logger.info("开始从文本识别文书", extra={"action": "recognize_document_from_text", "text_length": len(text)}) | |
| try: | |
| # 1. 分类文书类型 | |
| doc_type, confidence = self.classifier.classify(text) | |
| # 2. 提取关键信息 | |
| case_number = None | |
| key_time = None | |
| if doc_type == DocumentType.SUMMONS: | |
| info = self.extractor.extract_summons_info(text) | |
| case_number = info.get("case_number") | |
| key_time = info.get("court_time") | |
| elif doc_type == DocumentType.EXECUTION_RULING: | |
| info = self.extractor.extract_execution_info(text) | |
| case_number = info.get("case_number") | |
| key_time = info.get("preservation_deadline") | |
| result = RecognitionResult( | |
| document_type=doc_type, | |
| case_number=case_number, | |
| key_time=key_time, | |
| raw_text=text, | |
| confidence=confidence, | |
| extraction_method="text_input", | |
| ) | |
| logger.info( | |
| "文本识别完成", | |
| extra={ | |
| "action": "recognize_document_from_text", | |
| "document_type": doc_type.value, | |
| "case_number": case_number, | |
| "confidence": confidence, | |
| }, | |
| ) | |
| return result | |
| except (ValidationException, ServiceUnavailableError, RecognitionTimeoutError): | |
| raise | |
| except Exception as e: | |
| logger.error( | |
| f"文本识别失败: {e}", | |
| extra={"action": "recognize_document_from_text", "error_type": type(e).__name__, "error": str(e)}, | |
| exc_info=True, | |
| ) | |
| raise | |
| def _rename_document(self, file_path: str, document_type: DocumentType, case_name: str) -> str: | |
| """ | |
| 重命名文书文件 | |
| 格式:{主标题}({案件名称})_{YYYYMMDD}收.pdf | |
| Args: | |
| file_path: 原始文件路径 | |
| document_type: 文书类型 | |
| case_name: 案件名称 | |
| Returns: | |
| 重命名后的文件路径,失败时返回原路径 | |
| """ | |
| try: | |
| # 根据文书类型确定标题 | |
| title_map = { | |
| DocumentType.SUMMONS: "传票", | |
| DocumentType.EXECUTION_RULING: "执行裁定书", | |
| DocumentType.OTHER: "司法文书", | |
| } | |
| title = title_map.get(document_type, "司法文书") | |
| # 生成新文件名 | |
| new_filename = self.document_renamer.generate_filename( | |
| title=title, case_name=case_name, received_date=date.today() | |
| ) | |
| # 构建新文件路径 | |
| from pathlib import Path | |
| original_path = Path(file_path) | |
| new_path = original_path.parent / new_filename | |
| # 如果新文件名已存在,添加数字后缀 | |
| counter = 1 | |
| while new_path.exists(): | |
| base_filename = new_filename.replace("收.pdf", f"收{counter}.pdf") | |
| new_path = original_path.parent / base_filename | |
| counter += 1 | |
| if counter > 100: | |
| break | |
| # 重命名文件 | |
| original_path.rename(new_path) | |
| logger.info( | |
| "文书重命名成功", | |
| extra={ | |
| "action": "rename_document", | |
| "original_path": file_path, | |
| "new_path": str(new_path), | |
| "document_type": document_type.value, | |
| "case_name": case_name, | |
| }, | |
| ) | |
| return str(new_path) | |
| except Exception as e: | |
| logger.warning( | |
| "文书重命名失败,保留原文件名", | |
| extra={"action": "rename_document", "file_path": file_path, "error": str(e)}, | |
| ) | |
| return file_path | |