Download src/main.py from pimcore/local-inference-service: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/spaces/pimcore/local-inference-service/resolve/main/src/main.py
- Command line
-
hf download hf://spaces/pimcore/local-inference-service/src/main.py
-
curl -L -o main.py https://huggingface.co/spaces/pimcore/local-inference-service/resolve/main/src/main.py
11.5 kB
| # ------------------------------------------------------------------- | |
| # This source file is available under the terms of the | |
| # Pimcore Open Core License (POCL) | |
| # Full copyright and license information is available in | |
| # LICENSE.md which is distributed with this source code. | |
| # | |
| # @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com) | |
| # @license Pimcore Open Core License (POCL) | |
| # ------------------------------------------------------------------- | |
| import logging | |
| import os | |
| from typing import Annotated | |
| import torch | |
| from fastapi import Body, FastAPI, Path, Request | |
| from .classification import ClassificationTaskService | |
| from .embeddings import ( | |
| EmbeddingsRequest, | |
| EmbeddingsResponse, | |
| ImageEmbeddingTaskService, | |
| TextEmbeddingTaskService, | |
| VectorSizeResponse, | |
| ) | |
| from .middleware import MaxBodySizeMiddleware | |
| from .openapi_examples import ( | |
| EMBEDDINGS_RESPONSES, | |
| IMAGE_EMBEDDING_EXAMPLES, | |
| TEXT_EMBEDDING_EXAMPLES, | |
| ) | |
| from .text_to_image import TextToImageTaskService | |
| from .translation_task import TranslationTaskService | |
| app = FastAPI( | |
| title="Pimcore Local Inference Service", | |
| description=( | |
| "HF inference-provider-compatible inference for models not available at HF inference " | |
| "providers (translation, classification, image-to-text, and text/image embeddings)." | |
| ), | |
| version="1.0.0", | |
| ) | |
| # Cap request bodies so an oversized upload can't exhaust memory before a handler runs. A batch of | |
| # base64 images is the largest legitimate payload; 100 MB covers a full image batch with headroom. | |
| # Override via MAX_BODY_SIZE_BYTES. | |
| MAX_BODY_SIZE_BYTES = int(os.environ.get("MAX_BODY_SIZE_BYTES", 100 * 1024 * 1024)) | |
| app.add_middleware(MaxBodySizeMiddleware, max_body_size=MAX_BODY_SIZE_BYTES) | |
| # The DEBUG records say which extraction branch each embedding model took (pooler_output vs | |
| # get_image_features vs masked mean-pool) — the first thing you need when a model's embeddings look | |
| # wrong — but they are far too chatty to leave on in production, hence a knob instead of a constant. | |
| # getLevelName() returns a "Level <name>" string for anything it doesn't recognise, so an | |
| # unparseable LOG_LEVEL degrades to INFO rather than taking the service down at import. | |
| _requested_log_level = logging.getLevelName(os.environ.get("LOG_LEVEL", "INFO").strip().upper()) | |
| LOG_LEVEL: int = _requested_log_level if isinstance(_requested_log_level, int) else logging.INFO | |
| logging.basicConfig(format="%(asctime)s %(levelname)-8s %(message)s", level=LOG_LEVEL) | |
| logger = logging.getLogger(__name__) | |
| # Singleton service instances: models and processors are cached on the instance, so reusing one | |
| # instance across requests keeps the (expensive) weights loaded instead of reloading per call. | |
| image_embedding_service = ImageEmbeddingTaskService(logger) | |
| text_embedding_service = TextEmbeddingTaskService(logger) | |
| translation_service = TranslationTaskService(logger) | |
| image_to_text_service = TextToImageTaskService(logger) | |
| zero_shot_image_classification_service = ClassificationTaskService(logger, "zero-shot-image-classification") | |
| image_classification_service = ClassificationTaskService(logger, "image-classification") | |
| zero_shot_text_classification_service = ClassificationTaskService(logger, "zero-shot-classification") | |
| text_classification_service = ClassificationTaskService(logger, "text-classification") | |
| async def gpu_check() -> dict: | |
| """Check if a GPU is available.""" | |
| available = torch.cuda.is_available() | |
| logger.info("GPU is available" if available else "GPU is not available") | |
| return {"success": True, "gpu": "GPU is available" if available else "GPU not available"} | |
| # ========================= | |
| # Translation Task | |
| # ========================= | |
| async def translate( | |
| request: Request, | |
| model_name: str = Path( | |
| ..., | |
| description="The name of the translation model (e.g. Helsinki-NLP/opus-mt-en-de)", | |
| examples=["Helsinki-NLP/opus-mt-en-de"], | |
| ), | |
| ): | |
| """Execute translation tasks.""" | |
| return await translation_service.translate(request, model_name.rstrip("/")) | |
| # ========================= | |
| # Zero-Shot Image Classification Task | |
| # ========================= | |
| async def zero_shot_image_classification( | |
| request: Request, | |
| model_name: str = Path( | |
| ..., | |
| description="The name of the zero-shot classification model (e.g., openai/clip-vit-large-patch14-336)", | |
| examples=["openai/clip-vit-large-patch14-336"], | |
| ), | |
| ): | |
| """Execute zero-shot image classification tasks.""" | |
| return await zero_shot_image_classification_service.classify(request, model_name.rstrip("/")) | |
| # ========================= | |
| # Image Classification Task | |
| # ========================= | |
| async def image_classification( | |
| request: Request, | |
| model_name: str = Path( | |
| ..., | |
| description="The name of the image classification model (e.g., pimcore/car-countries-classification)", | |
| examples=["pimcore/car-countries-classification"], | |
| ), | |
| ): | |
| """Execute image classification tasks.""" | |
| return await image_classification_service.classify(request, model_name.rstrip("/")) | |
| # ========================= | |
| # Zero-Shot Text Classification Task | |
| # ========================= | |
| async def zero_shot_text_classification( | |
| request: Request, | |
| model_name: str = Path( | |
| ..., | |
| description="The name of the zero-shot text classification model (e.g., facebook/bart-large-mnli)", | |
| examples=["facebook/bart-large-mnli"], | |
| ), | |
| ): | |
| """Execute zero-shot text classification tasks.""" | |
| return await zero_shot_text_classification_service.classify(request, model_name.rstrip("/")) | |
| # ========================= | |
| # Text Classification Task | |
| # ========================= | |
| async def text_classification( | |
| request: Request, | |
| model_name: str = Path( | |
| ..., | |
| description="The name of the text classification model (e.g., pimcore/car-class-classification)", | |
| examples=["pimcore/car-class-classification"], | |
| ), | |
| ): | |
| """Execute text classification tasks.""" | |
| return await text_classification_service.classify(request, model_name.rstrip("/")) | |
| # ========================= | |
| # Image to Text Task | |
| # ========================= | |
| async def image_to_text( | |
| request: Request, | |
| model_name: str = Path( | |
| ..., | |
| description="The name of the image-to-text model (e.g., Salesforce/blip-image-captioning-base)", | |
| examples=["Salesforce/blip-image-captioning-base"], | |
| ), | |
| ): | |
| """Execute image-to-text tasks.""" | |
| return await image_to_text_service.extract(request, model_name.rstrip("/")) | |
| # ========================= | |
| # Text embeddings | |
| # ========================= | |
| async def text_embedding( | |
| body: Annotated[EmbeddingsRequest, Body(openapi_examples=TEXT_EMBEDDING_EXAMPLES)], | |
| ) -> EmbeddingsResponse: | |
| """Text embeddings: `{"model": ..., "input": "..."|[...]}` -> `{"data": [{index, embedding}]}`. | |
| The model is taken from the body; `input` may be one string or a list, and a list is embedded | |
| in a single forward pass. Every vector comes back L2-normalized. | |
| """ | |
| return await text_embedding_service.generate_embeddings(body) | |
| # ========================= | |
| # Image embeddings | |
| # ========================= | |
| async def image_embedding( | |
| body: Annotated[EmbeddingsRequest, Body(openapi_examples=IMAGE_EMBEDDING_EXAMPLES)], | |
| ) -> EmbeddingsResponse: | |
| """Image embeddings, same shape as /text-embedding: `input` is one base64-encoded image or a | |
| list of them. | |
| An image that cannot be decoded is skipped rather than failing the batch — it is omitted from | |
| `data`, so its `index` is absent instead of shifting the others. | |
| """ | |
| return await image_embedding_service.generate_embeddings(body) | |
| # ========================= | |
| # Embedding Vector Size | |
| # ========================= | |
| async def embedding_vector_size( | |
| model_name: str = Path( | |
| ..., | |
| description="The name of the embedding model.", | |
| examples=["nomic-ai/nomic-embed-text-v1.5"], | |
| ), | |
| ) -> VectorSizeResponse: | |
| """Return the dimensionality of the embeddings a model produces.""" | |
| # Either embedding service works — they share the base class model cache. | |
| return await image_embedding_service.get_embedding_vector_size(model_name.rstrip("/")) | |