sentiment_analysis / processors /model_registry.py
vojmahdal's picture
v3
2e3f5aa verified
Raw
History Blame Contribute Delete
3.27 kB
"""
Generic loader/cache for Hugging Face pipelines pulled from the Hub at
request time.
This lets a caller pick *any* HF model id - for sentiment analysis, named
entity recognition, or zero-shot topic classification - instead of being
limited to the built-in default models. Pipelines are expensive to
instantiate (they download weights on first use), so loaded pipelines are
kept in a small in-memory cache, namespaced by task so the same model id
used for two different tasks never collides.
Security note: ``trust_remote_code`` is never enabled. Enabling it would let
an arbitrary Hub repository execute Python code inside this process, which
is not acceptable for a model id supplied by an API caller.
"""
from __future__ import annotations
import threading
from typing import Any
# Maximum number of distinct (task, model) pipelines kept warm in memory at
# once. Oldest (first loaded) is evicted when the cache is full - simple
# FIFO, adequate for a demo/thesis system that isn't serving many concurrent
# model ids.
_MAX_CACHED_PIPELINES = 6
_cache: dict[str, Any] = {}
_cache_order: list[str] = []
_load_errors: dict[str, str] = {}
_lock = threading.Lock()
def _cache_key(model_id: str, task: str) -> str:
return f"{task}::{model_id}"
def get_pipeline(model_id: str, task: str = "sentiment-analysis", **pipeline_kwargs: Any):
"""
Return a cached (or newly loaded) Hugging Face pipeline for
``(task, model_id)``. Extra ``pipeline_kwargs`` (e.g.
``aggregation_strategy="simple"`` for NER) are forwarded to
``transformers.pipeline`` on first load only.
Raises ``RuntimeError`` if the model cannot be loaded (unknown repo,
incompatible with the task, network error, ...) so callers can turn it
into a clean HTTP error instead of crashing.
"""
key = _cache_key(model_id, task)
with _lock:
if key in _cache:
return _cache[key]
if key in _load_errors:
raise RuntimeError(_load_errors[key])
try:
from transformers import pipeline
clf = pipeline(
task,
model=model_id,
tokenizer=model_id,
trust_remote_code=False,
**pipeline_kwargs,
)
except Exception as e: # pragma: no cover - depends on network/model
with _lock:
_load_errors[key] = str(e)
raise RuntimeError(f"Could not load model '{model_id}' for task '{task}': {e}") from e
with _lock:
_cache[key] = clf
_cache_order.append(key)
while len(_cache_order) > _MAX_CACHED_PIPELINES:
oldest = _cache_order.pop(0)
_cache.pop(oldest, None)
return clf
def cached_models(task: str | None = None) -> dict[str, list[str]]:
"""
Model ids currently kept warm in memory, grouped by task.
If ``task`` is given, returns just that task's list under the same key
(still as a dict, for a consistent return type).
"""
with _lock:
keys = list(_cache_order)
grouped: dict[str, list[str]] = {}
for key in keys:
key_task, _, model_id = key.partition("::")
if task is not None and key_task != task:
continue
grouped.setdefault(key_task, []).append(model_id)
return grouped