diff --git a/client/python/gradio_client/documentation.py b/client/python/gradio_client/documentation.py index f21234ed65..cc78afbffe 100644 --- a/client/python/gradio_client/documentation.py +++ b/client/python/gradio_client/documentation.py @@ -61,6 +61,7 @@ def extract_instance_attr_doc(cls, attr): ("gradio_client.", "py-client"), ("gradio.utils", "helpers"), ("gradio.renderable", "renderable"), + ("gradio.validators", "validators"), ] diff --git a/gradio/__init__.py b/gradio/__init__.py index 56718dcde0..768799cae5 100644 --- a/gradio/__init__.py +++ b/gradio/__init__.py @@ -5,7 +5,7 @@ import gradio.processing_utils import gradio.sketch import gradio.templates -from gradio import components, layouts, mcp, themes +from gradio import components, layouts, mcp, themes, validators from gradio.blocks import Blocks from gradio.chat_interface import ChatInterface from gradio.cli import deploy @@ -277,4 +277,5 @@ "get_video", "get_model3d", "get_file", + "validators", ] diff --git a/gradio/components/audio.py b/gradio/components/audio.py index 807809ba79..e73bf818c8 100644 --- a/gradio/components/audio.py +++ b/gradio/components/audio.py @@ -20,7 +20,6 @@ from gradio.components.base import Component, StreamingInput, StreamingOutput from gradio.data_classes import FileData, FileDataDict, MediaStreamChunk from gradio.events import Events -from gradio.exceptions import Error from gradio.i18n import I18nData if TYPE_CHECKING: @@ -106,8 +105,6 @@ def __init__( show_download_button: bool | None = None, show_share_button: bool | None = None, editable: bool = True, - min_length: int | None = None, - max_length: int | None = None, waveform_options: WaveformOptions | dict | None = None, loop: bool = False, recording: bool = False, @@ -138,8 +135,6 @@ def __init__( show_download_button: If True, will show a download button in the corner of the component for saving audio. If False, icon does not appear. By default, it will be True for output components and False for input components. show_share_button: If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise. editable: If True, allows users to manipulate the audio file if the component is interactive. Defaults to True. - min_length: The minimum length of audio (in seconds) that the user can pass into the prediction function. If None, there is no minimum length. - max_length: The maximum length of audio (in seconds) that the user can pass into the prediction function. If None, there is no maximum length. waveform_options: A dictionary of options for the waveform display. Options include: waveform_color (str), waveform_progress_color (str), skip_length (int), trim_region_color (str). Default is None, which uses the default values for these options. [See `gr.WaveformOptions` docs](#waveform-options). loop: If True, the audio will loop when it reaches the end and continue playing from the beginning. recording: If True, the audio component will be set to record audio from the microphone if the source is set to "microphone". Defaults to False. @@ -193,8 +188,6 @@ def __init__( self.waveform_options = WaveformOptions(**waveform_options) else: self.waveform_options = waveform_options - self.min_length = min_length - self.max_length = max_length self.recording = recording super().__init__( label=label, @@ -253,18 +246,6 @@ def preprocess( if self.format is not None and original_suffix != f".{self.format}": needs_conversion = True - if self.min_length is not None or self.max_length is not None: - sample_rate, data = processing_utils.audio_from_file(payload.path) - duration = len(data) / sample_rate - if self.min_length is not None and duration < self.min_length: - raise Error( - f"Audio is too short, and must be at least {self.min_length} seconds" - ) - if self.max_length is not None and duration > self.max_length: - raise Error( - f"Audio is too long, and must be at most {self.max_length} seconds" - ) - if self.type == "numpy": return processing_utils.audio_from_file(payload.path) elif self.type == "filepath": diff --git a/gradio/components/video.py b/gradio/components/video.py index ea87e3dbfd..d8fd6eae1d 100644 --- a/gradio/components/video.py +++ b/gradio/components/video.py @@ -15,7 +15,6 @@ from gradio_client import utils as client_utils from gradio_client.documentation import document -import gradio as gr from gradio import processing_utils, utils from gradio.components.base import Component, StreamingOutput from gradio.components.image_editor import WatermarkOptions, WebcamOptions @@ -92,8 +91,6 @@ def __init__( autoplay: bool = False, show_share_button: bool | None = None, show_download_button: bool | None = None, - min_length: int | None = None, - max_length: int | None = None, loop: bool = False, streaming: bool = False, watermark: WatermarkOptions | None = None, @@ -123,8 +120,6 @@ def __init__( autoplay: whether to automatically play the video when the component is used as an output. Note: browsers will not autoplay video files if the user has not interacted with the page yet. show_share_button: if True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise. show_download_button: if True, will show a download icon in the corner of the component that allows user to download the output. If False, icon does not appear. By default, it will be True for output components and False for input components. - min_length: the minimum length of video (in seconds) that the user can pass into the prediction function. If None, there is no minimum length. - max_length: the maximum length of video (in seconds) that the user can pass into the prediction function. If None, there is no maximum length. loop: if True, the video will loop when it reaches the end and continue playing from the beginning. streaming: when used set as an output, takes video chunks yielded from the backend and combines them into one streaming video output. Each chunk should be a video file with a .ts extension using an h.264 encoding. Mp4 files are also accepted but they will be converted to h.264 encoding. watermark: A `gr.WatermarkOptions` instance that includes an image file and position to be used as a watermark on the video. The image is not scaled and is displayed on the provided position on the video. Valid formats for the image are: jpeg, png. @@ -171,8 +166,6 @@ def __init__( else show_share_button ) self.show_download_button = show_download_button - self.min_length = min_length - self.max_length = max_length self.streaming = streaming super().__init__( label=label, @@ -208,19 +201,6 @@ def preprocess(self, payload: VideoData | None) -> str | None: uploaded_format = file_name.suffix.replace(".", "") needs_formatting = self.format is not None and uploaded_format != self.format flip = self.sources == ["webcam"] and self.webcam_options.mirror - - if self.min_length is not None or self.max_length is not None: - # With this if-clause, avoid unnecessary execution of `processing_utils.get_video_length`. - # This is necessary for the Wasm-mode, because it uses ffprobe, which is not available in the browser. - duration = processing_utils.get_video_length(file_name) - if self.min_length is not None and duration < self.min_length: - raise gr.Error( - f"Video is too short, and must be at least {self.min_length} seconds" - ) - if self.max_length is not None and duration > self.max_length: - raise gr.Error( - f"Video is too long, and must be at most {self.max_length} seconds" - ) # TODO: Check other image extensions to see if they work. valid_watermark_extensions = [".png", ".jpg", ".jpeg"] if self.watermark.watermark is not None: diff --git a/gradio/templates.py b/gradio/templates.py index d046b587e8..3c60d91adb 100644 --- a/gradio/templates.py +++ b/gradio/templates.py @@ -410,8 +410,6 @@ def __init__( autoplay: bool = False, show_share_button: bool | None = None, show_download_button: bool | None = None, - min_length: int | None = None, - max_length: int | None = None, loop: bool = False, streaming: bool = False, watermark: str | Path | None = None, @@ -441,8 +439,6 @@ def __init__( autoplay=autoplay, show_share_button=show_share_button, show_download_button=show_download_button, - min_length=min_length, - max_length=max_length, loop=loop, streaming=streaming, watermark=watermark, @@ -492,8 +488,6 @@ def __init__( show_download_button: bool | None = None, show_share_button: bool | None = None, editable: bool = True, - min_length: int | None = None, - max_length: int | None = None, waveform_options: WaveformOptions | dict | None = None, loop: bool = False, recording: bool = False, @@ -524,8 +518,6 @@ def __init__( show_download_button=show_download_button, show_share_button=show_share_button, editable=editable, - min_length=min_length, - max_length=max_length, waveform_options=waveform_options, loop=loop, recording=recording, diff --git a/gradio/validators.py b/gradio/validators.py new file mode 100644 index 0000000000..b3d1b84c4c --- /dev/null +++ b/gradio/validators.py @@ -0,0 +1,71 @@ +from typing import TYPE_CHECKING, Any + +from gradio_client.documentation import document + +if TYPE_CHECKING: + import numpy as np + + +@document() +def is_audio_correct_length( + audio: tuple[int, "np.ndarray"], min_length: float | None, max_length: float | None +) -> dict[str, Any]: + """ + Validates that the audio length is within the specified min and max length (in seconds). + + Parameters: + audio: A tuple of (sample rate in Hz, audio data as numpy array). + min_length: Minimum length of audio in seconds. If None, no minimum length check is performed. + max_length: Maximum length of audio in seconds. If None, no maximum length check is performed. + Returns: + A dict corresponding to `gr.validate()` indicating whether the audio length is valid and an optional message. + """ + if min_length is not None or max_length is not None: + sample_rate, data = audio + duration = len(data) / sample_rate + if min_length is not None and duration < min_length: + return { + "__type__": "validate", + "is_valid": False, + "message": f"Audio is too short. It must be at least {min_length} seconds", + } + if max_length is not None and duration > max_length: + return { + "__type__": "validate", + "is_valid": False, + "message": f"Audio is too long. It must be at most {max_length} seconds", + } + return {"__type__": "validate", "is_valid": True} + + +@document() +def is_video_correct_length( + video: str, min_length: float | None, max_length: float | None +) -> dict[str, Any]: + """ + Validates that the video file length is within the specified min and max length (in seconds). + + Parameters: + video: The path to the video file. + min_length: Minimum length of video in seconds. If None, no minimum length check is performed. + max_length: Maximum length of video in seconds. If None, no maximum length check is performed. + Returns: + A dict corresponding to `gr.validate()` indicating whether the audio length is valid and an optional message. + """ + from gradio.processing_utils import get_video_length + + if min_length is not None or max_length is not None: + duration = get_video_length(video) + if min_length is not None and duration < min_length: + return { + "__type__": "validate", + "is_valid": False, + "message": f"Video is too short. It must be at least {min_length} seconds", + } + if max_length is not None and duration > max_length: + return { + "__type__": "validate", + "is_valid": False, + "message": f"Video is too long. It must be at most {max_length} seconds", + } + return {"__type__": "validate", "is_valid": True} diff --git a/js/_website/generate_jsons/src/docs/__init__.py b/js/_website/generate_jsons/src/docs/__init__.py index 81db6c4014..b719568013 100644 --- a/js/_website/generate_jsons/src/docs/__init__.py +++ b/js/_website/generate_jsons/src/docs/__init__.py @@ -31,6 +31,8 @@ def add_component_shortcuts(): "Uses default values", ) ] + if not hasattr(component["class"], "__subclasses__"): + continue for subcls in component["class"].__subclasses__(): if getattr(subcls, "is_template", False): _, tags, _ = document_cls(subcls) diff --git a/js/_website/src/lib/templates/gradio/03_components/audio.svx b/js/_website/src/lib/templates/gradio/03_components/audio.svx index 6cb233aad4..d2d6a6efdf 100644 --- a/js/_website/src/lib/templates/gradio/03_components/audio.svx +++ b/js/_website/src/lib/templates/gradio/03_components/audio.svx @@ -11,6 +11,7 @@ let obj = get_object("audio"); let waveform_obj = get_object("waveformoptions"); + let validator_obj = get_object("is_audio_correct_length") @@ -103,6 +104,27 @@ gradio.WaveformOptions(···) #### Initialization + +### is_audio_correct_length + +Validates that the audio length is within the specified min and max length (in seconds). +You can use this to construct a validator that will check if the user-provided audio is either too short or too long. + + +```python +import gradio as gr +demo = gr.Interface( + lambda x: x, + inputs="audio", + outputs="audio", + validator=lambda audio: gr.validators.is_audio_correct_length(audio, min_length=1, max_length=5) +) +demo.launch() +``` + + +#### Initialization + diff --git a/js/_website/src/lib/templates/gradio/03_components/video.svx b/js/_website/src/lib/templates/gradio/03_components/video.svx index d98b2ceb88..d0d630738c 100644 --- a/js/_website/src/lib/templates/gradio/03_components/video.svx +++ b/js/_website/src/lib/templates/gradio/03_components/video.svx @@ -11,6 +11,7 @@ let obj = get_object("video"); let webcam_options_obj = get_object("webcamoptions") + let validator_obj = get_object("is_video_correct_length") @@ -103,6 +104,28 @@ gradio.WebcamOptions(···) #### Initialization + +### is_video_correct_length + +Validates that the audio length is within the specified min and max length (in seconds). +You can use this to construct a validator that will check if the user-provided audio is either too short or too long. + + +```python +import gradio as gr +demo = gr.Interface( + lambda x: x, + inputs="video", + outputs="video", + validator=lambda video: gr.validators.is_video_correct_length(video, min_length=1, max_length=5) +) +demo.launch() +``` + + +#### Initialization + + {#if obj.guides && obj.guides.length > 0} diff --git a/test/components/test_audio.py b/test/components/test_audio.py index 9fc2dd2434..f7d7a57bd7 100644 --- a/test/components/test_audio.py +++ b/test/components/test_audio.py @@ -59,8 +59,6 @@ async def test_component_functions(self, gradio_temp_dir): "format": None, "recording": False, "streamable": False, - "max_length": None, - "min_length": None, "waveform_options": { "sample_rate": 44100, "show_recording_waveform": True, @@ -103,8 +101,6 @@ async def test_component_functions(self, gradio_temp_dir): "streaming": False, "show_label": True, "label": None, - "max_length": None, - "min_length": None, "container": True, "editable": True, "min_width": 160, @@ -210,3 +206,15 @@ async def test_combine_stream_audio(self, gradio_temp_dir): bytes_output, desired_output_format=None ) assert str(output.path).endswith("mp3") + + +def test_duration_validator(): + assert gr.validators.is_audio_correct_length((8000, np.zeros((8000,))), 1, 2)[ + "is_valid" + ] + assert not gr.validators.is_audio_correct_length((8000, np.zeros((8000,))), 2, 3)[ + "is_valid" + ] + assert not gr.validators.is_audio_correct_length( + (8000, np.zeros((8000,))), 0.25, 0.75 + )["is_valid"] diff --git a/test/components/test_video.py b/test/components/test_video.py index 65b2546049..3adf00ef61 100644 --- a/test/components/test_video.py +++ b/test/components/test_video.py @@ -63,8 +63,6 @@ async def test_component_functions(self): "webcam_options": {"constraints": None, "mirror": True}, "include_audio": True, "format": None, - "min_length": None, - "max_length": None, "_selectable": False, "key": None, "preserved_by_key": ["value"], @@ -254,3 +252,31 @@ def test_video_preprocessing_flips_video_for_webcam(self, mock_ffmpeg): assert "flip" not in Path(list(output_params.keys())[0]).name assert ".avi" in list(output_params.keys())[0] assert ".avi" in output_file + + +def test_is_video_correct_length(): + test_file_dir = Path(__file__).parent.parent / "test_files" + video_path = str(test_file_dir / "muted_video_sample.mp4") + assert ( + gr.validators.is_video_correct_length(video_path, None, None)["is_valid"] + is True + ) + assert ( + gr.validators.is_video_correct_length(video_path, 1, None)["is_valid"] is True + ) + assert ( + gr.validators.is_video_correct_length(video_path, 1000, None)["is_valid"] + is False + ) + assert ( + gr.validators.is_video_correct_length(video_path, None, 1000)["is_valid"] + is True + ) + assert ( + gr.validators.is_video_correct_length(video_path, None, 1)["is_valid"] is False + ) + assert ( + gr.validators.is_video_correct_length(video_path, 1, 1000)["is_valid"] is True + ) + assert gr.validators.is_video_correct_length(video_path, 1, 5)["is_valid"] is True + assert gr.validators.is_video_correct_length(video_path, 1, 2)["is_valid"] is False