""" Module containing common utility functions and classes for the frontend. """ from typing import Any, Callable, Concatenate, Literal, Sequence from typings.extra import ( ComponentVisibilityKwArgs, DropdownChoices, DropdownValue, F0Method, P, T, TextBoxArgs, UpdateDropdownArgs, ) from dataclasses import dataclass from functools import partial import gradio as gr from gradio.components.base import Component from gradio.events import Dependency from backend.generate_song_cover import get_named_song_dirs, get_song_cover_name from backend.manage_audio import get_output_audio PROGRESS_BAR = gr.Progress() def exception_harness(fun: Callable[P, T]) -> Callable[P, T]: """ Wrap a function in a harness that catches exceptions and re-raises them as instances of `gradio.Error`. Parameters ---------- fun : Callable[P, T] The function to wrap. Returns ------- Callable[P, T] The wrapped function. """ def _wrapped_fun(*args: P.args, **kwargs: P.kwargs) -> T: try: return fun(*args, **kwargs) except Exception as e: raise gr.Error(str(e)) return _wrapped_fun def confirmation_harness(fun: Callable[P, T]) -> Callable[Concatenate[bool, P], T]: """ Wrap a function in a harness that requires a confirmation before executing and catches exceptions, re-raising them as instances of `gradio.Error`. Parameters ---------- fun : Callable[P, T] The function to wrap. Returns ------- Callable[Concatenate[bool, P], T] The wrapped function. """ def _wrapped_fun(confirm: bool, *args: P.args, **kwargs: P.kwargs) -> T: if confirm: return exception_harness(fun)(*args, **kwargs) else: raise gr.Error("Confirmation missing!") return _wrapped_fun def confirm_box_js(msg: str) -> str: """ Generate JavaScript code for a confirmation box. Parameters ---------- msg : str Message to display in the confirmation box. Returns ------- str JavaScript code for the confirmation box. """ formatted_msg = f"'{msg}'" return f"(x) => confirm({formatted_msg})" def identity(x: T) -> T: """ Identity function. Parameters ---------- x : T Value to return. Returns ------- T The value. """ return x def update_value(x: Any) -> dict[str, Any]: """ Update the value of a component. Parameters ---------- x : Any New value for the component. Returns ------- dict[str, Any] Dictionary which updates the value of the component. """ return gr.update(value=x) def update_dropdowns( fn: Callable[P, DropdownChoices], num_components: int, value: DropdownValue = None, value_indices: Sequence[int] = [], *args: P.args, **kwargs: P.kwargs, ) -> gr.Dropdown | tuple[gr.Dropdown, ...]: """ Update the choices and optionally the value of one or more dropdown components. Parameters ---------- fn : Callable[P, DropdownChoices] Function to get updated choices for the dropdown components. num_components : int Number of dropdown components to update. value : DropdownValue, optional New value for dropdown components. value_indices : Sequence[int], default=[] Indices of dropdown components to update the value for. args : P.args Positional arguments to pass to the function used to update choices. kwargs : P.kwargs Keyword arguments to pass to the function used to update choices. Returns ------- gr.Dropdown|tuple[gr.Dropdown,...] Updated dropdown component or components. Raises ------ ValueError If value indices are not unique or if an index exceeds the number of components. """ if len(value_indices) != len(set(value_indices)): raise ValueError("Value indices must be unique.") if value_indices and max(value_indices) >= num_components: raise ValueError( "Index of a component to update value for exceeds number of components." ) updated_choices = fn(*args, **kwargs) update_args: list[UpdateDropdownArgs] = [ {"choices": updated_choices} for _ in range(num_components) ] for index in value_indices: update_args[index]["value"] = value if len(update_args) == 1: # NOTE This is a workaround as gradio does not support # singleton tuples for components. return gr.Dropdown(**update_args[0]) return tuple(gr.Dropdown(**update_arg) for update_arg in update_args) def update_cached_input_songs( num_components: int, value: DropdownValue = None, value_indices: Sequence[int] = [] ) -> gr.Dropdown | tuple[gr.Dropdown, ...]: """ Updates the choices of one or more dropdown components to the current set of cached input songs. Optionally updates the default value of one or more of these components. Parameters ---------- num_components : int Number of dropdown components to update. value : DropdownValue, optional New value for dropdown components. value_indices : Sequence[int], default=[] Indices of dropdown components to update the value for. Returns ------- gr.Dropdown|tuple[gr.Dropdown,...] Updated dropdown component or components. """ return update_dropdowns(get_named_song_dirs, num_components, value, value_indices) def update_output_audio( num_components: int, value: DropdownValue = None, value_indices: Sequence[int] = [] ) -> gr.Dropdown | tuple[gr.Dropdown, ...]: """ Updates the choices of one or more dropdown components to the current set of output audio files. Optionally updates the default value of one or more of these components. Parameters ---------- num_components : int Number of dropdown components to update. value : DropdownValue, optional New value for dropdown components. value_indices : Sequence[int], default=[] Indices of dropdown components to update the value for. Returns ------- gr.Dropdown|tuple[gr.Dropdown,...] Updated dropdown component or components. """ return update_dropdowns(get_output_audio, num_components, value, value_indices) def toggle_visible_component( num_components: int, visible_index: int ) -> dict[str, Any] | tuple[dict[str, Any], ...]: """ Reveal a single component from a set of components. All other components are hidden. Parameters ---------- num_components : int Number of components to set visibility for. visible_index : int Index of the component to reveal. Returns ------- dict|tuple[dict,...] A single dictionary or a tuple of dictionaries that update the visibility of the components. """ if visible_index >= num_components: raise ValueError("Visible index must be less than number of components.") update_args: list[ComponentVisibilityKwArgs] = [ {"visible": False, "value": None} for _ in range(num_components) ] update_args[visible_index]["visible"] = True if num_components == 1: return gr.update(**update_args[0]) return tuple(gr.update(**update_arg) for update_arg in update_args) def _toggle_component_interactivity( num_components: int, interactive: bool ) -> dict[str, Any] | tuple[dict[str, Any], ...]: """ Toggle interactivity of one or more components. Parameters ---------- num_components : int Number of components to toggle interactivity for. interactive : bool Whether to make the components interactive or not. Returns ------- dict|tuple[dict,...] A single dictionary or a tuple of dictionaries that update the interactivity of the components. """ if num_components == 1: return gr.update(interactive=interactive) return tuple(gr.update(interactive=interactive) for _ in range(num_components)) def show_hop_slider(pitch_detection_algo: F0Method) -> gr.Slider: """ Show or hide a slider component based on the given pitch extraction algorithm. Parameters ---------- pitch_detection_algo : F0Method Pitch detection algorithm to determine visibility of the slider. Returns ------- gr.Slider Slider component with visibility set accordingly. """ if pitch_detection_algo == "mangio-crepe": return gr.Slider(visible=True) else: return gr.Slider(visible=False) def update_song_cover_name( mixed_vocals: str | None = None, song_dir: str | None = None, voice_model: str | None = None, update_placeholder: bool = False, ) -> gr.Textbox: """ Updates a textbox component so that it displays a suitable name for a cover of a given song. If the path of an existing song directory is provided, the original song name is inferred from that directory. If a voice model is not provided but the path of an existing song directory and the path of a mixed vocals file in that directory are provided, then the voice model is inferred from the mixed vocals file. Parameters ---------- mixed_vocals : str, optional The path to a mixed vocals file. song_dir : str, optional The path to a song directory. voice_model : str, optional The name of a voice model. update_placeholder : bool, default=False Whether to update the placeholder text of the textbox component. Returns ------- gr.Textbox Updated textbox component. """ update_args: TextBoxArgs = {} update_key = "placeholder" if update_placeholder else "value" if mixed_vocals or song_dir or voice_model: name = exception_harness(get_song_cover_name)( mixed_vocals, song_dir, voice_model, progress_bar=PROGRESS_BAR ) update_args[update_key] = name else: update_args[update_key] = None return gr.Textbox(**update_args) @dataclass class EventArgs: """ Data class to store arguments for setting up event listeners. Attributes ---------- fn : Callable[..., Any] Function to call when an event is triggered. inputs : Sequence[Component], optional Components to serve as inputs to the function. outputs : Sequence[Component], optional Components where to store the outputs of the function. name : Literal["click", "success", "then"], default="success" Name of the event to listen for. show_progress : Literal["full", "minimal", "hidden"], default="full" Level of progress bar to show when the event is triggered. """ fn: Callable[..., Any] inputs: Sequence[Component] | None = None outputs: Sequence[Component] | None = None name: Literal["click", "success", "then"] = "success" show_progress: Literal["full", "minimal", "hidden"] = "full" def setup_consecutive_event_listeners( component: Component, event_args_list: list[EventArgs] ) -> Dependency | Component: """ Set up a chain of event listeners on a component. Parameters ---------- component : Component The component to set up event listeners on. event_args_list : list[EventArgs] List of event arguments to set up event listeners with. Returns ------- Dependency | Component The last dependency in the chain of event listeners. """ if len(event_args_list) == 0: raise ValueError("Event args list must not be empty.") dependency = component for event_args in event_args_list: event_listener = getattr(dependency, event_args.name) dependency = event_listener( event_args.fn, inputs=event_args.inputs, outputs=event_args.outputs, show_progress=event_args.show_progress, ) return dependency def setup_consecutive_event_listeners_with_toggled_interactivity( component: Component, event_args_list: list[EventArgs], toggled_components: Sequence[Component], ) -> Dependency | Component: """ Set up a chain of event listeners on a component with interactivity toggled for a set of other components. While the chain of event listeners is being executed, the other components are made non-interactive. When the chain of event listeners is completed, the other components are made interactive again. Parameters ---------- component : Component The component to set up event listeners on. event_args_list : list[EventArgs] List of event arguments to set up event listeners with. toggled_components : Sequence[Component] Components to toggle interactivity for. Returns ------- Dependency | Component The last dependency in the chain of event listeners. """ if len(event_args_list) == 0: raise ValueError("Event args list must not be empty.") disable_event_args = EventArgs( partial(_toggle_component_interactivity, len(toggled_components), False), outputs=toggled_components, name="click", show_progress="hidden", ) enable_event_args = EventArgs( partial(_toggle_component_interactivity, len(toggled_components), True), outputs=toggled_components, name="then", show_progress="hidden", ) event_args_list_augmented = ( [disable_event_args] + event_args_list + [enable_event_args] ) return setup_consecutive_event_listeners(component, event_args_list_augmented)