Final typing: interpretation.py and helpers.py (#2911)

* started pathlib

* blocks.py

* more changes

* fixes

* typing

* formatting

* typing

* renaming files

* changelog

* script

* changelog

* lint

* routes

* renamed

* state

* formatting

* state

* type check script

* remove strictness

* switched to pyright

* switched to pyright

* fixed flaky tests

* fixed test xray

* fixed load test

* fixed blocks tests

* formatting

* fixed components test

* uncomment tests

* fixed interpretation tests

* formatting

* last tests hopefully

* argh lint

* component

* fixed based on review

* refactor

* components.py t yping

* components.py

* formatting

* lint script

* merge

* merge

* lint

* pathlib

* lint

* events too

* lint script

* fixing tests

* lint

* examples

* serializing

* more files

* formatting

* flagging.py

* added to lint script

* fixed tab

* interface.py

* attempt fix

* refactoring interface

* interface refactor

* formatting

* fix for live interfaces

* lint

* refactor interpret

* more typing

* undo blocks change

* undo test comp change

* created classes

* interpretation

* fix

* final fixes to interpretation

* type check

* helpers

* type hintg

* label

* initialize interpret

* fixed progress bar

* removed unnecessary assertion
This commit is contained in:
Abubakar Abid 2023-01-04 14:30:21 -05:00 committed by GitHub
parent ad42e80dca
commit c02001da7d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 190 additions and 483 deletions

View File

@ -82,5 +82,7 @@ from gradio.templates import (
Webcam, Webcam,
) )
current_pkg_version = pkgutil.get_data(__name__, "version.txt").decode("ascii").strip() current_pkg_version = (
(pkgutil.get_data(__name__, "version.txt") or b"").decode("ascii").strip()
)
__version__ = current_pkg_version __version__ = current_pkg_version

View File

@ -46,6 +46,7 @@ from gradio.events import (
Submittable, Submittable,
Uploadable, Uploadable,
) )
from gradio.interpretation import NeighborInterpretable, TokenInterpretable
from gradio.layouts import Column, Form, Row from gradio.layouts import Column, Form, Row
from gradio.processing_utils import TempFileManager from gradio.processing_utils import TempFileManager
from gradio.serializing import ( from gradio.serializing import (
@ -186,8 +187,6 @@ class IOComponent(Component, Serializable):
if callable(load_fn): if callable(load_fn):
self.load_event = self.attach_load_event(load_fn, every) self.load_event = self.attach_load_event(load_fn, every)
self.set_interpret_parameters()
def get_config(self): def get_config(self):
return { return {
"label": self.label, "label": self.label,
@ -196,38 +195,6 @@ class IOComponent(Component, Serializable):
**super().get_config(), **super().get_config(),
} }
def set_interpret_parameters(self):
"""
Set any parameters for interpretation.
"""
return self
def get_interpretation_neighbors(self, x: Any) -> Tuple[List, Dict, bool]:
"""
Generates values similar to input to be used to interpret the significance of the input in the final output.
Parameters:
x: Input to interface
Returns: (neighbor_values, interpret_kwargs, interpret_by_removal)
neighbor_values: Neighboring values to input x to compute for interpretation
interpret_kwargs: Keyword arguments to be passed to get_interpretation_scores
interpret_by_removal: If True, returned neighbors are values where the interpreted subsection was removed. If False, returned neighbors are values where the interpreted subsection was modified to a different value.
"""
return [], {}, True
def get_interpretation_scores(
self, x: Any, neighbors: List[Any], scores: List[float], **kwargs
) -> List:
"""
Arrange the output values from the neighbors into interpretation scores for the interface to render.
Parameters:
x: Input to interface
neighbors: Neighboring values to input x used for interpretation.
scores: Output value corresponding to each neighbor in neighbors
Returns:
Arrangement of interpretation scores for interfaces to render.
"""
return []
def generate_sample(self) -> Any: def generate_sample(self) -> Any:
""" """
Returns a sample value of the input that would be accepted by the api. Used for api documentation. Returns a sample value of the input that would be accepted by the api. Used for api documentation.
@ -275,7 +242,13 @@ class FormComponent:
@document("change", "submit", "blur", "style") @document("change", "submit", "blur", "style")
class Textbox( class Textbox(
FormComponent, Changeable, Submittable, Blurrable, IOComponent, SimpleSerializable FormComponent,
Changeable,
Submittable,
Blurrable,
IOComponent,
SimpleSerializable,
TokenInterpretable,
): ):
""" """
Creates a textarea for user to enter string input or display string output. Creates a textarea for user to enter string input or display string output.
@ -324,7 +297,6 @@ class Textbox(
self.lines = lines self.lines = lines
self.max_lines = max_lines if type == "text" else 1 self.max_lines = max_lines if type == "text" else 1
self.placeholder = placeholder self.placeholder = placeholder
self.interpret_by_tokens = True
IOComponent.__init__( IOComponent.__init__(
self, self,
label=label, label=label,
@ -336,6 +308,7 @@ class Textbox(
value=value, value=value,
**kwargs, **kwargs,
) )
TokenInterpretable.__init__(self)
self.cleared_value = "" self.cleared_value = ""
self.test_input = value self.test_input = value
self.type = type self.type = type
@ -456,7 +429,13 @@ class Textbox(
@document("change", "submit", "style") @document("change", "submit", "style")
class Number( class Number(
FormComponent, Changeable, Submittable, Blurrable, IOComponent, SimpleSerializable FormComponent,
Changeable,
Submittable,
Blurrable,
IOComponent,
SimpleSerializable,
NeighborInterpretable,
): ):
""" """
Creates a numeric field for user to enter numbers as input or display numeric output. Creates a numeric field for user to enter numbers as input or display numeric output.
@ -492,7 +471,6 @@ class Number(
precision: Precision to round input/output to. If set to 0, will round to nearest integer and covert type to int. If None, no rounding happens. precision: Precision to round input/output to. If set to 0, will round to nearest integer and covert type to int. If None, no rounding happens.
""" """
self.precision = precision self.precision = precision
self.interpret_by_tokens = False
IOComponent.__init__( IOComponent.__init__(
self, self,
label=label, label=label,
@ -504,6 +482,7 @@ class Number(
value=value, value=value,
**kwargs, **kwargs,
) )
NeighborInterpretable.__init__(self)
self.test_input = self.value if self.value is not None else 1 self.test_input = self.value if self.value is not None else 1
@staticmethod @staticmethod
@ -627,7 +606,9 @@ class Number(
@document("change", "style") @document("change", "style")
class Slider(FormComponent, Changeable, IOComponent, SimpleSerializable): class Slider(
FormComponent, Changeable, IOComponent, SimpleSerializable, NeighborInterpretable
):
""" """
Creates a slider that ranges from `minimum` to `maximum` with a step size of `step`. Creates a slider that ranges from `minimum` to `maximum` with a step size of `step`.
Preprocessing: passes slider value as a {float} into the function. Preprocessing: passes slider value as a {float} into the function.
@ -689,9 +670,9 @@ class Slider(FormComponent, Changeable, IOComponent, SimpleSerializable):
value=value, value=value,
**kwargs, **kwargs,
) )
NeighborInterpretable.__init__(self)
self.cleared_value = self.value self.cleared_value = self.value
self.test_input = self.value self.test_input = self.value
self.interpret_by_tokens = False
def get_config(self): def get_config(self):
return { return {
@ -764,15 +745,6 @@ class Slider(FormComponent, Changeable, IOComponent, SimpleSerializable):
{}, {},
) )
def get_interpretation_scores(
self, x, neighbors, scores: List[float], **kwargs
) -> List[float]:
"""
Returns:
Each value represents the score corresponding to an evenly spaced range of inputs between the minimum and maximum slider values.
"""
return scores
def style( def style(
self, self,
*, *,
@ -790,7 +762,9 @@ class Slider(FormComponent, Changeable, IOComponent, SimpleSerializable):
@document("change", "style") @document("change", "style")
class Checkbox(FormComponent, Changeable, IOComponent, SimpleSerializable): class Checkbox(
FormComponent, Changeable, IOComponent, SimpleSerializable, NeighborInterpretable
):
""" """
Creates a checkbox that can be set to `True` or `False`. Creates a checkbox that can be set to `True` or `False`.
@ -823,7 +797,6 @@ class Checkbox(FormComponent, Changeable, IOComponent, SimpleSerializable):
elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles. elem_id: An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.
""" """
self.test_input = True self.test_input = True
self.interpret_by_tokens = False
IOComponent.__init__( IOComponent.__init__(
self, self,
label=label, label=label,
@ -835,6 +808,7 @@ class Checkbox(FormComponent, Changeable, IOComponent, SimpleSerializable):
value=value, value=value,
**kwargs, **kwargs,
) )
NeighborInterpretable.__init__(self)
def get_config(self): def get_config(self):
return { return {
@ -863,12 +837,6 @@ class Checkbox(FormComponent, Changeable, IOComponent, SimpleSerializable):
def generate_sample(self): def generate_sample(self):
return True return True
def set_interpret_parameters(self):
"""
Calculates interpretation score of the input by comparing the output against the output when the input is the inverse boolean value of x.
"""
return self
def get_interpretation_neighbors(self, x): def get_interpretation_neighbors(self, x):
return [not x], {} return [not x], {}
@ -884,7 +852,9 @@ class Checkbox(FormComponent, Changeable, IOComponent, SimpleSerializable):
@document("change", "style") @document("change", "style")
class CheckboxGroup(FormComponent, Changeable, IOComponent, SimpleSerializable): class CheckboxGroup(
FormComponent, Changeable, IOComponent, SimpleSerializable, NeighborInterpretable
):
""" """
Creates a set of checkboxes of which a subset can be checked. Creates a set of checkboxes of which a subset can be checked.
Preprocessing: passes the list of checked checkboxes as a {List[str]} or their indices as a {List[int]} into the function, depending on `type`. Preprocessing: passes the list of checked checkboxes as a {List[str]} or their indices as a {List[int]} into the function, depending on `type`.
@ -928,7 +898,6 @@ class CheckboxGroup(FormComponent, Changeable, IOComponent, SimpleSerializable):
) )
self.type = type self.type = type
self.test_input = self.choices self.test_input = self.choices
self.interpret_by_tokens = False
IOComponent.__init__( IOComponent.__init__(
self, self,
label=label, label=label,
@ -940,6 +909,7 @@ class CheckboxGroup(FormComponent, Changeable, IOComponent, SimpleSerializable):
value=value, value=value,
**kwargs, **kwargs,
) )
NeighborInterpretable.__init__(self)
def get_config(self): def get_config(self):
return { return {
@ -1006,12 +976,6 @@ class CheckboxGroup(FormComponent, Changeable, IOComponent, SimpleSerializable):
y = [y] y = [y]
return y return y
def set_interpret_parameters(self):
"""
Calculates interpretation score of each choice in the input by comparing the output against the outputs when each choice in the input is independently either removed or added.
"""
return self
def get_interpretation_neighbors(self, x): def get_interpretation_neighbors(self, x):
leave_one_out_sets = [] leave_one_out_sets = []
for choice in self.choices: for choice in self.choices:
@ -1057,7 +1021,9 @@ class CheckboxGroup(FormComponent, Changeable, IOComponent, SimpleSerializable):
@document("change", "style") @document("change", "style")
class Radio(FormComponent, Changeable, IOComponent, SimpleSerializable): class Radio(
FormComponent, Changeable, IOComponent, SimpleSerializable, NeighborInterpretable
):
""" """
Creates a set of radio buttons of which only one can be selected. Creates a set of radio buttons of which only one can be selected.
Preprocessing: passes the value of the selected radio button as a {str} or its index as an {int} into the function, depending on `type`. Preprocessing: passes the value of the selected radio button as a {str} or its index as an {int} into the function, depending on `type`.
@ -1101,7 +1067,6 @@ class Radio(FormComponent, Changeable, IOComponent, SimpleSerializable):
) )
self.type = type self.type = type
self.test_input = self.choices[0] if len(self.choices) else None self.test_input = self.choices[0] if len(self.choices) else None
self.interpret_by_tokens = False
IOComponent.__init__( IOComponent.__init__(
self, self,
label=label, label=label,
@ -1113,6 +1078,7 @@ class Radio(FormComponent, Changeable, IOComponent, SimpleSerializable):
value=value, value=value,
**kwargs, **kwargs,
) )
NeighborInterpretable.__init__(self)
self.cleared_value = self.value self.cleared_value = self.value
def get_config(self): def get_config(self):
@ -1166,12 +1132,6 @@ class Radio(FormComponent, Changeable, IOComponent, SimpleSerializable):
+ ". Please choose from: 'value', 'index'." + ". Please choose from: 'value', 'index'."
) )
def set_interpret_parameters(self):
"""
Calculates interpretation score of each choice by comparing the output against each of the outputs when alternative choices are selected.
"""
return self
def get_interpretation_neighbors(self, x): def get_interpretation_neighbors(self, x):
choices = list(self.choices) choices = list(self.choices)
choices.remove(x) choices.remove(x)
@ -1274,6 +1234,7 @@ class Image(
Uploadable, Uploadable,
IOComponent, IOComponent,
ImgSerializable, ImgSerializable,
TokenInterpretable,
): ):
""" """
Creates an image component that can be used to upload/draw images (as an input) or display images (as an output). Creates an image component that can be used to upload/draw images (as an input) or display images (as an output).
@ -1343,7 +1304,6 @@ class Image(
self.tool = tool self.tool = tool
self.invert_colors = invert_colors self.invert_colors = invert_colors
self.test_input = deepcopy(media_data.BASE64_IMAGE) self.test_input = deepcopy(media_data.BASE64_IMAGE)
self.interpret_by_tokens = True
self.streaming = streaming self.streaming = streaming
if streaming and source != "webcam": if streaming and source != "webcam":
raise ValueError("Image streaming only available if source is 'webcam'.") raise ValueError("Image streaming only available if source is 'webcam'.")
@ -1359,6 +1319,7 @@ class Image(
value=value, value=value,
**kwargs, **kwargs,
) )
TokenInterpretable.__init__(self)
def get_config(self): def get_config(self):
return { return {
@ -1860,6 +1821,7 @@ class Audio(
IOComponent, IOComponent,
FileSerializable, FileSerializable,
TempFileManager, TempFileManager,
TokenInterpretable,
): ):
""" """
Creates an audio component that can be used to upload/record audio (as an input) or display audio (as an output). Creates an audio component that can be used to upload/record audio (as an input) or display audio (as an output).
@ -1911,7 +1873,6 @@ class Audio(
) )
self.type = type self.type = type
self.test_input = deepcopy(media_data.BASE64_AUDIO) self.test_input = deepcopy(media_data.BASE64_AUDIO)
self.interpret_by_tokens = True
self.streaming = streaming self.streaming = streaming
if streaming and source != "microphone": if streaming and source != "microphone":
raise ValueError( raise ValueError(
@ -1929,6 +1890,7 @@ class Audio(
value=value, value=value,
**kwargs, **kwargs,
) )
TokenInterpretable.__init__(self)
def get_config(self): def get_config(self):
return { return {
@ -2080,15 +2042,6 @@ class Audio(
masked_inputs.append(masked_data) masked_inputs.append(masked_data)
return masked_inputs return masked_inputs
def get_interpretation_scores(
self, x, neighbors, scores, masks=None, tokens=None
) -> List[float]:
"""
Returns:
Each value represents the interpretation score corresponding to an evenly spaced subsection of audio.
"""
return list(scores)
def generate_sample(self): def generate_sample(self):
return deepcopy(media_data.BASE64_AUDIO) return deepcopy(media_data.BASE64_AUDIO)

View File

@ -1,6 +1,5 @@
"""Pydantic data models and other dataclasses. This is the only file that uses Optional[] """Pydantic data models and other dataclasses. This is the only file that uses Optional[]
typing syntax instead of | None syntax to work with pydantic""" typing syntax instead of | None syntax to work with pydantic"""
from enum import Enum, auto from enum import Enum, auto
from typing import Any, Dict, List, Optional, Union from typing import Any, Dict, List, Optional, Union

View File

@ -1,327 +0,0 @@
"""
Defines helper methods useful for loading and caching Interface examples.
"""
from __future__ import annotations
import ast
import csv
import os
import warnings
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, List
from gradio import utils
from gradio.components import Dataset
from gradio.context import Context
from gradio.documentation import document, set_documentation_group
from gradio.flagging import CSVLogger
if TYPE_CHECKING: # Only import for type checking (to avoid circular imports).
from gradio.components import IOComponent
CACHED_FOLDER = "gradio_cached_examples"
LOG_FILE = "log.csv"
set_documentation_group("component-helpers")
def create_examples(
examples: List[Any] | List[List[Any]] | str,
inputs: IOComponent | List[IOComponent],
outputs: IOComponent | List[IOComponent] | None = None,
fn: Callable | None = None,
cache_examples: bool = False,
examples_per_page: int = 10,
_api_mode: bool = False,
label: str | None = None,
elem_id: str | None = None,
run_on_click: bool = False,
preprocess: bool = True,
postprocess: bool = True,
batch: bool = False,
):
"""Top-level synchronous function that creates Examples. Provided for backwards compatibility, i.e. so that gr.Examples(...) can be used to create the Examples component."""
examples_obj = Examples(
examples=examples,
inputs=inputs,
outputs=outputs,
fn=fn,
cache_examples=cache_examples,
examples_per_page=examples_per_page,
_api_mode=_api_mode,
label=label,
elem_id=elem_id,
run_on_click=run_on_click,
preprocess=preprocess,
postprocess=postprocess,
batch=batch,
_initiated_directly=False,
)
utils.synchronize_async(examples_obj.create)
return examples_obj
@document()
class Examples:
"""
This class is a wrapper over the Dataset component and can be used to create Examples
for Blocks / Interfaces. Populates the Dataset component with examples and
assigns event listener so that clicking on an example populates the input/output
components. Optionally handles example caching for fast inference.
Demos: blocks_inputs, fake_gan
Guides: more_on_examples_and_flagging, using_hugging_face_integrations, image_classification_in_pytorch, image_classification_in_tensorflow, image_classification_with_vision_transformers, create_your_own_friends_with_a_gan
"""
def __init__(
self,
examples: List[Any] | List[List[Any]] | str,
inputs: IOComponent | List[IOComponent],
outputs: IOComponent | List[IOComponent] | None = None,
fn: Callable | None = None,
cache_examples: bool = False,
examples_per_page: int = 10,
_api_mode: bool = False,
label: str | None = "Examples",
elem_id: str | None = None,
run_on_click: bool = False,
preprocess: bool = True,
postprocess: bool = True,
batch: bool = False,
_initiated_directly: bool = True,
):
"""
Parameters:
examples: example inputs that can be clicked to populate specific components. Should be nested list, in which the outer list consists of samples and each inner list consists of an input corresponding to each input component. A string path to a directory of examples can also be provided but it should be within the directory with the python file running the gradio app. If there are multiple input components and a directory is provided, a log.csv file must be present in the directory to link corresponding inputs.
inputs: the component or list of components corresponding to the examples
outputs: optionally, provide the component or list of components corresponding to the output of the examples. Required if `cache` is True.
fn: optionally, provide the function to run to generate the outputs corresponding to the examples. Required if `cache` is True.
cache_examples: if True, caches examples for fast runtime. If True, then `fn` and `outputs` need to be provided
examples_per_page: how many examples to show per page.
label: the label to use for the examples component (by default, "Examples")
elem_id: an optional string that is assigned as the id of this component in the HTML DOM.
run_on_click: if cache_examples is False, clicking on an example does not run the function when an example is clicked. Set this to True to run the function when an example is clicked. Has no effect if cache_examples is True.
preprocess: if True, preprocesses the example input before running the prediction function and caching the output. Only applies if cache_examples is True.
postprocess: if True, postprocesses the example output after running the prediction function and before caching. Only applies if cache_examples is True.
batch: If True, then the function should process a batch of inputs, meaning that it should accept a list of input values for each parameter. Used only if cache_examples is True.
"""
if _initiated_directly:
warnings.warn(
"Please use gr.Examples(...) instead of gr.examples.Examples(...) to create the Examples.",
)
if cache_examples and (fn is None or outputs is None):
raise ValueError("If caching examples, `fn` and `outputs` must be provided")
if not isinstance(inputs, list):
inputs = [inputs]
if outputs and not isinstance(outputs, list):
outputs = [outputs]
working_directory = Path().absolute()
if examples is None:
raise ValueError("The parameter `examples` cannot be None")
elif isinstance(examples, list) and (
len(examples) == 0 or isinstance(examples[0], list)
):
pass
elif (
isinstance(examples, list) and len(inputs) == 1
): # If there is only one input component, examples can be provided as a regular list instead of a list of lists
examples = [[e] for e in examples]
elif isinstance(examples, str):
if not Path(examples).exists():
raise FileNotFoundError(
"Could not find examples directory: " + examples
)
working_directory = examples
if not (Path(examples) / LOG_FILE).exists():
if len(inputs) == 1:
examples = [[e] for e in os.listdir(examples)]
else:
raise FileNotFoundError(
"Could not find log file (required for multiple inputs): "
+ LOG_FILE
)
else:
with open(Path(examples) / LOG_FILE) as logs:
examples = list(csv.reader(logs))
examples = [
examples[i][: len(inputs)] for i in range(1, len(examples))
] # remove header and unnecessary columns
else:
raise ValueError(
"The parameter `examples` must either be a string directory or a list"
"(if there is only 1 input component) or (more generally), a nested "
"list, where each sublist represents a set of inputs."
)
input_has_examples = [False] * len(inputs)
for example in examples:
for idx, example_for_input in enumerate(example):
if not (example_for_input is None):
try:
input_has_examples[idx] = True
except IndexError:
pass # If there are more example components than inputs, ignore. This can sometimes be intentional (e.g. loading from a log file where outputs and timestamps are also logged)
inputs_with_examples = [
inp for (inp, keep) in zip(inputs, input_has_examples) if keep
]
non_none_examples = [
[ex for (ex, keep) in zip(example, input_has_examples) if keep]
for example in examples
]
self.examples = examples
self.non_none_examples = non_none_examples
self.inputs = inputs
self.inputs_with_examples = inputs_with_examples
self.outputs = outputs
self.fn = fn
self.cache_examples = cache_examples
self._api_mode = _api_mode
self.preprocess = preprocess
self.postprocess = postprocess
self.batch = batch
with utils.set_directory(working_directory):
self.processed_examples = [
[
component.postprocess(sample)
for component, sample in zip(inputs, example)
]
for example in examples
]
self.non_none_processed_examples = [
[ex for (ex, keep) in zip(example, input_has_examples) if keep]
for example in self.processed_examples
]
if cache_examples:
for example in self.examples:
if len([ex for ex in example if ex is not None]) != len(self.inputs):
warnings.warn(
"Examples are being cached but not all input components have "
"example values. This may result in an exception being thrown by "
"your function. If you do get an error while caching examples, make "
"sure all of your inputs have example values for all of your examples "
"or you provide default values for those particular parameters in your function."
)
break
with utils.set_directory(working_directory):
self.dataset = Dataset(
components=inputs_with_examples,
samples=non_none_examples,
type="index",
label=label,
samples_per_page=examples_per_page,
elem_id=elem_id,
)
self.cached_folder = Path(CACHED_FOLDER) / str(self.dataset._id)
self.cached_file = Path(self.cached_folder) / "log.csv"
self.cache_examples = cache_examples
self.run_on_click = run_on_click
async def create(self) -> None:
"""Caches the examples if self.cache_examples is True and creates the Dataset
component to hold the examples"""
async def load_example(example_id):
if self.cache_examples:
processed_example = self.non_none_processed_examples[
example_id
] + await self.load_from_cache(example_id)
else:
processed_example = self.non_none_processed_examples[example_id]
return utils.resolve_singleton(processed_example)
if Context.root_block:
if self.cache_examples and self.outputs:
targets = self.inputs_with_examples
else:
targets = self.inputs
self.dataset.click(
load_example,
inputs=[self.dataset],
outputs=targets, # type: ignore
postprocess=False,
queue=False,
)
if self.run_on_click and not self.cache_examples:
if self.fn is None:
raise ValueError("Cannot run_on_click if no function is provided")
self.dataset.click(
self.fn,
inputs=self.inputs, # type: ignore
outputs=self.outputs, # type: ignore
)
if self.cache_examples:
await self.cache()
async def cache(self) -> None:
"""
Caches all of the examples so that their predictions can be shown immediately.
"""
if Path(self.cached_file).exists():
print(
f"Using cache from '{Path(self.cached_folder).resolve()}' directory. If method or examples have changed since last caching, delete this folder to clear cache."
)
else:
if Context.root_block is None:
raise ValueError("Cannot cache examples if not in a Blocks context")
print(f"Caching examples at: '{Path(self.cached_file).resolve()}'")
cache_logger = CSVLogger()
# create a fake dependency to process the examples and get the predictions
dependency = Context.root_block.set_event_trigger(
event_name="fake_event",
fn=self.fn,
inputs=self.inputs_with_examples, # type: ignore
outputs=self.outputs, # type: ignore
preprocess=self.preprocess and not self._api_mode,
postprocess=self.postprocess and not self._api_mode,
batch=self.batch,
)
fn_index = Context.root_block.dependencies.index(dependency)
assert self.outputs is not None
cache_logger.setup(self.outputs, self.cached_folder)
for example_id, _ in enumerate(self.examples):
processed_input = self.processed_examples[example_id]
if self.batch:
processed_input = [[value] for value in processed_input]
prediction = await Context.root_block.process_api(
fn_index=fn_index, inputs=processed_input, request=None, state={}
)
output = prediction["data"]
if self.batch:
output = [value[0] for value in output]
cache_logger.flag(output)
# Remove the "fake_event" to prevent bugs in loading interfaces from spaces
Context.root_block.dependencies.remove(dependency)
Context.root_block.fns.pop(fn_index)
async def load_from_cache(self, example_id: int) -> List[Any]:
"""Loads a particular cached example for the interface.
Parameters:
example_id: The id of the example to process (zero-indexed).
"""
with open(self.cached_file) as cache:
examples = list(csv.reader(cache))
example = examples[example_id + 1] # +1 to adjust for header
output = []
assert self.outputs is not None
for component, value in zip(self.outputs, example):
try:
value_as_dict = ast.literal_eval(value)
assert utils.is_update(value_as_dict)
output.append(value_as_dict)
except (ValueError, TypeError, SyntaxError, AssertionError):
output.append(component.serialize(value, self.cached_folder))
return output

View File

@ -12,14 +12,15 @@ import tempfile
import threading import threading
import warnings import warnings
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable, Iterable, List, Optional, Tuple from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, List, Tuple
import matplotlib import matplotlib
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
import numpy as np import numpy as np
import PIL import PIL
import PIL.Image
from gradio import processing_utils, routes, utils from gradio import components, processing_utils, routes, utils
from gradio.context import Context from gradio.context import Context
from gradio.documentation import document, set_documentation_group from gradio.documentation import document, set_documentation_group
from gradio.flagging import CSVLogger from gradio.flagging import CSVLogger
@ -85,13 +86,13 @@ class Examples:
self, self,
examples: List[Any] | List[List[Any]] | str, examples: List[Any] | List[List[Any]] | str,
inputs: IOComponent | List[IOComponent], inputs: IOComponent | List[IOComponent],
outputs: Optional[IOComponent | List[IOComponent]] = None, outputs: IOComponent | List[IOComponent] | None = None,
fn: Optional[Callable] = None, fn: Callable | None = None,
cache_examples: bool = False, cache_examples: bool = False,
examples_per_page: int = 10, examples_per_page: int = 10,
_api_mode: bool = False, _api_mode: bool = False,
label: str = "Examples", label: str | None = "Examples",
elem_id: Optional[str] = None, elem_id: str | None = None,
run_on_click: bool = False, run_on_click: bool = False,
preprocess: bool = True, preprocess: bool = True,
postprocess: bool = True, postprocess: bool = True,
@ -123,7 +124,7 @@ class Examples:
if not isinstance(inputs, list): if not isinstance(inputs, list):
inputs = [inputs] inputs = [inputs]
if not isinstance(outputs, list): if outputs and not isinstance(outputs, list):
outputs = [outputs] outputs = [outputs]
working_directory = Path().absolute() working_directory = Path().absolute()
@ -139,12 +140,12 @@ class Examples:
): # If there is only one input component, examples can be provided as a regular list instead of a list of lists ): # If there is only one input component, examples can be provided as a regular list instead of a list of lists
examples = [[e] for e in examples] examples = [[e] for e in examples]
elif isinstance(examples, str): elif isinstance(examples, str):
if not os.path.exists(examples): if not Path(examples).exists():
raise FileNotFoundError( raise FileNotFoundError(
"Could not find examples directory: " + examples "Could not find examples directory: " + examples
) )
working_directory = examples working_directory = examples
if not os.path.exists(os.path.join(examples, LOG_FILE)): if not (Path(examples) / LOG_FILE).exists():
if len(inputs) == 1: if len(inputs) == 1:
examples = [[e] for e in os.listdir(examples)] examples = [[e] for e in os.listdir(examples)]
else: else:
@ -153,7 +154,7 @@ class Examples:
+ LOG_FILE + LOG_FILE
) )
else: else:
with open(os.path.join(examples, LOG_FILE)) as logs: with open(Path(examples) / LOG_FILE) as logs:
examples = list(csv.reader(logs)) examples = list(csv.reader(logs))
examples = [ examples = [
examples[i][: len(inputs)] for i in range(1, len(examples)) examples[i][: len(inputs)] for i in range(1, len(examples))
@ -219,10 +220,8 @@ class Examples:
) )
break break
from gradio.components import Dataset
with utils.set_directory(working_directory): with utils.set_directory(working_directory):
self.dataset = Dataset( self.dataset = components.Dataset(
components=inputs_with_examples, components=inputs_with_examples,
samples=non_none_examples, samples=non_none_examples,
type="index", type="index",
@ -231,8 +230,8 @@ class Examples:
elem_id=elem_id, elem_id=elem_id,
) )
self.cached_folder = os.path.join(CACHED_FOLDER, str(self.dataset._id)) self.cached_folder = Path(CACHED_FOLDER) / str(self.dataset._id)
self.cached_file = os.path.join(self.cached_folder, "log.csv") self.cached_file = Path(self.cached_folder) / "log.csv"
self.cache_examples = cache_examples self.cache_examples = cache_examples
self.run_on_click = run_on_click self.run_on_click = run_on_click
@ -250,19 +249,24 @@ class Examples:
return utils.resolve_singleton(processed_example) return utils.resolve_singleton(processed_example)
if Context.root_block: if Context.root_block:
if self.cache_examples and self.outputs:
targets = self.inputs_with_examples
else:
targets = self.inputs
self.dataset.click( self.dataset.click(
load_example, load_example,
inputs=[self.dataset], inputs=[self.dataset],
outputs=self.inputs_with_examples outputs=targets, # type: ignore
+ (self.outputs if self.cache_examples else []),
postprocess=False, postprocess=False,
queue=False, queue=False,
) )
if self.run_on_click and not self.cache_examples: if self.run_on_click and not self.cache_examples:
if self.fn is None:
raise ValueError("Cannot run_on_click if no function is provided")
self.dataset.click( self.dataset.click(
self.fn, self.fn,
inputs=self.inputs, inputs=self.inputs, # type: ignore
outputs=self.outputs, outputs=self.outputs, # type: ignore
) )
if self.cache_examples: if self.cache_examples:
@ -272,29 +276,30 @@ class Examples:
""" """
Caches all of the examples so that their predictions can be shown immediately. Caches all of the examples so that their predictions can be shown immediately.
""" """
if os.path.exists(self.cached_file): if Path(self.cached_file).exists():
print( print(
f"Using cache from '{os.path.abspath(self.cached_folder)}' directory. If method or examples have changed since last caching, delete this folder to clear cache." f"Using cache from '{Path(self.cached_folder).resolve()}' directory. If method or examples have changed since last caching, delete this folder to clear cache."
) )
else: else:
if Context.root_block is None: if Context.root_block is None:
raise ValueError("Cannot cache examples if not in a Blocks context") raise ValueError("Cannot cache examples if not in a Blocks context")
print(f"Caching examples at: '{os.path.abspath(self.cached_file)}'") print(f"Caching examples at: '{Path(self.cached_file).resolve()}'")
cache_logger = CSVLogger() cache_logger = CSVLogger()
# create a fake dependency to process the examples and get the predictions # create a fake dependency to process the examples and get the predictions
dependency = Context.root_block.set_event_trigger( dependency = Context.root_block.set_event_trigger(
event_name="fake_event", event_name="fake_event",
fn=self.fn, fn=self.fn,
inputs=self.inputs_with_examples, inputs=self.inputs_with_examples, # type: ignore
outputs=self.outputs, outputs=self.outputs, # type: ignore
preprocess=self.preprocess and not self._api_mode, preprocess=self.preprocess and not self._api_mode,
postprocess=self.postprocess and not self._api_mode, postprocess=self.postprocess and not self._api_mode,
batch=self.batch, batch=self.batch,
) )
fn_index = Context.root_block.dependencies.index(dependency) fn_index = Context.root_block.dependencies.index(dependency)
assert self.outputs is not None
cache_logger.setup(self.outputs, self.cached_folder) cache_logger.setup(self.outputs, self.cached_folder)
for example_id, _ in enumerate(self.examples): for example_id, _ in enumerate(self.examples):
processed_input = self.processed_examples[example_id] processed_input = self.processed_examples[example_id]
@ -320,6 +325,7 @@ class Examples:
examples = list(csv.reader(cache)) examples = list(csv.reader(cache))
example = examples[example_id + 1] # +1 to adjust for header example = examples[example_id + 1] # +1 to adjust for header
output = [] output = []
assert self.outputs is not None
for component, value in zip(self.outputs, example): for component, value in zip(self.outputs, example):
try: try:
value_as_dict = ast.literal_eval(value) value_as_dict = ast.literal_eval(value)
@ -333,13 +339,13 @@ class Examples:
class TrackedIterable: class TrackedIterable:
def __init__( def __init__(
self, self,
iterable: Iterable, iterable: Iterable | None,
index: int | None, index: int | None,
length: int | None, length: int | None,
desc: str | None, desc: str | None,
unit: str | None, unit: str | None,
_tqdm=None, _tqdm=None,
progress: float = None, progress: float | None = None,
) -> None: ) -> None:
self.iterable = iterable self.iterable = iterable
self.index = index self.index = index
@ -373,16 +379,14 @@ class Progress(Iterable):
def __init__( def __init__(
self, self,
track_tqdm: bool = False, track_tqdm: bool = False,
_active: bool = False, _callback: Callable | None = None, # for internal use only
_callback: Callable = None, _event_id: str | None = None,
_event_id: str = None,
): ):
""" """
Parameters: Parameters:
track_tqdm: If True, the Progress object will track any tqdm.tqdm iterations with the tqdm library in the function. track_tqdm: If True, the Progress object will track any tqdm.tqdm iterations with the tqdm library in the function.
""" """
self.track_tqdm = track_tqdm self.track_tqdm = track_tqdm
self._active = _active
self._callback = _callback self._callback = _callback
self._event_id = _event_id self._event_id = _event_id
self.iterables: List[TrackedIterable] = [] self.iterables: List[TrackedIterable] = []
@ -397,7 +401,7 @@ class Progress(Iterable):
""" """
Updates progress tracker with next item in iterable. Updates progress tracker with next item in iterable.
""" """
if self._active: if self._callback:
current_iterable = self.iterables[-1] current_iterable = self.iterables[-1]
while ( while (
not hasattr(current_iterable.iterable, "__next__") not hasattr(current_iterable.iterable, "__next__")
@ -408,9 +412,10 @@ class Progress(Iterable):
event_id=self._event_id, event_id=self._event_id,
iterables=self.iterables, iterables=self.iterables,
) )
assert current_iterable.index is not None, "Index not set."
current_iterable.index += 1 current_iterable.index += 1
try: try:
return next(current_iterable.iterable) return next(current_iterable.iterable) # type: ignore
except StopIteration: except StopIteration:
self.iterables.pop() self.iterables.pop()
raise StopIteration raise StopIteration
@ -421,7 +426,7 @@ class Progress(Iterable):
self, self,
progress: float | Tuple[int, int | None] | None, progress: float | Tuple[int, int | None] | None,
desc: str | None = None, desc: str | None = None,
total: float | None = None, total: int | None = None,
unit: str = "steps", unit: str = "steps",
_tqdm=None, _tqdm=None,
): ):
@ -433,7 +438,7 @@ class Progress(Iterable):
total: estimated total number of steps. total: estimated total number of steps.
unit: unit of iterations. unit: unit of iterations.
""" """
if self._active: if self._callback:
if isinstance(progress, tuple): if isinstance(progress, tuple):
index, total = progress index, total = progress
progress = None progress = None
@ -450,8 +455,8 @@ class Progress(Iterable):
def tqdm( def tqdm(
self, self,
iterable: Iterable | None, iterable: Iterable | None,
desc: str = None, desc: str | None = None,
total: float = None, total: int | None = None,
unit: str = "steps", unit: str = "steps",
_tqdm=None, _tqdm=None,
*args, *args,
@ -465,15 +470,16 @@ class Progress(Iterable):
total: estimated total number of steps. total: estimated total number of steps.
unit: unit of iterations. unit: unit of iterations.
""" """
if iterable is None: if self._callback:
new_iterable = TrackedIterable(None, 0, total, desc, unit, _tqdm) if iterable is None:
self.iterables.append(new_iterable) new_iterable = TrackedIterable(None, 0, total, desc, unit, _tqdm)
self._callback(event_id=self._event_id, iterables=self.iterables) self.iterables.append(new_iterable)
return self._callback(event_id=self._event_id, iterables=self.iterables)
length = len(iterable) if hasattr(iterable, "__len__") else None return self
self.iterables.append( length = len(iterable) if hasattr(iterable, "__len__") else None # type: ignore
TrackedIterable(iter(iterable), 0, length, desc, unit, _tqdm) self.iterables.append(
) TrackedIterable(iter(iterable), 0, length, desc, unit, _tqdm)
)
return self return self
def update(self, n=1): def update(self, n=1):
@ -482,8 +488,9 @@ class Progress(Iterable):
Parameters: Parameters:
n: number of steps completed. n: number of steps completed.
""" """
if self._active and len(self.iterables) > 0: if self._callback and len(self.iterables) > 0:
current_iterable = self.iterables[-1] current_iterable = self.iterables[-1]
assert current_iterable.index is not None, "Index not set."
current_iterable.index += n current_iterable.index += n
self._callback( self._callback(
event_id=self._event_id, event_id=self._event_id,
@ -496,7 +503,7 @@ class Progress(Iterable):
""" """
Removes iterable with given _tqdm. Removes iterable with given _tqdm.
""" """
if self._active: if self._callback:
for i in range(len(self.iterables)): for i in range(len(self.iterables)):
if id(self.iterables[i]._tqdm) == id(_tqdm): if id(self.iterables[i]._tqdm) == id(_tqdm):
self.iterables.pop(i) self.iterables.pop(i)
@ -511,9 +518,7 @@ class Progress(Iterable):
def create_tracker(root_blocks, event_id, fn, track_tqdm): def create_tracker(root_blocks, event_id, fn, track_tqdm):
progress = Progress( progress = Progress(_callback=root_blocks._queue.set_progress, _event_id=event_id)
_active=True, _callback=root_blocks._queue.set_progress, _event_id=event_id
)
if not track_tqdm: if not track_tqdm:
return progress, fn return progress, fn
@ -677,7 +682,7 @@ def make_waveform(
audio: str | Tuple[int, np.ndarray], audio: str | Tuple[int, np.ndarray],
*, *,
bg_color: str = "#f3f4f6", bg_color: str = "#f3f4f6",
bg_image: str = None, bg_image: str | None = None,
fg_alpha: float = 0.75, fg_alpha: float = 0.75,
bars_color: str | Tuple[str, str] = ("#fbbf24", "#ea580c"), bars_color: str | Tuple[str, str] = ("#fbbf24", "#ea580c"),
bar_count: int = 50, bar_count: int = 50,
@ -748,7 +753,7 @@ def make_waveform(
plt.axis("off") plt.axis("off")
plt.margins(x=0) plt.margins(x=0)
tmp_img = tempfile.NamedTemporaryFile(suffix=".png", delete=False) tmp_img = tempfile.NamedTemporaryFile(suffix=".png", delete=False)
savefig_kwargs = {"bbox_inches": "tight"} savefig_kwargs: Dict[str, Any] = {"bbox_inches": "tight"}
if bg_image is not None: if bg_image is not None:
savefig_kwargs["transparent"] = True savefig_kwargs["transparent"] = True
else: else:

View File

@ -1,13 +1,75 @@
"""Contains classes and methods related to interpretation for components in Gradio."""
from __future__ import annotations
import copy import copy
import math import math
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Any, Dict, List, Tuple
import numpy as np import numpy as np
from gradio import utils from gradio import components, utils
from gradio.components import Label, Number
if TYPE_CHECKING: # Only import for type checking (is False at runtime).
from gradio import Interface
async def run_interpret(interface, raw_input): class Interpretable(ABC):
def __init__(self) -> None:
self.set_interpret_parameters()
def set_interpret_parameters(self):
"""
Set any parameters for interpretation. Properties can be set here to be
used in get_interpretation_neighbors and get_interpretation_scores.
"""
pass
def get_interpretation_scores(
self, x: Any, neighbors: List[Any] | None, scores: List[float], **kwargs
) -> List:
"""
Arrange the output values from the neighbors into interpretation scores for the interface to render.
Parameters:
x: Input to interface
neighbors: Neighboring values to input x used for interpretation.
scores: Output value corresponding to each neighbor in neighbors
Returns:
Arrangement of interpretation scores for interfaces to render.
"""
return scores
class TokenInterpretable(Interpretable, ABC):
@abstractmethod
def tokenize(self, x: Any) -> Tuple[List, List, None]:
"""
Interprets an input data point x by splitting it into a list of tokens (e.g
a string into words or an image into super-pixels).
"""
return [], [], None
@abstractmethod
def get_masked_inputs(self, tokens: List, binary_mask_matrix: List[List]) -> List:
return []
class NeighborInterpretable(Interpretable, ABC):
@abstractmethod
def get_interpretation_neighbors(self, x: Any) -> Tuple[List, Dict]:
"""
Generates values similar to input to be used to interpret the significance of the input in the final output.
Parameters:
x: Input to interface
Returns: (neighbor_values, interpret_kwargs, interpret_by_removal)
neighbor_values: Neighboring values to input x to compute for interpretation
interpret_kwargs: Keyword arguments to be passed to get_interpretation_scores
"""
return [], {}
async def run_interpret(interface: Interface, raw_input: List):
""" """
Runs the interpretation command for the machine learning model. Handles both the "default" out-of-the-box Runs the interpretation command for the machine learning model. Handles both the "default" out-of-the-box
interpretation for a certain set of UI component types, as well as the custom interpretation case. interpretation for a certain set of UI component types, as well as the custom interpretation case.
@ -31,7 +93,7 @@ async def run_interpret(interface, raw_input):
if interp == "default": if interp == "default":
input_component = interface.input_components[i] input_component = interface.input_components[i]
neighbor_raw_input = list(raw_input) neighbor_raw_input = list(raw_input)
if input_component.interpret_by_tokens: if isinstance(input_component, TokenInterpretable):
tokens, neighbor_values, masks = input_component.tokenize(x) tokens, neighbor_values, masks = input_component.tokenize(x)
interface_scores = [] interface_scores = []
alternative_output = [] alternative_output = []
@ -73,7 +135,7 @@ async def run_interpret(interface, raw_input):
tokens=tokens, tokens=tokens,
) )
) )
else: elif isinstance(input_component, NeighborInterpretable):
( (
neighbor_values, neighbor_values,
interpret_kwargs, interpret_kwargs,
@ -114,9 +176,13 @@ async def run_interpret(interface, raw_input):
raw_input[i], raw_input[i],
neighbor_values, neighbor_values,
interface_scores, interface_scores,
**interpret_kwargs **interpret_kwargs,
) )
) )
else:
raise ValueError(
f"Component {input_component} does not support interpretation"
)
elif interp == "shap" or interp == "shapley": elif interp == "shap" or interp == "shapley":
try: try:
import shap # type: ignore import shap # type: ignore
@ -125,7 +191,7 @@ async def run_interpret(interface, raw_input):
"The package `shap` is required for this interpretation method. Try: `pip install shap`" "The package `shap` is required for this interpretation method. Try: `pip install shap`"
) )
input_component = interface.input_components[i] input_component = interface.input_components[i]
if not (input_component.interpret_by_tokens): if not isinstance(input_component, TokenInterpretable):
raise ValueError( raise ValueError(
"Input component {} does not support `shap` interpretation".format( "Input component {} does not support `shap` interpretation".format(
input_component input_component
@ -136,6 +202,7 @@ async def run_interpret(interface, raw_input):
# construct a masked version of the input # construct a masked version of the input
def get_masked_prediction(binary_mask): def get_masked_prediction(binary_mask):
assert isinstance(input_component, TokenInterpretable)
masked_xs = input_component.get_masked_inputs(tokens, binary_mask) masked_xs = input_component.get_masked_inputs(tokens, binary_mask)
preds = [] preds = []
for masked_x in masked_xs: for masked_x in masked_xs:
@ -162,9 +229,14 @@ async def run_interpret(interface, raw_input):
nsamples=int(interface.num_shap * num_total_segments), nsamples=int(interface.num_shap * num_total_segments),
silent=True, silent=True,
) )
assert shap_values is not None, "SHAP values could not be calculated"
scores.append( scores.append(
input_component.get_interpretation_scores( input_component.get_interpretation_scores(
raw_input[i], None, shap_values[0], masks=masks, tokens=tokens raw_input[i],
None,
shap_values[0].tolist(),
masks=masks,
tokens=tokens,
) )
) )
alternative_outputs.append([]) alternative_outputs.append([])
@ -174,7 +246,7 @@ async def run_interpret(interface, raw_input):
else: else:
raise ValueError("Unknown intepretation method: {}".format(interp)) raise ValueError("Unknown intepretation method: {}".format(interp))
return scores, alternative_outputs return scores, alternative_outputs
else: # custom interpretation function elif interface.interpretation: # custom interpretation function
processed_input = [ processed_input = [
input_component.preprocess(raw_input[i]) input_component.preprocess(raw_input[i])
for i, input_component in enumerate(interface.input_components) for i, input_component in enumerate(interface.input_components)
@ -184,9 +256,11 @@ async def run_interpret(interface, raw_input):
if len(raw_input) == 1: if len(raw_input) == 1:
interpretation = [interpretation] interpretation = [interpretation]
return interpretation, [] return interpretation, []
else:
raise ValueError("No interpretation method specified.")
def diff(original, perturbed): def diff(original: Any, perturbed: Any) -> int | float:
try: # try computing numerical difference try: # try computing numerical difference
score = float(original) - float(perturbed) score = float(original) - float(perturbed)
except ValueError: # otherwise, look at strict difference in label except ValueError: # otherwise, look at strict difference in label
@ -194,12 +268,14 @@ def diff(original, perturbed):
return score return score
def quantify_difference_in_label(interface, original_output, perturbed_output): def quantify_difference_in_label(
interface: Interface, original_output: List, perturbed_output: List
) -> int | float:
output_component = interface.output_components[0] output_component = interface.output_components[0]
post_original_output = output_component.postprocess(original_output[0]) post_original_output = output_component.postprocess(original_output[0])
post_perturbed_output = output_component.postprocess(perturbed_output[0]) post_perturbed_output = output_component.postprocess(perturbed_output[0])
if isinstance(output_component, Label): if isinstance(output_component, components.Label):
original_label = post_original_output["label"] original_label = post_original_output["label"]
perturbed_label = post_perturbed_output["label"] perturbed_label = post_perturbed_output["label"]
@ -212,7 +288,7 @@ def quantify_difference_in_label(interface, original_output, perturbed_output):
score = diff(original_label, perturbed_label) score = diff(original_label, perturbed_label)
return score return score
elif isinstance(output_component, Number): elif isinstance(output_component, components.Number):
score = diff(post_original_output, post_perturbed_output) score = diff(post_original_output, post_perturbed_output)
return score return score
@ -225,14 +301,14 @@ def quantify_difference_in_label(interface, original_output, perturbed_output):
def get_regression_or_classification_value( def get_regression_or_classification_value(
interface, original_output, perturbed_output interface: Interface, original_output: List, perturbed_output: List
): ) -> int | float:
"""Used to combine regression/classification for Shap interpretation method.""" """Used to combine regression/classification for Shap interpretation method."""
output_component = interface.output_components[0] output_component = interface.output_components[0]
post_original_output = output_component.postprocess(original_output[0]) post_original_output = output_component.postprocess(original_output[0])
post_perturbed_output = output_component.postprocess(perturbed_output[0]) post_perturbed_output = output_component.postprocess(perturbed_output[0])
if type(output_component) == Label: if isinstance(output_component, components.Label):
original_label = post_original_output["label"] original_label = post_original_output["label"]
perturbed_label = post_perturbed_output["label"] perturbed_label = post_perturbed_output["label"]

View File

@ -5,5 +5,4 @@ pip_required
pip install --upgrade pip pip install --upgrade pip
pip install pyright pip install pyright
cd gradio pyright gradio/*.py
pyright blocks.py components.py context.py data_classes.py deprecation.py documentation.py encryptor.py events.py examples.py exceptions.py external.py external_utils.py serializing.py layouts.py flagging.py interface.py mix.py networking.py pipelines.py processing_utils.py queueing.py reload.py routes.py serializing.py strings.py tunneling.py utils.py templates.py