Spaces:
Build error
Build error
File size: 7,537 Bytes
6a49f21 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | """
GROBID extractor plugin.
Registers the GrobidTrainingExtractor with the extraction registry.
Provides a download endpoint for reviewers to download complete training packages.
"""
import logging
from pathlib import Path
from typing import Any, Callable
from fastapi_app.lib.plugins.plugin_base import Plugin, PluginContext
from fastapi_app.lib.plugins.plugin_tools import get_plugin_config
from fastapi_app.plugins.tei_wizard.plugin import TeiWizardPlugin
from fastapi_app.lib.extraction import ExtractorRegistry
from fastapi_app.lib.sse.event_bus import get_event_bus
from .extractor import GrobidTrainingExtractor
logger = logging.getLogger(__name__)
class GrobidPlugin(Plugin):
"""Plugin that provides GROBID-based extraction."""
def __init__(self) -> None:
get_plugin_config("plugin.grobid.server.url", "GROBID_SERVER_URL", default="")
get_plugin_config("plugin.grobid.server.timeout", "GROBID_SERVER_TIMEOUT", default=10)
get_plugin_config("plugin.grobid.extraction.timeout", "GROBID_EXTRACTION_TIMEOUT", default=300)
get_plugin_config("plugin.grobid.cache.disabled", "GROBID_DISABLE_CACHE", default=False)
@property
def metadata(self) -> dict[str, Any]:
"""Return plugin metadata."""
return {
"id": "grobid",
"name": "GROBID Extractor",
"description": "Extract training data using GROBID server",
"category": "extractor",
"version": "1.0.0",
"required_roles": ["user"],
"endpoints": [
{
"name": "download_training",
"label": "Download GROBID Training Data",
"description": "Download complete GROBID training package for a collection",
"category": "collection",
"state_params": ["collection"],
"required_roles": ["reviewer"],
},
],
"dependencies": ["tei-wizard"],
}
def get_endpoints(self) -> dict[str, Callable]:
"""Return available endpoints."""
return {
"download_training": self.download_training,
}
@classmethod
def is_available(cls) -> bool:
"""Check if GROBID server URL is configured."""
from fastapi_app.lib.utils.config_utils import get_config
return bool(get_config().get("plugin.grobid.server.url"))
async def initialize(self, context: PluginContext) -> None:
"""Register the GROBID extractor and event handlers."""
from fastapi_app.lib.plugins.frontend_extension_registry import FrontendExtensionRegistry
ext_registry = FrontendExtensionRegistry.get_instance()
extension_file = Path(__file__).parent / "extensions" / "grobid-sync.js"
logger.debug("DEBUG grobid-sync extension file path: %s (exists=%s)", extension_file, extension_file.exists())
if extension_file.exists():
ext_registry.register_extension(extension_file, self.metadata["id"])
logger.info("Registered grobid-sync frontend extension")
else:
logger.warning("grobid-sync extension file not found: %s", extension_file)
registry = ExtractorRegistry.get_instance()
registry.register(GrobidTrainingExtractor)
# Register event handler for file deletion cache cleanup
event_bus = get_event_bus()
event_bus.on("file.deleted", self._on_file_deleted)
"""Register the TEI header enrichment enhancement with tei-wizard."""
# todo: create auto-discover reusable utility func
tei_wizard = context.get_dependency("tei-wizard")
if isinstance(tei_wizard, TeiWizardPlugin):
for enhancement_filename in ["split-bibl.js", "segment-footnotes.js", "desegment-footnotes.js"]:
enhancement_file = Path(__file__).parent / "enhancements" / enhancement_filename
if enhancement_file.exists():
tei_wizard.register_enhancement(enhancement_file, self.metadata["id"])
else:
logger.warning(f"Enhancement file not found: {enhancement_file}")
else:
logger.debug("tei-wizard dependency not available")
logger.info("GROBID extractor plugin initialized")
async def cleanup(self) -> None:
"""Unregister the GROBID extractor and event handlers."""
registry = ExtractorRegistry.get_instance()
registry.unregister("grobid")
# Unregister event handler
event_bus = get_event_bus()
event_bus.off("file.deleted", self._on_file_deleted)
logger.info("GROBID extractor plugin cleaned up")
async def _on_file_deleted(self, stable_id: str, **kwargs) -> None:
"""
Clean up cached GROBID training data when a file is deleted.
This handler is called when any file is deleted. It checks if the file
was a PDF and removes any cached training data for that document.
Args:
stable_id: The stable_id of the deleted file
"""
from fastapi_app.lib.core.dependencies import get_db
from fastapi_app.lib.repository.file_repository import FileRepository
try:
# Get file info to check if it was a PDF
db = get_db()
file_repo = FileRepository(db)
file_info = file_repo.get_file_by_stable_id(stable_id)
if not file_info or file_info.file_type != "pdf":
return
doc_id = file_info.doc_id
if not doc_id:
return
# Check if any other TEI files still exist for this doc_id
doc_files = file_repo.get_files_by_doc_id(doc_id)
other_teis = [f for f in doc_files if f.file_type == "tei" and f.stable_id != stable_id and not f.deleted]
if other_teis:
# Other PDFs exist, don't delete cache
return
# Delete cached training data for this document
from fastapi_app.plugins.grobid.cache import delete_cache_for_doc
if delete_cache_for_doc(doc_id):
logger.info(f"Deleted cached GROBID training data for {doc_id}")
except Exception as e:
logger.warning(f"Failed to clean up GROBID cache for {stable_id}: {e}")
async def download_training(
self, context: PluginContext, params: dict[str, Any]
) -> dict[str, Any]:
"""
Generate download URL for GROBID training package.
Args:
context: Plugin context
params: Parameters including 'collection' (collection ID)
Returns:
downloadUrl pointing to the download route
"""
collection = params.get("collection")
if not collection:
return {
"error": "No collection selected",
"message": "Please select a collection first.",
}
# Build download URL with optional parameters
# no_progress=false enables SSE progress events for UI usage
download_url = f"/api/plugins/grobid/download?collection={collection}&no_progress=false"
# Add optional parameters if provided
if params.get("force_refresh"):
download_url += "&force_refresh=true"
if params.get("flavor"):
download_url += f"&flavor={params['flavor']}"
return {
"downloadUrl": download_url,
"collection": collection,
}
|