From 40259bdd01e9884065ac9dcb03042abfb82595b2 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Fri, 11 Oct 2024 15:14:13 +0200 Subject: [PATCH 01/14] Factorize inference payload build and add test --- src/huggingface_hub/inference/_client.py | 236 ++++++++---------- .../inference/_generated/_async_client.py | 236 ++++++++---------- tests/test_inference_client.py | 59 +++++ 3 files changed, 267 insertions(+), 264 deletions(-) diff --git a/src/huggingface_hub/inference/_client.py b/src/huggingface_hub/inference/_client.py index 38b37b71e3..79f2f6990e 100644 --- a/src/huggingface_hub/inference/_client.py +++ b/src/huggingface_hub/inference/_client.py @@ -37,6 +37,8 @@ import re import time import warnings +from dataclasses import dataclass +from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Literal, Optional, Union, overload from requests import HTTPError @@ -109,6 +111,12 @@ MODEL_KWARGS_NOT_USED_REGEX = re.compile(r"The following `model_kwargs` are not used by the model: \[(.*?)\]") +@dataclass +class _InferenceInputs: + json: Optional[Dict[str, Any]] = None + raw_data: Optional[ContentT] = None + + class InferenceClient: """ Initialize a new Inference Client. @@ -364,18 +372,8 @@ def audio_classification( ``` """ parameters = {"function_to_apply": function_to_apply, "top_k": top_k} - if all(parameter is None for parameter in parameters.values()): - # if no parameters are provided, send audio as raw data - data = audio - payload: Optional[Dict[str, Any]] = None - else: - # Or some parameters are provided -> send audio as base64 encoded string - data = None - payload = {"inputs": _b64_encode(audio)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, data=data, model=model, task="audio-classification") + payload = self._prepare_payload(audio, parameters=parameters) + response = self.post(json=payload.json, data=payload.raw_data, model=model, task="audio-classification") return AudioClassificationOutputElement.parse_obj_as_list(response) def audio_to_audio( @@ -988,7 +986,7 @@ def document_question_answering( [DocumentQuestionAnsweringOutputElement(answer='us-001', end=16, score=0.9999666213989258, start=16, words=None)] ``` """ - payload: Dict[str, Any] = {"question": question, "image": _b64_encode(image)} + inputs: Dict[str, Any] = {"question": question, "image": _b64_encode(image)} parameters = { "doc_stride": doc_stride, "handle_impossible_answer": handle_impossible_answer, @@ -999,10 +997,8 @@ def document_question_answering( "top_k": top_k, "word_boxes": word_boxes, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, model=model, task="document-question-answering") + payload = self._prepare_payload(inputs, parameters=parameters) + response = self.post(json=payload.json, model=model, task="document-question-answering") return DocumentQuestionAnsweringOutputElement.parse_obj_as_list(response) def feature_extraction( @@ -1060,17 +1056,14 @@ def feature_extraction( [ 0.28552425, -0.928395 , -1.2077185 , ..., 0.76810825, -2.1069427 , 0.6236161 ]], dtype=float32) ``` """ - payload: Dict = {"inputs": text} parameters = { "normalize": normalize, "prompt_name": prompt_name, "truncate": truncate, "truncation_direction": truncation_direction, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, model=model, task="feature-extraction") + payload = self._prepare_payload(text, parameters=parameters) + response = self.post(json=payload.json, model=model, task="feature-extraction") np = _import_numpy() return np.array(_bytes_to_dict(response), dtype="float32") @@ -1119,12 +1112,9 @@ def fill_mask( ] ``` """ - payload: Dict = {"inputs": text} parameters = {"targets": targets, "top_k": top_k} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, model=model, task="fill-mask") + payload = self._prepare_payload(text, parameters=parameters) + response = self.post(json=payload.json, model=model, task="fill-mask") return FillMaskOutputElement.parse_obj_as_list(response) def image_classification( @@ -1166,19 +1156,8 @@ def image_classification( ``` """ parameters = {"function_to_apply": function_to_apply, "top_k": top_k} - - if all(parameter is None for parameter in parameters.values()): - data = image - payload: Optional[Dict[str, Any]] = None - - else: - data = None - payload = {"inputs": _b64_encode(image)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - - response = self.post(json=payload, data=data, model=model, task="image-classification") + payload = self._prepare_payload(image, parameters=parameters) + response = self.post(json=payload.json, data=payload.raw_data, model=model, task="image-classification") return ImageClassificationOutputElement.parse_obj_as_list(response) def image_segmentation( @@ -1237,18 +1216,8 @@ def image_segmentation( "subtask": subtask, "threshold": threshold, } - if all(parameter is None for parameter in parameters.values()): - # if no parameters are provided, the image can be raw bytes, an image file, or URL to an online image - data = image - payload: Optional[Dict[str, Any]] = None - else: - # if parameters are provided, the image needs to be a base64-encoded string - data = None - payload = {"inputs": _b64_encode(image)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, data=data, model=model, task="image-segmentation") + payload = self._prepare_payload(image, parameters=parameters) + response = self.post(json=payload.json, data=payload.raw_data, model=model, task="image-segmentation") output = ImageSegmentationOutputElement.parse_obj_as_list(response) for item in output: item.mask = _b64_to_image(item.mask) # type: ignore [assignment] @@ -1323,19 +1292,8 @@ def image_to_image( "guidance_scale": guidance_scale, **kwargs, } - if all(parameter is None for parameter in parameters.values()): - # Either only an image to send => send as raw bytes - data = image - payload: Optional[Dict[str, Any]] = None - else: - # if parameters are provided, the image needs to be a base64-encoded string - data = None - payload = {"inputs": _b64_encode(image)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - - response = self.post(json=payload, data=data, model=model, task="image-to-image") + payload = self._prepare_payload(image, parameters=parameters) + response = self.post(json=payload.json, data=payload.raw_data, model=model, task="image-to-image") return _bytes_to_image(response) def image_to_text(self, image: ContentT, *, model: Optional[str] = None) -> ImageToTextOutput: @@ -1493,25 +1451,15 @@ def object_detection( ```py >>> from huggingface_hub import InferenceClient >>> client = InferenceClient() - >>> client.object_detection("people.jpg"): + >>> client.object_detection("people.jpg") [ObjectDetectionOutputElement(score=0.9486683011054993, label='person', box=ObjectDetectionBoundingBox(xmin=59, ymin=39, xmax=420, ymax=510)), ...] ``` """ parameters = { "threshold": threshold, } - if all(parameter is None for parameter in parameters.values()): - # if no parameters are provided, the image can be raw bytes, an image file, or URL to an online image - data = image - payload: Optional[Dict[str, Any]] = None - else: - # if parameters are provided, the image needs to be a base64-encoded string - data = None - payload = {"inputs": _b64_encode(image)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, data=data, model=model, task="object-detection") + payload = self._prepare_payload(image, parameters=parameters) + response = self.post(json=payload.json, data=payload.raw_data, model=model, task="object-detection") return ObjectDetectionOutputElement.parse_obj_as_list(response) def question_answering( @@ -1587,12 +1535,10 @@ def question_answering( "max_seq_len": max_seq_len, "top_k": top_k, } - payload: Dict[str, Any] = {"question": question, "context": context} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value + inputs: Dict[str, Any] = {"question": question, "context": context} + payload = self._prepare_payload(inputs, parameters=parameters) response = self.post( - json=payload, + json=payload.json, model=model, task="question-answering", ) @@ -1700,19 +1646,14 @@ def summarization( SummarizationOutput(generated_text="The Eiffel tower is one of the most famous landmarks in the world....") ``` """ - payload: Dict[str, Any] = {"inputs": text} - if parameters is not None: - payload["parameters"] = parameters - else: + if parameters is None: parameters = { "clean_up_tokenization_spaces": clean_up_tokenization_spaces, "generate_parameters": generate_parameters, "truncation": truncation, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, model=model, task="summarization") + payload = self._prepare_payload(text, parameters=parameters) + response = self.post(json=payload.json, model=model, task="summarization") return SummarizationOutput.parse_obj_as_list(response)[0] def table_question_answering( @@ -1757,15 +1698,13 @@ def table_question_answering( TableQuestionAnsweringOutputElement(answer='36542', coordinates=[[0, 1]], cells=['36542'], aggregator='AVERAGE') ``` """ - payload: Dict[str, Any] = { + inputs = { "query": query, "table": table, } - - if parameters is not None: - payload["parameters"] = parameters + payload = self._prepare_payload(inputs, parameters=parameters) response = self.post( - json=payload, + json=payload.json, model=model, task="table-question-answering", ) @@ -1899,15 +1838,12 @@ def text_classification( ] ``` """ - payload: Dict[str, Any] = {"inputs": text} parameters = { "function_to_apply": function_to_apply, "top_k": top_k, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, model=model, task="text-classification") + payload = self._prepare_payload(text, parameters=parameters) + response = self.post(json=payload.json, model=model, task="text-classification") return TextClassificationOutputElement.parse_obj_as_list(response)[0] # type: ignore [return-value] @overload @@ -2481,7 +2417,7 @@ def text_to_image( >>> image.save("better_astronaut.png") ``` """ - payload = {"inputs": prompt} + parameters = { "negative_prompt": negative_prompt, "height": height, @@ -2493,10 +2429,8 @@ def text_to_image( "seed": seed, **kwargs, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value # type: ignore - response = self.post(json=payload, model=model, task="text-to-image") + payload = self._prepare_payload(prompt, parameters=parameters) + response = self.post(json=payload.json, model=model, task="text-to-image") return _bytes_to_image(response) def text_to_speech( @@ -2599,7 +2533,6 @@ def text_to_speech( >>> Path("hello_world.flac").write_bytes(audio) ``` """ - payload: Dict[str, Any] = {"inputs": text} parameters = { "do_sample": do_sample, "early_stopping": early_stopping, @@ -2618,10 +2551,8 @@ def text_to_speech( "typical_p": typical_p, "use_cache": use_cache, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, model=model, task="text-to-speech") + payload = self._prepare_payload(text, parameters=parameters) + response = self.post(json=payload.json, model=model, task="text-to-speech") return response def token_classification( @@ -2683,17 +2614,15 @@ def token_classification( ] ``` """ - payload: Dict[str, Any] = {"inputs": text} + parameters = { "aggregation_strategy": aggregation_strategy, "ignore_labels": ignore_labels, "stride": stride, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value + payload = self._prepare_payload(text, parameters=parameters) response = self.post( - json=payload, + json=payload.json, model=model, task="token-classification", ) @@ -2769,7 +2698,6 @@ def translation( if src_lang is None and tgt_lang is not None: raise ValueError("You cannot specify `tgt_lang` without specifying `src_lang`.") - payload: Dict[str, Any] = {"inputs": text} parameters = { "src_lang": src_lang, "tgt_lang": tgt_lang, @@ -2777,10 +2705,8 @@ def translation( "truncation": truncation, "generate_parameters": generate_parameters, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = self.post(json=payload, model=model, task="translation") + payload = self._prepare_payload(text, parameters=parameters) + response = self.post(json=payload.json, model=model, task="translation") return TranslationOutput.parse_obj_as_list(response)[0] def visual_question_answering( @@ -2924,12 +2850,9 @@ def zero_shot_classification( parameters = {"candidate_labels": labels, "multi_label": multi_label} if hypothesis_template is not None: parameters["hypothesis_template"] = hypothesis_template - + payload = self._prepare_payload(text, parameters=parameters) response = self.post( - json={ - "inputs": text, - "parameters": parameters, - }, + json=payload.json, task="zero-shot-classification", model=model, ) @@ -2986,18 +2909,67 @@ def zero_shot_image_classification( if len(labels) < 2: raise ValueError("You must specify at least 2 classes to compare.") - payload = { - "inputs": {"image": _b64_encode(image), "candidateLabels": ",".join(labels)}, - } - if hypothesis_template is not None: - payload.setdefault("parameters", {})["hypothesis_template"] = hypothesis_template + inputs = {"image": _b64_encode(image), "candidateLabels": ",".join(labels)} + parameters = {"hypothesis_template": hypothesis_template} if hypothesis_template is not None else None + payload = self._prepare_payload(inputs, parameters=parameters) response = self.post( - json=payload, + json=payload.json, model=model, task="zero-shot-image-classification", ) return ZeroShotImageClassificationOutputElement.parse_obj_as_list(response) + @staticmethod + def _prepare_payload( + inputs: Union[str, Dict[str, Any], ContentT], + parameters: Optional[Dict[str, Any]] = None, + ) -> _InferenceInputs: + """ + Prepare payload for an API request, handling various input types and parameters. + + Args: + inputs (`Union[str, Dict[str, Any], ContentT]`): + The input data, which can be a string, dictionary, or raw content (e.g., image or audio bytes). + parameters (`Optional[Dict[str, Any]]`): + Optional inference parameters. + + Returns: + `_InferenceInputs`: + An instance of `_InferenceInputs` containing: + - The JSON payload (dict) if parameters are provided or inputs is not raw content, else None. + - The raw content (ContentT) if inputs is raw content and no parameters are provided, else None. + """ + + def is_raw_content(inputs: Union[str, ContentT]) -> bool: + return isinstance(inputs, (bytes, Path)) or ( + isinstance(inputs, str) and inputs.startswith(("http://", "https://")) + ) + + json = None + raw_data = None + if parameters is None: + parameters = {} + parameters = {k: v for k, v in parameters.items() if v is not None} + has_parameters = bool(parameters) + if not has_parameters and is_raw_content(inputs): + # Send inputs as raw content when no parameters are provided + raw_data = inputs + return _InferenceInputs(json, raw_data) + + json = {} + if isinstance(inputs, dict): + json.update(inputs) + elif isinstance(inputs, (bytes, Path)): + json["inputs"] = _b64_encode(inputs) + elif isinstance(inputs, str): + json["inputs"] = inputs + else: + raise TypeError(f"Unsupported type for inputs: {type(inputs)}") + + if has_parameters: + json["parameters"] = parameters + return _InferenceInputs(json, raw_data) + def _resolve_url(self, model: Optional[str] = None, task: Optional[str] = None) -> str: model = model or self.model or self.base_url diff --git a/src/huggingface_hub/inference/_generated/_async_client.py b/src/huggingface_hub/inference/_generated/_async_client.py index 8a1384a671..5b5a979229 100644 --- a/src/huggingface_hub/inference/_generated/_async_client.py +++ b/src/huggingface_hub/inference/_generated/_async_client.py @@ -24,6 +24,8 @@ import re import time import warnings +from dataclasses import dataclass +from pathlib import Path from typing import TYPE_CHECKING, Any, AsyncIterable, Dict, List, Literal, Optional, Set, Union, overload from requests.structures import CaseInsensitiveDict @@ -98,6 +100,12 @@ MODEL_KWARGS_NOT_USED_REGEX = re.compile(r"The following `model_kwargs` are not used by the model: \[(.*?)\]") +@dataclass +class _InferenceInputs: + json: Optional[Dict[str, Any]] = None + raw_data: Optional[ContentT] = None + + class AsyncInferenceClient: """ Initialize a new Inference Client. @@ -398,18 +406,8 @@ async def audio_classification( ``` """ parameters = {"function_to_apply": function_to_apply, "top_k": top_k} - if all(parameter is None for parameter in parameters.values()): - # if no parameters are provided, send audio as raw data - data = audio - payload: Optional[Dict[str, Any]] = None - else: - # Or some parameters are provided -> send audio as base64 encoded string - data = None - payload = {"inputs": _b64_encode(audio)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, data=data, model=model, task="audio-classification") + payload = self._prepare_payload(audio, parameters=parameters) + response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="audio-classification") return AudioClassificationOutputElement.parse_obj_as_list(response) async def audio_to_audio( @@ -1031,7 +1029,7 @@ async def document_question_answering( [DocumentQuestionAnsweringOutputElement(answer='us-001', end=16, score=0.9999666213989258, start=16, words=None)] ``` """ - payload: Dict[str, Any] = {"question": question, "image": _b64_encode(image)} + inputs: Dict[str, Any] = {"question": question, "image": _b64_encode(image)} parameters = { "doc_stride": doc_stride, "handle_impossible_answer": handle_impossible_answer, @@ -1042,10 +1040,8 @@ async def document_question_answering( "top_k": top_k, "word_boxes": word_boxes, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, model=model, task="document-question-answering") + payload = self._prepare_payload(inputs, parameters=parameters) + response = await self.post(json=payload.json, model=model, task="document-question-answering") return DocumentQuestionAnsweringOutputElement.parse_obj_as_list(response) async def feature_extraction( @@ -1104,17 +1100,14 @@ async def feature_extraction( [ 0.28552425, -0.928395 , -1.2077185 , ..., 0.76810825, -2.1069427 , 0.6236161 ]], dtype=float32) ``` """ - payload: Dict = {"inputs": text} parameters = { "normalize": normalize, "prompt_name": prompt_name, "truncate": truncate, "truncation_direction": truncation_direction, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, model=model, task="feature-extraction") + payload = self._prepare_payload(text, parameters=parameters) + response = await self.post(json=payload.json, model=model, task="feature-extraction") np = _import_numpy() return np.array(_bytes_to_dict(response), dtype="float32") @@ -1164,12 +1157,9 @@ async def fill_mask( ] ``` """ - payload: Dict = {"inputs": text} parameters = {"targets": targets, "top_k": top_k} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, model=model, task="fill-mask") + payload = self._prepare_payload(text, parameters=parameters) + response = await self.post(json=payload.json, model=model, task="fill-mask") return FillMaskOutputElement.parse_obj_as_list(response) async def image_classification( @@ -1212,19 +1202,8 @@ async def image_classification( ``` """ parameters = {"function_to_apply": function_to_apply, "top_k": top_k} - - if all(parameter is None for parameter in parameters.values()): - data = image - payload: Optional[Dict[str, Any]] = None - - else: - data = None - payload = {"inputs": _b64_encode(image)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - - response = await self.post(json=payload, data=data, model=model, task="image-classification") + payload = self._prepare_payload(image, parameters=parameters) + response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="image-classification") return ImageClassificationOutputElement.parse_obj_as_list(response) async def image_segmentation( @@ -1284,18 +1263,8 @@ async def image_segmentation( "subtask": subtask, "threshold": threshold, } - if all(parameter is None for parameter in parameters.values()): - # if no parameters are provided, the image can be raw bytes, an image file, or URL to an online image - data = image - payload: Optional[Dict[str, Any]] = None - else: - # if parameters are provided, the image needs to be a base64-encoded string - data = None - payload = {"inputs": _b64_encode(image)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, data=data, model=model, task="image-segmentation") + payload = self._prepare_payload(image, parameters=parameters) + response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="image-segmentation") output = ImageSegmentationOutputElement.parse_obj_as_list(response) for item in output: item.mask = _b64_to_image(item.mask) # type: ignore [assignment] @@ -1371,19 +1340,8 @@ async def image_to_image( "guidance_scale": guidance_scale, **kwargs, } - if all(parameter is None for parameter in parameters.values()): - # Either only an image to send => send as raw bytes - data = image - payload: Optional[Dict[str, Any]] = None - else: - # if parameters are provided, the image needs to be a base64-encoded string - data = None - payload = {"inputs": _b64_encode(image)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - - response = await self.post(json=payload, data=data, model=model, task="image-to-image") + payload = self._prepare_payload(image, parameters=parameters) + response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="image-to-image") return _bytes_to_image(response) async def image_to_text(self, image: ContentT, *, model: Optional[str] = None) -> ImageToTextOutput: @@ -1549,25 +1507,15 @@ async def object_detection( # Must be run in an async context >>> from huggingface_hub import AsyncInferenceClient >>> client = AsyncInferenceClient() - >>> await client.object_detection("people.jpg"): + >>> await client.object_detection("people.jpg") [ObjectDetectionOutputElement(score=0.9486683011054993, label='person', box=ObjectDetectionBoundingBox(xmin=59, ymin=39, xmax=420, ymax=510)), ...] ``` """ parameters = { "threshold": threshold, } - if all(parameter is None for parameter in parameters.values()): - # if no parameters are provided, the image can be raw bytes, an image file, or URL to an online image - data = image - payload: Optional[Dict[str, Any]] = None - else: - # if parameters are provided, the image needs to be a base64-encoded string - data = None - payload = {"inputs": _b64_encode(image)} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, data=data, model=model, task="object-detection") + payload = self._prepare_payload(image, parameters=parameters) + response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="object-detection") return ObjectDetectionOutputElement.parse_obj_as_list(response) async def question_answering( @@ -1644,12 +1592,10 @@ async def question_answering( "max_seq_len": max_seq_len, "top_k": top_k, } - payload: Dict[str, Any] = {"question": question, "context": context} - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value + inputs: Dict[str, Any] = {"question": question, "context": context} + payload = self._prepare_payload(inputs, parameters=parameters) response = await self.post( - json=payload, + json=payload.json, model=model, task="question-answering", ) @@ -1759,19 +1705,14 @@ async def summarization( SummarizationOutput(generated_text="The Eiffel tower is one of the most famous landmarks in the world....") ``` """ - payload: Dict[str, Any] = {"inputs": text} - if parameters is not None: - payload["parameters"] = parameters - else: + if parameters is None: parameters = { "clean_up_tokenization_spaces": clean_up_tokenization_spaces, "generate_parameters": generate_parameters, "truncation": truncation, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, model=model, task="summarization") + payload = self._prepare_payload(text, parameters=parameters) + response = await self.post(json=payload.json, model=model, task="summarization") return SummarizationOutput.parse_obj_as_list(response)[0] async def table_question_answering( @@ -1817,15 +1758,13 @@ async def table_question_answering( TableQuestionAnsweringOutputElement(answer='36542', coordinates=[[0, 1]], cells=['36542'], aggregator='AVERAGE') ``` """ - payload: Dict[str, Any] = { + inputs = { "query": query, "table": table, } - - if parameters is not None: - payload["parameters"] = parameters + payload = self._prepare_payload(inputs, parameters=parameters) response = await self.post( - json=payload, + json=payload.json, model=model, task="table-question-answering", ) @@ -1962,15 +1901,12 @@ async def text_classification( ] ``` """ - payload: Dict[str, Any] = {"inputs": text} parameters = { "function_to_apply": function_to_apply, "top_k": top_k, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, model=model, task="text-classification") + payload = self._prepare_payload(text, parameters=parameters) + response = await self.post(json=payload.json, model=model, task="text-classification") return TextClassificationOutputElement.parse_obj_as_list(response)[0] # type: ignore [return-value] @overload @@ -2546,7 +2482,7 @@ async def text_to_image( >>> image.save("better_astronaut.png") ``` """ - payload = {"inputs": prompt} + parameters = { "negative_prompt": negative_prompt, "height": height, @@ -2558,10 +2494,8 @@ async def text_to_image( "seed": seed, **kwargs, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value # type: ignore - response = await self.post(json=payload, model=model, task="text-to-image") + payload = self._prepare_payload(prompt, parameters=parameters) + response = await self.post(json=payload.json, model=model, task="text-to-image") return _bytes_to_image(response) async def text_to_speech( @@ -2665,7 +2599,6 @@ async def text_to_speech( >>> Path("hello_world.flac").write_bytes(audio) ``` """ - payload: Dict[str, Any] = {"inputs": text} parameters = { "do_sample": do_sample, "early_stopping": early_stopping, @@ -2684,10 +2617,8 @@ async def text_to_speech( "typical_p": typical_p, "use_cache": use_cache, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, model=model, task="text-to-speech") + payload = self._prepare_payload(text, parameters=parameters) + response = await self.post(json=payload.json, model=model, task="text-to-speech") return response async def token_classification( @@ -2750,17 +2681,15 @@ async def token_classification( ] ``` """ - payload: Dict[str, Any] = {"inputs": text} + parameters = { "aggregation_strategy": aggregation_strategy, "ignore_labels": ignore_labels, "stride": stride, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value + payload = self._prepare_payload(text, parameters=parameters) response = await self.post( - json=payload, + json=payload.json, model=model, task="token-classification", ) @@ -2837,7 +2766,6 @@ async def translation( if src_lang is None and tgt_lang is not None: raise ValueError("You cannot specify `tgt_lang` without specifying `src_lang`.") - payload: Dict[str, Any] = {"inputs": text} parameters = { "src_lang": src_lang, "tgt_lang": tgt_lang, @@ -2845,10 +2773,8 @@ async def translation( "truncation": truncation, "generate_parameters": generate_parameters, } - for key, value in parameters.items(): - if value is not None: - payload.setdefault("parameters", {})[key] = value - response = await self.post(json=payload, model=model, task="translation") + payload = self._prepare_payload(text, parameters=parameters) + response = await self.post(json=payload.json, model=model, task="translation") return TranslationOutput.parse_obj_as_list(response)[0] async def visual_question_answering( @@ -2995,12 +2921,9 @@ async def zero_shot_classification( parameters = {"candidate_labels": labels, "multi_label": multi_label} if hypothesis_template is not None: parameters["hypothesis_template"] = hypothesis_template - + payload = self._prepare_payload(text, parameters=parameters) response = await self.post( - json={ - "inputs": text, - "parameters": parameters, - }, + json=payload.json, task="zero-shot-classification", model=model, ) @@ -3058,18 +2981,67 @@ async def zero_shot_image_classification( if len(labels) < 2: raise ValueError("You must specify at least 2 classes to compare.") - payload = { - "inputs": {"image": _b64_encode(image), "candidateLabels": ",".join(labels)}, - } - if hypothesis_template is not None: - payload.setdefault("parameters", {})["hypothesis_template"] = hypothesis_template + inputs = {"image": _b64_encode(image), "candidateLabels": ",".join(labels)} + parameters = {"hypothesis_template": hypothesis_template} if hypothesis_template is not None else None + payload = self._prepare_payload(inputs, parameters=parameters) response = await self.post( - json=payload, + json=payload.json, model=model, task="zero-shot-image-classification", ) return ZeroShotImageClassificationOutputElement.parse_obj_as_list(response) + @staticmethod + def _prepare_payload( + inputs: Union[str, Dict[str, Any], ContentT], + parameters: Optional[Dict[str, Any]] = None, + ) -> _InferenceInputs: + """ + Prepare payload for an API request, handling various input types and parameters. + + Args: + inputs (`Union[str, Dict[str, Any], ContentT]`): + The input data, which can be a string, dictionary, or raw content (e.g., image or audio bytes). + parameters (`Optional[Dict[str, Any]]`): + Optional inference parameters. + + Returns: + `_InferenceInputs`: + An instance of `_InferenceInputs` containing: + - The JSON payload (dict) if parameters are provided or inputs is not raw content, else None. + - The raw content (ContentT) if inputs is raw content and no parameters are provided, else None. + """ + + def is_raw_content(inputs: Union[str, ContentT]) -> bool: + return isinstance(inputs, (bytes, Path)) or ( + isinstance(inputs, str) and inputs.startswith(("http://", "https://")) + ) + + json = None + raw_data = None + if parameters is None: + parameters = {} + parameters = {k: v for k, v in parameters.items() if v is not None} + has_parameters = bool(parameters) + if not has_parameters and is_raw_content(inputs): + # Send inputs as raw content when no parameters are provided + raw_data = inputs + return _InferenceInputs(json, raw_data) + + json = {} + if isinstance(inputs, dict): + json.update(inputs) + elif isinstance(inputs, (bytes, Path)): + json["inputs"] = _b64_encode(inputs) + elif isinstance(inputs, str): + json["inputs"] = inputs + else: + raise TypeError(f"Unsupported type for inputs: {type(inputs)}") + + if has_parameters: + json["parameters"] = parameters + return _InferenceInputs(json, raw_data) + def _get_client_session(self, headers: Optional[Dict] = None) -> "ClientSession": aiohttp = _import_aiohttp() client_headers = self.headers.copy() diff --git a/tests/test_inference_client.py b/tests/test_inference_client.py index a4fa971a2b..3f81ec4a0f 100644 --- a/tests/test_inference_client.py +++ b/tests/test_inference_client.py @@ -1080,3 +1080,62 @@ def test_resolve_chat_completion_url( client = InferenceClient(model=client_model, base_url=client_base_url) url = client._resolve_chat_completion_url(model) assert url == expected_url + + +@pytest.mark.parametrize( + "inputs, parameters, expected_payload, expected_data", + [ + # Case 1: inputs is a simple string without parameters + ("simple text", None, {"inputs": "simple text"}, None), + # Case 2: inputs is a simple string with parameters + ("simple text", {"param1": "value1"}, {"inputs": "simple text", "parameters": {"param1": "value1"}}, None), + # Case 3: inputs is a dict without parameters + ({"input_key": "input_value"}, None, {"input_key": "input_value"}, None), + # Case 4: inputs is a dict with parameters + ( + {"input_key": "input_value"}, + {"param1": "value1"}, + {"input_key": "input_value", "parameters": {"param1": "value1"}}, + None, + ), + # Case 5: inputs is bytes without parameters + (b"binary data", None, None, b"binary data"), + # Case 6: inputs is bytes with parameters + ( + b"binary data", + {"param1": "value1"}, + {"inputs": "encoded_data", "parameters": {"param1": "value1"}}, + None, + ), + # Case 7: inputs is a Path object without parameters + (Path("test_file.txt"), None, None, Path("test_file.txt")), + # Case 8: inputs is a Path object with parameters + ( + Path("test_file.txt"), + {"param1": "value1"}, + {"inputs": "encoded_data", "parameters": {"param1": "value1"}}, + None, + ), + # Case 9: inputs is a URL string without parameters + ("http://example.com", None, None, "http://example.com"), + # Case 10: inputs is a URL string with parameters + ( + "http://example.com", + {"param1": "value1"}, + {"inputs": "http://example.com", "parameters": {"param1": "value1"}}, + None, + ), + # Case 11: parameters contain None values + ( + "simple text", + {"param1": None, "param2": "value2"}, + {"inputs": "simple text", "parameters": {"param2": "value2"}}, + None, + ), + ], +) +def test_prepare_payload(inputs, parameters, expected_payload, expected_data): + with patch("huggingface_hub.inference._client._b64_encode", return_value="encoded_data"): + payload = InferenceClient._prepare_payload(inputs, parameters) + assert payload.json == expected_payload + assert payload.raw_data == expected_data From bc85ecc69b0c702bc66f09b076deaa1b7353ba73 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Fri, 11 Oct 2024 15:32:55 +0200 Subject: [PATCH 02/14] Add comments --- src/huggingface_hub/inference/_client.py | 26 ++++--------------- .../inference/_generated/_async_client.py | 26 ++++--------------- 2 files changed, 10 insertions(+), 42 deletions(-) diff --git a/src/huggingface_hub/inference/_client.py b/src/huggingface_hub/inference/_client.py index 79f2f6990e..e35d9aaef7 100644 --- a/src/huggingface_hub/inference/_client.py +++ b/src/huggingface_hub/inference/_client.py @@ -2924,22 +2924,6 @@ def _prepare_payload( inputs: Union[str, Dict[str, Any], ContentT], parameters: Optional[Dict[str, Any]] = None, ) -> _InferenceInputs: - """ - Prepare payload for an API request, handling various input types and parameters. - - Args: - inputs (`Union[str, Dict[str, Any], ContentT]`): - The input data, which can be a string, dictionary, or raw content (e.g., image or audio bytes). - parameters (`Optional[Dict[str, Any]]`): - Optional inference parameters. - - Returns: - `_InferenceInputs`: - An instance of `_InferenceInputs` containing: - - The JSON payload (dict) if parameters are provided or inputs is not raw content, else None. - - The raw content (ContentT) if inputs is raw content and no parameters are provided, else None. - """ - def is_raw_content(inputs: Union[str, ContentT]) -> bool: return isinstance(inputs, (bytes, Path)) or ( isinstance(inputs, str) and inputs.startswith(("http://", "https://")) @@ -2951,21 +2935,21 @@ def is_raw_content(inputs: Union[str, ContentT]) -> bool: parameters = {} parameters = {k: v for k, v in parameters.items() if v is not None} has_parameters = bool(parameters) + # Send inputs as raw content when no parameters are provided if not has_parameters and is_raw_content(inputs): - # Send inputs as raw content when no parameters are provided raw_data = inputs return _InferenceInputs(json, raw_data) - json = {} + # If inputs is a dict, update the json payload with its content if isinstance(inputs, dict): json.update(inputs) + # If inputs is a bytes-like object, encode it to base64 elif isinstance(inputs, (bytes, Path)): json["inputs"] = _b64_encode(inputs) + # If inputs is a string, send it as is elif isinstance(inputs, str): json["inputs"] = inputs - else: - raise TypeError(f"Unsupported type for inputs: {type(inputs)}") - + # Add parameters to the json payload if any if has_parameters: json["parameters"] = parameters return _InferenceInputs(json, raw_data) diff --git a/src/huggingface_hub/inference/_generated/_async_client.py b/src/huggingface_hub/inference/_generated/_async_client.py index 5b5a979229..407554117b 100644 --- a/src/huggingface_hub/inference/_generated/_async_client.py +++ b/src/huggingface_hub/inference/_generated/_async_client.py @@ -2996,22 +2996,6 @@ def _prepare_payload( inputs: Union[str, Dict[str, Any], ContentT], parameters: Optional[Dict[str, Any]] = None, ) -> _InferenceInputs: - """ - Prepare payload for an API request, handling various input types and parameters. - - Args: - inputs (`Union[str, Dict[str, Any], ContentT]`): - The input data, which can be a string, dictionary, or raw content (e.g., image or audio bytes). - parameters (`Optional[Dict[str, Any]]`): - Optional inference parameters. - - Returns: - `_InferenceInputs`: - An instance of `_InferenceInputs` containing: - - The JSON payload (dict) if parameters are provided or inputs is not raw content, else None. - - The raw content (ContentT) if inputs is raw content and no parameters are provided, else None. - """ - def is_raw_content(inputs: Union[str, ContentT]) -> bool: return isinstance(inputs, (bytes, Path)) or ( isinstance(inputs, str) and inputs.startswith(("http://", "https://")) @@ -3023,21 +3007,21 @@ def is_raw_content(inputs: Union[str, ContentT]) -> bool: parameters = {} parameters = {k: v for k, v in parameters.items() if v is not None} has_parameters = bool(parameters) + # Send inputs as raw content when no parameters are provided if not has_parameters and is_raw_content(inputs): - # Send inputs as raw content when no parameters are provided raw_data = inputs return _InferenceInputs(json, raw_data) - json = {} + # If inputs is a dict, update the json payload with its content if isinstance(inputs, dict): json.update(inputs) + # If inputs is a bytes-like object, encode it to base64 elif isinstance(inputs, (bytes, Path)): json["inputs"] = _b64_encode(inputs) + # If inputs is a string, send it as is elif isinstance(inputs, str): json["inputs"] = inputs - else: - raise TypeError(f"Unsupported type for inputs: {type(inputs)}") - + # Add parameters to the json payload if any if has_parameters: json["parameters"] = parameters return _InferenceInputs(json, raw_data) From a939a76adbb9bb56f5e98a72cdb44c39f2f24b1f Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Fri, 11 Oct 2024 15:34:59 +0200 Subject: [PATCH 03/14] Add method description --- src/huggingface_hub/inference/_client.py | 4 ++++ src/huggingface_hub/inference/_generated/_async_client.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/huggingface_hub/inference/_client.py b/src/huggingface_hub/inference/_client.py index e35d9aaef7..353529343c 100644 --- a/src/huggingface_hub/inference/_client.py +++ b/src/huggingface_hub/inference/_client.py @@ -2924,6 +2924,10 @@ def _prepare_payload( inputs: Union[str, Dict[str, Any], ContentT], parameters: Optional[Dict[str, Any]] = None, ) -> _InferenceInputs: + """ + Prepare payload for an API request, handling various input types and parameters. + """ + def is_raw_content(inputs: Union[str, ContentT]) -> bool: return isinstance(inputs, (bytes, Path)) or ( isinstance(inputs, str) and inputs.startswith(("http://", "https://")) diff --git a/src/huggingface_hub/inference/_generated/_async_client.py b/src/huggingface_hub/inference/_generated/_async_client.py index 407554117b..5c1be7ab12 100644 --- a/src/huggingface_hub/inference/_generated/_async_client.py +++ b/src/huggingface_hub/inference/_generated/_async_client.py @@ -2996,6 +2996,10 @@ def _prepare_payload( inputs: Union[str, Dict[str, Any], ContentT], parameters: Optional[Dict[str, Any]] = None, ) -> _InferenceInputs: + """ + Prepare payload for an API request, handling various input types and parameters. + """ + def is_raw_content(inputs: Union[str, ContentT]) -> bool: return isinstance(inputs, (bytes, Path)) or ( isinstance(inputs, str) and inputs.startswith(("http://", "https://")) From 719c9e42e62e0d70b79b37ce72810d47b4f5bca1 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Fri, 11 Oct 2024 15:44:46 +0200 Subject: [PATCH 04/14] fix style --- src/huggingface_hub/inference/_client.py | 8 ++++---- src/huggingface_hub/inference/_generated/_async_client.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/huggingface_hub/inference/_client.py b/src/huggingface_hub/inference/_client.py index 353529343c..7c0d4368af 100644 --- a/src/huggingface_hub/inference/_client.py +++ b/src/huggingface_hub/inference/_client.py @@ -2928,20 +2928,20 @@ def _prepare_payload( Prepare payload for an API request, handling various input types and parameters. """ - def is_raw_content(inputs: Union[str, ContentT]) -> bool: + def is_raw_content(inputs: Union[str, Dict[str, Any], ContentT]) -> bool: return isinstance(inputs, (bytes, Path)) or ( isinstance(inputs, str) and inputs.startswith(("http://", "https://")) ) - json = None - raw_data = None + json: Dict[str, Any] | None = None + raw_data: ContentT | None = None if parameters is None: parameters = {} parameters = {k: v for k, v in parameters.items() if v is not None} has_parameters = bool(parameters) # Send inputs as raw content when no parameters are provided if not has_parameters and is_raw_content(inputs): - raw_data = inputs + raw_data = inputs # type: ignore return _InferenceInputs(json, raw_data) json = {} # If inputs is a dict, update the json payload with its content diff --git a/src/huggingface_hub/inference/_generated/_async_client.py b/src/huggingface_hub/inference/_generated/_async_client.py index 5c1be7ab12..ff66fe2c55 100644 --- a/src/huggingface_hub/inference/_generated/_async_client.py +++ b/src/huggingface_hub/inference/_generated/_async_client.py @@ -3000,20 +3000,20 @@ def _prepare_payload( Prepare payload for an API request, handling various input types and parameters. """ - def is_raw_content(inputs: Union[str, ContentT]) -> bool: + def is_raw_content(inputs: Union[str, Dict[str, Any], ContentT]) -> bool: return isinstance(inputs, (bytes, Path)) or ( isinstance(inputs, str) and inputs.startswith(("http://", "https://")) ) - json = None - raw_data = None + json: Dict[str, Any] | None = None + raw_data: ContentT | None = None if parameters is None: parameters = {} parameters = {k: v for k, v in parameters.items() if v is not None} has_parameters = bool(parameters) # Send inputs as raw content when no parameters are provided if not has_parameters and is_raw_content(inputs): - raw_data = inputs + raw_data = inputs # type: ignore return _InferenceInputs(json, raw_data) json = {} # If inputs is a dict, update the json payload with its content From 0d6a0589aa6f103868ef2eeb3e33f4f7cd4d66a8 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Fri, 11 Oct 2024 15:48:07 +0200 Subject: [PATCH 05/14] fix style again --- src/huggingface_hub/inference/_client.py | 4 ++-- src/huggingface_hub/inference/_generated/_async_client.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/huggingface_hub/inference/_client.py b/src/huggingface_hub/inference/_client.py index 7c0d4368af..252ca09c74 100644 --- a/src/huggingface_hub/inference/_client.py +++ b/src/huggingface_hub/inference/_client.py @@ -2933,8 +2933,8 @@ def is_raw_content(inputs: Union[str, Dict[str, Any], ContentT]) -> bool: isinstance(inputs, str) and inputs.startswith(("http://", "https://")) ) - json: Dict[str, Any] | None = None - raw_data: ContentT | None = None + json: Optional[Dict[str, Any]] = None + raw_data: Optional[ContentT] = None if parameters is None: parameters = {} parameters = {k: v for k, v in parameters.items() if v is not None} diff --git a/src/huggingface_hub/inference/_generated/_async_client.py b/src/huggingface_hub/inference/_generated/_async_client.py index ff66fe2c55..014a3c2893 100644 --- a/src/huggingface_hub/inference/_generated/_async_client.py +++ b/src/huggingface_hub/inference/_generated/_async_client.py @@ -3005,8 +3005,8 @@ def is_raw_content(inputs: Union[str, Dict[str, Any], ContentT]) -> bool: isinstance(inputs, str) and inputs.startswith(("http://", "https://")) ) - json: Dict[str, Any] | None = None - raw_data: ContentT | None = None + json: Optional[Dict[str, Any]] = None + raw_data: Optional[ContentT] = None if parameters is None: parameters = {} parameters = {k: v for k, v in parameters.items() if v is not None} From d669e790cbcbae2349ee901f6a052499f859e683 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 14 Oct 2024 11:41:53 +0200 Subject: [PATCH 06/14] fix prepare payload helper --- src/huggingface_hub/inference/_client.py | 130 +++++++----------- src/huggingface_hub/inference/_common.py | 45 +++++- .../inference/_generated/_async_client.py | 130 +++++++----------- tests/test_inference_client.py | 73 ++++++++-- 4 files changed, 192 insertions(+), 186 deletions(-) diff --git a/src/huggingface_hub/inference/_client.py b/src/huggingface_hub/inference/_client.py index 252ca09c74..bcddc0d887 100644 --- a/src/huggingface_hub/inference/_client.py +++ b/src/huggingface_hub/inference/_client.py @@ -37,8 +37,6 @@ import re import time import warnings -from dataclasses import dataclass -from pathlib import Path from typing import TYPE_CHECKING, Any, Dict, Iterable, List, Literal, Optional, Union, overload from requests import HTTPError @@ -59,6 +57,7 @@ _get_unsupported_text_generation_kwargs, _import_numpy, _open_as_binary, + _prepare_payload, _set_unsupported_text_generation_kwargs, _stream_chat_completion_response, _stream_text_generation_response, @@ -111,12 +110,6 @@ MODEL_KWARGS_NOT_USED_REGEX = re.compile(r"The following `model_kwargs` are not used by the model: \[(.*?)\]") -@dataclass -class _InferenceInputs: - json: Optional[Dict[str, Any]] = None - raw_data: Optional[ContentT] = None - - class InferenceClient: """ Initialize a new Inference Client. @@ -372,8 +365,8 @@ def audio_classification( ``` """ parameters = {"function_to_apply": function_to_apply, "top_k": top_k} - payload = self._prepare_payload(audio, parameters=parameters) - response = self.post(json=payload.json, data=payload.raw_data, model=model, task="audio-classification") + payload = _prepare_payload(audio, parameters=parameters, expect_binary=True) + response = self.post(**payload, model=model, task="audio-classification") return AudioClassificationOutputElement.parse_obj_as_list(response) def audio_to_audio( @@ -997,8 +990,8 @@ def document_question_answering( "top_k": top_k, "word_boxes": word_boxes, } - payload = self._prepare_payload(inputs, parameters=parameters) - response = self.post(json=payload.json, model=model, task="document-question-answering") + payload = _prepare_payload(inputs, parameters=parameters) + response = self.post(**payload, model=model, task="document-question-answering") return DocumentQuestionAnsweringOutputElement.parse_obj_as_list(response) def feature_extraction( @@ -1062,8 +1055,8 @@ def feature_extraction( "truncate": truncate, "truncation_direction": truncation_direction, } - payload = self._prepare_payload(text, parameters=parameters) - response = self.post(json=payload.json, model=model, task="feature-extraction") + payload = _prepare_payload(text, parameters=parameters) + response = self.post(**payload, model=model, task="feature-extraction") np = _import_numpy() return np.array(_bytes_to_dict(response), dtype="float32") @@ -1113,8 +1106,8 @@ def fill_mask( ``` """ parameters = {"targets": targets, "top_k": top_k} - payload = self._prepare_payload(text, parameters=parameters) - response = self.post(json=payload.json, model=model, task="fill-mask") + payload = _prepare_payload(text, parameters=parameters) + response = self.post(**payload, model=model, task="fill-mask") return FillMaskOutputElement.parse_obj_as_list(response) def image_classification( @@ -1156,8 +1149,8 @@ def image_classification( ``` """ parameters = {"function_to_apply": function_to_apply, "top_k": top_k} - payload = self._prepare_payload(image, parameters=parameters) - response = self.post(json=payload.json, data=payload.raw_data, model=model, task="image-classification") + payload = _prepare_payload(image, parameters=parameters, expect_binary=True) + response = self.post(**payload, model=model, task="image-classification") return ImageClassificationOutputElement.parse_obj_as_list(response) def image_segmentation( @@ -1216,8 +1209,8 @@ def image_segmentation( "subtask": subtask, "threshold": threshold, } - payload = self._prepare_payload(image, parameters=parameters) - response = self.post(json=payload.json, data=payload.raw_data, model=model, task="image-segmentation") + payload = _prepare_payload(image, parameters=parameters, expect_binary=True) + response = self.post(**payload, model=model, task="image-segmentation") output = ImageSegmentationOutputElement.parse_obj_as_list(response) for item in output: item.mask = _b64_to_image(item.mask) # type: ignore [assignment] @@ -1292,8 +1285,8 @@ def image_to_image( "guidance_scale": guidance_scale, **kwargs, } - payload = self._prepare_payload(image, parameters=parameters) - response = self.post(json=payload.json, data=payload.raw_data, model=model, task="image-to-image") + payload = _prepare_payload(image, parameters=parameters, expect_binary=True) + response = self.post(**payload, model=model, task="image-to-image") return _bytes_to_image(response) def image_to_text(self, image: ContentT, *, model: Optional[str] = None) -> ImageToTextOutput: @@ -1458,8 +1451,8 @@ def object_detection( parameters = { "threshold": threshold, } - payload = self._prepare_payload(image, parameters=parameters) - response = self.post(json=payload.json, data=payload.raw_data, model=model, task="object-detection") + payload = _prepare_payload(image, parameters=parameters, expect_binary=True) + response = self.post(**payload, model=model, task="object-detection") return ObjectDetectionOutputElement.parse_obj_as_list(response) def question_answering( @@ -1536,9 +1529,9 @@ def question_answering( "top_k": top_k, } inputs: Dict[str, Any] = {"question": question, "context": context} - payload = self._prepare_payload(inputs, parameters=parameters) + payload = _prepare_payload(inputs, parameters=parameters) response = self.post( - json=payload.json, + **payload, model=model, task="question-answering", ) @@ -1652,8 +1645,8 @@ def summarization( "generate_parameters": generate_parameters, "truncation": truncation, } - payload = self._prepare_payload(text, parameters=parameters) - response = self.post(json=payload.json, model=model, task="summarization") + payload = _prepare_payload(text, parameters=parameters) + response = self.post(**payload, model=model, task="summarization") return SummarizationOutput.parse_obj_as_list(response)[0] def table_question_answering( @@ -1702,9 +1695,9 @@ def table_question_answering( "query": query, "table": table, } - payload = self._prepare_payload(inputs, parameters=parameters) + payload = _prepare_payload(inputs, parameters=parameters) response = self.post( - json=payload.json, + **payload, model=model, task="table-question-answering", ) @@ -1752,7 +1745,11 @@ def tabular_classification(self, table: Dict[str, Any], *, model: Optional[str] ["5", "5", "5"] ``` """ - response = self.post(json={"table": table}, model=model, task="tabular-classification") + response = self.post( + json={"table": table}, + model=model, + task="tabular-classification", + ) return _bytes_to_list(response) def tabular_regression(self, table: Dict[str, Any], *, model: Optional[str] = None) -> List[float]: @@ -1842,8 +1839,12 @@ def text_classification( "function_to_apply": function_to_apply, "top_k": top_k, } - payload = self._prepare_payload(text, parameters=parameters) - response = self.post(json=payload.json, model=model, task="text-classification") + payload = _prepare_payload(text, parameters=parameters) + response = self.post( + **payload, + model=model, + task="text-classification", + ) return TextClassificationOutputElement.parse_obj_as_list(response)[0] # type: ignore [return-value] @overload @@ -2429,8 +2430,8 @@ def text_to_image( "seed": seed, **kwargs, } - payload = self._prepare_payload(prompt, parameters=parameters) - response = self.post(json=payload.json, model=model, task="text-to-image") + payload = _prepare_payload(prompt, parameters=parameters) + response = self.post(**payload, model=model, task="text-to-image") return _bytes_to_image(response) def text_to_speech( @@ -2551,8 +2552,8 @@ def text_to_speech( "typical_p": typical_p, "use_cache": use_cache, } - payload = self._prepare_payload(text, parameters=parameters) - response = self.post(json=payload.json, model=model, task="text-to-speech") + payload = _prepare_payload(text, parameters=parameters) + response = self.post(**payload, model=model, task="text-to-speech") return response def token_classification( @@ -2620,9 +2621,9 @@ def token_classification( "ignore_labels": ignore_labels, "stride": stride, } - payload = self._prepare_payload(text, parameters=parameters) + payload = _prepare_payload(text, parameters=parameters) response = self.post( - json=payload.json, + **payload, model=model, task="token-classification", ) @@ -2705,8 +2706,8 @@ def translation( "truncation": truncation, "generate_parameters": generate_parameters, } - payload = self._prepare_payload(text, parameters=parameters) - response = self.post(json=payload.json, model=model, task="translation") + payload = _prepare_payload(text, parameters=parameters) + response = self.post(**payload, model=model, task="translation") return TranslationOutput.parse_obj_as_list(response)[0] def visual_question_answering( @@ -2850,9 +2851,9 @@ def zero_shot_classification( parameters = {"candidate_labels": labels, "multi_label": multi_label} if hypothesis_template is not None: parameters["hypothesis_template"] = hypothesis_template - payload = self._prepare_payload(text, parameters=parameters) + payload = _prepare_payload(text, parameters=parameters) response = self.post( - json=payload.json, + **payload, task="zero-shot-classification", model=model, ) @@ -2911,53 +2912,14 @@ def zero_shot_image_classification( inputs = {"image": _b64_encode(image), "candidateLabels": ",".join(labels)} parameters = {"hypothesis_template": hypothesis_template} if hypothesis_template is not None else None - payload = self._prepare_payload(inputs, parameters=parameters) + payload = _prepare_payload(inputs, parameters=parameters) response = self.post( - json=payload.json, + **payload, model=model, task="zero-shot-image-classification", ) return ZeroShotImageClassificationOutputElement.parse_obj_as_list(response) - @staticmethod - def _prepare_payload( - inputs: Union[str, Dict[str, Any], ContentT], - parameters: Optional[Dict[str, Any]] = None, - ) -> _InferenceInputs: - """ - Prepare payload for an API request, handling various input types and parameters. - """ - - def is_raw_content(inputs: Union[str, Dict[str, Any], ContentT]) -> bool: - return isinstance(inputs, (bytes, Path)) or ( - isinstance(inputs, str) and inputs.startswith(("http://", "https://")) - ) - - json: Optional[Dict[str, Any]] = None - raw_data: Optional[ContentT] = None - if parameters is None: - parameters = {} - parameters = {k: v for k, v in parameters.items() if v is not None} - has_parameters = bool(parameters) - # Send inputs as raw content when no parameters are provided - if not has_parameters and is_raw_content(inputs): - raw_data = inputs # type: ignore - return _InferenceInputs(json, raw_data) - json = {} - # If inputs is a dict, update the json payload with its content - if isinstance(inputs, dict): - json.update(inputs) - # If inputs is a bytes-like object, encode it to base64 - elif isinstance(inputs, (bytes, Path)): - json["inputs"] = _b64_encode(inputs) - # If inputs is a string, send it as is - elif isinstance(inputs, str): - json["inputs"] = inputs - # Add parameters to the json payload if any - if has_parameters: - json["parameters"] = parameters - return _InferenceInputs(json, raw_data) - def _resolve_url(self, model: Optional[str] = None, task: Optional[str] = None) -> str: model = model or self.model or self.base_url diff --git a/src/huggingface_hub/inference/_common.py b/src/huggingface_hub/inference/_common.py index a92d8fad4a..0be7c5282f 100644 --- a/src/huggingface_hub/inference/_common.py +++ b/src/huggingface_hub/inference/_common.py @@ -58,10 +58,7 @@ is_numpy_available, is_pillow_available, ) -from ._generated.types import ( - ChatCompletionStreamOutput, - TextGenerationStreamOutput, -) +from ._generated.types import ChatCompletionStreamOutput, TextGenerationStreamOutput if TYPE_CHECKING: @@ -438,3 +435,43 @@ def _parse_text_generation_error(error: Optional[str], error_type: Optional[str] if error_type == "validation": return ValidationError(error) # type: ignore return UnknownError(error) # type: ignore + + +def _prepare_payload( + inputs: Union[str, Dict[str, Any], ContentT], + parameters: Optional[Dict[str, Any]], + expect_binary: bool = False, +) -> Dict[str, Any]: + """ + Prepare payload for an API request, handling various input types and parameters. + """ + if parameters is None: + parameters = {} + parameters = {k: v for k, v in parameters.items() if v is not None} + has_parameters = len(parameters) > 0 + + is_binary = isinstance(inputs, (bytes, Path)) + # If expect_binary is True, inputs must be a binary object or a local path or a URL. + if expect_binary and not is_binary and not isinstance(inputs, str): + raise ValueError("Expected binary inputs or a local path or a URL.") + # Send inputs as raw content when no parameters are provided + if expect_binary and not has_parameters: + return {"data": inputs} + # If expect_binary is False, inputs must not be a binary object. + if not expect_binary and is_binary: + raise ValueError("Unexpected binary inputs.") + + json: Dict[str, Any] = {} + # If inputs is a dict, update the json payload with its content + if isinstance(inputs, dict): + json.update(inputs) + # If inputs is a bytes-like object, encode it to base64 + elif isinstance(inputs, (bytes, Path)) or (isinstance(inputs, str) and inputs.startswith(("http://", "https://"))): + json["inputs"] = _b64_encode(inputs) + # If inputs is a string, send it as is + elif isinstance(inputs, str): + json["inputs"] = inputs + # Add parameters to the json payload if any + if has_parameters: + json["parameters"] = parameters + return {"json": json} diff --git a/src/huggingface_hub/inference/_generated/_async_client.py b/src/huggingface_hub/inference/_generated/_async_client.py index 014a3c2893..079901d65f 100644 --- a/src/huggingface_hub/inference/_generated/_async_client.py +++ b/src/huggingface_hub/inference/_generated/_async_client.py @@ -24,8 +24,6 @@ import re import time import warnings -from dataclasses import dataclass -from pathlib import Path from typing import TYPE_CHECKING, Any, AsyncIterable, Dict, List, Literal, Optional, Set, Union, overload from requests.structures import CaseInsensitiveDict @@ -47,6 +45,7 @@ _get_unsupported_text_generation_kwargs, _import_numpy, _open_as_binary, + _prepare_payload, _set_unsupported_text_generation_kwargs, raise_text_generation_error, ) @@ -100,12 +99,6 @@ MODEL_KWARGS_NOT_USED_REGEX = re.compile(r"The following `model_kwargs` are not used by the model: \[(.*?)\]") -@dataclass -class _InferenceInputs: - json: Optional[Dict[str, Any]] = None - raw_data: Optional[ContentT] = None - - class AsyncInferenceClient: """ Initialize a new Inference Client. @@ -406,8 +399,8 @@ async def audio_classification( ``` """ parameters = {"function_to_apply": function_to_apply, "top_k": top_k} - payload = self._prepare_payload(audio, parameters=parameters) - response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="audio-classification") + payload = _prepare_payload(audio, parameters=parameters, expect_binary=True) + response = await self.post(**payload, model=model, task="audio-classification") return AudioClassificationOutputElement.parse_obj_as_list(response) async def audio_to_audio( @@ -1040,8 +1033,8 @@ async def document_question_answering( "top_k": top_k, "word_boxes": word_boxes, } - payload = self._prepare_payload(inputs, parameters=parameters) - response = await self.post(json=payload.json, model=model, task="document-question-answering") + payload = _prepare_payload(inputs, parameters=parameters) + response = await self.post(**payload, model=model, task="document-question-answering") return DocumentQuestionAnsweringOutputElement.parse_obj_as_list(response) async def feature_extraction( @@ -1106,8 +1099,8 @@ async def feature_extraction( "truncate": truncate, "truncation_direction": truncation_direction, } - payload = self._prepare_payload(text, parameters=parameters) - response = await self.post(json=payload.json, model=model, task="feature-extraction") + payload = _prepare_payload(text, parameters=parameters) + response = await self.post(**payload, model=model, task="feature-extraction") np = _import_numpy() return np.array(_bytes_to_dict(response), dtype="float32") @@ -1158,8 +1151,8 @@ async def fill_mask( ``` """ parameters = {"targets": targets, "top_k": top_k} - payload = self._prepare_payload(text, parameters=parameters) - response = await self.post(json=payload.json, model=model, task="fill-mask") + payload = _prepare_payload(text, parameters=parameters) + response = await self.post(**payload, model=model, task="fill-mask") return FillMaskOutputElement.parse_obj_as_list(response) async def image_classification( @@ -1202,8 +1195,8 @@ async def image_classification( ``` """ parameters = {"function_to_apply": function_to_apply, "top_k": top_k} - payload = self._prepare_payload(image, parameters=parameters) - response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="image-classification") + payload = _prepare_payload(image, parameters=parameters, expect_binary=True) + response = await self.post(**payload, model=model, task="image-classification") return ImageClassificationOutputElement.parse_obj_as_list(response) async def image_segmentation( @@ -1263,8 +1256,8 @@ async def image_segmentation( "subtask": subtask, "threshold": threshold, } - payload = self._prepare_payload(image, parameters=parameters) - response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="image-segmentation") + payload = _prepare_payload(image, parameters=parameters, expect_binary=True) + response = await self.post(**payload, model=model, task="image-segmentation") output = ImageSegmentationOutputElement.parse_obj_as_list(response) for item in output: item.mask = _b64_to_image(item.mask) # type: ignore [assignment] @@ -1340,8 +1333,8 @@ async def image_to_image( "guidance_scale": guidance_scale, **kwargs, } - payload = self._prepare_payload(image, parameters=parameters) - response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="image-to-image") + payload = _prepare_payload(image, parameters=parameters, expect_binary=True) + response = await self.post(**payload, model=model, task="image-to-image") return _bytes_to_image(response) async def image_to_text(self, image: ContentT, *, model: Optional[str] = None) -> ImageToTextOutput: @@ -1514,8 +1507,8 @@ async def object_detection( parameters = { "threshold": threshold, } - payload = self._prepare_payload(image, parameters=parameters) - response = await self.post(json=payload.json, data=payload.raw_data, model=model, task="object-detection") + payload = _prepare_payload(image, parameters=parameters, expect_binary=True) + response = await self.post(**payload, model=model, task="object-detection") return ObjectDetectionOutputElement.parse_obj_as_list(response) async def question_answering( @@ -1593,9 +1586,9 @@ async def question_answering( "top_k": top_k, } inputs: Dict[str, Any] = {"question": question, "context": context} - payload = self._prepare_payload(inputs, parameters=parameters) + payload = _prepare_payload(inputs, parameters=parameters) response = await self.post( - json=payload.json, + **payload, model=model, task="question-answering", ) @@ -1711,8 +1704,8 @@ async def summarization( "generate_parameters": generate_parameters, "truncation": truncation, } - payload = self._prepare_payload(text, parameters=parameters) - response = await self.post(json=payload.json, model=model, task="summarization") + payload = _prepare_payload(text, parameters=parameters) + response = await self.post(**payload, model=model, task="summarization") return SummarizationOutput.parse_obj_as_list(response)[0] async def table_question_answering( @@ -1762,9 +1755,9 @@ async def table_question_answering( "query": query, "table": table, } - payload = self._prepare_payload(inputs, parameters=parameters) + payload = _prepare_payload(inputs, parameters=parameters) response = await self.post( - json=payload.json, + **payload, model=model, task="table-question-answering", ) @@ -1813,7 +1806,11 @@ async def tabular_classification(self, table: Dict[str, Any], *, model: Optional ["5", "5", "5"] ``` """ - response = await self.post(json={"table": table}, model=model, task="tabular-classification") + response = await self.post( + json={"table": table}, + model=model, + task="tabular-classification", + ) return _bytes_to_list(response) async def tabular_regression(self, table: Dict[str, Any], *, model: Optional[str] = None) -> List[float]: @@ -1905,8 +1902,12 @@ async def text_classification( "function_to_apply": function_to_apply, "top_k": top_k, } - payload = self._prepare_payload(text, parameters=parameters) - response = await self.post(json=payload.json, model=model, task="text-classification") + payload = _prepare_payload(text, parameters=parameters) + response = await self.post( + **payload, + model=model, + task="text-classification", + ) return TextClassificationOutputElement.parse_obj_as_list(response)[0] # type: ignore [return-value] @overload @@ -2494,8 +2495,8 @@ async def text_to_image( "seed": seed, **kwargs, } - payload = self._prepare_payload(prompt, parameters=parameters) - response = await self.post(json=payload.json, model=model, task="text-to-image") + payload = _prepare_payload(prompt, parameters=parameters) + response = await self.post(**payload, model=model, task="text-to-image") return _bytes_to_image(response) async def text_to_speech( @@ -2617,8 +2618,8 @@ async def text_to_speech( "typical_p": typical_p, "use_cache": use_cache, } - payload = self._prepare_payload(text, parameters=parameters) - response = await self.post(json=payload.json, model=model, task="text-to-speech") + payload = _prepare_payload(text, parameters=parameters) + response = await self.post(**payload, model=model, task="text-to-speech") return response async def token_classification( @@ -2687,9 +2688,9 @@ async def token_classification( "ignore_labels": ignore_labels, "stride": stride, } - payload = self._prepare_payload(text, parameters=parameters) + payload = _prepare_payload(text, parameters=parameters) response = await self.post( - json=payload.json, + **payload, model=model, task="token-classification", ) @@ -2773,8 +2774,8 @@ async def translation( "truncation": truncation, "generate_parameters": generate_parameters, } - payload = self._prepare_payload(text, parameters=parameters) - response = await self.post(json=payload.json, model=model, task="translation") + payload = _prepare_payload(text, parameters=parameters) + response = await self.post(**payload, model=model, task="translation") return TranslationOutput.parse_obj_as_list(response)[0] async def visual_question_answering( @@ -2921,9 +2922,9 @@ async def zero_shot_classification( parameters = {"candidate_labels": labels, "multi_label": multi_label} if hypothesis_template is not None: parameters["hypothesis_template"] = hypothesis_template - payload = self._prepare_payload(text, parameters=parameters) + payload = _prepare_payload(text, parameters=parameters) response = await self.post( - json=payload.json, + **payload, task="zero-shot-classification", model=model, ) @@ -2983,53 +2984,14 @@ async def zero_shot_image_classification( inputs = {"image": _b64_encode(image), "candidateLabels": ",".join(labels)} parameters = {"hypothesis_template": hypothesis_template} if hypothesis_template is not None else None - payload = self._prepare_payload(inputs, parameters=parameters) + payload = _prepare_payload(inputs, parameters=parameters) response = await self.post( - json=payload.json, + **payload, model=model, task="zero-shot-image-classification", ) return ZeroShotImageClassificationOutputElement.parse_obj_as_list(response) - @staticmethod - def _prepare_payload( - inputs: Union[str, Dict[str, Any], ContentT], - parameters: Optional[Dict[str, Any]] = None, - ) -> _InferenceInputs: - """ - Prepare payload for an API request, handling various input types and parameters. - """ - - def is_raw_content(inputs: Union[str, Dict[str, Any], ContentT]) -> bool: - return isinstance(inputs, (bytes, Path)) or ( - isinstance(inputs, str) and inputs.startswith(("http://", "https://")) - ) - - json: Optional[Dict[str, Any]] = None - raw_data: Optional[ContentT] = None - if parameters is None: - parameters = {} - parameters = {k: v for k, v in parameters.items() if v is not None} - has_parameters = bool(parameters) - # Send inputs as raw content when no parameters are provided - if not has_parameters and is_raw_content(inputs): - raw_data = inputs # type: ignore - return _InferenceInputs(json, raw_data) - json = {} - # If inputs is a dict, update the json payload with its content - if isinstance(inputs, dict): - json.update(inputs) - # If inputs is a bytes-like object, encode it to base64 - elif isinstance(inputs, (bytes, Path)): - json["inputs"] = _b64_encode(inputs) - # If inputs is a string, send it as is - elif isinstance(inputs, str): - json["inputs"] = inputs - # Add parameters to the json payload if any - if has_parameters: - json["parameters"] = parameters - return _InferenceInputs(json, raw_data) - def _get_client_session(self, headers: Optional[Dict] = None) -> "ClientSession": aiohttp = _import_aiohttp() client_headers = self.headers.copy() diff --git a/tests/test_inference_client.py b/tests/test_inference_client.py index 3f81ec4a0f..394bc322e3 100644 --- a/tests/test_inference_client.py +++ b/tests/test_inference_client.py @@ -49,7 +49,11 @@ from huggingface_hub.constants import ALL_INFERENCE_API_FRAMEWORKS, MAIN_INFERENCE_API_FRAMEWORKS from huggingface_hub.errors import HfHubHTTPError, ValidationError from huggingface_hub.inference._client import _open_as_binary -from huggingface_hub.inference._common import _stream_chat_completion_response, _stream_text_generation_response +from huggingface_hub.inference._common import ( + _prepare_payload, + _stream_chat_completion_response, + _stream_text_generation_response, +) from huggingface_hub.utils import build_hf_headers from .testing_utils import with_production_testing @@ -1083,59 +1087,100 @@ def test_resolve_chat_completion_url( @pytest.mark.parametrize( - "inputs, parameters, expected_payload, expected_data", + "inputs, parameters, expect_binary, expected_json, expected_data", [ # Case 1: inputs is a simple string without parameters - ("simple text", None, {"inputs": "simple text"}, None), + ( + "simple text", + None, + False, + {"inputs": "simple text"}, + None, + ), # Case 2: inputs is a simple string with parameters - ("simple text", {"param1": "value1"}, {"inputs": "simple text", "parameters": {"param1": "value1"}}, None), + ( + "simple text", + {"param1": "value1"}, + False, + {"inputs": "simple text", "parameters": {"param1": "value1"}}, + None, + ), # Case 3: inputs is a dict without parameters - ({"input_key": "input_value"}, None, {"input_key": "input_value"}, None), + ( + {"input_key": "input_value"}, + None, + False, + {"input_key": "input_value"}, + None, + ), # Case 4: inputs is a dict with parameters ( {"input_key": "input_value"}, {"param1": "value1"}, + False, {"input_key": "input_value", "parameters": {"param1": "value1"}}, None, ), # Case 5: inputs is bytes without parameters - (b"binary data", None, None, b"binary data"), + ( + b"binary data", + None, + True, + None, + b"binary data", + ), # Case 6: inputs is bytes with parameters ( b"binary data", {"param1": "value1"}, + True, {"inputs": "encoded_data", "parameters": {"param1": "value1"}}, None, ), # Case 7: inputs is a Path object without parameters - (Path("test_file.txt"), None, None, Path("test_file.txt")), + ( + Path("test_file.txt"), + None, + True, + None, + Path("test_file.txt"), + ), # Case 8: inputs is a Path object with parameters ( Path("test_file.txt"), {"param1": "value1"}, + True, {"inputs": "encoded_data", "parameters": {"param1": "value1"}}, None, ), # Case 9: inputs is a URL string without parameters - ("http://example.com", None, None, "http://example.com"), + ( + "http://example.com", + None, + True, + None, + "http://example.com", + ), # Case 10: inputs is a URL string with parameters ( "http://example.com", {"param1": "value1"}, - {"inputs": "http://example.com", "parameters": {"param1": "value1"}}, + True, + {"inputs": "encoded_data", "parameters": {"param1": "value1"}}, None, ), # Case 11: parameters contain None values ( "simple text", {"param1": None, "param2": "value2"}, + False, {"inputs": "simple text", "parameters": {"param2": "value2"}}, None, ), ], ) -def test_prepare_payload(inputs, parameters, expected_payload, expected_data): - with patch("huggingface_hub.inference._client._b64_encode", return_value="encoded_data"): - payload = InferenceClient._prepare_payload(inputs, parameters) - assert payload.json == expected_payload - assert payload.raw_data == expected_data +def test_prepare_payload(inputs, parameters, expect_binary, expected_json, expected_data): + with patch("huggingface_hub.inference._common._b64_encode", return_value="encoded_data"): + payload = _prepare_payload(inputs, parameters, expect_binary=expect_binary) + assert payload.get("json") == expected_json + assert payload.get("data") == expected_data From 64fc43cfaf1ac43ca549500f1bd004880dc4203c Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 14 Oct 2024 11:59:46 +0200 Subject: [PATCH 07/14] experiment: try old version of workflow --- .github/workflows/build_pr_documentation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_pr_documentation.yaml b/.github/workflows/build_pr_documentation.yaml index b41ac2b036..50c4b2d58a 100644 --- a/.github/workflows/build_pr_documentation.yaml +++ b/.github/workflows/build_pr_documentation.yaml @@ -9,7 +9,7 @@ concurrency: jobs: build: - uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@main + uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@f694f033afdba723e459c394cd8b690c6c53edb1 with: commit_sha: ${{ github.event.pull_request.head.sha }} pr_number: ${{ github.event.number }} From 59eb39a8d429d97e6ef93f9a1ce25725035e3578 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 14 Oct 2024 12:04:52 +0200 Subject: [PATCH 08/14] revert experiment: try old version of workflow --- .github/workflows/build_pr_documentation.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_pr_documentation.yaml b/.github/workflows/build_pr_documentation.yaml index 50c4b2d58a..b41ac2b036 100644 --- a/.github/workflows/build_pr_documentation.yaml +++ b/.github/workflows/build_pr_documentation.yaml @@ -9,7 +9,7 @@ concurrency: jobs: build: - uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@f694f033afdba723e459c394cd8b690c6c53edb1 + uses: huggingface/doc-builder/.github/workflows/build_pr_documentation.yml@main with: commit_sha: ${{ github.event.pull_request.head.sha }} pr_number: ${{ github.event.number }} From fb8e864c57fd7c47af231aa42c0964690af67316 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 14 Oct 2024 12:11:49 +0200 Subject: [PATCH 09/14] Add docstring --- src/huggingface_hub/inference/_common.py | 91 +++++++++++++----------- 1 file changed, 51 insertions(+), 40 deletions(-) diff --git a/src/huggingface_hub/inference/_common.py b/src/huggingface_hub/inference/_common.py index 0be7c5282f..3d0921d375 100644 --- a/src/huggingface_hub/inference/_common.py +++ b/src/huggingface_hub/inference/_common.py @@ -256,6 +256,57 @@ def _bytes_to_image(content: bytes) -> "Image": return Image.open(io.BytesIO(content)) +## PAYLOAD UTILS + + +def _prepare_payload( + inputs: Union[str, Dict[str, Any], ContentT], + parameters: Optional[Dict[str, Any]], + expect_binary: bool = False, +) -> Dict[str, Any]: + """ + Prepare payload for an API request, handling various input types and parameters. + + Args: + inputs (`Union[str, Dict[str, Any], ContentT]`): + The input data. Can be a string, a dictionary, a binary object or a local path or URL. + parameters (`Dict[str, Any]`): + The inference parameters. + expect_binary (`bool`, defaults to `False`): + If `True`, the inputs must be a binary object or a local path or a URL. + """ + if parameters is None: + parameters = {} + parameters = {k: v for k, v in parameters.items() if v is not None} + has_parameters = len(parameters) > 0 + + is_binary = isinstance(inputs, (bytes, Path)) + # If expect_binary is True, inputs must be a binary object or a local path or a URL. + if expect_binary and not is_binary and not isinstance(inputs, str): + raise ValueError("Expected binary inputs or a local path or a URL.") + # Send inputs as raw content when no parameters are provided + if expect_binary and not has_parameters: + return {"data": inputs} + # If expect_binary is False, inputs must not be a binary object. + if not expect_binary and is_binary: + raise ValueError("Unexpected binary inputs.") + + json: Dict[str, Any] = {} + # If inputs is a dict, update the json payload with its content + if isinstance(inputs, dict): + json.update(inputs) + # If inputs is a bytes-like object, encode it to base64 + elif isinstance(inputs, (bytes, Path)) or (isinstance(inputs, str) and inputs.startswith(("http://", "https://"))): + json["inputs"] = _b64_encode(inputs) + # If inputs is a string, send it as is + elif isinstance(inputs, str): + json["inputs"] = inputs + # Add parameters to the json payload if any + if has_parameters: + json["parameters"] = parameters + return {"json": json} + + ## STREAMING UTILS @@ -435,43 +486,3 @@ def _parse_text_generation_error(error: Optional[str], error_type: Optional[str] if error_type == "validation": return ValidationError(error) # type: ignore return UnknownError(error) # type: ignore - - -def _prepare_payload( - inputs: Union[str, Dict[str, Any], ContentT], - parameters: Optional[Dict[str, Any]], - expect_binary: bool = False, -) -> Dict[str, Any]: - """ - Prepare payload for an API request, handling various input types and parameters. - """ - if parameters is None: - parameters = {} - parameters = {k: v for k, v in parameters.items() if v is not None} - has_parameters = len(parameters) > 0 - - is_binary = isinstance(inputs, (bytes, Path)) - # If expect_binary is True, inputs must be a binary object or a local path or a URL. - if expect_binary and not is_binary and not isinstance(inputs, str): - raise ValueError("Expected binary inputs or a local path or a URL.") - # Send inputs as raw content when no parameters are provided - if expect_binary and not has_parameters: - return {"data": inputs} - # If expect_binary is False, inputs must not be a binary object. - if not expect_binary and is_binary: - raise ValueError("Unexpected binary inputs.") - - json: Dict[str, Any] = {} - # If inputs is a dict, update the json payload with its content - if isinstance(inputs, dict): - json.update(inputs) - # If inputs is a bytes-like object, encode it to base64 - elif isinstance(inputs, (bytes, Path)) or (isinstance(inputs, str) and inputs.startswith(("http://", "https://"))): - json["inputs"] = _b64_encode(inputs) - # If inputs is a string, send it as is - elif isinstance(inputs, str): - json["inputs"] = inputs - # Add parameters to the json payload if any - if has_parameters: - json["parameters"] = parameters - return {"json": json} From bfe82f9abe7a0f3b9728866772260e96e7cacc3e Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 14 Oct 2024 12:23:22 +0200 Subject: [PATCH 10/14] update docstring --- src/huggingface_hub/inference/_common.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/huggingface_hub/inference/_common.py b/src/huggingface_hub/inference/_common.py index 3d0921d375..196b72b743 100644 --- a/src/huggingface_hub/inference/_common.py +++ b/src/huggingface_hub/inference/_common.py @@ -265,15 +265,8 @@ def _prepare_payload( expect_binary: bool = False, ) -> Dict[str, Any]: """ - Prepare payload for an API request, handling various input types and parameters. - - Args: - inputs (`Union[str, Dict[str, Any], ContentT]`): - The input data. Can be a string, a dictionary, a binary object or a local path or URL. - parameters (`Dict[str, Any]`): - The inference parameters. - expect_binary (`bool`, defaults to `False`): - If `True`, the inputs must be a binary object or a local path or a URL. + Used in `InferenceClient` and `AsyncInferenceClient` to prepare the payload for an API request, handling various input types and parameters. + `expect_binary` is set to `True` when the inputs are a binary object or a local path or URL. This is the case for image and audio inputs. """ if parameters is None: parameters = {} From efef1a91e33ed8b56081759ce8a24384f9541aa6 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 14 Oct 2024 15:59:27 +0200 Subject: [PATCH 11/14] simplify json payload construction when inputs is a dict --- src/huggingface_hub/inference/_common.py | 13 ++++----- tests/test_inference_client.py | 34 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/huggingface_hub/inference/_common.py b/src/huggingface_hub/inference/_common.py index 196b72b743..949fd5b20b 100644 --- a/src/huggingface_hub/inference/_common.py +++ b/src/huggingface_hub/inference/_common.py @@ -276,23 +276,20 @@ def _prepare_payload( is_binary = isinstance(inputs, (bytes, Path)) # If expect_binary is True, inputs must be a binary object or a local path or a URL. if expect_binary and not is_binary and not isinstance(inputs, str): - raise ValueError("Expected binary inputs or a local path or a URL.") + raise ValueError(f"Expected binary inputs or a local path or a URL. Got {inputs}") # Send inputs as raw content when no parameters are provided if expect_binary and not has_parameters: return {"data": inputs} # If expect_binary is False, inputs must not be a binary object. if not expect_binary and is_binary: - raise ValueError("Unexpected binary inputs.") + raise ValueError(f"Unexpected binary inputs. Got {inputs}") json: Dict[str, Any] = {} - # If inputs is a dict, update the json payload with its content - if isinstance(inputs, dict): - json.update(inputs) # If inputs is a bytes-like object, encode it to base64 - elif isinstance(inputs, (bytes, Path)) or (isinstance(inputs, str) and inputs.startswith(("http://", "https://"))): + if isinstance(inputs, (bytes, Path)) or (isinstance(inputs, str) and inputs.startswith(("http://", "https://"))): json["inputs"] = _b64_encode(inputs) - # If inputs is a string, send it as is - elif isinstance(inputs, str): + # If inputs is a string or a dict, send it as is + elif isinstance(inputs, (dict, str)): json["inputs"] = inputs # Add parameters to the json payload if any if has_parameters: diff --git a/tests/test_inference_client.py b/tests/test_inference_client.py index 394bc322e3..c580c284b6 100644 --- a/tests/test_inference_client.py +++ b/tests/test_inference_client.py @@ -1102,7 +1102,10 @@ def test_resolve_chat_completion_url( "simple text", {"param1": "value1"}, False, - {"inputs": "simple text", "parameters": {"param1": "value1"}}, + { + "inputs": "simple text", + "parameters": {"param1": "value1"}, + }, None, ), # Case 3: inputs is a dict without parameters @@ -1110,15 +1113,18 @@ def test_resolve_chat_completion_url( {"input_key": "input_value"}, None, False, - {"input_key": "input_value"}, + {"inputs": {"input_key": "input_value"}}, None, ), # Case 4: inputs is a dict with parameters ( - {"input_key": "input_value"}, + {"input_key": "input_value", "input_key2": "input_value2"}, {"param1": "value1"}, False, - {"input_key": "input_value", "parameters": {"param1": "value1"}}, + { + "inputs": {"input_key": "input_value", "input_key2": "input_value2"}, + "parameters": {"param1": "value1"}, + }, None, ), # Case 5: inputs is bytes without parameters @@ -1134,7 +1140,10 @@ def test_resolve_chat_completion_url( b"binary data", {"param1": "value1"}, True, - {"inputs": "encoded_data", "parameters": {"param1": "value1"}}, + { + "inputs": "encoded_data", + "parameters": {"param1": "value1"}, + }, None, ), # Case 7: inputs is a Path object without parameters @@ -1150,7 +1159,10 @@ def test_resolve_chat_completion_url( Path("test_file.txt"), {"param1": "value1"}, True, - {"inputs": "encoded_data", "parameters": {"param1": "value1"}}, + { + "inputs": "encoded_data", + "parameters": {"param1": "value1"}, + }, None, ), # Case 9: inputs is a URL string without parameters @@ -1166,7 +1178,10 @@ def test_resolve_chat_completion_url( "http://example.com", {"param1": "value1"}, True, - {"inputs": "encoded_data", "parameters": {"param1": "value1"}}, + { + "inputs": "encoded_data", + "parameters": {"param1": "value1"}, + }, None, ), # Case 11: parameters contain None values @@ -1174,7 +1189,10 @@ def test_resolve_chat_completion_url( "simple text", {"param1": None, "param2": "value2"}, False, - {"inputs": "simple text", "parameters": {"param2": "value2"}}, + { + "inputs": "simple text", + "parameters": {"param2": "value2"}, + }, None, ), ], From b4bd2b57e69739116ec6a4de03e7736f7706ebd6 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Mon, 14 Oct 2024 16:04:51 +0200 Subject: [PATCH 12/14] ignore mypy str bytes warning --- src/huggingface_hub/inference/_common.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/huggingface_hub/inference/_common.py b/src/huggingface_hub/inference/_common.py index 949fd5b20b..2829356a90 100644 --- a/src/huggingface_hub/inference/_common.py +++ b/src/huggingface_hub/inference/_common.py @@ -276,13 +276,13 @@ def _prepare_payload( is_binary = isinstance(inputs, (bytes, Path)) # If expect_binary is True, inputs must be a binary object or a local path or a URL. if expect_binary and not is_binary and not isinstance(inputs, str): - raise ValueError(f"Expected binary inputs or a local path or a URL. Got {inputs}") + raise ValueError(f"Expected binary inputs or a local path or a URL. Got {inputs}") # type: ignore # Send inputs as raw content when no parameters are provided if expect_binary and not has_parameters: return {"data": inputs} # If expect_binary is False, inputs must not be a binary object. if not expect_binary and is_binary: - raise ValueError(f"Unexpected binary inputs. Got {inputs}") + raise ValueError(f"Unexpected binary inputs. Got {inputs}") # type: ignore json: Dict[str, Any] = {} # If inputs is a bytes-like object, encode it to base64 From 9ac79df56507586cfada4853afcd33a63dc872f7 Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Tue, 15 Oct 2024 12:54:53 +0200 Subject: [PATCH 13/14] fix encoding condition --- src/huggingface_hub/inference/_common.py | 8 ++++---- tests/test_inference_client.py | 25 ++++++++++++++++++++++-- 2 files changed, 27 insertions(+), 6 deletions(-) diff --git a/src/huggingface_hub/inference/_common.py b/src/huggingface_hub/inference/_common.py index 2829356a90..a19636a506 100644 --- a/src/huggingface_hub/inference/_common.py +++ b/src/huggingface_hub/inference/_common.py @@ -286,10 +286,10 @@ def _prepare_payload( json: Dict[str, Any] = {} # If inputs is a bytes-like object, encode it to base64 - if isinstance(inputs, (bytes, Path)) or (isinstance(inputs, str) and inputs.startswith(("http://", "https://"))): - json["inputs"] = _b64_encode(inputs) - # If inputs is a string or a dict, send it as is - elif isinstance(inputs, (dict, str)): + if expect_binary: + json["inputs"] = _b64_encode(inputs) # type: ignore + # Otherwise (string, dict, list) send it as is + else: json["inputs"] = inputs # Add parameters to the json payload if any if has_parameters: diff --git a/tests/test_inference_client.py b/tests/test_inference_client.py index c580c284b6..b97c62d165 100644 --- a/tests/test_inference_client.py +++ b/tests/test_inference_client.py @@ -1173,7 +1173,17 @@ def test_resolve_chat_completion_url( None, "http://example.com", ), - # Case 10: inputs is a URL string with parameters + # Case 10: inputs is a URL string without parameters but expect_binary is False + ( + "http://example.com", + None, + False, + { + "inputs": "http://example.com", + }, + None, + ), + # Case 11: inputs is a URL string with parameters ( "http://example.com", {"param1": "value1"}, @@ -1184,7 +1194,18 @@ def test_resolve_chat_completion_url( }, None, ), - # Case 11: parameters contain None values + # Case 12: inputs is a URL string with parameters but expect_binary is False + ( + "http://example.com", + {"param1": "value1"}, + False, + { + "inputs": "http://example.com", + "parameters": {"param1": "value1"}, + }, + None, + ), + # Case 13: parameters contain None values ( "simple text", {"param1": None, "param2": "value2"}, From c8e7cc830a87b774caec959ac74f58078568f5da Mon Sep 17 00:00:00 2001 From: Celina Hanouti Date: Tue, 15 Oct 2024 15:17:56 +0200 Subject: [PATCH 14/14] remove unnecessary checks for parameters --- src/huggingface_hub/inference/_client.py | 10 ++++++---- .../inference/_generated/_async_client.py | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/huggingface_hub/inference/_client.py b/src/huggingface_hub/inference/_client.py index bcddc0d887..8d5a8e6a38 100644 --- a/src/huggingface_hub/inference/_client.py +++ b/src/huggingface_hub/inference/_client.py @@ -2848,9 +2848,11 @@ def zero_shot_classification( ``` """ - parameters = {"candidate_labels": labels, "multi_label": multi_label} - if hypothesis_template is not None: - parameters["hypothesis_template"] = hypothesis_template + parameters = { + "candidate_labels": labels, + "multi_label": multi_label, + "hypothesis_template": hypothesis_template, + } payload = _prepare_payload(text, parameters=parameters) response = self.post( **payload, @@ -2911,7 +2913,7 @@ def zero_shot_image_classification( raise ValueError("You must specify at least 2 classes to compare.") inputs = {"image": _b64_encode(image), "candidateLabels": ",".join(labels)} - parameters = {"hypothesis_template": hypothesis_template} if hypothesis_template is not None else None + parameters = {"hypothesis_template": hypothesis_template} payload = _prepare_payload(inputs, parameters=parameters) response = self.post( **payload, diff --git a/src/huggingface_hub/inference/_generated/_async_client.py b/src/huggingface_hub/inference/_generated/_async_client.py index 079901d65f..5c3a8044fc 100644 --- a/src/huggingface_hub/inference/_generated/_async_client.py +++ b/src/huggingface_hub/inference/_generated/_async_client.py @@ -2919,9 +2919,11 @@ async def zero_shot_classification( ``` """ - parameters = {"candidate_labels": labels, "multi_label": multi_label} - if hypothesis_template is not None: - parameters["hypothesis_template"] = hypothesis_template + parameters = { + "candidate_labels": labels, + "multi_label": multi_label, + "hypothesis_template": hypothesis_template, + } payload = _prepare_payload(text, parameters=parameters) response = await self.post( **payload, @@ -2983,7 +2985,7 @@ async def zero_shot_image_classification( raise ValueError("You must specify at least 2 classes to compare.") inputs = {"image": _b64_encode(image), "candidateLabels": ",".join(labels)} - parameters = {"hypothesis_template": hypothesis_template} if hypothesis_template is not None else None + parameters = {"hypothesis_template": hypothesis_template} payload = _prepare_payload(inputs, parameters=parameters) response = await self.post( **payload,