commit 8a313bd700d82f499036b0e30293bcad4bf4d984 Author: 杍超 Date: Tue Jan 21 13:55:45 2025 +0800 t :# 请为您的变更输入提交说明。以 '#' 开始的行将被忽略,而一个空的提交 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..f58f8c4 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,28 @@ +name: docs +on: + push: + branches: + - main +permissions: + contents: write +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Configure Git Credentials + run: | + git config user.name github-actions[bot] + git config user.email 41898282+github-actions[bot]@users.noreply.github.com + - uses: actions/setup-python@v5 + with: + python-version: 3.x + - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV + - uses: actions/cache@v4 + with: + key: mkdocs-material-${{ env.cache_id }} + path: .cache + restore-keys: | + mkdocs-material- + - run: pip install mkdocs-material + - run: mkdocs gh-deploy --force \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5a9f2b3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,17 @@ +.eggs/ +dist/ +*.pyc +__pycache__/ +*.py[cod] +*$py.class +__tmp/* +*.pyi +.mypycache +.ruff_cache +node_modules +backend/**/templates/ +demo/MobileNetSSD_deploy.caffemodel +demo/MobileNetSSD_deploy.prototxt.txt +.DS_Store +test/ +.env \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..799e91a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 Freddy Boulton + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ddb403 --- /dev/null +++ b/README.md @@ -0,0 +1,441 @@ +

Gradio WebRTC ⚡️

+ +
+Static Badge +Static Badge +Static Badge +
+ +

+Stream video and audio in real time with Gradio using WebRTC. +

+ +## Installation + +```bash +pip install gradio_webrtc +``` + +to use built-in pause detection (see [ReplyOnPause](https://freddyaboulton.github.io/gradio-webrtc//user-guide/#reply-on-pause)), install the `vad` extra: + +```bash +pip install gradio_webrtc[vad] +``` + +For stop word detection (see [ReplyOnStopWords](https://freddyaboulton.github.io/gradio-webrtc//user-guide/#reply-on-stopwords)), install the `stopword` extra: + +```bash +pip install gradio_webrtc[stopword] +``` + +## Docs + +https://freddyaboulton.github.io/gradio-webrtc/ + +## Examples + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

🗣️ Audio Input/Output with mini-omni2

+

Build a GPT-4o like experience with mini-omni2, an audio-native LLM.

+ +

+Demo | +Code +

+
+

🗣️ Talk to Claude

+

Use the Anthropic and Play.Ht APIs to have an audio conversation with Claude.

+ +

+Demo | +Code +

+
+

🗣️ Kyutai Moshi

+

Kyutai's moshi is a novel speech-to-speech model for modeling human conversations.

+ +

+Demo | +Code +

+
+

🗣️ Hello Llama: Stop Word Detection

+

A code editor built with Llama 3.3 70b that is triggered by the phrase "Hello Llama". Build a Siri-like coding assistant in 100 lines of code!

+ +

+Demo | +Code +

+
+

🤖 Llama Code Editor

+

Create and edit HTML pages with just your voice! Powered by SambaNova systems.

+ +

+Demo | +Code +

+
+

🗣️ Talk to Ultravox

+

Talk to Fixie.AI's audio-native Ultravox LLM with the transformers library.

+ +

+Demo | +Code +

+
+

🗣️ Talk to Llama 3.2 3b

+

Use the Lepton API to make Llama 3.2 talk back to you!

+ +

+Demo | +Code +

+
+

🤖 Talk to Qwen2-Audio

+

Qwen2-Audio is a SOTA audio-to-text LLM developed by Alibaba.

+ +

+Demo | +Code +

+
+

📷 Yolov10 Object Detection

+

Run the Yolov10 model on a user webcam stream in real time!

+ +

+Demo | +Code +

+
+

📷 Video Object Detection with RT-DETR

+

Upload a video and stream out frames with detected objects (powered by RT-DETR) model.

+

+Demo | +Code +

+
+

🔊 Text-to-Speech with Parler

+

Stream out audio generated by Parler TTS!

+

+Demo | +Code +

+
+
+ +## Usage + +This is an shortened version of the official [usage guide](https://freddyaboulton.github.io/gradio-webrtc/user-guide/). + +To get started with WebRTC streams, all that's needed is to import the `WebRTC` component from this package and implement its `stream` event. + +### Reply on Pause + +Typically, you want to run an AI model that generates audio when the user has stopped speaking. This can be done by wrapping a python generator with the `ReplyOnPause` class +and passing it to the `stream` event of the `WebRTC` component. + +```py +import gradio as gr +from gradio_webrtc import WebRTC, ReplyOnPause + +def response(audio: tuple[int, np.ndarray]): # (1) + """This function must yield audio frames""" + ... + for numpy_array in generated_audio: + yield (sampling_rate, numpy_array, "mono") # (2) + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Chat (Powered by WebRTC ⚡️) +

+ """ + ) + with gr.Column(): + with gr.Group(): + audio = WebRTC( + mode="send-receive", # (3) + modality="audio", + ) + audio.stream(fn=ReplyOnPause(response), + inputs=[audio], outputs=[audio], # (4) + time_limit=60) # (5) + +demo.launch() +``` + +1. The python generator will receive the **entire** audio up until the user stopped. It will be a tuple of the form (sampling_rate, numpy array of audio). The array will have a shape of (1, num_samples). You can also pass in additional input components. + +2. The generator must yield audio chunks as a tuple of (sampling_rate, numpy audio array). Each numpy audio array must have a shape of (1, num_samples). + +3. The `mode` and `modality` arguments must be set to `"send-receive"` and `"audio"`. + +4. The `WebRTC` component must be the first input and output component. + +5. Set a `time_limit` to control how long a conversation will last. If the `concurrency_count` is 1 (default), only one conversation will be handled at a time. + + +### Reply On Stopwords + +You can configure your AI model to run whenever a set of "stop words" are detected, like "Hey Siri" or "computer", with the `ReplyOnStopWords` class. + +The API is similar to `ReplyOnPause` with the addition of a `stop_words` parameter. + + +```py +import gradio as gr +from gradio_webrtc import WebRTC, ReplyOnPause + +def response(audio: tuple[int, np.ndarray]): + """This function must yield audio frames""" + ... + for numpy_array in generated_audio: + yield (sampling_rate, numpy_array, "mono") + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Chat (Powered by WebRTC ⚡️) +

+ """ + ) + with gr.Column(): + with gr.Group(): + audio = WebRTC( + mode="send", + modality="audio", + ) + webrtc.stream(ReplyOnStopWords(generate, + input_sample_rate=16000, + stop_words=["computer"]), # (1) + inputs=[webrtc, history, code], + outputs=[webrtc], time_limit=90, + concurrency_limit=10) + +demo.launch() +``` + +1. The `stop_words` can be single words or pairs of words. Be sure to include common misspellings of your word for more robust detection, e.g. "llama", "lamma". In my experience, it's best to use two very distinct words like "ok computer" or "hello iris". + + +### Audio Server-To-Clien + +To stream only from the server to the client, implement a python generator and pass it to the component's `stream` event. The stream event must also specify a `trigger` corresponding to a UI interaction that starts the stream. In this case, it's a button click. + + + +```py +import gradio as gr +from gradio_webrtc import WebRTC +from pydub import AudioSegment + +def generation(num_steps): + for _ in range(num_steps): + segment = AudioSegment.from_file("audio_file.wav") + array = np.array(segment.get_array_of_samples()).reshape(1, -1) + yield (segment.frame_rate, array) + +with gr.Blocks() as demo: + audio = WebRTC(label="Stream", mode="receive", # (1) + modality="audio") + num_steps = gr.Slider(label="Number of Steps", minimum=1, + maximum=10, step=1, value=5) + button = gr.Button("Generate") + + audio.stream( + fn=generation, inputs=[num_steps], outputs=[audio], + trigger=button.click # (2) + ) +``` + +1. Set `mode="receive"` to only receive audio from the server. +2. The `stream` event must take a `trigger` that corresponds to the gradio event that starts the stream. In this case, it's the button click. + + +### Video Input/Output Streaming +Set up a video Input/Output stream to continuosly receive webcam frames from the user and run an arbitrary python function to return a modified frame. + +```py +import gradio as gr +from gradio_webrtc import WebRTC + + +def detection(image, conf_threshold=0.3): # (1) + ... your detection code here ... + return modified_frame # (2) + + +with gr.Blocks() as demo: + image = WebRTC(label="Stream", mode="send-receive", modality="video") # (3) + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + image.stream( + fn=detection, + inputs=[image, conf_threshold], # (4) + outputs=[image], time_limit=10 + ) + +if __name__ == "__main__": + demo.launch() +``` + +1. The webcam frame will be represented as a numpy array of shape (height, width, RGB). +2. The function must return a numpy array. It can take arbitrary values from other components. +3. Set the `modality="video"` and `mode="send-receive"` +4. The `inputs` parameter should be a list where the first element is the WebRTC component. The only output allowed is the WebRTC component. + +### Server-to-Client Only + +Set up a server-to-client stream to stream video from an arbitrary user interaction. + +```py +import gradio as gr +from gradio_webrtc import WebRTC +import cv2 + +def generation(): + url = "https://download.tsi.telecom-paristech.fr/gpac/dataset/dash/uhd/mux_sources/hevcds_720p30_2M.mp4" + cap = cv2.VideoCapture(url) + iterating = True + while iterating: + iterating, frame = cap.read() + yield frame # (1) + +with gr.Blocks() as demo: + output_video = WebRTC(label="Video Stream", mode="receive", # (2) + modality="video") + button = gr.Button("Start", variant="primary") + output_video.stream( + fn=generation, inputs=None, outputs=[output_video], + trigger=button.click # (3) + ) + demo.launch() +``` + +1. The `stream` event's `fn` parameter is a generator function that yields the next frame from the video as a **numpy array**. +2. Set `mode="receive"` to only receive audio from the server. +3. The `trigger` parameter the gradio event that will trigger the stream. In this case, the button click event. + + +### Additional Outputs + +In order to modify other components from within the WebRTC stream, you must yield an instance of `AdditionalOutputs` and add an `on_additional_outputs` event to the `WebRTC` component. + +This is common for displaying a multimodal text/audio conversation in a Chatbot UI. + + + +``` py title="Additional Outputs" +from gradio_webrtc import AdditionalOutputs, WebRTC + +def transcribe(audio: tuple[int, np.ndarray], + transformers_convo: list[dict], + gradio_convo: list[dict]): + response = model.generate(**inputs, max_length=256) + transformers_convo.append({"role": "assistant", "content": response}) + gradio_convo.append({"role": "assistant", "content": response}) + yield AdditionalOutputs(transformers_convo, gradio_convo) # (1) + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Talk to Qwen2Audio (Powered by WebRTC ⚡️) +

+ """ + ) + transformers_convo = gr.State(value=[]) + with gr.Row(): + with gr.Column(): + audio = WebRTC( + label="Stream", + mode="send", # (2) + modality="audio", + ) + with gr.Column(): + transcript = gr.Chatbot(label="transcript", type="messages") + + audio.stream(ReplyOnPause(transcribe), + inputs=[audio, transformers_convo, transcript], + outputs=[audio], time_limit=90) + audio.on_additional_outputs(lambda s,a: (s,a), # (3) + outputs=[transformers_convo, transcript], + queue=False, show_progress="hidden") + demo.launch() +``` + + 1. Pass your data to `AdditionalOutputs` and yield it. + 2. In this case, no audio is being returned, so we set `mode="send"`. However, if we set `mode="send-receive"`, we could also yield generated audio and `AdditionalOutputs`. + 3. The `on_additional_outputs` event does not take `inputs`. It's common practice to not run this event on the queue since it is just a quick UI update. +=== "Notes" + 1. Pass your data to `AdditionalOutputs` and yield it. + 2. In this case, no audio is being returned, so we set `mode="send"`. However, if we set `mode="send-receive"`, we could also yield generated audio and `AdditionalOutputs`. + 3. The `on_additional_outputs` event does not take `inputs`. It's common practice to not run this event on the queue since it is just a quick UI update. + + +## Deployment + +When deploying in a cloud environment (like Hugging Face Spaces, EC2, etc), you need to set up a TURN server to relay the WebRTC traffic. +The easiest way to do this is to use a service like Twilio. + +```python +from twilio.rest import Client +import os + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +client = Client(account_sid, auth_token) + +token = client.tokens.create() + +rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", +} + +with gr.Blocks() as demo: + ... + rtc = WebRTC(rtc_configuration=rtc_configuration, ...) + ... +``` \ No newline at end of file diff --git a/backend/gradio_webrtc/__init__.py b/backend/gradio_webrtc/__init__.py new file mode 100644 index 0000000..8f0db37 --- /dev/null +++ b/backend/gradio_webrtc/__init__.py @@ -0,0 +1,54 @@ +from .credentials import ( + get_hf_turn_credentials, + get_turn_credentials, + get_twilio_turn_credentials, +) +from .reply_on_pause import AlgoOptions, ReplyOnPause, SileroVadOptions +from .reply_on_stopwords import ReplyOnStopWords +from .speech_to_text import stt, stt_for_chunks +from .utils import ( + AdditionalOutputs, + Warning, + WebRTCError, + aggregate_bytes_to_16bit, + async_aggregate_bytes_to_16bit, + audio_to_bytes, + audio_to_file, + audio_to_float32, +) +from .webrtc import ( + AsyncAudioVideoStreamHandler, + AsyncStreamHandler, + AudioVideoStreamHandler, + StreamHandler, + WebRTC, + VideoEmitType, + AudioEmitType, +) + +__all__ = [ + "AsyncStreamHandler", + "AudioVideoStreamHandler", + "AudioEmitType", + "AsyncAudioVideoStreamHandler", + "AlgoOptions", + "AdditionalOutputs", + "aggregate_bytes_to_16bit", + "async_aggregate_bytes_to_16bit", + "audio_to_bytes", + "audio_to_file", + "audio_to_float32", + "get_hf_turn_credentials", + "get_twilio_turn_credentials", + "get_turn_credentials", + "ReplyOnPause", + "ReplyOnStopWords", + "SileroVadOptions", + "stt", + "stt_for_chunks", + "StreamHandler", + "VideoEmitType", + "WebRTC", + "WebRTCError", + "Warning", +] diff --git a/backend/gradio_webrtc/credentials.py b/backend/gradio_webrtc/credentials.py new file mode 100644 index 0000000..ea5e83a --- /dev/null +++ b/backend/gradio_webrtc/credentials.py @@ -0,0 +1,52 @@ +import os +from typing import Literal + +import requests + + +def get_hf_turn_credentials(token=None): + if token is None: + token = os.getenv("HF_TOKEN") + credentials = requests.get( + "https://freddyaboulton-turn-server-login.hf.space/credentials", + headers={"X-HF-Access-Token": token}, + ) + if not credentials.status_code == 200: + raise ValueError("Failed to get credentials from HF turn server") + return { + "iceServers": [ + { + "urls": "turn:gradio-turn.com:80", + **credentials.json(), + }, + ] + } + + +def get_twilio_turn_credentials(twilio_sid=None, twilio_token=None): + try: + from twilio.rest import Client + except ImportError: + raise ImportError("Please install twilio with `pip install twilio`") + + if not twilio_sid and not twilio_token: + twilio_sid = os.environ.get("TWILIO_ACCOUNT_SID") + twilio_token = os.environ.get("TWILIO_AUTH_TOKEN") + + client = Client(twilio_sid, twilio_token) + + token = client.tokens.create() + + return { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } + + +def get_turn_credentials(method: Literal["hf", "twilio"] = "hf", **kwargs): + if method == "hf": + return get_hf_turn_credentials(**kwargs) + elif method == "twilio": + return get_twilio_turn_credentials(**kwargs) + else: + raise ValueError("Invalid method. Must be 'hf' or 'twilio'") diff --git a/backend/gradio_webrtc/pause_detection/__init__.py b/backend/gradio_webrtc/pause_detection/__init__.py new file mode 100644 index 0000000..e4874b7 --- /dev/null +++ b/backend/gradio_webrtc/pause_detection/__init__.py @@ -0,0 +1,3 @@ +from .vad import SileroVADModel, SileroVadOptions + +__all__ = ["SileroVADModel", "SileroVadOptions"] diff --git a/backend/gradio_webrtc/pause_detection/vad.py b/backend/gradio_webrtc/pause_detection/vad.py new file mode 100644 index 0000000..bf4bb1e --- /dev/null +++ b/backend/gradio_webrtc/pause_detection/vad.py @@ -0,0 +1,320 @@ +import logging +import warnings +from dataclasses import dataclass +from typing import List, Literal, overload + +import numpy as np +from huggingface_hub import hf_hub_download +from numpy.typing import NDArray + +from ..utils import AudioChunk + +logger = logging.getLogger(__name__) + +# The code below is adapted from https://github.com/snakers4/silero-vad. +# The code below is adapted from https://github.com/gpt-omni/mini-omni/blob/main/utils/vad.py + + +@dataclass +class SileroVadOptions: + """VAD options. + + Attributes: + threshold: Speech threshold. Silero VAD outputs speech probabilities for each audio chunk, + probabilities ABOVE this value are considered as SPEECH. It is better to tune this + parameter for each dataset separately, but "lazy" 0.5 is pretty good for most datasets. + min_speech_duration_ms: Final speech chunks shorter min_speech_duration_ms are thrown out. + max_speech_duration_s: Maximum duration of speech chunks in seconds. Chunks longer + than max_speech_duration_s will be split at the timestamp of the last silence that + lasts more than 100ms (if any), to prevent aggressive cutting. Otherwise, they will be + split aggressively just before max_speech_duration_s. + min_silence_duration_ms: In the end of each speech chunk wait for min_silence_duration_ms + before separating it + window_size_samples: Audio chunks of window_size_samples size are fed to the silero VAD model. + WARNING! Silero VAD models were trained using 512, 1024, 1536 samples for 16000 sample rate. + Values other than these may affect model performance!! + speech_pad_ms: Final speech chunks are padded by speech_pad_ms each side + speech_duration: If the length of the speech is less than this value, a pause will be detected. + """ + + threshold: float = 0.5 + min_speech_duration_ms: int = 250 + max_speech_duration_s: float = float("inf") + min_silence_duration_ms: int = 2000 + window_size_samples: int = 1024 + speech_pad_ms: int = 400 + + +class SileroVADModel: + @staticmethod + def download_model() -> str: + return hf_hub_download( + repo_id="freddyaboulton/silero-vad", filename="silero_vad.onnx" + ) + + def __init__(self): + try: + import onnxruntime + except ImportError as e: + raise RuntimeError( + "Applying the VAD filter requires the onnxruntime package" + ) from e + + path = self.download_model() + + opts = onnxruntime.SessionOptions() + opts.inter_op_num_threads = 1 + opts.intra_op_num_threads = 1 + opts.log_severity_level = 4 + + self.session = onnxruntime.InferenceSession( + path, + providers=["CPUExecutionProvider"], + sess_options=opts, + ) + + def get_initial_state(self, batch_size: int): + h = np.zeros((2, batch_size, 64), dtype=np.float32) + c = np.zeros((2, batch_size, 64), dtype=np.float32) + return h, c + + @staticmethod + def collect_chunks(audio: np.ndarray, chunks: List[AudioChunk]) -> np.ndarray: + """Collects and concatenates audio chunks.""" + if not chunks: + return np.array([], dtype=np.float32) + + return np.concatenate( + [audio[chunk["start"] : chunk["end"]] for chunk in chunks] + ) + + def get_speech_timestamps( + self, + audio: np.ndarray, + vad_options: SileroVadOptions, + **kwargs, + ) -> List[AudioChunk]: + """This method is used for splitting long audios into speech chunks using silero VAD. + + Args: + audio: One dimensional float array. + vad_options: Options for VAD processing. + kwargs: VAD options passed as keyword arguments for backward compatibility. + + Returns: + List of dicts containing begin and end samples of each speech chunk. + """ + + threshold = vad_options.threshold + min_speech_duration_ms = vad_options.min_speech_duration_ms + max_speech_duration_s = vad_options.max_speech_duration_s + min_silence_duration_ms = vad_options.min_silence_duration_ms + window_size_samples = vad_options.window_size_samples + speech_pad_ms = vad_options.speech_pad_ms + + if window_size_samples not in [512, 1024, 1536]: + warnings.warn( + "Unusual window_size_samples! Supported window_size_samples:\n" + " - [512, 1024, 1536] for 16000 sampling_rate" + ) + + sampling_rate = 16000 + min_speech_samples = sampling_rate * min_speech_duration_ms / 1000 + speech_pad_samples = sampling_rate * speech_pad_ms / 1000 + max_speech_samples = ( + sampling_rate * max_speech_duration_s + - window_size_samples + - 2 * speech_pad_samples + ) + min_silence_samples = sampling_rate * min_silence_duration_ms / 1000 + min_silence_samples_at_max_speech = sampling_rate * 98 / 1000 + + audio_length_samples = len(audio) + + state = self.get_initial_state(batch_size=1) + + speech_probs = [] + for current_start_sample in range(0, audio_length_samples, window_size_samples): + chunk = audio[ + current_start_sample : current_start_sample + window_size_samples + ] + if len(chunk) < window_size_samples: + chunk = np.pad(chunk, (0, int(window_size_samples - len(chunk)))) + speech_prob, state = self(chunk, state, sampling_rate) + speech_probs.append(speech_prob) + + triggered = False + speeches = [] + current_speech = {} + neg_threshold = threshold - 0.15 + + # to save potential segment end (and tolerate some silence) + temp_end = 0 + # to save potential segment limits in case of maximum segment size reached + prev_end = next_start = 0 + + for i, speech_prob in enumerate(speech_probs): + if (speech_prob >= threshold) and temp_end: + temp_end = 0 + if next_start < prev_end: + next_start = window_size_samples * i + + if (speech_prob >= threshold) and not triggered: + triggered = True + current_speech["start"] = window_size_samples * i + continue + + if ( + triggered + and (window_size_samples * i) - current_speech["start"] + > max_speech_samples + ): + if prev_end: + current_speech["end"] = prev_end + speeches.append(current_speech) + current_speech = {} + # previously reached silence (< neg_thres) and is still not speech (< thres) + if next_start < prev_end: + triggered = False + else: + current_speech["start"] = next_start + prev_end = next_start = temp_end = 0 + else: + current_speech["end"] = window_size_samples * i + speeches.append(current_speech) + current_speech = {} + prev_end = next_start = temp_end = 0 + triggered = False + continue + + if (speech_prob < neg_threshold) and triggered: + if not temp_end: + temp_end = window_size_samples * i + # condition to avoid cutting in very short silence + if ( + window_size_samples * i + ) - temp_end > min_silence_samples_at_max_speech: + prev_end = temp_end + if (window_size_samples * i) - temp_end < min_silence_samples: + continue + else: + current_speech["end"] = temp_end + if ( + current_speech["end"] - current_speech["start"] + ) > min_speech_samples: + speeches.append(current_speech) + current_speech = {} + prev_end = next_start = temp_end = 0 + triggered = False + continue + + if ( + current_speech + and (audio_length_samples - current_speech["start"]) > min_speech_samples + ): + current_speech["end"] = audio_length_samples + speeches.append(current_speech) + + for i, speech in enumerate(speeches): + if i == 0: + speech["start"] = int(max(0, speech["start"] - speech_pad_samples)) + if i != len(speeches) - 1: + silence_duration = speeches[i + 1]["start"] - speech["end"] + if silence_duration < 2 * speech_pad_samples: + speech["end"] += int(silence_duration // 2) + speeches[i + 1]["start"] = int( + max(0, speeches[i + 1]["start"] - silence_duration // 2) + ) + else: + speech["end"] = int( + min(audio_length_samples, speech["end"] + speech_pad_samples) + ) + speeches[i + 1]["start"] = int( + max(0, speeches[i + 1]["start"] - speech_pad_samples) + ) + else: + speech["end"] = int( + min(audio_length_samples, speech["end"] + speech_pad_samples) + ) + + return speeches + + @overload + def vad( + self, + audio_tuple: tuple[int, NDArray], + vad_parameters: None | SileroVadOptions, + return_chunks: Literal[True], + ) -> tuple[float, List[AudioChunk]]: ... + + @overload + def vad( + self, + audio_tuple: tuple[int, NDArray], + vad_parameters: None | SileroVadOptions, + return_chunks: bool = False, + ) -> float: ... + + def vad( + self, + audio_tuple: tuple[int, NDArray], + vad_parameters: None | SileroVadOptions, + return_chunks: bool = False, + ) -> float | tuple[float, List[AudioChunk]]: + sampling_rate, audio = audio_tuple + logger.debug("VAD audio shape input: %s", audio.shape) + try: + if audio.dtype != np.float32: + audio = audio.astype(np.float32) / 32768.0 + sr = 16000 + if sr != sampling_rate: + try: + import librosa # type: ignore + except ImportError as e: + raise RuntimeError( + "Applying the VAD filter requires the librosa if the input sampling rate is not 16000hz" + ) from e + audio = librosa.resample(audio, orig_sr=sampling_rate, target_sr=sr) + + if not vad_parameters: + vad_parameters = SileroVadOptions() + speech_chunks = self.get_speech_timestamps(audio, vad_parameters) + logger.debug("VAD speech chunks: %s", speech_chunks) + audio = self.collect_chunks(audio, speech_chunks) + logger.debug("VAD audio shape: %s", audio.shape) + duration_after_vad = audio.shape[0] / sr + if return_chunks: + return duration_after_vad, speech_chunks + return duration_after_vad + except Exception as e: + import math + import traceback + + logger.debug("VAD Exception: %s", str(e)) + exec = traceback.format_exc() + logger.debug("traceback %s", exec) + return math.inf + + def __call__(self, x, state, sr: int): + if len(x.shape) == 1: + x = np.expand_dims(x, 0) + if len(x.shape) > 2: + raise ValueError( + f"Too many dimensions for input audio chunk {len(x.shape)}" + ) + if sr / x.shape[1] > 31.25: # type: ignore + raise ValueError("Input audio chunk is too short") + + h, c = state + + ort_inputs = { + "input": x, + "h": h, + "c": c, + "sr": np.array(sr, dtype="int64"), + } + + out, h, c = self.session.run(None, ort_inputs) + state = (h, c) + + return out, state diff --git a/backend/gradio_webrtc/reply_on_pause.py b/backend/gradio_webrtc/reply_on_pause.py new file mode 100644 index 0000000..5733fd4 --- /dev/null +++ b/backend/gradio_webrtc/reply_on_pause.py @@ -0,0 +1,188 @@ +import asyncio +import inspect +from dataclasses import dataclass +from functools import lru_cache +from logging import getLogger +from threading import Event +from typing import Any, Callable, Generator, Literal, Union, cast + +import numpy as np + +from gradio_webrtc.pause_detection import SileroVADModel, SileroVadOptions +from gradio_webrtc.webrtc import EmitType, StreamHandler + +logger = getLogger(__name__) + +counter = 0 + + +@lru_cache +def get_vad_model() -> SileroVADModel: + """Returns the VAD model instance.""" + return SileroVADModel() + + +@dataclass +class AlgoOptions: + """Algorithm options.""" + + audio_chunk_duration: float = 0.6 + started_talking_threshold: float = 0.2 + speech_threshold: float = 0.1 + + +@dataclass +class AppState: + stream: np.ndarray | None = None + sampling_rate: int = 0 + pause_detected: bool = False + started_talking: bool = False + responding: bool = False + stopped: bool = False + buffer: np.ndarray | None = None + + +ReplyFnGenerator = Union[ + # For two arguments + Callable[ + [tuple[int, np.ndarray], list[dict[Any, Any]]], + Generator[EmitType, None, None], + ], + Callable[ + [tuple[int, np.ndarray]], + Generator[EmitType, None, None], + ], +] + + +async def iterate(generator: Generator) -> Any: + return next(generator) + + +class ReplyOnPause(StreamHandler): + def __init__( + self, + fn: ReplyFnGenerator, + algo_options: AlgoOptions | None = None, + model_options: SileroVadOptions | None = None, + expected_layout: Literal["mono", "stereo"] = "mono", + output_sample_rate: int = 24000, + output_frame_size: int = 480, + input_sample_rate: int = 48000, + ): + super().__init__( + expected_layout, + output_sample_rate, + output_frame_size, + input_sample_rate=input_sample_rate, + ) + self.expected_layout: Literal["mono", "stereo"] = expected_layout + self.output_sample_rate = output_sample_rate + self.output_frame_size = output_frame_size + self.model = get_vad_model() + self.fn = fn + self.is_async = inspect.isasyncgenfunction(fn) + self.event = Event() + self.state = AppState() + self.generator: Generator[EmitType, None, None] | None = None + self.model_options = model_options + self.algo_options = algo_options or AlgoOptions() + + @property + def _needs_additional_inputs(self) -> bool: + return len(inspect.signature(self.fn).parameters) > 1 + + def copy(self): + return ReplyOnPause( + self.fn, + self.algo_options, + self.model_options, + self.expected_layout, + self.output_sample_rate, + self.output_frame_size, + self.input_sample_rate, + ) + + def determine_pause( + self, audio: np.ndarray, sampling_rate: int, state: AppState + ) -> bool: + """Take in the stream, determine if a pause happened""" + duration = len(audio) / sampling_rate + + if duration >= self.algo_options.audio_chunk_duration: + dur_vad = self.model.vad((sampling_rate, audio), self.model_options) + logger.debug("VAD duration: %s", dur_vad) + if ( + dur_vad > self.algo_options.started_talking_threshold + and not state.started_talking + ): + state.started_talking = True + logger.debug("Started talking") + if state.started_talking: + if state.stream is None: + state.stream = audio + else: + state.stream = np.concatenate((state.stream, audio)) + state.buffer = None + if dur_vad < self.algo_options.speech_threshold and state.started_talking: + return True + return False + + def process_audio(self, audio: tuple[int, np.ndarray], state: AppState) -> None: + frame_rate, array = audio + array = np.squeeze(array) + if not state.sampling_rate: + state.sampling_rate = frame_rate + if state.buffer is None: + state.buffer = array + else: + state.buffer = np.concatenate((state.buffer, array)) + + pause_detected = self.determine_pause( + state.buffer, state.sampling_rate, self.state + ) + state.pause_detected = pause_detected + + def receive(self, frame: tuple[int, np.ndarray]) -> None: + if self.state.responding: + return + self.process_audio(frame, self.state) + if self.state.pause_detected: + self.event.set() + + def reset(self): + super().reset() + self.generator = None + self.event.clear() + self.state = AppState() + + async def async_iterate(self, generator) -> EmitType: + return await anext(generator) + + def emit(self): + if not self.event.is_set(): + return None + else: + if not self.generator: + if self._needs_additional_inputs and not self.args_set.is_set(): + asyncio.run_coroutine_threadsafe( + self.wait_for_args(), self.loop + ).result() + logger.debug("Creating generator") + audio = cast(np.ndarray, self.state.stream).reshape(1, -1) + if self._needs_additional_inputs: + self.latest_args[0] = (self.state.sampling_rate, audio) + self.generator = self.fn(*self.latest_args) + else: + self.generator = self.fn((self.state.sampling_rate, audio)) # type: ignore + logger.debug("Latest args: %s", self.latest_args) + self.state.responding = True + try: + if self.is_async: + return asyncio.run_coroutine_threadsafe( + self.async_iterate(self.generator), self.loop + ).result() + else: + return next(self.generator) + except (StopIteration, StopAsyncIteration): + self.reset() diff --git a/backend/gradio_webrtc/reply_on_stopwords.py b/backend/gradio_webrtc/reply_on_stopwords.py new file mode 100644 index 0000000..0f7f9ac --- /dev/null +++ b/backend/gradio_webrtc/reply_on_stopwords.py @@ -0,0 +1,147 @@ +import asyncio +import logging +import re +from typing import Literal + +import numpy as np + +from .reply_on_pause import ( + AlgoOptions, + AppState, + ReplyFnGenerator, + ReplyOnPause, + SileroVadOptions, +) +from .speech_to_text import get_stt_model, stt_for_chunks +from .utils import audio_to_float32 + +logger = logging.getLogger(__name__) + + +class ReplyOnStopWordsState(AppState): + stop_word_detected: bool = False + post_stop_word_buffer: np.ndarray | None = None + started_talking_pre_stop_word: bool = False + + +class ReplyOnStopWords(ReplyOnPause): + def __init__( + self, + fn: ReplyFnGenerator, + stop_words: list[str], + algo_options: AlgoOptions | None = None, + model_options: SileroVadOptions | None = None, + expected_layout: Literal["mono", "stereo"] = "mono", + output_sample_rate: int = 24000, + output_frame_size: int = 480, + input_sample_rate: int = 48000, + ): + super().__init__( + fn, + algo_options=algo_options, + model_options=model_options, + expected_layout=expected_layout, + output_sample_rate=output_sample_rate, + output_frame_size=output_frame_size, + input_sample_rate=input_sample_rate, + ) + self.stop_words = stop_words + self.state = ReplyOnStopWordsState() + # Download Model + get_stt_model() + + def stop_word_detected(self, text: str) -> bool: + for stop_word in self.stop_words: + stop_word = stop_word.lower().strip().split(" ") + if bool( + re.search(r"\b" + r"\s+".join(map(re.escape, stop_word)) + r"\b", text) + ): + logger.debug("Stop word detected: %s", stop_word) + return True + return False + + async def _send_stopword( + self, + ): + if self.channel: + self.channel.send("stopword") + logger.debug("Sent stopword") + + def send_stopword(self): + asyncio.run_coroutine_threadsafe(self._send_stopword(), self.loop) + + def determine_pause( # type: ignore + self, audio: np.ndarray, sampling_rate: int, state: ReplyOnStopWordsState + ) -> bool: + """Take in the stream, determine if a pause happened""" + import librosa + + duration = len(audio) / sampling_rate + + if duration >= self.algo_options.audio_chunk_duration: + if not state.stop_word_detected: + audio_f32 = audio_to_float32((sampling_rate, audio)) + audio_rs = librosa.resample( + audio_f32, orig_sr=sampling_rate, target_sr=16000 + ) + if state.post_stop_word_buffer is None: + state.post_stop_word_buffer = audio_rs + else: + state.post_stop_word_buffer = np.concatenate( + (state.post_stop_word_buffer, audio_rs) + ) + if len(state.post_stop_word_buffer) / 16000 > 2: + state.post_stop_word_buffer = state.post_stop_word_buffer[-32000:] + dur_vad, chunks = self.model.vad( + (16000, state.post_stop_word_buffer), + self.model_options, + return_chunks=True, + ) + text = stt_for_chunks((16000, state.post_stop_word_buffer), chunks) + logger.debug(f"STT: {text}") + state.stop_word_detected = self.stop_word_detected(text) + if state.stop_word_detected: + logger.debug("Stop word detected") + self.send_stopword() + state.buffer = None + else: + dur_vad = self.model.vad((sampling_rate, audio), self.model_options) + logger.debug("VAD duration: %s", dur_vad) + if ( + dur_vad > self.algo_options.started_talking_threshold + and not state.started_talking + and state.stop_word_detected + ): + state.started_talking = True + logger.debug("Started talking") + if state.started_talking: + if state.stream is None: + state.stream = audio + else: + state.stream = np.concatenate((state.stream, audio)) + state.buffer = None + if ( + dur_vad < self.algo_options.speech_threshold + and state.started_talking + and state.stop_word_detected + ): + return True + return False + + def reset(self): + super().reset() + self.generator = None + self.event.clear() + self.state = ReplyOnStopWordsState() + + def copy(self): + return ReplyOnStopWords( + self.fn, + self.stop_words, + self.algo_options, + self.model_options, + self.expected_layout, + self.output_sample_rate, + self.output_frame_size, + self.input_sample_rate, + ) diff --git a/backend/gradio_webrtc/speech_to_text/__init__.py b/backend/gradio_webrtc/speech_to_text/__init__.py new file mode 100644 index 0000000..8569c11 --- /dev/null +++ b/backend/gradio_webrtc/speech_to_text/__init__.py @@ -0,0 +1,3 @@ +from .stt_ import get_stt_model, stt, stt_for_chunks + +__all__ = ["stt", "stt_for_chunks", "get_stt_model"] diff --git a/backend/gradio_webrtc/speech_to_text/stt_.py b/backend/gradio_webrtc/speech_to_text/stt_.py new file mode 100644 index 0000000..6b2f696 --- /dev/null +++ b/backend/gradio_webrtc/speech_to_text/stt_.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from functools import lru_cache +from typing import Callable + +import numpy as np +from numpy.typing import NDArray + +from ..utils import AudioChunk + + +@dataclass +class STTModel: + encoder: Callable + decoder: Callable + + +@lru_cache +def get_stt_model() -> STTModel: + from silero import silero_stt + + model, decoder, _ = silero_stt(language="en", version="v6", jit_model="jit_xlarge") + return STTModel(model, decoder) + + +def stt(audio: tuple[int, NDArray[np.int16]]) -> str: + model = get_stt_model() + sr, audio_np = audio + if audio_np.dtype != np.float32: + print("converting") + audio_np = audio_np.astype(np.float32) / 32768.0 + try: + import torch + except ImportError: + raise ImportError( + "PyTorch is required to run speech-to-text for stopword detection. Run `pip install torch`." + ) + audio_torch = torch.tensor(audio_np, dtype=torch.float32) + if audio_torch.ndim == 1: + audio_torch = audio_torch.unsqueeze(0) + assert audio_torch.ndim == 2, "Audio must have a batch dimension" + print("before") + res = model.decoder(model.encoder(audio_torch)[0]) + print("after") + return res + + +def stt_for_chunks( + audio: tuple[int, NDArray[np.int16]], chunks: list[AudioChunk] +) -> str: + sr, audio_np = audio + return " ".join( + [stt((sr, audio_np[chunk["start"] : chunk["end"]])) for chunk in chunks] + ) diff --git a/backend/gradio_webrtc/utils.py b/backend/gradio_webrtc/utils.py new file mode 100644 index 0000000..a2823ad --- /dev/null +++ b/backend/gradio_webrtc/utils.py @@ -0,0 +1,313 @@ +import asyncio +import fractions +import io +import json +import logging +import tempfile +from contextvars import ContextVar +from typing import Any, Callable, Protocol, TypedDict, cast + +import av +import numpy as np +from pydub import AudioSegment + +logger = logging.getLogger(__name__) + + +AUDIO_PTIME = 0.020 + + +class AudioChunk(TypedDict): + start: int + end: int + + +class AdditionalOutputs: + def __init__(self, *args) -> None: + self.args = args + + +class DataChannel(Protocol): + def send(self, message: str) -> None: ... + + +current_channel: ContextVar[DataChannel | None] = ContextVar( + "current_channel", default=None +) + + +def _send_log(message: str, type: str) -> None: + async def _send(channel: DataChannel) -> None: + channel.send( + json.dumps( + { + "type": type, + "message": message, + } + ) + ) + + if channel := current_channel.get(): + print("channel", channel) + try: + loop = asyncio.get_running_loop() + asyncio.run_coroutine_threadsafe(_send(channel), loop) + except RuntimeError: + asyncio.run(_send(channel)) + + +def Warning( # noqa: N802 + message: str = "Warning issued.", +): + """ + Send a warning message that is deplayed in the UI of the application. + + Parameters + ---------- + audio : str + The warning message to send + + Returns + ------- + None + """ + _send_log(message, "warning") + + +class WebRTCError(Exception): + def __init__(self, message: str) -> None: + super().__init__(message) + _send_log(message, "error") + + +def split_output(data: tuple | Any) -> tuple[Any, AdditionalOutputs | None]: + if isinstance(data, AdditionalOutputs): + return None, data + if isinstance(data, tuple): + # handle the bare audio case + if 2 <= len(data) <= 3 and isinstance(data[1], np.ndarray): + return data, None + if not len(data) == 2: + raise ValueError( + "The tuple must have exactly two elements: the data and an instance of AdditionalOutputs." + ) + if not isinstance(data[-1], AdditionalOutputs): + raise ValueError( + "The last element of the tuple must be an instance of AdditionalOutputs." + ) + return data[0], cast(AdditionalOutputs, data[1]) + return data, None + + +async def player_worker_decode( + next_frame: Callable, + queue: asyncio.Queue, + thread_quit: asyncio.Event, + channel: Callable[[], DataChannel | None] | None, + set_additional_outputs: Callable | None, + quit_on_none: bool = False, + sample_rate: int = 48000, + frame_size: int = int(48000 * AUDIO_PTIME), +): + audio_samples = 0 + audio_time_base = fractions.Fraction(1, sample_rate) + audio_resampler = av.AudioResampler( # type: ignore + format="s16", + layout="stereo", + rate=sample_rate, + frame_size=frame_size, + ) + + while not thread_quit.is_set(): + try: + # Get next frame + frame, outputs = split_output( + await asyncio.wait_for(next_frame(), timeout=60) + ) + if ( + isinstance(outputs, AdditionalOutputs) + and set_additional_outputs + and channel + and channel() + ): + set_additional_outputs(outputs) + cast(DataChannel, channel()).send("change") + + if frame is None: + if quit_on_none: + await queue.put(None) + break + continue + + if len(frame) == 2: + sample_rate, audio_array = frame + layout = "mono" + elif len(frame) == 3: + sample_rate, audio_array, layout = frame + + logger.debug( + "received array with shape %s sample rate %s layout %s", + audio_array.shape, # type: ignore + sample_rate, + layout, # type: ignore + ) + format = "s16" if audio_array.dtype == "int16" else "fltp" # type: ignore + + # Convert to audio frame and resample + # This runs in the same timeout context + frame = av.AudioFrame.from_ndarray( # type: ignore + audio_array, # type: ignore + format=format, + layout=layout, # type: ignore + ) + frame.sample_rate = sample_rate + + for processed_frame in audio_resampler.resample(frame): + processed_frame.pts = audio_samples + processed_frame.time_base = audio_time_base + audio_samples += processed_frame.samples + await queue.put(processed_frame) + logger.debug("Queue size utils.py: %s", queue.qsize()) + + except (TimeoutError, asyncio.TimeoutError): + logger.warning( + "Timeout in frame processing cycle after %s seconds - resetting", 60 + ) + continue + except Exception as e: + import traceback + + exec = traceback.format_exc() + logger.debug("traceback %s", exec) + logger.error("Error processing frame: %s", str(e)) + continue + + +def audio_to_bytes(audio: tuple[int, np.ndarray]) -> bytes: + """ + Convert an audio tuple containing sample rate and numpy array data into bytes. + + Parameters + ---------- + audio : tuple[int, np.ndarray] + A tuple containing: + - sample_rate (int): The audio sample rate in Hz + - data (np.ndarray): The audio data as a numpy array + + Returns + ------- + bytes + The audio data encoded as bytes, suitable for transmission or storage + + Example + ------- + >>> sample_rate = 44100 + >>> audio_data = np.array([0.1, -0.2, 0.3]) # Example audio samples + >>> audio_tuple = (sample_rate, audio_data) + >>> audio_bytes = audio_to_bytes(audio_tuple) + """ + audio_buffer = io.BytesIO() + segment = AudioSegment( + audio[1].tobytes(), + frame_rate=audio[0], + sample_width=audio[1].dtype.itemsize, + channels=1, + ) + segment.export(audio_buffer, format="mp3") + return audio_buffer.getvalue() + + +def audio_to_file(audio: tuple[int, np.ndarray]) -> str: + """ + Save an audio tuple containing sample rate and numpy array data to a file. + + Parameters + ---------- + audio : tuple[int, np.ndarray] + A tuple containing: + - sample_rate (int): The audio sample rate in Hz + - data (np.ndarray): The audio data as a numpy array + + Returns + ------- + str + The path to the saved audio file + + Example + ------- + >>> sample_rate = 44100 + >>> audio_data = np.array([0.1, -0.2, 0.3]) # Example audio samples + >>> audio_tuple = (sample_rate, audio_data) + >>> file_path = audio_to_file(audio_tuple) + >>> print(f"Audio saved to: {file_path}") + """ + bytes_ = audio_to_bytes(audio) + with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: + f.write(bytes_) + return f.name + + +def audio_to_float32(audio: tuple[int, np.ndarray]) -> np.ndarray: + """ + Convert an audio tuple containing sample rate (int16) and numpy array data to float32. + + Parameters + ---------- + audio : tuple[int, np.ndarray] + A tuple containing: + - sample_rate (int): The audio sample rate in Hz + - data (np.ndarray): The audio data as a numpy array + + Returns + ------- + np.ndarray + The audio data as a numpy array with dtype float32 + + Example + ------- + >>> sample_rate = 44100 + >>> audio_data = np.array([0.1, -0.2, 0.3]) # Example audio samples + >>> audio_tuple = (sample_rate, audio_data) + >>> audio_float32 = audio_to_float32(audio_tuple) + """ + return audio[1].astype(np.float32) / 32768.0 + + +def aggregate_bytes_to_16bit(chunks_iterator): + leftover = b"" # Store incomplete bytes between chunks + + for chunk in chunks_iterator: + # Combine with any leftover bytes from previous chunk + current_bytes = leftover + chunk + + # Calculate complete samples + n_complete_samples = len(current_bytes) // 2 # int16 = 2 bytes + bytes_to_process = n_complete_samples * 2 + + # Split into complete samples and leftover + to_process = current_bytes[:bytes_to_process] + leftover = current_bytes[bytes_to_process:] + + if to_process: # Only yield if we have complete samples + audio_array = np.frombuffer(to_process, dtype=np.int16).reshape(1, -1) + yield audio_array + + +async def async_aggregate_bytes_to_16bit(chunks_iterator): + leftover = b"" # Store incomplete bytes between chunks + + async for chunk in chunks_iterator: + # Combine with any leftover bytes from previous chunk + current_bytes = leftover + chunk + + # Calculate complete samples + n_complete_samples = len(current_bytes) // 2 # int16 = 2 bytes + bytes_to_process = n_complete_samples * 2 + + # Split into complete samples and leftover + to_process = current_bytes[:bytes_to_process] + leftover = current_bytes[bytes_to_process:] + + if to_process: # Only yield if we have complete samples + audio_array = np.frombuffer(to_process, dtype=np.int16).reshape(1, -1) + yield audio_array diff --git a/backend/gradio_webrtc/webrtc.py b/backend/gradio_webrtc/webrtc.py new file mode 100644 index 0000000..a722ca3 --- /dev/null +++ b/backend/gradio_webrtc/webrtc.py @@ -0,0 +1,1151 @@ +"""gr.WebRTC() component.""" + +from __future__ import annotations + +import asyncio +import functools +import inspect +import logging +import threading +import time +import traceback +from abc import ABC, abstractmethod +from collections import defaultdict +from collections.abc import Callable +from typing import ( + TYPE_CHECKING, + Any, + Concatenate, + Generator, + Iterable, + Literal, + ParamSpec, + Sequence, + TypeAlias, + TypeVar, + Union, + cast, +) + +import anyio.to_thread +import av +import numpy as np +from aiortc import ( + AudioStreamTrack, + MediaStreamTrack, + RTCPeerConnection, + RTCSessionDescription, + VideoStreamTrack, +) +from aiortc.contrib.media import AudioFrame, MediaRelay, VideoFrame # type: ignore +from aiortc.mediastreams import MediaStreamError +from gradio import wasm_utils +from gradio.components.base import Component, server +from gradio_client import handle_file +from numpy import typing as npt + +from gradio_webrtc.utils import ( + AdditionalOutputs, + DataChannel, + current_channel, + player_worker_decode, + split_output, +) + +if TYPE_CHECKING: + from gradio.blocks import Block + from gradio.components import Timer + from gradio.events import Dependency + + +if wasm_utils.IS_WASM: + raise ValueError("Not supported in gradio-lite!") + + +logger = logging.getLogger(__name__) + +VideoEmitType = Union[ + AdditionalOutputs, tuple[npt.ArrayLike, AdditionalOutputs], npt.ArrayLike, None +] +VideoEventHandler = Callable[[npt.ArrayLike], VideoEmitType] + + +class VideoCallback(VideoStreamTrack): + """ + This works for streaming input and output + """ + + kind = "video" + + def __init__( + self, + track: MediaStreamTrack, + event_handler: VideoEventHandler, + channel: DataChannel | None = None, + set_additional_outputs: Callable | None = None, + mode: Literal["send-receive", "send"] = "send-receive", + ) -> None: + super().__init__() # don't forget this! + self.track = track + self.event_handler = event_handler + self.latest_args: str | list[Any] = "not_set" + self.channel = channel + self.set_additional_outputs = set_additional_outputs + self.thread_quit = asyncio.Event() + self.mode = mode + self.channel_set = asyncio.Event() + self.has_started = False + + def set_channel(self, channel: DataChannel): + self.channel = channel + current_channel.set(channel) + self.channel_set.set() + + def set_args(self, args: list[Any]): + self.latest_args = ["__webrtc_value__"] + list(args) + + def add_frame_to_payload( + self, args: list[Any], frame: np.ndarray | None + ) -> list[Any]: + new_args = [] + for val in args: + if isinstance(val, str) and val == "__webrtc_value__": + new_args.append(frame) + else: + new_args.append(val) + return new_args + + def array_to_frame(self, array: np.ndarray) -> VideoFrame: + return VideoFrame.from_ndarray(array, format="bgr24") + + async def process_frames(self): + while not self.thread_quit.is_set(): + try: + await self.recv() + except TimeoutError: + continue + + def start( + self, + ): + asyncio.create_task(self.process_frames()) + + def stop(self): + super().stop() + logger.debug("video callback stop") + self.thread_quit.set() + + async def wait_for_channel(self): + if not self.channel_set.is_set(): + await self.channel_set.wait() + if current_channel.get() != self.channel: + current_channel.set(self.channel) + + async def recv(self): # type: ignore + try: + try: + frame = cast(VideoFrame, await self.track.recv()) + except MediaStreamError: + self.stop() + return + + await self.wait_for_channel() + frame_array = frame.to_ndarray(format="bgr24") + if self.latest_args == "not_set": + return frame + + args = self.add_frame_to_payload(cast(list, self.latest_args), frame_array) + + array, outputs = split_output(self.event_handler(*args)) + if ( + isinstance(outputs, AdditionalOutputs) + and self.set_additional_outputs + and self.channel + ): + self.set_additional_outputs(outputs) + self.channel.send("change") + if array is None and self.mode == "send": + return + + new_frame = self.array_to_frame(array) + if frame: + new_frame.pts = frame.pts + new_frame.time_base = frame.time_base + else: + pts, time_base = await self.next_timestamp() + new_frame.pts = pts + new_frame.time_base = time_base + + return new_frame + except Exception as e: + logger.debug("exception %s", e) + exec = traceback.format_exc() + logger.debug("traceback %s", exec) + + +class StreamHandlerBase(ABC): + def __init__( + self, + expected_layout: Literal["mono", "stereo"] = "mono", + output_sample_rate: int = 24000, + output_frame_size: int = 960, + input_sample_rate: int = 48000, + ) -> None: + self.expected_layout = expected_layout + self.output_sample_rate = output_sample_rate + self.output_frame_size = output_frame_size + self.input_sample_rate = input_sample_rate + self.latest_args: list[Any] = [] + self._resampler = None + self._channel: DataChannel | None = None + self._loop: asyncio.AbstractEventLoop + self.args_set = asyncio.Event() + self.channel_set = asyncio.Event() + + @property + def loop(self) -> asyncio.AbstractEventLoop: + return cast(asyncio.AbstractEventLoop, self._loop) + + @property + def channel(self) -> DataChannel | None: + return self._channel + + def set_channel(self, channel: DataChannel): + self._channel = channel + self.channel_set.set() + + async def fetch_args( + self, + ): + if self.channel: + self.channel.send("tick") + logger.debug("Sent tick") + + async def wait_for_args(self): + await self.fetch_args() + await self.args_set.wait() + + def wait_for_args_sync(self): + asyncio.run_coroutine_threadsafe(self.wait_for_args(), self.loop).result() + + def set_args(self, args: list[Any]): + logger.debug("setting args in audio callback %s", args) + self.latest_args = ["__webrtc_value__"] + list(args) + self.args_set.set() + + def reset(self): + self.args_set.clear() + + def shutdown(self): + pass + + @abstractmethod + def copy(self) -> "StreamHandlerBase": + pass + + def resample(self, frame: AudioFrame) -> Generator[AudioFrame, None, None]: + if self._resampler is None: + self._resampler = av.AudioResampler( # type: ignore + format="s16", + layout=self.expected_layout, + rate=self.input_sample_rate, + frame_size=frame.samples, + ) + yield from self._resampler.resample(frame) + + +EmitType: TypeAlias = Union[ + tuple[int, np.ndarray], + tuple[int, np.ndarray, Literal["mono", "stereo"]], + AdditionalOutputs, + tuple[tuple[int, np.ndarray], AdditionalOutputs], + None, +] +AudioEmitType = EmitType + + +class StreamHandler(StreamHandlerBase): + @abstractmethod + def receive(self, frame: tuple[int, np.ndarray]) -> None: + pass + + @abstractmethod + def emit( + self, + ) -> EmitType: + pass + + +class AsyncStreamHandler(StreamHandlerBase): + @abstractmethod + async def receive(self, frame: tuple[int, np.ndarray]) -> None: + pass + + @abstractmethod + async def emit( + self, + ) -> EmitType: + pass + + +StreamHandlerImpl = Union[StreamHandler, AsyncStreamHandler] + + +class AudioVideoStreamHandler(StreamHandlerBase): + @abstractmethod + def video_receive(self, frame: npt.NDArray) -> None: + pass + + @abstractmethod + def video_emit( + self, + ) -> VideoEmitType: + pass + + +class AsyncAudioVideoStreamHandler(StreamHandlerBase): + @abstractmethod + async def video_receive(self, frame: npt.NDArray) -> None: + pass + + @abstractmethod + async def video_emit( + self, + ) -> VideoEmitType: + pass + + +VideoStreamHandlerImpl = Union[AudioVideoStreamHandler, AsyncAudioVideoStreamHandler] +AudioVideoStreamHandlerImpl = Union[ + AudioVideoStreamHandler, AsyncAudioVideoStreamHandler +] +AsyncHandler = Union[AsyncStreamHandler, AsyncAudioVideoStreamHandler] + + +class VideoStreamHander(VideoCallback): + async def process_frames(self): + while not self.thread_quit.is_set(): + try: + await self.channel_set.wait() + frame = cast(VideoFrame, await self.track.recv()) + frame_array = frame.to_ndarray(format="bgr24") + handler = cast(VideoStreamHandlerImpl, self.event_handler) + if inspect.iscoroutinefunction(handler.video_receive): + await handler.video_receive(frame_array) + else: + handler.video_receive(frame_array) + except MediaStreamError: + self.stop() + + def start(self): + if not self.has_started: + asyncio.create_task(self.process_frames()) + self.has_started = True + + async def recv(self): # type: ignore + self.start() + try: + handler = cast(VideoStreamHandlerImpl, self.event_handler) + if inspect.iscoroutinefunction(handler.video_emit): + outputs = await handler.video_emit() + else: + outputs = handler.video_emit() + + array, outputs = split_output(outputs) + if ( + isinstance(outputs, AdditionalOutputs) + and self.set_additional_outputs + and self.channel + ): + self.set_additional_outputs(outputs) + self.channel.send("change") + if array is None and self.mode == "send": + return + + new_frame = self.array_to_frame(array) + + # Will probably have to give developer ability to set pts and time_base + pts, time_base = await self.next_timestamp() + new_frame.pts = pts + new_frame.time_base = time_base + + return new_frame + except Exception as e: + logger.debug("exception %s", e) + exec = traceback.format_exc() + logger.debug("traceback %s", exec) + + +class AudioCallback(AudioStreamTrack): + kind = "audio" + + def __init__( + self, + track: MediaStreamTrack, + event_handler: StreamHandlerBase, + channel: DataChannel | None = None, + set_additional_outputs: Callable | None = None, + ) -> None: + super().__init__() + self.track = track + self.event_handler = cast(StreamHandlerImpl, event_handler) + self.current_timestamp = 0 + self.latest_args: str | list[Any] = "not_set" + self.queue = asyncio.Queue() + self.thread_quit = asyncio.Event() + self._start: float | None = None + self.has_started = False + self.last_timestamp = 0 + self.channel = channel + self.set_additional_outputs = set_additional_outputs + + def set_channel(self, channel: DataChannel): + self.channel = channel + self.event_handler.set_channel(channel) + + def set_args(self, args: list[Any]): + self.event_handler.set_args(args) + + def event_handler_receive(self, frame: tuple[int, np.ndarray]) -> None: + current_channel.set(self.event_handler.channel) + return cast(Callable, self.event_handler.receive)(frame) + + async def process_input_frames(self) -> None: + while not self.thread_quit.is_set(): + try: + frame = cast(AudioFrame, await self.track.recv()) + for frame in self.event_handler.resample(frame): + numpy_array = frame.to_ndarray() + if isinstance(self.event_handler, AsyncHandler): + await self.event_handler.receive( + (frame.sample_rate, numpy_array) + ) + else: + await anyio.to_thread.run_sync( + self.event_handler_receive, (frame.sample_rate, numpy_array) + ) + except MediaStreamError: + logger.debug("MediaStreamError in process_input_frames") + break + + def start(self): + if not self.has_started: + loop = asyncio.get_running_loop() + if isinstance(self.event_handler, AsyncHandler): + callable = self.event_handler.emit + else: + callable = functools.partial( + loop.run_in_executor, None, self.event_handler.emit + ) + asyncio.create_task(self.process_input_frames()) + asyncio.create_task( + player_worker_decode( + callable, + self.queue, + self.thread_quit, + lambda: self.channel, + self.set_additional_outputs, + False, + self.event_handler.output_sample_rate, + self.event_handler.output_frame_size, + ) + ) + self.has_started = True + + async def recv(self): # type: ignore + try: + if self.readyState != "live": + raise MediaStreamError + + if not self.event_handler.channel_set.is_set(): + await self.event_handler.channel_set.wait() + if current_channel.get() != self.event_handler.channel: + current_channel.set(self.event_handler.channel) + + self.start() + + frame = await self.queue.get() + logger.debug("frame %s", frame) + + data_time = frame.time + + if time.time() - self.last_timestamp > 10 * ( + self.event_handler.output_frame_size + / self.event_handler.output_sample_rate + ): + self._start = None + + # control playback rate + if self._start is None: + self._start = time.time() - data_time # type: ignore + else: + wait = self._start + data_time - time.time() + await asyncio.sleep(wait) + self.last_timestamp = time.time() + return frame + except Exception as e: + logger.debug("exception %s", e) + exec = traceback.format_exc() + logger.debug("traceback %s", exec) + + def stop(self): + logger.debug("audio callback stop") + self.thread_quit.set() + super().stop() + + def shutdown(self): + self.event_handler.shutdown() + + +class ServerToClientVideo(VideoStreamTrack): + """ + This works for streaming input and output + """ + + kind = "video" + + def __init__( + self, + event_handler: Callable, + channel: DataChannel | None = None, + set_additional_outputs: Callable | None = None, + ) -> None: + super().__init__() # don't forget this! + self.event_handler = event_handler + self.args_set = asyncio.Event() + self.latest_args: str | list[Any] = "not_set" + self.generator: Generator[Any, None, Any] | None = None + self.channel = channel + self.set_additional_outputs = set_additional_outputs + + def array_to_frame(self, array: np.ndarray) -> VideoFrame: + return VideoFrame.from_ndarray(array, format="bgr24") + + def set_channel(self, channel: DataChannel): + self.channel = channel + + def set_args(self, args: list[Any]): + self.latest_args = list(args) + self.args_set.set() + + async def recv(self): # type: ignore + try: + pts, time_base = await self.next_timestamp() + await self.args_set.wait() + if self.generator is None: + self.generator = cast( + Generator[Any, None, Any], self.event_handler(*self.latest_args) + ) + current_channel.set(self.channel) + try: + next_array, outputs = split_output(next(self.generator)) + if ( + isinstance(outputs, AdditionalOutputs) + and self.set_additional_outputs + and self.channel + ): + self.set_additional_outputs(outputs) + self.channel.send("change") + except StopIteration: + self.stop() + return + + next_frame = self.array_to_frame(next_array) + next_frame.pts = pts + next_frame.time_base = time_base + return next_frame + except Exception as e: + logger.debug("exception %s", e) + exec = traceback.format_exc() + logger.debug("traceback %s ", exec) + + +class ServerToClientAudio(AudioStreamTrack): + kind = "audio" + + def __init__( + self, + event_handler: Callable, + channel: DataChannel | None = None, + set_additional_outputs: Callable | None = None, + ) -> None: + self.generator: Generator[Any, None, Any] | None = None + self.event_handler = event_handler + self.current_timestamp = 0 + self.latest_args: str | list[Any] = "not_set" + self.args_set = threading.Event() + self.queue = asyncio.Queue() + self.thread_quit = asyncio.Event() + self.channel = channel + self.set_additional_outputs = set_additional_outputs + self.has_started = False + self._start: float | None = None + super().__init__() + + def set_channel(self, channel: DataChannel): + self.channel = channel + + def set_args(self, args: list[Any]): + self.latest_args = list(args) + self.args_set.set() + + def next(self) -> tuple[int, np.ndarray] | None: + self.args_set.wait() + current_channel.set(self.channel) + if self.generator is None: + self.generator = self.event_handler(*self.latest_args) + if self.generator is not None: + try: + frame = next(self.generator) + return frame + except StopIteration: + self.thread_quit.set() + + def start(self): + if not self.has_started: + loop = asyncio.get_running_loop() + callable = functools.partial(loop.run_in_executor, None, self.next) + asyncio.create_task( + player_worker_decode( + callable, + self.queue, + self.thread_quit, + lambda: self.channel, + self.set_additional_outputs, + True, + ) + ) + self.has_started = True + + async def recv(self): # type: ignore + try: + if self.readyState != "live": + raise MediaStreamError + + self.start() + data = await self.queue.get() + if data is None: + self.stop() + return + + data_time = data.time + + # control playback rate + if data_time is not None: + if self._start is None: + self._start = time.time() - data_time # type: ignore + else: + wait = self._start + data_time - time.time() + await asyncio.sleep(wait) + + return data + except Exception as e: + logger.debug("exception %s", e) + exec = traceback.format_exc() + logger.debug("traceback %s", exec) + + def stop(self): + logger.debug("audio-to-client stop callback") + self.thread_quit.set() + super().stop() + + +# For the return type +R = TypeVar("R") +# For the parameter specification +P = ParamSpec("P") + + +class WebRTC(Component): + """ + Creates a video component that can be used to upload/record videos (as an input) or display videos (as an output). + For the video to be playable in the browser it must have a compatible container and codec combination. Allowed + combinations are .mp4 with h264 codec, .ogg with theora codec, and .webm with vp9 codec. If the component detects + that the output video would not be playable in the browser it will attempt to convert it to a playable mp4 video. + If the conversion fails, the original video is returned. + + Demos: video_identity_2 + """ + + pcs: set[RTCPeerConnection] = set([]) + relay = MediaRelay() + connections: dict[ + str, + list[VideoCallback | ServerToClientVideo | ServerToClientAudio | AudioCallback], + ] = defaultdict(list) + data_channels: dict[str, DataChannel] = {} + additional_outputs: dict[str, list[AdditionalOutputs]] = {} + handlers: dict[str, StreamHandlerBase | Callable] = {} + + EVENTS = ["tick", "state_change"] + + def __init__( + self, + value: None = None, + height: int | str | None = None, + width: int | str | None = None, + label: str | None = None, + every: Timer | float | None = None, + inputs: Component | Sequence[Component] | set[Component] | None = None, + show_label: bool | None = None, + container: bool = True, + scale: int | None = None, + min_width: int = 160, + interactive: bool | None = None, + visible: bool = True, + elem_id: str | None = None, + elem_classes: list[str] | str | None = None, + render: bool = True, + key: int | str | None = None, + mirror_webcam: bool = True, + rtc_configuration: dict[str, Any] | None = None, + track_constraints: dict[str, Any] | None = None, + time_limit: float | None = None, + mode: Literal["send-receive", "receive", "send"] = "send-receive", + modality: Literal["video", "audio", "audio-video"] = "video", + rtp_params: dict[str, Any] | None = None, + icon: str | None = None, + icon_button_color: str | None = None, + pulse_color: str | None = None, + button_labels: dict | None = None, + ): + """ + Parameters: + value: path or URL for the default value that WebRTC component is going to take. Can also be a tuple consisting of (video filepath, subtitle filepath). If a subtitle file is provided, it should be of type .srt or .vtt. Or can be callable, in which case the function will be called whenever the app loads to set the initial value of the component. + format: the file extension with which to save video, such as 'avi' or 'mp4'. This parameter applies both when this component is used as an input to determine which file format to convert user-provided video to, and when this component is used as an output to determine the format of video returned to the user. If None, no file format conversion is done and the video is kept as is. Use 'mp4' to ensure browser playability. + height: The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video. + width: The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video. + label: the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to. + every: continously calls `value` to recalculate it if `value` is a function (has no effect otherwise). Can provide a Timer whose tick resets `value`, or a float that provides the regular interval for the reset Timer. + inputs: components that are used as inputs to calculate `value` if `value` is a function (has no effect otherwise). `value` is recalculated any time the inputs change. + show_label: if True, will display label. + container: if True, will place the component in a container - providing some extra padding around the border. + scale: relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True. + min_width: minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first. + interactive: if True, will allow users to upload a video; if False, can only be used to display videos. If not provided, this is inferred based on whether the component is used as an input or output. + visible: if False, component will be hidden. + 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_classes: an optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles. + render: if False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later. + key: if assigned, will be used to assume identity across a re-render. Components that have the same key across a re-render will have their value preserved. + mirror_webcam: if True webcam will be mirrored. Default is True. + rtc_configuration: WebRTC configuration options. See https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection . If running the demo on a remote server, you will need to specify a rtc_configuration. See https://freddyaboulton.github.io/gradio-webrtc/deployment/ + track_constraints: Media track constraints for WebRTC. For example, to set video height, width use {"width": {"exact": 800}, "height": {"exact": 600}, "aspectRatio": {"exact": 1.33333}} + time_limit: Maximum duration in seconds for recording. + mode: WebRTC mode - "send-receive", "receive", or "send". + modality: Type of media - "video" or "audio". + rtp_params: See https://developer.mozilla.org/en-US/docs/Web/API/RTCRtpSender/setParameters. If you are changing the video resolution, you can set this to {"degradationPreference": "maintain-framerate"} to keep the frame rate consistent. + icon: Icon to display on the button instead of the wave animation. The icon should be a path/url to a .svg/.png/.jpeg file. + icon_button_color: Color of the icon button. Default is var(--color-accent) of the demo theme. + pulse_color: Color of the pulse animation. Default is var(--color-accent) of the demo theme. + button_labels: Text to display on the audio or video start, stop, waiting buttons. Dict with keys "start", "stop", "waiting" mapping to the text to display on the buttons. + """ + self.time_limit = time_limit + self.height = height + self.width = width + self.mirror_webcam = mirror_webcam + self.concurrency_limit = 1 + self.rtc_configuration = rtc_configuration + self.mode = mode + self.modality = modality + self.icon_button_color = icon_button_color + self.pulse_color = pulse_color + self.rtp_params = rtp_params or {} + self.button_labels = { + "start": "", + "stop": "", + "waiting": "", + **(button_labels or {}), + } + if track_constraints is None and modality == "audio": + track_constraints = { + "echoCancellation": True, + "noiseSuppression": {"exact": True}, + "autoGainControl": {"exact": True}, + "sampleRate": {"ideal": 24000}, + "sampleSize": {"ideal": 16}, + "channelCount": {"exact": 1}, + } + if track_constraints is None and modality == "video": + track_constraints = { + "facingMode": "user", + "width": {"ideal": 500}, + "height": {"ideal": 500}, + "frameRate": {"ideal": 30}, + } + if track_constraints is None and modality == "audio-video": + track_constraints = { + "video": { + "facingMode": "user", + "width": {"ideal": 500}, + "height": {"ideal": 500}, + "frameRate": {"ideal": 30}, + }, + "audio": { + "echoCancellation": True, + "noiseSuppression": {"exact": True}, + "autoGainControl": {"exact": True}, + "sampleRate": {"ideal": 24000}, + "sampleSize": {"ideal": 16}, + "channelCount": {"exact": 1}, + }, + } + self.track_constraints = track_constraints + self.event_handler: Callable | StreamHandler | None = None + super().__init__( + label=label, + every=every, + inputs=inputs, + show_label=show_label, + container=container, + scale=scale, + min_width=min_width, + interactive=interactive, + visible=visible, + elem_id=elem_id, + elem_classes=elem_classes, + render=render, + key=key, + value=value, + ) + # need to do this here otherwise the proxy_url is not set + self.icon = ( + icon if not icon else cast(dict, self.serve_static_file(icon)).get("url") + ) + + def set_additional_outputs( + self, webrtc_id: str + ) -> Callable[[AdditionalOutputs], None]: + def set_outputs(outputs: AdditionalOutputs): + if webrtc_id not in self.additional_outputs: + self.additional_outputs[webrtc_id] = [] + self.additional_outputs[webrtc_id].append(outputs) + + return set_outputs + + def preprocess(self, payload: str) -> str: + """ + Parameters: + payload: An instance of VideoData containing the video and subtitle files. + Returns: + Passes the uploaded video as a `str` filepath or URL whose extension can be modified by `format`. + """ + return payload + + def postprocess(self, value: Any) -> str: + """ + Parameters: + value: Expects a {str} or {pathlib.Path} filepath to a video which is displayed, or a {Tuple[str | pathlib.Path, str | pathlib.Path | None]} where the first element is a filepath to a video and the second element is an optional filepath to a subtitle file. + Returns: + VideoData object containing the video and subtitle files. + """ + return value + + def set_input(self, webrtc_id: str, *args): + if webrtc_id in self.connections: + for conn in self.connections[webrtc_id]: + conn.set_args(list(args)) + + def on_additional_outputs( + self, + fn: Callable[Concatenate[P], R], + inputs: Block | Sequence[Block] | set[Block] | None = None, + outputs: Block | Sequence[Block] | set[Block] | None = None, + js: str | None = None, + concurrency_limit: int | None | Literal["default"] = "default", + concurrency_id: str | None = None, + show_progress: Literal["full", "minimal", "hidden"] = "full", + queue: bool = True, + ): + inputs = inputs or [] + if inputs and not isinstance(inputs, Iterable): + inputs = [inputs] + inputs = list(inputs) + + def handler(webrtc_id: str, *args): + if ( + webrtc_id in self.additional_outputs + and len(self.additional_outputs[webrtc_id]) > 0 + ): + next_outputs = self.additional_outputs[webrtc_id].pop(0) + return fn(*args, *next_outputs.args) # type: ignore + return ( + tuple([None for _ in range(len(outputs))]) + if isinstance(outputs, Iterable) + else None + ) + + return self.state_change( # type: ignore + fn=handler, + inputs=[self] + cast(list, inputs), + outputs=outputs, + js=js, + concurrency_limit=concurrency_limit, + concurrency_id=concurrency_id, + show_progress=show_progress, + queue=queue, + trigger_mode="multiple", + ) + + def stream( + self, + fn: Callable[..., Any] + | StreamHandlerImpl + | AudioVideoStreamHandlerImpl + | None = None, + inputs: Block | Sequence[Block] | set[Block] | None = None, + outputs: Block | Sequence[Block] | set[Block] | None = None, + js: str | None = None, + concurrency_limit: int | None | Literal["default"] = "default", + concurrency_id: str | None = None, + time_limit: float | None = None, + trigger: Dependency | None = None, + ): + from gradio.blocks import Block + + if inputs is None: + inputs = [] + if outputs is None: + outputs = [] + if isinstance(inputs, Block): + inputs = [inputs] + if isinstance(outputs, Block): + outputs = [outputs] + + self.concurrency_limit = ( + 1 if concurrency_limit in ["default", None] else concurrency_limit + ) + self.event_handler = fn # type: ignore + self.time_limit = time_limit + + if ( + self.mode == "send-receive" + and self.modality in ["audio", "audio-video"] + and not isinstance(self.event_handler, StreamHandlerBase) + ): + raise ValueError( + "In the send-receive mode for audio, the event handler must be an instance of StreamHandlerBase." + ) + + if self.mode == "send-receive" or self.mode == "send": + if cast(list[Block], inputs)[0] != self: + raise ValueError( + "In the webrtc stream event, the first input component must be the WebRTC component." + ) + + if ( + len(cast(list[Block], outputs)) != 1 + and cast(list[Block], outputs)[0] != self + ): + raise ValueError( + "In the webrtc stream event, the only output component must be the WebRTC component." + ) + for input_component in inputs[1:]: # type: ignore + if hasattr(input_component, "change"): + input_component.change( # type: ignore + self.set_input, + inputs=inputs, + outputs=None, + concurrency_id=concurrency_id, + concurrency_limit=None, + time_limit=None, + js=js, + ) + return self.tick( # type: ignore + self.set_input, + inputs=inputs, + outputs=None, + concurrency_id=concurrency_id, + concurrency_limit=None, + time_limit=None, + js=js, + ) + elif self.mode == "receive": + if isinstance(inputs, list) and self in cast(list[Block], inputs): + raise ValueError( + "In the receive mode stream event, the WebRTC component cannot be an input." + ) + if ( + len(cast(list[Block], outputs)) != 1 + and cast(list[Block], outputs)[0] != self + ): + raise ValueError( + "In the receive mode stream, the only output component must be the WebRTC component." + ) + if trigger is None: + raise ValueError( + "In the receive mode stream event, the trigger parameter must be provided" + ) + trigger(lambda: "start_webrtc_stream", inputs=None, outputs=self) + self.tick( # type: ignore + self.set_input, + inputs=[self] + list(inputs), + outputs=None, + concurrency_id=concurrency_id, + ) + + @staticmethod + async def wait_for_time_limit(pc: RTCPeerConnection, time_limit: float): + await asyncio.sleep(time_limit) + await pc.close() + + def clean_up(self, webrtc_id: str): + self.handlers.pop(webrtc_id, None) + connection = self.connections.pop(webrtc_id, []) + for conn in connection: + if isinstance(conn, AudioCallback): + conn.event_handler.shutdown() + self.additional_outputs.pop(webrtc_id, None) + self.data_channels.pop(webrtc_id, None) + return connection + + @server + async def offer(self, body): + logger.debug("Starting to handle offer") + logger.debug("Offer body %s", body) + if len(self.connections) >= cast(int, self.concurrency_limit): + return {"status": "failed"} + + offer = RTCSessionDescription(sdp=body["sdp"], type=body["type"]) + + pc = RTCPeerConnection() + self.pcs.add(pc) + + if isinstance(self.event_handler, StreamHandlerBase): + handler = self.event_handler.copy() + else: + handler = cast(Callable, self.event_handler) + + self.handlers[body["webrtc_id"]] = handler + + set_outputs = self.set_additional_outputs(body["webrtc_id"]) + + @pc.on("iceconnectionstatechange") + async def on_iceconnectionstatechange(): + logger.debug("ICE connection state change %s", pc.iceConnectionState) + if pc.iceConnectionState == "failed": + await pc.close() + self.connections.pop(body["webrtc_id"], None) + self.pcs.discard(pc) + + @pc.on("connectionstatechange") + async def on_connectionstatechange(): + logger.debug("pc.connectionState %s", pc.connectionState) + if pc.connectionState in ["failed", "closed"]: + await pc.close() + connection = self.clean_up(body["webrtc_id"]) + if connection: + for conn in connection: + conn.stop() + self.pcs.discard(pc) + if pc.connectionState == "connected": + if self.time_limit is not None: + asyncio.create_task(self.wait_for_time_limit(pc, self.time_limit)) + + @pc.on("track") + def on_track(track): + relay = MediaRelay() + handler = self.handlers[body["webrtc_id"]] + + if self.modality == "video" and track.kind == "video": + cb = VideoCallback( + relay.subscribe(track), + event_handler=cast(VideoEventHandler, handler), + set_additional_outputs=set_outputs, + mode=cast(Literal["send", "send-receive"], self.mode), + ) + elif self.modality == "audio-video" and track.kind == "video": + cb = VideoStreamHander( + relay.subscribe(track), + event_handler=handler, # type: ignore + set_additional_outputs=set_outputs, + ) + elif self.modality in ["audio", "audio-video"] and track.kind == "audio": + eh = cast(StreamHandlerImpl, handler) + eh._loop = asyncio.get_running_loop() + cb = AudioCallback( + relay.subscribe(track), + event_handler=eh, + set_additional_outputs=set_outputs, + ) + else: + raise ValueError("Modality must be either video, audio, or audio-video") + if body["webrtc_id"] not in self.connections: + self.connections[body["webrtc_id"]] = [] + + self.connections[body["webrtc_id"]].append(cb) + if body["webrtc_id"] in self.data_channels: + for conn in self.connections[body["webrtc_id"]]: + conn.set_channel(self.data_channels[body["webrtc_id"]]) + if self.mode == "send-receive": + logger.debug("Adding track to peer connection %s", cb) + pc.addTrack(cb) + elif self.mode == "send": + cast(AudioCallback | VideoCallback, cb).start() + + if self.mode == "receive": + if self.modality == "video": + cb = ServerToClientVideo( + cast(Callable, self.event_handler), + set_additional_outputs=set_outputs, + ) + elif self.modality == "audio": + cb = ServerToClientAudio( + cast(Callable, self.event_handler), + set_additional_outputs=set_outputs, + ) + else: + raise ValueError("Modality must be either video or audio") + + logger.debug("Adding track to peer connection %s", cb) + pc.addTrack(cb) + self.connections[body["webrtc_id"]].append(cb) + cb.on("ended", lambda: self.clean_up(body["webrtc_id"])) + + @pc.on("datachannel") + def on_datachannel(channel): + logger.debug(f"Data channel established: {channel.label}") + + self.data_channels[body["webrtc_id"]] = channel + + async def set_channel(webrtc_id: str): + while not self.connections.get(webrtc_id): + await asyncio.sleep(0.05) + logger.debug("setting channel for webrtc id %s", webrtc_id) + for conn in self.connections[webrtc_id]: + conn.set_channel(channel) + + asyncio.create_task(set_channel(body["webrtc_id"])) + + @channel.on("message") + def on_message(message): + logger.debug(f"Received message: {message}") + if channel.readyState == "open": + channel.send(f"Server received: {message}") + + # handle offer + await pc.setRemoteDescription(offer) + + # send answer + answer = await pc.createAnswer() + await pc.setLocalDescription(answer) # type: ignore + logger.debug("done handling offer about to return") + await asyncio.sleep(0.1) + + return { + "sdp": pc.localDescription.sdp, + "type": pc.localDescription.type, + } + + def example_payload(self) -> Any: + return { + "video": handle_file( + "https://github.com/gradio-app/gradio/raw/main/demo/video_component/files/world.mp4" + ), + } + + def example_value(self) -> Any: + return "https://github.com/gradio-app/gradio/raw/main/demo/video_component/files/world.mp4" + + def api_info(self) -> Any: + return {"type": "number"} diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..16e1307 --- /dev/null +++ b/demo/README.md @@ -0,0 +1,44 @@ +--- +license: mit +tags: +- object-detection +- computer-vision +- yolov10 +datasets: +- detection-datasets/coco +sdk: gradio +sdk_version: 5.0.0b1 +--- + +### Model Description +[YOLOv10: Real-Time End-to-End Object Detection](https://arxiv.org/abs/2405.14458v1) + +- arXiv: https://arxiv.org/abs/2405.14458v1 +- github: https://github.com/THU-MIG/yolov10 + +### Installation +``` +pip install supervision git+https://github.com/THU-MIG/yolov10.git +``` + +### Yolov10 Inference +```python +from ultralytics import YOLOv10 +import supervision as sv +import cv2 + +IMAGE_PATH = 'dog.jpeg' + +model = YOLOv10.from_pretrained('jameslahm/yolov10{n/s/m/b/l/x}') +model.predict(IMAGE_PATH, show=True) +``` + +### BibTeX Entry and Citation Info + ``` +@article{wang2024yolov10, + title={YOLOv10: Real-Time End-to-End Object Detection}, + author={Wang, Ao and Chen, Hui and Liu, Lihao and Chen, Kai and Lin, Zijia and Han, Jungong and Ding, Guiguang}, + journal={arXiv preprint arXiv:2405.14458}, + year={2024} +} +``` \ No newline at end of file diff --git a/demo/__init__.py b/demo/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/demo/also_return_text.py b/demo/also_return_text.py new file mode 100644 index 0000000..85a682a --- /dev/null +++ b/demo/also_return_text.py @@ -0,0 +1,105 @@ +import logging +import os + +import gradio as gr +import numpy as np +from gradio_webrtc import AdditionalOutputs, WebRTC +from pydub import AudioSegment +from twilio.rest import Client + +# Configure the root logger to WARNING to suppress debug messages from other libraries +logging.basicConfig(level=logging.WARNING) + +# Create a console handler +console_handler = logging.FileHandler("gradio_webrtc.log") +console_handler.setLevel(logging.DEBUG) + +# Create a formatter +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +console_handler.setFormatter(formatter) + +# Configure the logger for your specific library +logger = logging.getLogger("gradio_webrtc") +logger.setLevel(logging.DEBUG) +logger.addHandler(console_handler) + + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +if account_sid and auth_token: + client = Client(account_sid, auth_token) + + token = client.tokens.create() + + rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } +else: + rtc_configuration = None + + +def generation(num_steps): + for i in range(num_steps): + segment = AudioSegment.from_file( + "/Users/freddy/sources/gradio/demo/scratch/audio-streaming/librispeech.mp3" + ) + yield ( + ( + segment.frame_rate, + np.array(segment.get_array_of_samples()).reshape(1, -1), + ), + AdditionalOutputs( + f"Hello, from step {i}!", + "/Users/freddy/sources/gradio/demo/scratch/audio-streaming/librispeech.mp3", + ), + ) + + +css = """.my-group {max-width: 600px !important; max-height: 600 !important;} + .my-column {display: flex !important; justify-content: center !important; align-items: center !important};""" + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Audio Streaming (Powered by WebRTC ⚡️) +

+ """ + ) + with gr.Column(elem_classes=["my-column"]): + with gr.Group(elem_classes=["my-group"]): + audio = WebRTC( + label="Stream", + rtc_configuration=rtc_configuration, + mode="receive", + modality="audio", + ) + num_steps = gr.Slider( + label="Number of Steps", + minimum=1, + maximum=10, + step=1, + value=5, + ) + button = gr.Button("Generate") + textbox = gr.Textbox(placeholder="Output will appear here.") + audio_file = gr.Audio() + + audio.stream( + fn=generation, inputs=[num_steps], outputs=[audio], trigger=button.click + ) + audio.on_additional_outputs( + fn=lambda t, a: (f"State changed to {t}.", a), + outputs=[textbox, audio_file], + ) + + +if __name__ == "__main__": + demo.launch( + allowed_paths=[ + "/Users/freddy/sources/gradio/demo/scratch/audio-streaming/librispeech.mp3" + ] + ) diff --git a/demo/app.py b/demo/app.py new file mode 100644 index 0000000..378a8a5 --- /dev/null +++ b/demo/app.py @@ -0,0 +1,367 @@ +import os + +import gradio as gr + +_docs = { + "WebRTC": { + "description": "Stream audio/video with WebRTC", + "members": { + "__init__": { + "rtc_configuration": { + "type": "dict[str, Any] | None", + "default": "None", + "description": "The configration dictionary to pass to the RTCPeerConnection constructor. If None, the default configuration is used.", + }, + "height": { + "type": "int | str | None", + "default": "None", + "description": "The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video.", + }, + "width": { + "type": "int | str | None", + "default": "None", + "description": "The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video.", + }, + "label": { + "type": "str | None", + "default": "None", + "description": "the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.", + }, + "show_label": { + "type": "bool | None", + "default": "None", + "description": "if True, will display label.", + }, + "container": { + "type": "bool", + "default": "True", + "description": "if True, will place the component in a container - providing some extra padding around the border.", + }, + "scale": { + "type": "int | None", + "default": "None", + "description": "relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.", + }, + "min_width": { + "type": "int", + "default": "160", + "description": "minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.", + }, + "interactive": { + "type": "bool | None", + "default": "None", + "description": "if True, will allow users to upload a video; if False, can only be used to display videos. If not provided, this is inferred based on whether the component is used as an input or output.", + }, + "visible": { + "type": "bool", + "default": "True", + "description": "if False, component will be hidden.", + }, + "elem_id": { + "type": "str | None", + "default": "None", + "description": "an optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.", + }, + "elem_classes": { + "type": "list[str] | str | None", + "default": "None", + "description": "an optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.", + }, + "render": { + "type": "bool", + "default": "True", + "description": "if False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.", + }, + "key": { + "type": "int | str | None", + "default": "None", + "description": "if assigned, will be used to assume identity across a re-render. Components that have the same key across a re-render will have their value preserved.", + }, + "mirror_webcam": { + "type": "bool", + "default": "True", + "description": "if True webcam will be mirrored. Default is True.", + }, + }, + "events": {"tick": {"type": None, "default": None, "description": ""}}, + }, + "__meta__": {"additional_interfaces": {}, "user_fn_refs": {"WebRTC": []}}, + } +} + + +abs_path = os.path.join(os.path.dirname(__file__), "css.css") + +with gr.Blocks( + css_paths=abs_path, + theme=gr.themes.Default( + font_mono=[ + gr.themes.GoogleFont("Inconsolata"), + "monospace", + ], + ), +) as demo: + gr.Markdown( + """ +

Gradio WebRTC ⚡️

+ +
+Static Badge +Static Badge +
+""", + elem_classes=["md-custom"], + header_links=True, + ) + gr.Markdown( + """ +## Installation + +```bash +pip install gradio_webrtc +``` + +## Examples: +1. [Object Detection from Webcam with YOLOv10](https://huggingface.co/spaces/freddyaboulton/webrtc-yolov10n) 📷 +2. [Streaming Object Detection from Video with RT-DETR](https://huggingface.co/spaces/freddyaboulton/rt-detr-object-detection-webrtc) 🎥 +3. [Text-to-Speech](https://huggingface.co/spaces/freddyaboulton/parler-tts-streaming-webrtc) 🗣️ +4. [Conversational AI](https://huggingface.co/spaces/freddyaboulton/omni-mini-webrtc) 🤖🗣️ + +## Usage + +The WebRTC component supports the following three use cases: +1. [Streaming video from the user webcam to the server and back](#h-streaming-video-from-the-user-webcam-to-the-server-and-back) +2. [Streaming Video from the server to the client](#h-streaming-video-from-the-server-to-the-client) +3. [Streaming Audio from the server to the client](#h-streaming-audio-from-the-server-to-the-client) +4. [Streaming Audio from the client to the server and back (conversational AI)](#h-conversational-ai) + + +## Streaming Video from the User Webcam to the Server and Back + +```python +import gradio as gr +from gradio_webrtc import WebRTC + + +def detection(image, conf_threshold=0.3): + ... your detection code here ... + + +with gr.Blocks() as demo: + image = WebRTC(label="Stream", mode="send-receive", modality="video") + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + image.stream( + fn=detection, + inputs=[image, conf_threshold], + outputs=[image], time_limit=10 + ) + +if __name__ == "__main__": + demo.launch() + +``` +* Set the `mode` parameter to `send-receive` and `modality` to "video". +* The `stream` event's `fn` parameter is a function that receives the next frame from the webcam +as a **numpy array** and returns the processed frame also as a **numpy array**. +* Numpy arrays are in (height, width, 3) format where the color channels are in RGB format. +* The `inputs` parameter should be a list where the first element is the WebRTC component. The only output allowed is the WebRTC component. +* The `time_limit` parameter is the maximum time in seconds the video stream will run. If the time limit is reached, the video stream will stop. + +## Streaming Video from the server to the client + +```python +import gradio as gr +from gradio_webrtc import WebRTC +import cv2 + +def generation(): + url = "https://download.tsi.telecom-paristech.fr/gpac/dataset/dash/uhd/mux_sources/hevcds_720p30_2M.mp4" + cap = cv2.VideoCapture(url) + iterating = True + while iterating: + iterating, frame = cap.read() + yield frame + +with gr.Blocks() as demo: + output_video = WebRTC(label="Video Stream", mode="receive", modality="video") + button = gr.Button("Start", variant="primary") + output_video.stream( + fn=generation, inputs=None, outputs=[output_video], + trigger=button.click + ) + +if __name__ == "__main__": + demo.launch() +``` + +* Set the "mode" parameter to "receive" and "modality" to "video". +* The `stream` event's `fn` parameter is a generator function that yields the next frame from the video as a **numpy array**. +* The only output allowed is the WebRTC component. +* The `trigger` parameter the gradio event that will trigger the webrtc connection. In this case, the button click event. + +## Streaming Audio from the Server to the Client + +```python +import gradio as gr +from pydub import AudioSegment + +def generation(num_steps): + for _ in range(num_steps): + segment = AudioSegment.from_file("/Users/freddy/sources/gradio/demo/audio_debugger/cantina.wav") + yield (segment.frame_rate, np.array(segment.get_array_of_samples()).reshape(1, -1)) + +with gr.Blocks() as demo: + audio = WebRTC(label="Stream", mode="receive", modality="audio") + num_steps = gr.Slider( + label="Number of Steps", + minimum=1, + maximum=10, + step=1, + value=5, + ) + button = gr.Button("Generate") + + audio.stream( + fn=generation, inputs=[num_steps], outputs=[audio], + trigger=button.click + ) +``` + +* Set the "mode" parameter to "receive" and "modality" to "audio". +* The `stream` event's `fn` parameter is a generator function that yields the next audio segment as a tuple of (frame_rate, audio_samples). +* The numpy array should be of shape (1, num_samples). +* The `outputs` parameter should be a list with the WebRTC component as the only element. + +## Conversational AI + +```python +import gradio as gr +import numpy as np +from gradio_webrtc import WebRTC, StreamHandler +from queue import Queue +import time + + +class EchoHandler(StreamHandler): + def __init__(self) -> None: + super().__init__() + self.queue = Queue() + + def receive(self, frame: tuple[int, np.ndarray] | np.ndarray) -> None: + self.queue.put(frame) + + def emit(self) -> None: + return self.queue.get() + + +with gr.Blocks() as demo: + with gr.Column(): + with gr.Group(): + audio = WebRTC( + label="Stream", + rtc_configuration=None, + mode="send-receive", + modality="audio", + ) + + audio.stream(fn=EchoHandler(), inputs=[audio], outputs=[audio], time_limit=15) + + +if __name__ == "__main__": + demo.launch() +``` + +* Instead of passing a function to the `stream` event's `fn` parameter, pass a `StreamHandler` implementation. The `StreamHandler` above simply echoes the audio back to the client. +* The `StreamHandler` class has two methods: `receive` and `emit`. The `receive` method is called when a new frame is received from the client, and the `emit` method returns the next frame to send to the client. +* An audio frame is represented as a tuple of (frame_rate, audio_samples) where `audio_samples` is a numpy array of shape (num_channels, num_samples). +* You can also specify the audio layout ("mono" or "stereo") in the emit method by retuning it as the third element of the tuple. If not specified, the default is "mono". +* The `time_limit` parameter is the maximum time in seconds the conversation will run. If the time limit is reached, the audio stream will stop. +* The `emit` method SHOULD NOT block. If a frame is not ready to be sent, the method should return None. + +## Deployment + +When deploying in a cloud environment (like Hugging Face Spaces, EC2, etc), you need to set up a TURN server to relay the WebRTC traffic. +The easiest way to do this is to use a service like Twilio. + +```python +from twilio.rest import Client +import os + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +client = Client(account_sid, auth_token) + +token = client.tokens.create() + +rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", +} + +with gr.Blocks() as demo: + ... + rtc = WebRTC(rtc_configuration=rtc_configuration, ...) + ... +``` +""", + elem_classes=["md-custom"], + header_links=True, + ) + + gr.Markdown( + """ +## +""", + elem_classes=["md-custom"], + header_links=True, + ) + + gr.ParamViewer(value=_docs["WebRTC"]["members"]["__init__"], linkify=[]) + + demo.load( + None, + js=r"""function() { + const refs = {}; + const user_fn_refs = { + WebRTC: [], }; + requestAnimationFrame(() => { + + Object.entries(user_fn_refs).forEach(([key, refs]) => { + if (refs.length > 0) { + const el = document.querySelector(`.${key}-user-fn`); + if (!el) return; + refs.forEach(ref => { + el.innerHTML = el.innerHTML.replace( + new RegExp("\\b"+ref+"\\b", "g"), + `${ref}` + ); + }) + } + }) + + Object.entries(refs).forEach(([key, refs]) => { + if (refs.length > 0) { + const el = document.querySelector(`.${key}`); + if (!el) return; + refs.forEach(ref => { + el.innerHTML = el.innerHTML.replace( + new RegExp("\\b"+ref+"\\b", "g"), + `${ref}` + ); + }) + } + }) + }) +} + +""", + ) + +demo.launch() diff --git a/demo/app_.py b/demo/app_.py new file mode 100644 index 0000000..378a8a5 --- /dev/null +++ b/demo/app_.py @@ -0,0 +1,367 @@ +import os + +import gradio as gr + +_docs = { + "WebRTC": { + "description": "Stream audio/video with WebRTC", + "members": { + "__init__": { + "rtc_configuration": { + "type": "dict[str, Any] | None", + "default": "None", + "description": "The configration dictionary to pass to the RTCPeerConnection constructor. If None, the default configuration is used.", + }, + "height": { + "type": "int | str | None", + "default": "None", + "description": "The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video.", + }, + "width": { + "type": "int | str | None", + "default": "None", + "description": "The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video.", + }, + "label": { + "type": "str | None", + "default": "None", + "description": "the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.", + }, + "show_label": { + "type": "bool | None", + "default": "None", + "description": "if True, will display label.", + }, + "container": { + "type": "bool", + "default": "True", + "description": "if True, will place the component in a container - providing some extra padding around the border.", + }, + "scale": { + "type": "int | None", + "default": "None", + "description": "relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.", + }, + "min_width": { + "type": "int", + "default": "160", + "description": "minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.", + }, + "interactive": { + "type": "bool | None", + "default": "None", + "description": "if True, will allow users to upload a video; if False, can only be used to display videos. If not provided, this is inferred based on whether the component is used as an input or output.", + }, + "visible": { + "type": "bool", + "default": "True", + "description": "if False, component will be hidden.", + }, + "elem_id": { + "type": "str | None", + "default": "None", + "description": "an optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.", + }, + "elem_classes": { + "type": "list[str] | str | None", + "default": "None", + "description": "an optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.", + }, + "render": { + "type": "bool", + "default": "True", + "description": "if False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.", + }, + "key": { + "type": "int | str | None", + "default": "None", + "description": "if assigned, will be used to assume identity across a re-render. Components that have the same key across a re-render will have their value preserved.", + }, + "mirror_webcam": { + "type": "bool", + "default": "True", + "description": "if True webcam will be mirrored. Default is True.", + }, + }, + "events": {"tick": {"type": None, "default": None, "description": ""}}, + }, + "__meta__": {"additional_interfaces": {}, "user_fn_refs": {"WebRTC": []}}, + } +} + + +abs_path = os.path.join(os.path.dirname(__file__), "css.css") + +with gr.Blocks( + css_paths=abs_path, + theme=gr.themes.Default( + font_mono=[ + gr.themes.GoogleFont("Inconsolata"), + "monospace", + ], + ), +) as demo: + gr.Markdown( + """ +

Gradio WebRTC ⚡️

+ +
+Static Badge +Static Badge +
+""", + elem_classes=["md-custom"], + header_links=True, + ) + gr.Markdown( + """ +## Installation + +```bash +pip install gradio_webrtc +``` + +## Examples: +1. [Object Detection from Webcam with YOLOv10](https://huggingface.co/spaces/freddyaboulton/webrtc-yolov10n) 📷 +2. [Streaming Object Detection from Video with RT-DETR](https://huggingface.co/spaces/freddyaboulton/rt-detr-object-detection-webrtc) 🎥 +3. [Text-to-Speech](https://huggingface.co/spaces/freddyaboulton/parler-tts-streaming-webrtc) 🗣️ +4. [Conversational AI](https://huggingface.co/spaces/freddyaboulton/omni-mini-webrtc) 🤖🗣️ + +## Usage + +The WebRTC component supports the following three use cases: +1. [Streaming video from the user webcam to the server and back](#h-streaming-video-from-the-user-webcam-to-the-server-and-back) +2. [Streaming Video from the server to the client](#h-streaming-video-from-the-server-to-the-client) +3. [Streaming Audio from the server to the client](#h-streaming-audio-from-the-server-to-the-client) +4. [Streaming Audio from the client to the server and back (conversational AI)](#h-conversational-ai) + + +## Streaming Video from the User Webcam to the Server and Back + +```python +import gradio as gr +from gradio_webrtc import WebRTC + + +def detection(image, conf_threshold=0.3): + ... your detection code here ... + + +with gr.Blocks() as demo: + image = WebRTC(label="Stream", mode="send-receive", modality="video") + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + image.stream( + fn=detection, + inputs=[image, conf_threshold], + outputs=[image], time_limit=10 + ) + +if __name__ == "__main__": + demo.launch() + +``` +* Set the `mode` parameter to `send-receive` and `modality` to "video". +* The `stream` event's `fn` parameter is a function that receives the next frame from the webcam +as a **numpy array** and returns the processed frame also as a **numpy array**. +* Numpy arrays are in (height, width, 3) format where the color channels are in RGB format. +* The `inputs` parameter should be a list where the first element is the WebRTC component. The only output allowed is the WebRTC component. +* The `time_limit` parameter is the maximum time in seconds the video stream will run. If the time limit is reached, the video stream will stop. + +## Streaming Video from the server to the client + +```python +import gradio as gr +from gradio_webrtc import WebRTC +import cv2 + +def generation(): + url = "https://download.tsi.telecom-paristech.fr/gpac/dataset/dash/uhd/mux_sources/hevcds_720p30_2M.mp4" + cap = cv2.VideoCapture(url) + iterating = True + while iterating: + iterating, frame = cap.read() + yield frame + +with gr.Blocks() as demo: + output_video = WebRTC(label="Video Stream", mode="receive", modality="video") + button = gr.Button("Start", variant="primary") + output_video.stream( + fn=generation, inputs=None, outputs=[output_video], + trigger=button.click + ) + +if __name__ == "__main__": + demo.launch() +``` + +* Set the "mode" parameter to "receive" and "modality" to "video". +* The `stream` event's `fn` parameter is a generator function that yields the next frame from the video as a **numpy array**. +* The only output allowed is the WebRTC component. +* The `trigger` parameter the gradio event that will trigger the webrtc connection. In this case, the button click event. + +## Streaming Audio from the Server to the Client + +```python +import gradio as gr +from pydub import AudioSegment + +def generation(num_steps): + for _ in range(num_steps): + segment = AudioSegment.from_file("/Users/freddy/sources/gradio/demo/audio_debugger/cantina.wav") + yield (segment.frame_rate, np.array(segment.get_array_of_samples()).reshape(1, -1)) + +with gr.Blocks() as demo: + audio = WebRTC(label="Stream", mode="receive", modality="audio") + num_steps = gr.Slider( + label="Number of Steps", + minimum=1, + maximum=10, + step=1, + value=5, + ) + button = gr.Button("Generate") + + audio.stream( + fn=generation, inputs=[num_steps], outputs=[audio], + trigger=button.click + ) +``` + +* Set the "mode" parameter to "receive" and "modality" to "audio". +* The `stream` event's `fn` parameter is a generator function that yields the next audio segment as a tuple of (frame_rate, audio_samples). +* The numpy array should be of shape (1, num_samples). +* The `outputs` parameter should be a list with the WebRTC component as the only element. + +## Conversational AI + +```python +import gradio as gr +import numpy as np +from gradio_webrtc import WebRTC, StreamHandler +from queue import Queue +import time + + +class EchoHandler(StreamHandler): + def __init__(self) -> None: + super().__init__() + self.queue = Queue() + + def receive(self, frame: tuple[int, np.ndarray] | np.ndarray) -> None: + self.queue.put(frame) + + def emit(self) -> None: + return self.queue.get() + + +with gr.Blocks() as demo: + with gr.Column(): + with gr.Group(): + audio = WebRTC( + label="Stream", + rtc_configuration=None, + mode="send-receive", + modality="audio", + ) + + audio.stream(fn=EchoHandler(), inputs=[audio], outputs=[audio], time_limit=15) + + +if __name__ == "__main__": + demo.launch() +``` + +* Instead of passing a function to the `stream` event's `fn` parameter, pass a `StreamHandler` implementation. The `StreamHandler` above simply echoes the audio back to the client. +* The `StreamHandler` class has two methods: `receive` and `emit`. The `receive` method is called when a new frame is received from the client, and the `emit` method returns the next frame to send to the client. +* An audio frame is represented as a tuple of (frame_rate, audio_samples) where `audio_samples` is a numpy array of shape (num_channels, num_samples). +* You can also specify the audio layout ("mono" or "stereo") in the emit method by retuning it as the third element of the tuple. If not specified, the default is "mono". +* The `time_limit` parameter is the maximum time in seconds the conversation will run. If the time limit is reached, the audio stream will stop. +* The `emit` method SHOULD NOT block. If a frame is not ready to be sent, the method should return None. + +## Deployment + +When deploying in a cloud environment (like Hugging Face Spaces, EC2, etc), you need to set up a TURN server to relay the WebRTC traffic. +The easiest way to do this is to use a service like Twilio. + +```python +from twilio.rest import Client +import os + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +client = Client(account_sid, auth_token) + +token = client.tokens.create() + +rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", +} + +with gr.Blocks() as demo: + ... + rtc = WebRTC(rtc_configuration=rtc_configuration, ...) + ... +``` +""", + elem_classes=["md-custom"], + header_links=True, + ) + + gr.Markdown( + """ +## +""", + elem_classes=["md-custom"], + header_links=True, + ) + + gr.ParamViewer(value=_docs["WebRTC"]["members"]["__init__"], linkify=[]) + + demo.load( + None, + js=r"""function() { + const refs = {}; + const user_fn_refs = { + WebRTC: [], }; + requestAnimationFrame(() => { + + Object.entries(user_fn_refs).forEach(([key, refs]) => { + if (refs.length > 0) { + const el = document.querySelector(`.${key}-user-fn`); + if (!el) return; + refs.forEach(ref => { + el.innerHTML = el.innerHTML.replace( + new RegExp("\\b"+ref+"\\b", "g"), + `${ref}` + ); + }) + } + }) + + Object.entries(refs).forEach(([key, refs]) => { + if (refs.length > 0) { + const el = document.querySelector(`.${key}`); + if (!el) return; + refs.forEach(ref => { + el.innerHTML = el.innerHTML.replace( + new RegExp("\\b"+ref+"\\b", "g"), + `${ref}` + ); + }) + } + }) + }) +} + +""", + ) + +demo.launch() diff --git a/demo/app_orig.py b/demo/app_orig.py new file mode 100644 index 0000000..31f3b3f --- /dev/null +++ b/demo/app_orig.py @@ -0,0 +1,73 @@ +import os + +import cv2 +import gradio as gr +from gradio_webrtc import WebRTC +from huggingface_hub import hf_hub_download +from inference import YOLOv10 +from twilio.rest import Client + +model_file = hf_hub_download( + repo_id="onnx-community/yolov10n", filename="onnx/model.onnx" +) + +model = YOLOv10(model_file) + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +if account_sid and auth_token: + client = Client(account_sid, auth_token) + + token = client.tokens.create() + + rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } +else: + rtc_configuration = None + + +def detection(image, conf_threshold=0.3): + image = cv2.resize(image, (model.input_width, model.input_height)) + new_image = model.detect_objects(image, conf_threshold) + return cv2.resize(new_image, (500, 500)) + + +css = """.my-group {max-width: 600px !important; max-height: 600 !important;} + .my-column {display: flex !important; justify-content: center !important; align-items: center !important};""" + + +with gr.Blocks(css=css) as demo: + gr.HTML( + """ +

+ YOLOv10 Webcam Stream (Powered by WebRTC ⚡️) +

+ """ + ) + gr.HTML( + """ +

+ arXiv | github +

+ """ + ) + with gr.Column(elem_classes=["my-column"]): + with gr.Group(elem_classes=["my-group"]): + image = WebRTC(label="Stream", rtc_configuration=rtc_configuration) + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + + image.stream( + fn=detection, inputs=[image, conf_threshold], outputs=[image], time_limit=10 + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/audio_out.py b/demo/audio_out.py new file mode 100644 index 0000000..72dffc2 --- /dev/null +++ b/demo/audio_out.py @@ -0,0 +1,71 @@ +import os + +import gradio as gr +import numpy as np +from gradio_webrtc import WebRTC +from pydub import AudioSegment +from twilio.rest import Client + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +if account_sid and auth_token: + client = Client(account_sid, auth_token) + + token = client.tokens.create() + + rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } +else: + rtc_configuration = None + + +def generation(num_steps): + for _ in range(num_steps): + segment = AudioSegment.from_file( + "/Users/freddy/sources/gradio/demo/audio_debugger/cantina.wav" + ) + yield ( + segment.frame_rate, + np.array(segment.get_array_of_samples()).reshape(1, -1), + ) + + +css = """.my-group {max-width: 600px !important; max-height: 600 !important;} + .my-column {display: flex !important; justify-content: center !important; align-items: center !important};""" + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Audio Streaming (Powered by WebRTC ⚡️) +

+ """ + ) + with gr.Column(elem_classes=["my-column"]): + with gr.Group(elem_classes=["my-group"]): + audio = WebRTC( + label="Stream", + rtc_configuration=rtc_configuration, + mode="receive", + modality="audio", + ) + num_steps = gr.Slider( + label="Number of Steps", + minimum=1, + maximum=10, + step=1, + value=5, + ) + button = gr.Button("Generate") + + audio.stream( + fn=generation, inputs=[num_steps], outputs=[audio], trigger=button.click + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/audio_out_2.py b/demo/audio_out_2.py new file mode 100644 index 0000000..eda279f --- /dev/null +++ b/demo/audio_out_2.py @@ -0,0 +1,64 @@ +import os +import time + +import gradio as gr +import numpy as np +from gradio_webrtc import WebRTC +from pydub import AudioSegment +from twilio.rest import Client + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +if account_sid and auth_token: + client = Client(account_sid, auth_token) + + token = client.tokens.create() + + rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } +else: + rtc_configuration = None + + +def generation(num_steps): + for _ in range(num_steps): + segment = AudioSegment.from_file( + "/Users/freddy/sources/gradio/demo/audio_debugger/cantina.wav" + ) + yield ( + segment.frame_rate, + np.array(segment.get_array_of_samples()).reshape(1, -1), + ) + time.sleep(3.5) + + +css = """.my-group {max-width: 600px !important; max-height: 600 !important;} + .my-column {display: flex !important; justify-content: center !important; align-items: center !important};""" + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Audio Streaming (Powered by WebRaTC ⚡️) +

+ """ + ) + with gr.Row(): + with gr.Column(): + gr.Slider() + with gr.Column(): + # audio = gr.Audio(interactive=False) + audio = WebRTC( + label="Stream", + rtc_configuration=rtc_configuration, + mode="receive", + modality="audio", + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/css.css b/demo/css.css new file mode 100644 index 0000000..486edd8 --- /dev/null +++ b/demo/css.css @@ -0,0 +1,161 @@ +html { + font-family: Inter; + font-size: 16px; + font-weight: 400; + line-height: 1.5; + -webkit-text-size-adjust: 100%; + background: #fff; + color: #323232; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; +} + +:root { + --space: 1; + --vspace: calc(var(--space) * 1rem); + --vspace-0: calc(3 * var(--space) * 1rem); + --vspace-1: calc(2 * var(--space) * 1rem); + --vspace-2: calc(1.5 * var(--space) * 1rem); + --vspace-3: calc(0.5 * var(--space) * 1rem); +} + +.app { + max-width: 748px !important; +} + +.prose p { + margin: var(--vspace) 0; + line-height: var(--vspace * 2); + font-size: 1rem; +} + +code { + font-family: "Inconsolata", sans-serif; + font-size: 16px; +} + +h1, +h1 code { + font-weight: 400; + line-height: calc(2.5 / var(--space) * var(--vspace)); +} + +h1 code { + background: none; + border: none; + letter-spacing: 0.05em; + padding-bottom: 5px; + position: relative; + padding: 0; +} + +h2 { + margin: var(--vspace-1) 0 var(--vspace-2) 0; + line-height: 1em; +} + +h3, +h3 code { + margin: var(--vspace-1) 0 var(--vspace-2) 0; + line-height: 1em; +} + +h4, +h5, +h6 { + margin: var(--vspace-3) 0 var(--vspace-3) 0; + line-height: var(--vspace); +} + +.bigtitle, +h1, +h1 code { + font-size: calc(8px * 4.5); + word-break: break-word; +} + +.title, +h2, +h2 code { + font-size: calc(8px * 3.375); + font-weight: lighter; + word-break: break-word; + border: none; + background: none; +} + +.subheading1, +h3, +h3 code { + font-size: calc(8px * 1.8); + font-weight: 600; + border: none; + background: none; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +h2 code { + padding: 0; + position: relative; + letter-spacing: 0.05em; +} + +blockquote { + font-size: calc(8px * 1.1667); + font-style: italic; + line-height: calc(1.1667 * var(--vspace)); + margin: var(--vspace-2) var(--vspace-2); +} + +.subheading2, +h4 { + font-size: calc(8px * 1.4292); + text-transform: uppercase; + font-weight: 600; +} + +.subheading3, +h5 { + font-size: calc(8px * 1.2917); + line-height: calc(1.2917 * var(--vspace)); + + font-weight: lighter; + text-transform: uppercase; + letter-spacing: 0.15em; +} + +h6 { + font-size: calc(8px * 1.1667); + font-size: 1.1667em; + font-weight: normal; + font-style: italic; + font-family: "le-monde-livre-classic-byol", serif !important; + letter-spacing: 0px !important; +} + +#start .md > *:first-child { + margin-top: 0; +} + +h2 + h3 { + margin-top: 0; +} + +.md hr { + border: none; + border-top: 1px solid var(--block-border-color); + margin: var(--vspace-2) 0 var(--vspace-2) 0; +} +.prose ul { + margin: var(--vspace-2) 0 var(--vspace-1) 0; +} + +.gap { + gap: 0; +} + +.md-custom { + overflow: hidden; +} diff --git a/demo/docs.py b/demo/docs.py new file mode 100644 index 0000000..dde0b2e --- /dev/null +++ b/demo/docs.py @@ -0,0 +1,99 @@ +_docs = { + "WebRTC": { + "description": "Stream audio/video with WebRTC", + "members": { + "__init__": { + "rtc_configuration": { + "type": "dict[str, Any] | None", + "default": "None", + "description": "The configration dictionary to pass to the RTCPeerConnection constructor. If None, the default configuration is used.", + }, + "height": { + "type": "int | str | None", + "default": "None", + "description": "The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video.", + }, + "width": { + "type": "int | str | None", + "default": "None", + "description": "The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video.", + }, + "label": { + "type": "str | None", + "default": "None", + "description": "the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.", + }, + "show_label": { + "type": "bool | None", + "default": "None", + "description": "if True, will display label.", + }, + "container": { + "type": "bool", + "default": "True", + "description": "if True, will place the component in a container - providing some extra padding around the border.", + }, + "scale": { + "type": "int | None", + "default": "None", + "description": "relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.", + }, + "min_width": { + "type": "int", + "default": "160", + "description": "minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.", + }, + "interactive": { + "type": "bool | None", + "default": "None", + "description": "if True, will allow users to upload a video; if False, can only be used to display videos. If not provided, this is inferred based on whether the component is used as an input or output.", + }, + "visible": { + "type": "bool", + "default": "True", + "description": "if False, component will be hidden.", + }, + "elem_id": { + "type": "str | None", + "default": "None", + "description": "an optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.", + }, + "elem_classes": { + "type": "list[str] | str | None", + "default": "None", + "description": "an optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.", + }, + "render": { + "type": "bool", + "default": "True", + "description": "if False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.", + }, + "key": { + "type": "int | str | None", + "default": "None", + "description": "if assigned, will be used to assume identity across a re-render. Components that have the same key across a re-render will have their value preserved.", + }, + "mirror_webcam": { + "type": "bool", + "default": "True", + "description": "if True webcam will be mirrored. Default is True.", + }, + "postprocess": { + "value": { + "type": "typing.Any", + "description": "Expects a {str} or {pathlib.Path} filepath to a video which is displayed, or a {Tuple[str | pathlib.Path, str | pathlib.Path | None]} where the first element is a filepath to a video and the second element is an optional filepath to a subtitle file.", + } + }, + "preprocess": { + "return": { + "type": "str", + "description": "Passes the uploaded video as a `str` filepath or URL whose extension can be modified by `format`.", + }, + "value": None, + }, + }, + "events": {"tick": {"type": None, "default": None, "description": ""}}, + }, + "__meta__": {"additional_interfaces": {}, "user_fn_refs": {"WebRTC": []}}, + } +} diff --git a/demo/echo_conversation.py b/demo/echo_conversation.py new file mode 100644 index 0000000..be58de7 --- /dev/null +++ b/demo/echo_conversation.py @@ -0,0 +1,61 @@ +import logging +from queue import Queue + +import gradio as gr +import numpy as np +from gradio_webrtc import StreamHandler, WebRTC + +# Configure the root logger to WARNING to suppress debug messages from other libraries +logging.basicConfig(level=logging.WARNING) + +# Create a console handler +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.DEBUG) + +# Create a formatter +formatter = logging.Formatter("%(name)s - %(levelname)s - %(message)s") +console_handler.setFormatter(formatter) + +# Configure the logger for your specific library +logger = logging.getLogger("gradio_webrtc") +logger.setLevel(logging.DEBUG) +logger.addHandler(console_handler) + + +class EchoHandler(StreamHandler): + def __init__(self) -> None: + super().__init__() + self.queue = Queue() + + def receive(self, frame: tuple[int, np.ndarray] | np.ndarray) -> None: + self.queue.put(frame) + + def emit(self) -> None: + return self.queue.get() + + def copy(self) -> StreamHandler: + return EchoHandler() + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Conversational AI (Powered by WebRTC ⚡️) +

+ """ + ) + with gr.Column(): + with gr.Group(): + audio = WebRTC( + label="Stream", + rtc_configuration=None, + mode="send-receive", + modality="audio", + ) + + audio.stream(fn=EchoHandler(), inputs=[audio], outputs=[audio], time_limit=15) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/inference.py b/demo/inference.py new file mode 100644 index 0000000..7b3fdff --- /dev/null +++ b/demo/inference.py @@ -0,0 +1,149 @@ +import time + +import cv2 +import numpy as np +import onnxruntime +from utils import draw_detections + + +class YOLOv10: + def __init__(self, path): + # Initialize model + self.initialize_model(path) + + def __call__(self, image): + return self.detect_objects(image) + + def initialize_model(self, path): + self.session = onnxruntime.InferenceSession( + path, providers=onnxruntime.get_available_providers() + ) + # Get model info + self.get_input_details() + self.get_output_details() + + def detect_objects(self, image, conf_threshold=0.3): + input_tensor = self.prepare_input(image) + + # Perform inference on the image + new_image = self.inference(image, input_tensor, conf_threshold) + + return new_image + + def prepare_input(self, image): + self.img_height, self.img_width = image.shape[:2] + + input_img = cv2.cvtColor(image, cv2.COLOR_BGR2RGB) + + # Resize input image + input_img = cv2.resize(input_img, (self.input_width, self.input_height)) + + # Scale input pixel values to 0 to 1 + input_img = input_img / 255.0 + input_img = input_img.transpose(2, 0, 1) + input_tensor = input_img[np.newaxis, :, :, :].astype(np.float32) + + return input_tensor + + def inference(self, image, input_tensor, conf_threshold=0.3): + start = time.perf_counter() + outputs = self.session.run( + self.output_names, {self.input_names[0]: input_tensor} + ) + + print(f"Inference time: {(time.perf_counter() - start)*1000:.2f} ms") + ( + boxes, + scores, + class_ids, + ) = self.process_output(outputs, conf_threshold) + return self.draw_detections(image, boxes, scores, class_ids) + + def process_output(self, output, conf_threshold=0.3): + predictions = np.squeeze(output[0]) + + # Filter out object confidence scores below threshold + scores = predictions[:, 4] + predictions = predictions[scores > conf_threshold, :] + scores = scores[scores > conf_threshold] + + if len(scores) == 0: + return [], [], [] + + # Get the class with the highest confidence + class_ids = np.argmax(predictions[:, 4:], axis=1) + + # Get bounding boxes for each object + boxes = self.extract_boxes(predictions) + + return boxes, scores, class_ids + + def extract_boxes(self, predictions): + # Extract boxes from predictions + boxes = predictions[:, :4] + + # Scale boxes to original image dimensions + boxes = self.rescale_boxes(boxes) + + # Convert boxes to xyxy format + # boxes = xywh2xyxy(boxes) + + return boxes + + def rescale_boxes(self, boxes): + # Rescale boxes to original image dimensions + input_shape = np.array( + [self.input_width, self.input_height, self.input_width, self.input_height] + ) + boxes = np.divide(boxes, input_shape, dtype=np.float32) + boxes *= np.array( + [self.img_width, self.img_height, self.img_width, self.img_height] + ) + return boxes + + def draw_detections( + self, image, boxes, scores, class_ids, draw_scores=True, mask_alpha=0.4 + ): + return draw_detections(image, boxes, scores, class_ids, mask_alpha) + + def get_input_details(self): + model_inputs = self.session.get_inputs() + self.input_names = [model_inputs[i].name for i in range(len(model_inputs))] + + self.input_shape = model_inputs[0].shape + self.input_height = self.input_shape[2] + self.input_width = self.input_shape[3] + + def get_output_details(self): + model_outputs = self.session.get_outputs() + self.output_names = [model_outputs[i].name for i in range(len(model_outputs))] + + +if __name__ == "__main__": + import tempfile + + import requests + from huggingface_hub import hf_hub_download + + model_file = hf_hub_download( + repo_id="onnx-community/yolov10s", filename="onnx/model.onnx" + ) + + yolov8_detector = YOLOv10(model_file) + + with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f: + f.write( + requests.get( + "https://live.staticflickr.com/13/19041780_d6fd803de0_3k.jpg" + ).content + ) + f.seek(0) + img = cv2.imread(f.name) + + # # Detect Objects + combined_image = yolov8_detector.detect_objects(img) + + # Draw detections + cv2.namedWindow("Output", cv2.WINDOW_NORMAL) + cv2.imshow("Output", combined_image) + cv2.waitKey(0) diff --git a/demo/old_app.py b/demo/old_app.py new file mode 100644 index 0000000..fe697b0 --- /dev/null +++ b/demo/old_app.py @@ -0,0 +1,74 @@ +import os + +import cv2 +import gradio as gr +from gradio_webrtc import WebRTC +from huggingface_hub import hf_hub_download +from inference import YOLOv10 +from twilio.rest import Client + +model_file = hf_hub_download( + repo_id="onnx-community/yolov10n", filename="onnx/model.onnx" +) + +model = YOLOv10(model_file) + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +if account_sid and auth_token: + client = Client(account_sid, auth_token) + + token = client.tokens.create() + + rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } +else: + rtc_configuration = None + + +def detection(frame, conf_threshold=0.3): + frame = cv2.flip(frame, 0) + return cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + + +css = """.my-group {max-width: 600px !important; max-height: 600 !important;} + .my-column {display: flex !important; justify-content: center !important; align-items: center !important};""" + + +with gr.Blocks(css=css) as demo: + gr.HTML( + """ +

+ YOLOv10 Webcam Stream (Powered by WebRTC ⚡️) +

+ """ + ) + gr.HTML( + """ +

+ arXiv | github +

+ """ + ) + with gr.Column(elem_classes=["my-column"]): + with gr.Group(elem_classes=["my-group"]): + image = WebRTC(label="Stream", rtc_configuration=rtc_configuration) + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + number = gr.Number() + + image.stream( + fn=detection, inputs=[image, conf_threshold], outputs=[image], time_limit=10 + ) + image.on_additional_outputs(lambda n: n, outputs=[number]) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/requirements.txt b/demo/requirements.txt new file mode 100644 index 0000000..b831040 --- /dev/null +++ b/demo/requirements.txt @@ -0,0 +1,6 @@ +safetensors==0.4.3 +opencv-python +twilio +https://huggingface.co/datasets/freddyaboulton/bucket/resolve/main/gradio-5.0.0b3-py3-none-any.whl +https://huggingface.co/datasets/freddyaboulton/bucket/resolve/main/gradio_webrtc-0.0.1-py3-none-any.whl +onnxruntime-gpu \ No newline at end of file diff --git a/demo/space.py b/demo/space.py new file mode 100644 index 0000000..7b7bc06 --- /dev/null +++ b/demo/space.py @@ -0,0 +1,321 @@ +import os + +import gradio as gr + +_docs = { + "WebRTC": { + "description": "Stream audio/video with WebRTC", + "members": { + "__init__": { + "rtc_configuration": { + "type": "dict[str, Any] | None", + "default": "None", + "description": "The configration dictionary to pass to the RTCPeerConnection constructor. If None, the default configuration is used.", + }, + "height": { + "type": "int | str | None", + "default": "None", + "description": "The height of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video.", + }, + "width": { + "type": "int | str | None", + "default": "None", + "description": "The width of the component, specified in pixels if a number is passed, or in CSS units if a string is passed. This has no effect on the preprocessed video file, but will affect the displayed video.", + }, + "label": { + "type": "str | None", + "default": "None", + "description": "the label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.", + }, + "show_label": { + "type": "bool | None", + "default": "None", + "description": "if True, will display label.", + }, + "container": { + "type": "bool", + "default": "True", + "description": "if True, will place the component in a container - providing some extra padding around the border.", + }, + "scale": { + "type": "int | None", + "default": "None", + "description": "relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.", + }, + "min_width": { + "type": "int", + "default": "160", + "description": "minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.", + }, + "interactive": { + "type": "bool | None", + "default": "None", + "description": "if True, will allow users to upload a video; if False, can only be used to display videos. If not provided, this is inferred based on whether the component is used as an input or output.", + }, + "visible": { + "type": "bool", + "default": "True", + "description": "if False, component will be hidden.", + }, + "elem_id": { + "type": "str | None", + "default": "None", + "description": "an optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.", + }, + "elem_classes": { + "type": "list[str] | str | None", + "default": "None", + "description": "an optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.", + }, + "render": { + "type": "bool", + "default": "True", + "description": "if False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.", + }, + "key": { + "type": "int | str | None", + "default": "None", + "description": "if assigned, will be used to assume identity across a re-render. Components that have the same key across a re-render will have their value preserved.", + }, + "mirror_webcam": { + "type": "bool", + "default": "True", + "description": "if True webcam will be mirrored. Default is True.", + }, + }, + "events": {"tick": {"type": None, "default": None, "description": ""}}, + }, + "__meta__": {"additional_interfaces": {}, "user_fn_refs": {"WebRTC": []}}, + } +} + + +abs_path = os.path.join(os.path.dirname(__file__), "css.css") + +with gr.Blocks( + css_paths=abs_path, + theme=gr.themes.Default( + font_mono=[ + gr.themes.GoogleFont("Inconsolata"), + "monospace", + ], + ), +) as demo: + gr.Markdown( + """ +

Gradio WebRTC ⚡️

+ +
+Static Badge +Static Badge +
+""", + elem_classes=["md-custom"], + header_links=True, + ) + gr.Markdown( + """ +## Installation + +```bash +pip install gradio_webrtc +``` + +## Examples: +1. [Object Detection from Webcam with YOLOv10](https://huggingface.co/spaces/freddyaboulton/webrtc-yolov10n) 📷 +2. [Streaming Object Detection from Video with RT-DETR](https://huggingface.co/spaces/freddyaboulton/rt-detr-object-detection-webrtc) 🎥 +3. [Text-to-Speech](https://huggingface.co/spaces/freddyaboulton/parler-tts-streaming-webrtc) 🗣️ + +## Usage + +The WebRTC component supports the following three use cases: +1. Streaming video from the user webcam to the server and back +2. Streaming Video from the server to the client +3. Streaming Audio from the server to the client + +Streaming Audio from client to the server and back (conversational AI) is not supported yet. + + +## Streaming Video from the User Webcam to the Server and Back + +```python +import gradio as gr +from gradio_webrtc import WebRTC + + +def detection(image, conf_threshold=0.3): + ... your detection code here ... + + +with gr.Blocks() as demo: + image = WebRTC(label="Stream", mode="send-receive", modality="video") + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + image.stream( + fn=detection, + inputs=[image, conf_threshold], + outputs=[image], time_limit=10 + ) + +if __name__ == "__main__": + demo.launch() + +``` +* Set the `mode` parameter to `send-receive` and `modality` to "video". +* The `stream` event's `fn` parameter is a function that receives the next frame from the webcam +as a **numpy array** and returns the processed frame also as a **numpy array**. +* Numpy arrays are in (height, width, 3) format where the color channels are in RGB format. +* The `inputs` parameter should be a list where the first element is the WebRTC component. The only output allowed is the WebRTC component. +* The `time_limit` parameter is the maximum time in seconds the video stream will run. If the time limit is reached, the video stream will stop. + +## Streaming Video from the User Webcam to the Server and Back + +```python +import gradio as gr +from gradio_webrtc import WebRTC +import cv2 + +def generation(): + url = "https://download.tsi.telecom-paristech.fr/gpac/dataset/dash/uhd/mux_sources/hevcds_720p30_2M.mp4" + cap = cv2.VideoCapture(url) + iterating = True + while iterating: + iterating, frame = cap.read() + yield frame + +with gr.Blocks() as demo: + output_video = WebRTC(label="Video Stream", mode="receive", modality="video") + button = gr.Button("Start", variant="primary") + output_video.stream( + fn=generation, inputs=None, outputs=[output_video], + trigger=button.click + ) + +if __name__ == "__main__": + demo.launch() +``` + +* Set the "mode" parameter to "receive" and "modality" to "video". +* The `stream` event's `fn` parameter is a generator function that yields the next frame from the video as a **numpy array**. +* The only output allowed is the WebRTC component. +* The `trigger` parameter the gradio event that will trigger the webrtc connection. In this case, the button click event. + +## Streaming Audio from the Server to the Client + +```python +import gradio as gr +from pydub import AudioSegment + +def generation(num_steps): + for _ in range(num_steps): + segment = AudioSegment.from_file("/Users/freddy/sources/gradio/demo/audio_debugger/cantina.wav") + yield (segment.frame_rate, np.array(segment.get_array_of_samples()).reshape(1, -1)) + +with gr.Blocks() as demo: + audio = WebRTC(label="Stream", mode="receive", modality="audio") + num_steps = gr.Slider( + label="Number of Steps", + minimum=1, + maximum=10, + step=1, + value=5, + ) + button = gr.Button("Generate") + + audio.stream( + fn=generation, inputs=[num_steps], outputs=[audio], + trigger=button.click + ) +``` + +* Set the "mode" parameter to "receive" and "modality" to "audio". +* The `stream` event's `fn` parameter is a generator function that yields the next audio segment as a tuple of (frame_rate, audio_samples). +* The numpy array should be of shape (1, num_samples). +* The `outputs` parameter should be a list with the WebRTC component as the only element. + +## Deployment + +When deploying in a cloud environment (like Hugging Face Spaces, EC2, etc), you need to set up a TURN server to relay the WebRTC traffic. +The easiest way to do this is to use a service like Twilio. + +```python +from twilio.rest import Client +import os + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +client = Client(account_sid, auth_token) + +token = client.tokens.create() + +rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", +} + +with gr.Blocks() as demo: + ... + rtc = WebRTC(rtc_configuration=rtc_configuration, ...) + ... +``` +""", + elem_classes=["md-custom"], + header_links=True, + ) + + gr.Markdown( + """ +## +""", + elem_classes=["md-custom"], + header_links=True, + ) + + gr.ParamViewer(value=_docs["WebRTC"]["members"]["__init__"], linkify=[]) + + demo.load( + None, + js=r"""function() { + const refs = {}; + const user_fn_refs = { + WebRTC: [], }; + requestAnimationFrame(() => { + + Object.entries(user_fn_refs).forEach(([key, refs]) => { + if (refs.length > 0) { + const el = document.querySelector(`.${key}-user-fn`); + if (!el) return; + refs.forEach(ref => { + el.innerHTML = el.innerHTML.replace( + new RegExp("\\b"+ref+"\\b", "g"), + `${ref}` + ); + }) + } + }) + + Object.entries(refs).forEach(([key, refs]) => { + if (refs.length > 0) { + const el = document.querySelector(`.${key}`); + if (!el) return; + refs.forEach(ref => { + el.innerHTML = el.innerHTML.replace( + new RegExp("\\b"+ref+"\\b", "g"), + `${ref}` + ); + }) + } + }) + }) +} + +""", + ) + +demo.launch() diff --git a/demo/stream_whisper.py b/demo/stream_whisper.py new file mode 100644 index 0000000..0da2614 --- /dev/null +++ b/demo/stream_whisper.py @@ -0,0 +1,53 @@ +import tempfile + +import gradio as gr +import numpy as np +from gradio_webrtc import AdditionalOutputs, ReplyOnPause, WebRTC +from openai import OpenAI +from pydub import AudioSegment + +from dotenv import load_dotenv + +load_dotenv() + + +client = OpenAI() + + +def transcribe(audio: tuple[int, np.ndarray], transcript: list[dict]): + print("audio", audio) + segment = AudioSegment( + audio[1].tobytes(), + frame_rate=audio[0], + sample_width=audio[1].dtype.itemsize, + channels=1, + ) + + transcript.append({"role": "user", "content": gr.Audio((audio[0], audio[1].squeeze()))}) + + with tempfile.NamedTemporaryFile(suffix=".mp3") as temp_audio: + segment.export(temp_audio.name, format="mp3") + next_chunk = client.audio.transcriptions.create( + model="whisper-1", file=open(temp_audio.name, "rb") + ).text + transcript.append({"role": "assistant", "content": next_chunk}) + yield AdditionalOutputs(transcript) + + +with gr.Blocks() as demo: + with gr.Row(): + with gr.Column(): + audio = WebRTC( + label="Stream", + mode="send", + modality="audio", + ) + with gr.Column(): + transcript = gr.Chatbot(label="transcript", type="messages") + + audio.stream(ReplyOnPause(transcribe), inputs=[audio, transcript], outputs=[audio], + time_limit=30) + audio.on_additional_outputs(lambda s: s, outputs=transcript) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/utils.py b/demo/utils.py new file mode 100644 index 0000000..04dadeb --- /dev/null +++ b/demo/utils.py @@ -0,0 +1,237 @@ +import cv2 +import numpy as np + +class_names = [ + "person", + "bicycle", + "car", + "motorcycle", + "airplane", + "bus", + "train", + "truck", + "boat", + "traffic light", + "fire hydrant", + "stop sign", + "parking meter", + "bench", + "bird", + "cat", + "dog", + "horse", + "sheep", + "cow", + "elephant", + "bear", + "zebra", + "giraffe", + "backpack", + "umbrella", + "handbag", + "tie", + "suitcase", + "frisbee", + "skis", + "snowboard", + "sports ball", + "kite", + "baseball bat", + "baseball glove", + "skateboard", + "surfboard", + "tennis racket", + "bottle", + "wine glass", + "cup", + "fork", + "knife", + "spoon", + "bowl", + "banana", + "apple", + "sandwich", + "orange", + "broccoli", + "carrot", + "hot dog", + "pizza", + "donut", + "cake", + "chair", + "couch", + "potted plant", + "bed", + "dining table", + "toilet", + "tv", + "laptop", + "mouse", + "remote", + "keyboard", + "cell phone", + "microwave", + "oven", + "toaster", + "sink", + "refrigerator", + "book", + "clock", + "vase", + "scissors", + "teddy bear", + "hair drier", + "toothbrush", +] + +# Create a list of colors for each class where each color is a tuple of 3 integer values +rng = np.random.default_rng(3) +colors = rng.uniform(0, 255, size=(len(class_names), 3)) + + +def nms(boxes, scores, iou_threshold): + # Sort by score + sorted_indices = np.argsort(scores)[::-1] + + keep_boxes = [] + while sorted_indices.size > 0: + # Pick the last box + box_id = sorted_indices[0] + keep_boxes.append(box_id) + + # Compute IoU of the picked box with the rest + ious = compute_iou(boxes[box_id, :], boxes[sorted_indices[1:], :]) + + # Remove boxes with IoU over the threshold + keep_indices = np.where(ious < iou_threshold)[0] + + # print(keep_indices.shape, sorted_indices.shape) + sorted_indices = sorted_indices[keep_indices + 1] + + return keep_boxes + + +def multiclass_nms(boxes, scores, class_ids, iou_threshold): + unique_class_ids = np.unique(class_ids) + + keep_boxes = [] + for class_id in unique_class_ids: + class_indices = np.where(class_ids == class_id)[0] + class_boxes = boxes[class_indices, :] + class_scores = scores[class_indices] + + class_keep_boxes = nms(class_boxes, class_scores, iou_threshold) + keep_boxes.extend(class_indices[class_keep_boxes]) + + return keep_boxes + + +def compute_iou(box, boxes): + # Compute xmin, ymin, xmax, ymax for both boxes + xmin = np.maximum(box[0], boxes[:, 0]) + ymin = np.maximum(box[1], boxes[:, 1]) + xmax = np.minimum(box[2], boxes[:, 2]) + ymax = np.minimum(box[3], boxes[:, 3]) + + # Compute intersection area + intersection_area = np.maximum(0, xmax - xmin) * np.maximum(0, ymax - ymin) + + # Compute union area + box_area = (box[2] - box[0]) * (box[3] - box[1]) + boxes_area = (boxes[:, 2] - boxes[:, 0]) * (boxes[:, 3] - boxes[:, 1]) + union_area = box_area + boxes_area - intersection_area + + # Compute IoU + iou = intersection_area / union_area + + return iou + + +def xywh2xyxy(x): + # Convert bounding box (x, y, w, h) to bounding box (x1, y1, x2, y2) + y = np.copy(x) + y[..., 0] = x[..., 0] - x[..., 2] / 2 + y[..., 1] = x[..., 1] - x[..., 3] / 2 + y[..., 2] = x[..., 0] + x[..., 2] / 2 + y[..., 3] = x[..., 1] + x[..., 3] / 2 + return y + + +def draw_detections(image, boxes, scores, class_ids, mask_alpha=0.3): + det_img = image.copy() + + img_height, img_width = image.shape[:2] + font_size = min([img_height, img_width]) * 0.0006 + text_thickness = int(min([img_height, img_width]) * 0.001) + + # det_img = draw_masks(det_img, boxes, class_ids, mask_alpha) + + # Draw bounding boxes and labels of detections + for class_id, box, score in zip(class_ids, boxes, scores): + color = colors[class_id] + + draw_box(det_img, box, color) + + label = class_names[class_id] + caption = f"{label} {int(score * 100)}%" + draw_text(det_img, caption, box, color, font_size, text_thickness) + + return det_img + + +def draw_box( + image: np.ndarray, + box: np.ndarray, + color: tuple[int, int, int] = (0, 0, 255), + thickness: int = 2, +) -> np.ndarray: + x1, y1, x2, y2 = box.astype(int) + return cv2.rectangle(image, (x1, y1), (x2, y2), color, thickness) + + +def draw_text( + image: np.ndarray, + text: str, + box: np.ndarray, + color: tuple[int, int, int] = (0, 0, 255), + font_size: float = 0.001, + text_thickness: int = 2, +) -> np.ndarray: + x1, y1, x2, y2 = box.astype(int) + (tw, th), _ = cv2.getTextSize( + text=text, + fontFace=cv2.FONT_HERSHEY_SIMPLEX, + fontScale=font_size, + thickness=text_thickness, + ) + th = int(th * 1.2) + + cv2.rectangle(image, (x1, y1), (x1 + tw, y1 - th), color, -1) + + return cv2.putText( + image, + text, + (x1, y1), + cv2.FONT_HERSHEY_SIMPLEX, + font_size, + (255, 255, 255), + text_thickness, + cv2.LINE_AA, + ) + + +def draw_masks( + image: np.ndarray, boxes: np.ndarray, classes: np.ndarray, mask_alpha: float = 0.3 +) -> np.ndarray: + mask_img = image.copy() + + # Draw bounding boxes and labels of detections + for box, class_id in zip(boxes, classes): + color = colors[class_id] + + x1, y1, x2, y2 = box.astype(int) + + # Draw fill rectangle in mask image + cv2.rectangle(mask_img, (x1, y1), (x2, y2), color, -1) + + return cv2.addWeighted(mask_img, mask_alpha, image, 1 - mask_alpha, 0) diff --git a/demo/video_out.py b/demo/video_out.py new file mode 100644 index 0000000..e244b51 --- /dev/null +++ b/demo/video_out.py @@ -0,0 +1,65 @@ +import os + +import cv2 +import gradio as gr +from gradio_webrtc import WebRTC +from twilio.rest import Client + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +if account_sid and auth_token: + client = Client(account_sid, auth_token) + + token = client.tokens.create() + + rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } +else: + rtc_configuration = None + + +def generation(input_video): + cap = cv2.VideoCapture(input_video) + + iterating = True + + while iterating: + iterating, frame = cap.read() + + # flip frame vertically + frame = cv2.flip(frame, 0) + display_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + yield display_frame + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Video Streaming (Powered by WebRTC ⚡️) +

+ """ + ) + with gr.Row(): + with gr.Column(): + input_video = gr.Video(sources="upload") + with gr.Column(): + output_video = WebRTC( + label="Video Stream", + rtc_configuration=rtc_configuration, + mode="receive", + modality="video", + ) + output_video.stream( + fn=generation, + inputs=[input_video], + outputs=[output_video], + trigger=input_video.upload, + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/video_out_stream.py b/demo/video_out_stream.py new file mode 100644 index 0000000..a5689ca --- /dev/null +++ b/demo/video_out_stream.py @@ -0,0 +1,54 @@ +import os + +import cv2 +import gradio as gr +from gradio_webrtc import WebRTC +from twilio.rest import Client + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +if account_sid and auth_token: + client = Client(account_sid, auth_token) + + token = client.tokens.create() + + rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } +else: + rtc_configuration = None + + +def generation(): + url = "https://download.tsi.telecom-paristech.fr/gpac/dataset/dash/uhd/mux_sources/hevcds_720p30_2M.mp4" + cap = cv2.VideoCapture(url) + iterating = True + while iterating: + iterating, frame = cap.read() + yield frame + + +with gr.Blocks() as demo: + gr.HTML( + """ +

+ Video Streaming (Powered by WebRTC ⚡️) +

+ """ + ) + output_video = WebRTC( + label="Video Stream", + rtc_configuration=rtc_configuration, + mode="receive", + modality="video", + ) + button = gr.Button("Start", variant="primary") + output_video.stream( + fn=generation, inputs=None, outputs=[output_video], trigger=button.click + ) + + +if __name__ == "__main__": + demo.launch() diff --git a/demo/video_send_output.py b/demo/video_send_output.py new file mode 100644 index 0000000..3ac5c9b --- /dev/null +++ b/demo/video_send_output.py @@ -0,0 +1,100 @@ +import logging +import os +import random + +import cv2 +import gradio as gr +from gradio_webrtc import AdditionalOutputs, WebRTC +from huggingface_hub import hf_hub_download +from inference import YOLOv10 +from twilio.rest import Client + +# Configure the root logger to WARNING to suppress debug messages from other libraries +logging.basicConfig(level=logging.WARNING) + +# Create a console handler +console_handler = logging.FileHandler("gradio_webrtc.log") +console_handler.setLevel(logging.DEBUG) + +# Create a formatter +formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") +console_handler.setFormatter(formatter) + +# Configure the logger for your specific library +logger = logging.getLogger("gradio_webrtc") +logger.setLevel(logging.DEBUG) +logger.addHandler(console_handler) + + +model_file = hf_hub_download( + repo_id="onnx-community/yolov10n", filename="onnx/model.onnx" +) + +model = YOLOv10(model_file) + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +if account_sid and auth_token: + client = Client(account_sid, auth_token) + + token = client.tokens.create() + + rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", + } +else: + rtc_configuration = None + + +def detection(frame, conf_threshold=0.3): + print("frame.shape", frame.shape) + frame = cv2.flip(frame, 0) + return AdditionalOutputs(1) + + +css = """.my-group {max-width: 600px !important; max-height: 600 !important;} + .my-column {display: flex !important; justify-content: center !important; align-items: center !important};""" + +with gr.Blocks(css=css) as demo: + gr.HTML( + """ +

+ YOLOv10 Webcam Stream (Powered by WebRTC ⚡️) +

+ """ + ) + gr.HTML( + """ +

+ arXiv | github +

+ """ + ) + with gr.Column(elem_classes=["my-column"]): + with gr.Group(elem_classes=["my-group"]): + image = WebRTC( + label="Stream", rtc_configuration=rtc_configuration, + mode="send", + track_constraints={"width": {"exact": 800}, + "height": {"exact": 600}, + "aspectRatio": {"exact": 1.33333} + }, + rtp_params={"degradationPreference": "maintain-resolution"} + ) + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + number = gr.Number() + + image.stream( + fn=detection, inputs=[image, conf_threshold], outputs=[image], time_limit=10 + ) + image.on_additional_outputs(lambda n: n, outputs=number) + +demo.launch() diff --git a/docs/advanced-configuration.md b/docs/advanced-configuration.md new file mode 100644 index 0000000..4dae09b --- /dev/null +++ b/docs/advanced-configuration.md @@ -0,0 +1,160 @@ +## Track Constraints + +You can specify the `track_constraints` parameter to control how the data is streamed to the server. The full documentation on track constraints is [here](https://developer.mozilla.org/en-US/docs/Web/API/MediaTrackConstraints#constraints). + +For example, you can control the size of the frames captured from the webcam like so: + +```python +track_constraints = { + "width": {"exact": 500}, + "height": {"exact": 500}, + "frameRate": {"ideal": 30}, +} +webrtc = WebRTC(track_constraints=track_constraints, + modality="video", + mode="send-receive") +``` + + +!!! warning + + WebRTC may not enforce your constaints. For example, it may rescale your video + (while keeping the same resolution) in order to maintain the desired (or reach a better) frame rate. If you + really want to enforce height, width and resolution constraints, use the `rtp_params` parameter as set `"degradationPreference": "maintain-resolution"`. + + ```python + image = WebRTC( + label="Stream", + mode="send", + track_constraints=track_constraints, + rtp_params={"degradationPreference": "maintain-resolution"} + ) + ``` + +## The RTC Configuration + +You can configure how the connection is created on the client by passing an `rtc_configuration` parameter to the `WebRTC` component constructor. +See the list of available arguments [here](https://developer.mozilla.org/en-US/docs/Web/API/RTCPeerConnection/RTCPeerConnection#configuration). + +When deploying on a remote server, an `rtc_configuration` parameter must be passed in. See [Deployment](/deployment). + +## Reply on Pause Voice-Activity-Detection + +The `ReplyOnPause` class runs a Voice Activity Detection (VAD) algorithm to determine when a user has stopped speaking. + +1. First, the algorithm determines when the user has started speaking. +2. Then it groups the audio into chunks. +3. On each chunk, we determine the length of human speech in the chunk. +4. If the length of human speech is below a threshold, a pause is detected. + +The following parameters control this argument: + +```python +from gradio_webrtc import AlgoOptions, ReplyOnPause, WebRTC + +options = AlgoOptions(audio_chunk_duration=0.6, # (1) + started_talking_threshold=0.2, # (2) + speech_threshold=0.1, # (3) + ) + +with gr.Blocks as demo: + audio = WebRTC(...) + audio.stream(ReplyOnPause(..., algo_options=algo_options) + ) + +demo.launch() +``` + +1. This is the length (in seconds) of audio chunks. +2. If the chunk has more than 0.2 seconds of speech, the user started talking. +3. If, after the user started speaking, there is a chunk with less than 0.1 seconds of speech, the user stopped speaking. + + +## Stream Handler Input Audio + +You can configure the sampling rate of the audio passed to the `ReplyOnPause` or `StreamHandler` instance with the `input_sampling_rate` parameter. The current default is `48000` + +```python +from gradio_webrtc import ReplyOnPause, WebRTC + +with gr.Blocks as demo: + audio = WebRTC(...) + audio.stream(ReplyOnPause(..., input_sampling_rate=24000) + ) + +demo.launch() +``` + + +## Stream Handler Output Audio + +You can configure the output audio chunk size of `ReplyOnPause` (and any `StreamHandler`) +with the `output_sample_rate` and `output_frame_size` parameters. + +The following code (which uses the default values of these parameters), states that each output chunk will be a frame of 960 samples at a frame rate of `24,000` hz. So it will correspond to `0.04` seconds. + +```python +from gradio_webrtc import ReplyOnPause, WebRTC + +with gr.Blocks as demo: + audio = WebRTC(...) + audio.stream(ReplyOnPause(..., output_sample_rate=24000, output_frame_size=960) + ) + +demo.launch() +``` + +!!! tip + + In general it is best to leave these settings untouched. In some cases, + lowering the output_frame_size can yield smoother audio playback. + + +## Audio Icon + +You can display an icon of your choice instead of the default wave animation for audio streaming. +Pass any local path or url to an image (svg, png, jpeg) to the components `icon` parameter. This will display the icon as a circular button. When audio is sent or recevied (depending on the `mode` parameter) a pulse animation will emanate from the button. + +You can control the button color and pulse color with `icon_button_color` and `pulse_color` parameters. They can take any valid css color. + +=== "Code" + ``` python + audio = WebRTC( + label="Stream", + rtc_configuration=rtc_configuration, + mode="receive", + modality="audio", + icon="phone-solid.svg", + ) + ``` + +=== "Code Custom colors" + ``` python + audio = WebRTC( + label="Stream", + rtc_configuration=rtc_configuration, + mode="receive", + modality="audio", + icon="phone-solid.svg", + icon_button_color="black", + pulse_color="black", + ) + ``` + + + +## Changing the Button Text + +You can supply a `button_labels` dictionary to change the text displayed in the `Start`, `Stop` and `Waiting` buttons that are displayed in the UI. +The keys must be `"start"`, `"stop"`, and `"waiting"`. + +``` python +webrtc = WebRTC( + label="Video Chat", + modality="audio-video", + mode="send-receive", + button_labels={"start": "Start Talking to Gemini"} +) +``` + + diff --git a/docs/bolt.svg b/docs/bolt.svg new file mode 100644 index 0000000..f3a0046 --- /dev/null +++ b/docs/bolt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/cookbook.md b/docs/cookbook.md new file mode 100644 index 0000000..0cb7910 --- /dev/null +++ b/docs/cookbook.md @@ -0,0 +1,172 @@ +
+ +- :speaking_head:{ .lg .middle }:eyes:{ .lg .middle } __Gemini Audio Video Chat__ + + --- + + Stream BOTH your webcam video and audio feeds to Google Gemini. You can also upload images to augment your conversation! + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/gemini-audio-video-chat) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/gemini-audio-video-chat/blob/main/app.py) + +- :speaking_head:{ .lg .middle } __Google Gemini Real Time Voice API__ + + --- + + Talk to Gemini in real time using Google's voice API. + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/gemini-voice) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/gemini-voice/blob/main/app.py) + +- :speaking_head:{ .lg .middle } __OpenAI Real Time Voice API__ + + --- + + Talk to ChatGPT in real time using OpenAI's voice API. + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/openai-realtime-voice) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/openai-realtime-voice/blob/main/app.py) + +- :speaking_head:{ .lg .middle } __Hello Llama: Stop Word Detection__ + + --- + + A code editor built with Llama 3.3 70b that is triggered by the phrase "Hello Llama". + Build a Siri-like coding assistant in 100 lines of code! + + + + [:octicons-arrow-right-24: Demo](hhttps://huggingface.co/spaces/freddyaboulton/hey-llama-code-editor) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/hey-llama-code-editor/blob/main/app.py) + +- :robot:{ .lg .middle } __Llama Code Editor__ + + --- + + Create and edit HTML pages with just your voice! Powered by SambaNova systems. + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/llama-code-editor) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/llama-code-editor/blob/main/app.py) + +- :speaking_head:{ .lg .middle } __Audio Input/Output with mini-omni2__ + + --- + + Build a GPT-4o like experience with mini-omni2, an audio-native LLM. + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/mini-omni2-webrtc) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/mini-omni2-webrtc/blob/main/app.py) + +- :speaking_head:{ .lg .middle } __Talk to Claude__ + + --- + + Use the Anthropic and Play.Ht APIs to have an audio conversation with Claude. + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/talk-to-claude) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/talk-to-claude/blob/main/app.py) + +- :speaking_head:{ .lg .middle } __Kyutai Moshi__ + + --- + + Kyutai's moshi is a novel speech-to-speech model for modeling human conversations. + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/talk-to-moshi) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/talk-to-moshi/blob/main/app.py) + +- :speaking_head:{ .lg .middle } __Talk to Ultravox__ + + --- + + Talk to Fixie.AI's audio-native Ultravox LLM with the transformers library. + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/talk-to-ultravox) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/talk-to-ultravox/blob/main/app.py) + + +- :speaking_head:{ .lg .middle } __Talk to Llama 3.2 3b__ + + --- + + Use the Lepton API to make Llama 3.2 talk back to you! + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/llama-3.2-3b-voice-webrtc) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/llama-3.2-3b-voice-webrtc/blob/main/app.py) + + +- :robot:{ .lg .middle } __Talk to Qwen2-Audio__ + + --- + + Qwen2-Audio is a SOTA audio-to-text LLM developed by Alibaba. + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/talk-to-qwen-webrtc) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/talk-to-qwen-webrtc/blob/main/app.py) + + +- :camera:{ .lg .middle } __Yolov10 Object Detection__ + + --- + + Run the Yolov10 model on a user webcam stream in real time! + + + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/webrtc-yolov10n) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/webrtc-yolov10n/blob/main/app.py) + +- :camera:{ .lg .middle } __Video Object Detection with RT-DETR__ + + --- + + Upload a video and stream out frames with detected objects (powered by RT-DETR) model. + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/rt-detr-object-detection-webrtc) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/rt-detr-object-detection-webrtc/blob/main/app.py) + +- :speaker:{ .lg .middle } __Text-to-Speech with Parler__ + + --- + + Stream out audio generated by Parler TTS! + + [:octicons-arrow-right-24: Demo](https://huggingface.co/spaces/freddyaboulton/parler-tts-streaming-webrtc) + + [:octicons-code-16: Code](https://huggingface.co/spaces/freddyaboulton/parler-tts-streaming-webrtc/blob/main/app.py) + + +
\ No newline at end of file diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..2ab85b9 --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,165 @@ +When deploying in a cloud environment (like Hugging Face Spaces, EC2, etc), you need to set up a TURN server to relay the WebRTC traffic. + +## Community Server + +Hugging Face graciously provides a TURN server for the community. +In order to use it, you need to first create a Hugging Face account by going to the [huggingface.co](https://huggingface.co/). + +Then navigate to this [space](https://huggingface.co/spaces/freddyaboulton/turn-server-login) and follow the instructions on the page. You just have to click the "Log in" button and then the "Sign Up" button. + +![turn_login](https://github.com/user-attachments/assets/d077c3a3-7059-45d6-8e50-eb3d8a4aa43f) + +Then you can use the `get_hf_turn_credentials` helper to get your credentials: + +```python +from gradio_webrtc import get_hf_turn_credentials, WebRTC + +# Pass a valid access token for your Hugging Face account +# or set the HF_TOKEN environment variable +credentials = get_hf_turn_credentials(token=None) + +with gr.Blcocks() as demo: + webrtc = WebRTC(rtc_configuration=credentials) + ... + +demo.launch() +``` + +!!! warning + + This is a shared resource so we make no latency/availability guarantees. + For more robust options, see the Twilio and self-hosting options below. + + +## Twilio API + +The easiest way to do this is to use a service like Twilio. + +Create a **free** [account](https://login.twilio.com/u/signup) and the install the `twilio` package with pip (`pip install twilio`). You can then connect from the WebRTC component like so: + +```python +from twilio.rest import Client +import os + +account_sid = os.environ.get("TWILIO_ACCOUNT_SID") +auth_token = os.environ.get("TWILIO_AUTH_TOKEN") + +client = Client(account_sid, auth_token) + +token = client.tokens.create() + +rtc_configuration = { + "iceServers": token.ice_servers, + "iceTransportPolicy": "relay", +} + +with gr.Blocks() as demo: + ... + rtc = WebRTC(rtc_configuration=rtc_configuration, ...) + ... +``` + +!!! tip "Automatic Login" + + You can log in automatically with the `get_twilio_turn_credentials` helper + + ```python + from gradio_webrtc import get_twilio_turn_credentials + + # Will automatically read the TWILIO_ACCOUNT_SID and TWILIO_AUTH_TOKEN + # env variables but you can also pass in the tokens as parameters + rtc_configuration = get_twilio_turn_credentials() + ``` + +## Self Hosting + +We have developed a script that can automatically deploy a TURN server to Amazon Web Services (AWS). You can follow the instructions [here](https://github.com/freddyaboulton/turn-server-deploy) or this guide. + +### Prerequisites + +Clone the following [repository](https://github.com/freddyaboulton/turn-server-deploy) and install the `aws` cli if you have not done so already (`pip install awscli`). + +Log into your AWS account and create an IAM user with the following permissions: + +- [AWSCloudFormationFullAccess](https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/policies/details/arn%3Aaws%3Aiam%3A%3Aaws%3Apolicy%2FAWSCloudFormationFullAccess) +- [AmazonEC2FullAccess](https://us-east-1.console.aws.amazon.com/iam/home?region=us-east-1#/policies/details/arn%3Aaws%3Aiam%3A%3Aaws%3Apolicy%2FAmazonEC2FullAccess) + + +Create a key pair for this user and write down the "access key" and "secret access key". Then log into the aws cli with these credentials (`aws configure`). + +Finally, create an ec2 keypair (replace `your-key-name` with the name you want to give it). + +``` +aws ec2 create-key-pair --key-name your-key-name --query 'KeyMaterial' --output text > your-key-name.pem +``` + +### Running the script + +Open the `parameters.json` file and fill in the correct values for all the parameters: + +- `KeyName`: The key file we just created, e.g. `your-key-name` (omit `.pem`). +- `TurnUserName`: The username needed to connect to the server. +- `TurnPassword`: The password needed to connect to the server. +- `InstanceType`: One of the following values `t3.micro`, `t3.small`, `t3.medium`, `c4.large`, `c5.large`. + + +Then run the deployment script: + +```bash +aws cloudformation create-stack \ + --stack-name turn-server \ + --template-body file://deployment.yml \ + --parameters file://parameters.json \ + --capabilities CAPABILITY_IAM +``` + +You can then wait for the stack to come up with: + +```bash +aws cloudformation wait stack-create-complete \ + --stack-name turn-server +``` + +Next, grab your EC2 server's public ip with: + +``` +aws cloudformation describe-stacks \ + --stack-name turn-server \ + --query 'Stacks[0].Outputs' > server-info.json +``` + +The `server-info.json` file will have the server's public IP and public DNS: + +```json +[ + { + "OutputKey": "PublicIP", + "OutputValue": "35.173.254.80", + "Description": "Public IP address of the TURN server" + }, + { + "OutputKey": "PublicDNS", + "OutputValue": "ec2-35-173-254-80.compute-1.amazonaws.com", + "Description": "Public DNS name of the TURN server" + } +] +``` + +Finally, you can connect to your EC2 server from the gradio WebRTC component via the `rtc_configuration` argument: + +```python +import gradio as gr +from gradio_webrtc import WebRTC +rtc_configuration = { + "iceServers": [ + { + "urls": "turn:35.173.254.80:80", + "username": "", + "credential": "" + }, + ] +} + +with gr.Blocks() as demo: + webrtc = WebRTC(rtc_configuration=rtc_configuration) +``` \ No newline at end of file diff --git a/docs/faq.md b/docs/faq.md new file mode 100644 index 0000000..f3a69f6 --- /dev/null +++ b/docs/faq.md @@ -0,0 +1,67 @@ +## Demo does not work when deploying to the cloud + +Make sure you are using a TURN server. See [deployment](/deployment). + +## Recorded input audio sounds muffled during output audio playback + +By default, the microphone is [configured](https://github.com/freddyaboulton/gradio-webrtc/blob/903f1f70bd586f638ad3b5a3940c7a8ec70ad1f5/backend/gradio_webrtc/webrtc.py#L575) to do echoCancellation. +This is what's causing the recorded audio to sound muffled when the streamed audio starts playing. +You can disable this via the `track_constraints` (see [advanced configuration](./advanced-configuration])) with the following code: + +```python + audio = WebRTC( + label="Stream", + track_constraints={ + "echoCancellation": False, + "noiseSuppression": {"exact": True}, + "autoGainControl": {"exact": True}, + "sampleRate": {"ideal": 24000}, + "sampleSize": {"ideal": 16}, + "channelCount": {"exact": 1}, + }, + rtc_configuration=None, + mode="send-receive", + modality="audio", + ) +``` + +## How to raise errors in the UI + +You can raise `WebRTCError` in order for an error message to show up in the user's screen. This is similar to how `gr.Error` works. + +Here is a simple example: + +```python +def generation(num_steps): + for _ in range(num_steps): + segment = AudioSegment.from_file( + "/Users/freddy/sources/gradio/demo/audio_debugger/cantina.wav" + ) + yield ( + segment.frame_rate, + np.array(segment.get_array_of_samples()).reshape(1, -1), + ) + time.sleep(3.5) + raise WebRTCError("This is a test error") + +with gr.Blocks() as demo: + audio = WebRTC( + label="Stream", + mode="receive", + modality="audio", + ) + num_steps = gr.Slider( + label="Number of Steps", + minimum=1, + maximum=10, + step=1, + value=5, + ) + button = gr.Button("Generate") + + audio.stream( + fn=generation, inputs=[num_steps], outputs=[audio], trigger=button.click + ) + +demo.launch() +``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..92923e7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,30 @@ +

Gradio WebRTC ⚡️

+ +
+Static Badge +Static Badge +
+ +

+Stream video and audio in real time with Gradio using WebRTC. +

+ +## Installation + +```bash +pip install gradio_webrtc +``` + +to use built-in pause detection (see [ReplyOnPause](/user-guide/#reply-on-pause)), install the `vad` extra: + +```bash +pip install gradio_webrtc[vad] +``` + +For stop word detection (see [ReplyOnStopWords](/user-guide/#reply-on-stopwords)), install the `stopword` extra: +```bash +pip install gradio_webrtc[stopword] +``` + +## Examples +See the [cookbook](/cookbook) \ No newline at end of file diff --git a/docs/user-guide.md b/docs/user-guide.md new file mode 100644 index 0000000..e596c2e --- /dev/null +++ b/docs/user-guide.md @@ -0,0 +1,505 @@ +# User Guide + +To get started with WebRTC streams, all that's needed is to import the `WebRTC` component from this package and implement its `stream` event. + +This page will show how to do so with simple code examples. +For complete implementations of common tasks, see the [cookbook](/cookbook). + +## Audio Streaming + +### Reply on Pause + +Typically, you want to run an AI model that generates audio when the user has stopped speaking. This can be done by wrapping a python generator with the `ReplyOnPause` class +and passing it to the `stream` event of the `WebRTC` component. + +=== "Code" + ``` py title="ReplyonPause" + import gradio as gr + from gradio_webrtc import WebRTC, ReplyOnPause + + def response(audio: tuple[int, np.ndarray]): # (1) + """This function must yield audio frames""" + ... + for numpy_array in generated_audio: + yield (sampling_rate, numpy_array, "mono") # (2) + + + with gr.Blocks() as demo: + gr.HTML( + """ +

+ Chat (Powered by WebRTC ⚡️) +

+ """ + ) + with gr.Column(): + with gr.Group(): + audio = WebRTC( + mode="send-receive", # (3) + modality="audio", + ) + audio.stream(fn=ReplyOnPause(response), + inputs=[audio], outputs=[audio], # (4) + time_limit=60) # (5) + + demo.launch() + ``` + + 1. The python generator will receive the **entire** audio up until the user stopped. It will be a tuple of the form (sampling_rate, numpy array of audio). The array will have a shape of (1, num_samples). You can also pass in additional input components. + + 2. The generator must yield audio chunks as a tuple of (sampling_rate, numpy audio array). Each numpy audio array must have a shape of (1, num_samples). + + 3. The `mode` and `modality` arguments must be set to `"send-receive"` and `"audio"`. + + 4. The `WebRTC` component must be the first input and output component. + + 5. Set a `time_limit` to control how long a conversation will last. If the `concurrency_count` is 1 (default), only one conversation will be handled at a time. +=== "Notes" + 1. The python generator will receive the **entire** audio up until the user stopped. It will be a tuple of the form (sampling_rate, numpy array of audio). The array will have a shape of (1, num_samples). You can also pass in additional input components. + + 2. The generator must yield audio chunks as a tuple of (sampling_rate, numpy audio arrays). Each numpy audio array must have a shape of (1, num_samples). + + 3. The `mode` and `modality` arguments must be set to `"send-receive"` and `"audio"`. + + 4. The `WebRTC` component must be the first input and output component. + + 5. Set a `time_limit` to control how long a conversation will last. If the `concurrency_count` is 1 (default), only one conversation will be handled at a time. + + +### Reply On Stopwords + +You can configure your AI model to run whenever a set of "stop words" are detected, like "Hey Siri" or "computer", with the `ReplyOnStopWords` class. + +The API is similar to `ReplyOnPause` with the addition of a `stop_words` parameter. + +=== "Code" + ``` py title="ReplyonPause" + import gradio as gr + from gradio_webrtc import WebRTC, ReplyOnPause + + def response(audio: tuple[int, np.ndarray]): + """This function must yield audio frames""" + ... + for numpy_array in generated_audio: + yield (sampling_rate, numpy_array, "mono") + + + with gr.Blocks() as demo: + gr.HTML( + """ +

+ Chat (Powered by WebRTC ⚡️) +

+ """ + ) + with gr.Column(): + with gr.Group(): + audio = WebRTC( + mode="send", + modality="audio", + ) + webrtc.stream(ReplyOnStopWords(generate, + input_sample_rate=16000, + stop_words=["computer"]), # (1) + inputs=[webrtc, history, code], + outputs=[webrtc], time_limit=90, + concurrency_limit=10) + + demo.launch() + ``` + + 1. The `stop_words` can be single words or pairs of words. Be sure to include common misspellings of your word for more robust detection, e.g. "llama", "lamma". In my experience, it's best to use two very distinct words like "ok computer" or "hello iris". + +=== "Notes" + 1. The `stop_words` can be single words or pairs of words. Be sure to include common misspellings of your word for more robust detection, e.g. "llama", "lamma". In my experience, it's best to use two very distinct words like "ok computer" or "hello iris". + +### Stream Handler + +`ReplyOnPause` is an implementation of a `StreamHandler`. The `StreamHandler` is a low-level +abstraction that gives you arbitrary control over how the input audio stream and output audio stream are created. The following example echos back the user audio. + +=== "Code" + ``` py title="Stream Handler" + import gradio as gr + from gradio_webrtc import WebRTC, StreamHandler + from queue import Queue + + class EchoHandler(StreamHandler): + def __init__(self) -> None: + super().__init__() + self.queue = Queue() + + def receive(self, frame: tuple[int, np.ndarray]) -> None: # (1) + self.queue.put(frame) + + def emit(self) -> None: # (2) + return self.queue.get() + + def copy(self) -> StreamHandler: + return EchoHandler() + + + with gr.Blocks() as demo: + with gr.Column(): + with gr.Group(): + audio = WebRTC( + mode="send-receive", + modality="audio", + ) + + audio.stream(fn=EchoHandler(), + inputs=[audio], outputs=[audio], + time_limit=15) + + demo.launch() + ``` + + 1. The `StreamHandler` class implements three methods: `receive`, `emit` and `copy`. The `receive` method is called when a new frame is received from the client, and the `emit` method returns the next frame to send to the client. The `copy` method is called at the beginning of the stream to ensure each user has a unique stream handler. + 2. The `emit` method SHOULD NOT block. If a frame is not ready to be sent, the method should return `None`. + +=== "Notes" + 1. The `StreamHandler` class implements three methods: `receive`, `emit` and `copy`. The `receive` method is called when a new frame is received from the client, and the `emit` method returns the next frame to send to the client. The `copy` method is called at the beginning of the stream to ensure each user has a unique stream handler. + 2. The `emit` method SHOULD NOT block. If a frame is not ready to be sent, the method should return `None`. + + +### Async Stream Handlers + +It is also possible to create asynchronous stream handlers. This is very convenient for accessing async APIs from major LLM developers, like Google and OpenAI. The main difference is that `receive` and `emit` are now defined with `async def`. + +Here is a complete example of using `AsyncStreamHandler` for using the Google Gemini real time API: + +=== "Code" + ``` py title="AsyncStreamHandler" + + import asyncio + import base64 + import logging + import os + + import gradio as gr + import numpy as np + from google import genai + from gradio_webrtc import ( + AsyncStreamHandler, + WebRTC, + async_aggregate_bytes_to_16bit, + get_twilio_turn_credentials, + ) + + class GeminiHandler(AsyncStreamHandler): + def __init__( + self, expected_layout="mono", output_sample_rate=24000, output_frame_size=480 + ) -> None: + super().__init__( + expected_layout, + output_sample_rate, + output_frame_size, + input_sample_rate=16000, + ) + self.client: genai.Client | None = None + self.input_queue = asyncio.Queue() + self.output_queue = asyncio.Queue() + self.quit = asyncio.Event() + + def copy(self) -> "GeminiHandler": + return GeminiHandler( + expected_layout=self.expected_layout, + output_sample_rate=self.output_sample_rate, + output_frame_size=self.output_frame_size, + ) + + async def stream(self): + while not self.quit.is_set(): + audio = await self.input_queue.get() + yield audio + + async def connect(self, api_key: str): + client = genai.Client(api_key=api_key, http_options={"api_version": "v1alpha"}) + config = {"response_modalities": ["AUDIO"]} + async with client.aio.live.connect( + model="gemini-2.0-flash-exp", config=config + ) as session: + async for audio in session.start_stream( + stream=self.stream(), mime_type="audio/pcm" + ): + if audio.data: + yield audio.data + + async def receive(self, frame: tuple[int, np.ndarray]) -> None: + _, array = frame + array = array.squeeze() + audio_message = base64.b64encode(array.tobytes()).decode("UTF-8") + self.input_queue.put_nowait(audio_message) + + async def generator(self): + async for audio_response in async_aggregate_bytes_to_16bit( + self.connect(api_key=self.latest_args[1]) + ): + self.output_queue.put_nowait(audio_response) + + async def emit(self): + if not self.args_set.is_set(): + await self.wait_for_args() + asyncio.create_task(self.generator()) + + array = await self.output_queue.get() + return (self.output_sample_rate, array) + + def shutdown(self) -> None: + self.quit.set() + + with gr.Blocks() as demo: + gr.HTML( + """ +
+

Gen AI SDK Voice Chat

+

Speak with Gemini using real-time audio streaming

+

Get an API Key here

+
+ """ + ) + with gr.Row() as api_key_row: + api_key = gr.Textbox( + label="API Key", + placeholder="Enter your API Key", + value=os.getenv("GOOGLE_API_KEY", ""), + type="password", + ) + with gr.Row(visible=False) as row: + webrtc = WebRTC( + label="Audio", + modality="audio", + mode="send-receive", + rtc_configuration=get_twilio_turn_credentials(), + pulse_color="rgb(35, 157, 225)", + icon_button_color="rgb(35, 157, 225)", + icon="https://www.gstatic.com/lamda/images/gemini_favicon_f069958c85030456e93de685481c559f160ea06b.png", + ) + + webrtc.stream( + GeminiHandler(), + inputs=[webrtc, api_key], + outputs=[webrtc], + time_limit=90, + concurrency_limit=2, + ) + api_key.submit( + lambda: (gr.update(visible=False), gr.update(visible=True)), + None, + [api_key_row, row], + ) + + demo.launch() + ``` + +### Accessing Other Component Values from a StreamHandler + +In the gemini demo above, you'll notice that we have the user input their google API key. This is stored in a `gr.Textbox` parameter. +We can access the value of this component via the `latest_args` prop of the `StreamHandler`. The `latest_args` is a list storing the values of each component in the WebRTC `stream` event `inputs` parameter. The value of the `WebRTC` component is the 0th index and it's always the dummy string `__webrtc_value__`. + +In order to fetch the latest value from the user however, we `await self.wait_for_args()`. In a synchronous `StreamHandler`, we would call `self.wait_for_args_sync()`. + + +### Server-To-Client Only + +To stream only from the server to the client, implement a python generator and pass it to the component's `stream` event. The stream event must also specify a `trigger` corresponding to a UI interaction that starts the stream. In this case, it's a button click. + +=== "Code" + + ``` py title="Server-To-CLient" + import gradio as gr + from gradio_webrtc import WebRTC + from pydub import AudioSegment + + def generation(num_steps): + for _ in range(num_steps): + segment = AudioSegment.from_file("audio_file.wav") + array = np.array(segment.get_array_of_samples()).reshape(1, -1) + yield (segment.frame_rate, array) + + with gr.Blocks() as demo: + audio = WebRTC(label="Stream", mode="receive", # (1) + modality="audio") + num_steps = gr.Slider(label="Number of Steps", minimum=1, + maximum=10, step=1, value=5) + button = gr.Button("Generate") + + audio.stream( + fn=generation, inputs=[num_steps], outputs=[audio], + trigger=button.click # (2) + ) + ``` + + 1. Set `mode="receive"` to only receive audio from the server. + 2. The `stream` event must take a `trigger` that corresponds to the gradio event that starts the stream. In this case, it's the button click. +=== "Notes" + 1. Set `mode="receive"` to only receive audio from the server. + 2. The `stream` event must take a `trigger` that corresponds to the gradio event that starts the stream. In this case, it's the button click. + +## Video Streaming + +### Input/Output Streaming +Set up a video Input/Output stream to continuosly receive webcam frames from the user and run an arbitrary python function to return a modified frame. + +=== "Code" + + ``` py title="Input/Output Streaming" + import gradio as gr + from gradio_webrtc import WebRTC + + + def detection(image, conf_threshold=0.3): # (1) + ... your detection code here ... + return modified_frame # (2) + + + with gr.Blocks() as demo: + image = WebRTC(label="Stream", mode="send-receive", modality="video") # (3) + conf_threshold = gr.Slider( + label="Confidence Threshold", + minimum=0.0, + maximum=1.0, + step=0.05, + value=0.30, + ) + image.stream( + fn=detection, + inputs=[image, conf_threshold], # (4) + outputs=[image], time_limit=10 + ) + + if __name__ == "__main__": + demo.launch() + ``` + + 1. The webcam frame will be represented as a numpy array of shape (height, width, RGB). + 2. The function must return a numpy array. It can take arbitrary values from other components. + 3. Set the `modality="video"` and `mode="send-receive"` + 4. The `inputs` parameter should be a list where the first element is the WebRTC component. The only output allowed is the WebRTC component. +=== "Notes" + 1. The webcam frame will be represented as a numpy array of shape (height, width, RGB). + 2. The function must return a numpy array. It can take arbitrary values from other components. + 3. Set the `modality="video"` and `mode="send-receive"` + 4. The `inputs` parameter should be a list where the first element is the WebRTC component. The only output allowed is the WebRTC component. + +### Server-to-Client Only + +Set up a server-to-client stream to stream video from an arbitrary user interaction. + +=== "Code" + ``` py title="Server-To-Client" + import gradio as gr + from gradio_webrtc import WebRTC + import cv2 + + def generation(): + url = "https://download.tsi.telecom-paristech.fr/gpac/dataset/dash/uhd/mux_sources/hevcds_720p30_2M.mp4" + cap = cv2.VideoCapture(url) + iterating = True + while iterating: + iterating, frame = cap.read() + yield frame # (1) + + with gr.Blocks() as demo: + output_video = WebRTC(label="Video Stream", mode="receive", # (2) + modality="video") + button = gr.Button("Start", variant="primary") + output_video.stream( + fn=generation, inputs=None, outputs=[output_video], + trigger=button.click # (3) + ) + demo.launch() + ``` + + 1. The `stream` event's `fn` parameter is a generator function that yields the next frame from the video as a **numpy array**. + 2. Set `mode="receive"` to only receive audio from the server. + 3. The `trigger` parameter the gradio event that will trigger the stream. In this case, the button click event. +=== "Notes" + 1. The `stream` event's `fn` parameter is a generator function that yields the next frame from the video as a **numpy array**. + 2. Set `mode="receive"` to only receive audio from the server. + 3. The `trigger` parameter the gradio event that will trigger the stream. In this case, the button click event. + +## Audio-Video Streaming + +You can simultaneously stream audio and video simultaneously to/from a server using `AudioVideoStreamHandler` or `AsyncAudioVideoStreamHandler`. +They are identical to the audio `StreamHandlers` with the addition of `video_receive` and `video_emit` methods which take and return a `numpy` array, respectively. + +Here is an example of the video handling functions for connecting with the Gemini multimodal API. In this case, we simply reflect the webcam feed back to the user but every second we'll send the latest webcam frame (and an additional image component) to the Gemini server. + +Please see the "Gemini Audio Video Chat" example in the [cookbook](/cookbook) for the complete code. + +``` python title="Async Gemini Video Handling" + +async def video_receive(self, frame: np.ndarray): + """Send video frames to the server""" + if self.session: + # send image every 1 second + # otherwise we flood the API + if time.time() - self.last_frame_time > 1: + self.last_frame_time = time.time() + await self.session.send(encode_image(frame)) + if self.latest_args[2] is not None: + await self.session.send(encode_image(self.latest_args[2])) + self.video_queue.put_nowait(frame) + +async def video_emit(self) -> VideoEmitType: + """Return video frames to the client""" + return await self.video_queue.get() +``` + + +## Additional Outputs + +In order to modify other components from within the WebRTC stream, you must yield an instance of `AdditionalOutputs` and add an `on_additional_outputs` event to the `WebRTC` component. + +This is common for displaying a multimodal text/audio conversation in a Chatbot UI. + +=== "Code" + + ``` py title="Additional Outputs" + from gradio_webrtc import AdditionalOutputs, WebRTC + + def transcribe(audio: tuple[int, np.ndarray], + transformers_convo: list[dict], + gradio_convo: list[dict]): + response = model.generate(**inputs, max_length=256) + transformers_convo.append({"role": "assistant", "content": response}) + gradio_convo.append({"role": "assistant", "content": response}) + yield AdditionalOutputs(transformers_convo, gradio_convo) # (1) + + + with gr.Blocks() as demo: + gr.HTML( + """ +

+ Talk to Qwen2Audio (Powered by WebRTC ⚡️) +

+ """ + ) + transformers_convo = gr.State(value=[]) + with gr.Row(): + with gr.Column(): + audio = WebRTC( + label="Stream", + mode="send", # (2) + modality="audio", + ) + with gr.Column(): + transcript = gr.Chatbot(label="transcript", type="messages") + + audio.stream(ReplyOnPause(transcribe), + inputs=[audio, transformers_convo, transcript], + outputs=[audio], time_limit=90) + audio.on_additional_outputs(lambda s,a: (s,a), # (3) + outputs=[transformers_convo, transcript], + queue=False, show_progress="hidden") + demo.launch() + ``` + + 1. Pass your data to `AdditionalOutputs` and yield it. + 2. In this case, no audio is being returned, so we set `mode="send"`. However, if we set `mode="send-receive"`, we could also yield generated audio and `AdditionalOutputs`. + 3. The `on_additional_outputs` event does not take `inputs`. It's common practice to not run this event on the queue since it is just a quick UI update. +=== "Notes" + 1. Pass your data to `AdditionalOutputs` and yield it. + 2. In this case, no audio is being returned, so we set `mode="send"`. However, if we set `mode="send-receive"`, we could also yield generated audio and `AdditionalOutputs`. + 3. The `on_additional_outputs` event does not take `inputs`. It's common practice to not run this event on the queue since it is just a quick UI update. \ No newline at end of file diff --git a/docs/utils.md b/docs/utils.md new file mode 100644 index 0000000..f267a2d --- /dev/null +++ b/docs/utils.md @@ -0,0 +1,54 @@ +# Utils + +## `audio_to_bytes` + +Convert an audio tuple containing sample rate and numpy array data into bytes. +Useful for sending data to external APIs from `ReplyOnPause` handler. + +Parameters +``` +audio : tuple[int, np.ndarray] + A tuple containing: + - sample_rate (int): The audio sample rate in Hz + - data (np.ndarray): The audio data as a numpy array +``` + +Returns +``` +bytes + The audio data encoded as bytes, suitable for transmission or storage +``` + +Example +```python +>>> sample_rate = 44100 +>>> audio_data = np.array([0.1, -0.2, 0.3]) # Example audio samples +>>> audio_tuple = (sample_rate, audio_data) +>>> audio_bytes = audio_to_bytes(audio_tuple) +``` + +## `audio_to_file` + +Save an audio tuple containing sample rate and numpy array data to a file. + +Parameters +``` +audio : tuple[int, np.ndarray] + A tuple containing: + - sample_rate (int): The audio sample rate in Hz + - data (np.ndarray): The audio data as a numpy array +``` +Returns +``` +str + The path to the saved audio file +``` +Example +``` +```python +>>> sample_rate = 44100 +>>> audio_data = np.array([0.1, -0.2, 0.3]) # Example audio samples +>>> audio_tuple = (sample_rate, audio_data) +>>> file_path = audio_to_file(audio_tuple) +>>> print(f"Audio saved to: {file_path}") +``` \ No newline at end of file diff --git a/frontend/Example.svelte b/frontend/Example.svelte new file mode 100644 index 0000000..34bb6c9 --- /dev/null +++ b/frontend/Example.svelte @@ -0,0 +1,73 @@ + + +{#if value} + {#if playable()} +
+
+ {:else} +
{value}
+ {/if} +{/if} + + diff --git a/frontend/Index.svelte b/frontend/Index.svelte new file mode 100644 index 0000000..8c996e5 --- /dev/null +++ b/frontend/Index.svelte @@ -0,0 +1,158 @@ + + + + + + gradio.dispatch("clear_status", loading_status)} + /> + + {#if mode == "receive" && modality === "video"} + gradio.dispatch("tick")} + on:error={({ detail }) => gradio.dispatch("error", detail)} + /> + {:else if mode == "receive" && modality === "audio"} + gradio.dispatch("tick")} + on:error={({ detail }) => gradio.dispatch("error", detail)} + + /> + {:else if (mode === "send-receive" || mode == "send") && (modality === "video" || modality == "audio-video")} + + {:else if (mode === "send-receive" || mode === "send") && modality === "audio"} + gradio.dispatch("tick")} + on:error={({ detail }) => gradio.dispatch("error", detail)} + on:warning={({ detail }) => gradio.dispatch("warning", detail)} + /> + {/if} + diff --git a/frontend/gradio.config.js b/frontend/gradio.config.js new file mode 100644 index 0000000..c096bf5 --- /dev/null +++ b/frontend/gradio.config.js @@ -0,0 +1,9 @@ +export default { + plugins: [], + svelte: { + preprocess: [], + }, + build: { + target: "modules", + }, +}; diff --git a/frontend/index.ts b/frontend/index.ts new file mode 100644 index 0000000..9ab744b --- /dev/null +++ b/frontend/index.ts @@ -0,0 +1,5 @@ +export { default as BaseInteractiveVideo } from "./shared/InteractiveVideo.svelte"; +export { prettyBytes, playable, loaded } from "./shared/utils"; +export { default as BaseExample } from "./Example.svelte"; +import { default as Index } from "./Index.svelte"; +export default Index; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..27915b3 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,5900 @@ +{ + "name": "gradio_webrtc", + "version": "0.11.0-beta.3", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gradio_webrtc", + "version": "0.11.0-beta.3", + "license": "ISC", + "dependencies": { + "@ffmpeg/ffmpeg": "^0.12.10", + "@ffmpeg/util": "^0.12.1", + "@gradio/atoms": "0.9.2", + "@gradio/client": "1.7.0", + "@gradio/icons": "0.8.0", + "@gradio/image": "0.16.4", + "@gradio/markdown": "^0.10.3", + "@gradio/statustracker": "0.9.1", + "@gradio/upload": "0.13.3", + "@gradio/utils": "0.7.0", + "@gradio/wasm": "0.14.2", + "hls.js": "^1.5.16", + "mrmime": "^2.0.0" + }, + "devDependencies": { + "@gradio/preview": "0.12.0", + "prettier": "3.3.3" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.3.3.tgz", + "integrity": "sha512-rE0Pygv0sEZ4vBWHlAgJLGDU7Pm8xoO6p3wsEceb7GYAjScrOHpEo8KK/eVkAcnSM+slAEtXjA2JpdjLp4fJQQ==", + "dev": true + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.8.tgz", + "integrity": "sha512-HcttkxzdPucv3nNFmfOOMfFf64KgdJVqm1KaCm25dPGMLElo9nsLvXeJECQg8UzPuBGLyTSA0ZzqCtDSzKTEoQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.8.tgz", + "integrity": "sha512-JWtuCu8VQsMladxVz/P4HzHUGCAwpuqacmowgXFs5XjxIgKuNjnLokQzuVjlTvIzODaDmpjT3oxcC48vyk9EWg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bundled-es-modules/cookie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.0.tgz", + "integrity": "sha512-Or6YHg/kamKHpxULAdSqhGqnWFneIXu1NKvvfBBzKGwpVsYuFIQ5aBPHDnnoR3ghW1nvSkALd+EF9iMtY7Vjxw==", + "dependencies": { + "cookie": "^0.5.0" + } + }, + "node_modules/@bundled-es-modules/statuses": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", + "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", + "dependencies": { + "statuses": "^2.0.1" + } + }, + "node_modules/@bundled-es-modules/tough-cookie": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", + "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", + "dependencies": { + "@types/tough-cookie": "^4.0.5", + "tough-cookie": "^4.1.4" + } + }, + "node_modules/@bundled-es-modules/tough-cookie/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.14.54.tgz", + "integrity": "sha512-bZBrLAIX1kpWelV0XemxBZllyRmM6vgFQQG2GdNb+r3Fkp0FOh1NJSvekXDs7jq70k4euu1cryLMfU+mTXlEpw==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@ffmpeg/ffmpeg": { + "version": "0.12.10", + "resolved": "https://registry.npmjs.org/@ffmpeg/ffmpeg/-/ffmpeg-0.12.10.tgz", + "integrity": "sha512-lVtk8PW8e+NUzGZhPTWj2P1J4/NyuCrbDD3O9IGpSeLYtUZKBqZO8CNj1WYGghep/MXoM8e1qVY1GztTkf8YYQ==", + "dependencies": { + "@ffmpeg/types": "^0.12.2" + }, + "engines": { + "node": ">=18.x" + } + }, + "node_modules/@ffmpeg/types": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@ffmpeg/types/-/types-0.12.2.tgz", + "integrity": "sha512-NJtxwPoLb60/z1Klv0ueshguWQ/7mNm106qdHkB4HL49LXszjhjCCiL+ldHJGQ9ai2Igx0s4F24ghigy//ERdA==", + "engines": { + "node": ">=16.x" + } + }, + "node_modules/@ffmpeg/util": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@ffmpeg/util/-/util-0.12.1.tgz", + "integrity": "sha512-10jjfAKWaDyb8+nAkijcsi9wgz/y26LOc1NKJradNMyCIl6usQcBbhkjX5qhALrSBcOy6TOeksunTYa+a03qNQ==", + "engines": { + "node": ">=18.x" + } + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.11.4.tgz", + "integrity": "sha512-EBikYFp2JCdIfGEb5G9dyCkTGDmC57KSHhRQOC3aYxoPWVZvfWCDjZwkGYHN7Lis/fmuWl906bnNTJifDQ3sXw==", + "dependencies": { + "@formatjs/intl-localematcher": "0.2.25", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/fast-memoize": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-1.2.1.tgz", + "integrity": "sha512-Rg0e76nomkz3vF9IPlKeV+Qynok0r7YZjL6syLz4/urSg0IbjPZCB/iYUMNsYA643gh4mgrX3T7KEIFIxJBQeg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.1.0.tgz", + "integrity": "sha512-Qxv/lmCN6hKpBSss2uQ8IROVnta2r9jd3ymUEIjm2UyIkUCHVcbUVRGL/KS/wv7876edvsPe+hjHVJ4z8YuVaw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/icu-skeleton-parser": "1.3.6", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.3.6.tgz", + "integrity": "sha512-I96mOxvml/YLrwU2Txnd4klA7V8fRhb6JG/4hm3VMNmeJo1F03IpV2L3wWt7EweqNLES59SZ4d6hVOPCSf80Bg==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@formatjs/intl-localematcher": { + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.2.25.tgz", + "integrity": "sha512-YmLcX70BxoSopLFdLr1Ds99NdlTI2oWoLbaUW2M406lxOIPzE1KQhRz2fPUkq34xVZQaihCoU29h0KK7An3bhA==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@gradio/atoms": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@gradio/atoms/-/atoms-0.9.2.tgz", + "integrity": "sha512-P4+XXBl7S3JfmggMaFc/Ni9si9GtKMRE74HkG6C76HKJfszhnAV2/u/sS87+fe5NffU8CgS0R47FwxUSKMsm5g==", + "dependencies": { + "@gradio/icons": "^0.8.0", + "@gradio/markdown": "^0.10.3", + "@gradio/utils": "^0.7.0" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@gradio/client": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@gradio/client/-/client-1.7.0.tgz", + "integrity": "sha512-yRo7qU4zz8ZT61L1LFy+dSU5M0L6UzpMYWIdryHskvd4DbnhpXOj0LMYH/r1Snb61YBUJYIPOSzAJV1YP1Tkug==", + "dependencies": { + "@types/eventsource": "^1.1.15", + "bufferutil": "^4.0.7", + "eventsource": "^2.0.2", + "fetch-event-stream": "^0.1.5", + "msw": "^2.2.1", + "semiver": "^1.1.0", + "textlinestream": "^1.1.1", + "typescript": "^5.0.0", + "ws": "^8.13.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@gradio/icons": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@gradio/icons/-/icons-0.8.0.tgz", + "integrity": "sha512-lFTFArV/ZXOuYArHX85stdxlRuxeyekB7Ig5Ed3uQX1sv2JwzyQ9ALkauLDWcA6jMcIMCjnV7rZWeTI++IzPAg==", + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@gradio/image": { + "version": "0.16.4", + "resolved": "https://registry.npmjs.org/@gradio/image/-/image-0.16.4.tgz", + "integrity": "sha512-BKH3FHe/N/nyRww4/Gm9uSYfkFjmVQSQAZDq5ONpFNHAoOOA6+SjjWJq6yY1vM9SaAMkMQreLN0eugbOiQwwxw==", + "dependencies": { + "@gradio/atoms": "^0.9.2", + "@gradio/client": "^1.7.0", + "@gradio/icons": "^0.8.0", + "@gradio/statustracker": "^0.9.1", + "@gradio/upload": "^0.13.3", + "@gradio/utils": "^0.7.0", + "@gradio/wasm": "^0.14.2", + "cropperjs": "^1.5.12", + "lazy-brush": "^1.0.1", + "resize-observer-polyfill": "^1.5.1" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@gradio/markdown": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@gradio/markdown/-/markdown-0.10.3.tgz", + "integrity": "sha512-qSywsn/tpnSL1kWPX3+SyeDoYke3y19mU9dxny7ZHn8gT3qdiYOY5Xgs1qcHXd7JEkJKsMkRdEMedziaFhwp1w==", + "dependencies": { + "@gradio/atoms": "^0.9.2", + "@gradio/icons": "^0.8.0", + "@gradio/sanitize": "^0.1.1", + "@gradio/statustracker": "^0.9.1", + "@gradio/utils": "^0.7.0", + "@types/dompurify": "^3.0.2", + "@types/katex": "^0.16.0", + "@types/prismjs": "1.26.4", + "dom-parser": "^1.1.5", + "github-slugger": "^2.0.0", + "isomorphic-dompurify": "^2.14.0", + "katex": "^0.16.7", + "marked": "^12.0.0", + "marked-gfm-heading-id": "^3.1.2", + "marked-highlight": "^2.0.1", + "prismjs": "1.29.0" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@gradio/preview": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@gradio/preview/-/preview-0.12.0.tgz", + "integrity": "sha512-VXmqZxieNuOZiXTFX/Bo+hOwoBRyJH7UaVcgZo5Cy1bXz1LsNZ1+EWNJB4L01u5Ga7UwLdSLrTaVrhF+GGHZ6A==", + "dev": true, + "dependencies": { + "@originjs/vite-plugin-commonjs": "^1.0.3", + "@rollup/plugin-sucrase": "^5.0.1", + "@sveltejs/vite-plugin-svelte": "^3.1.0", + "@types/which": "^3.0.0", + "coffeescript": "^2.7.0", + "lightningcss": "^1.21.7", + "pug": "^3.0.2", + "sass": "^1.66.1", + "stylus": "^0.63.0", + "sucrase": "^3.34.0", + "sugarss": "^4.0.1", + "svelte-hmr": "^0.16.0", + "svelte-preprocess": "^5.0.4", + "typescript": "^5.0.0", + "vite": "^5.2.9", + "which": "4.0.0", + "yootils": "^0.3.1" + }, + "optionalDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@gradio/preview/node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", + "dev": true + }, + "node_modules/@gradio/preview/node_modules/stylus": { + "version": "0.63.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.63.0.tgz", + "integrity": "sha512-OMlgrTCPzE/ibtRMoeLVhOY0RcNuNWh0rhAVqeKnk/QwcuUKQbnqhZ1kg2vzD8VU/6h3FoPTq4RJPHgLBvX6Bw==", + "dev": true, + "dependencies": { + "@adobe/css-tools": "~4.3.3", + "debug": "^4.3.2", + "glob": "^7.1.6", + "sax": "~1.3.0", + "source-map": "^0.7.3" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://opencollective.com/stylus" + } + }, + "node_modules/@gradio/sanitize": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@gradio/sanitize/-/sanitize-0.1.1.tgz", + "integrity": "sha512-7OpCtsFSlAcpEQwRLrWvJVWJrvjrEL/eE4JzBc0PATm2rV5hbKbEHq42QqNubOyrvJTQN41u0B+nxQMrrGruLA==", + "dependencies": { + "amuchina": "^1.0.12", + "sanitize-html": "^2.13.0" + } + }, + "node_modules/@gradio/statustracker": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@gradio/statustracker/-/statustracker-0.9.1.tgz", + "integrity": "sha512-Iz36z1MrA22oCDgxHdUQFT8U+jkPYdGM1kbq+jaCMTbnSg9Y/+oppyBcUV7vBFUGcBgi6WV7KqxmMCK+whTfbw==", + "dependencies": { + "@gradio/atoms": "^0.9.2", + "@gradio/icons": "^0.8.0", + "@gradio/utils": "^0.7.0", + "@types/dompurify": "^3.0.2", + "dompurify": "^3.0.3" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@gradio/theme": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@gradio/theme/-/theme-0.3.0.tgz", + "integrity": "sha512-PNguiOQFZO4Vim9446b2VRa0wO0ulkz9MUkDJXIdk7Cdx6HZQaqc5hErp/mlDV1scFKQbwTsG+9IzcClTnLXTg==", + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@gradio/upload": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@gradio/upload/-/upload-0.13.3.tgz", + "integrity": "sha512-ObL0vxWTghT07yWs2U1SQ5uILAKm9riZ5ys5dhn3kJZUsA7Pi55IUxcLA8NOIn2CnGHRKhHmyy8xe0UuTJ0qMw==", + "dependencies": { + "@gradio/atoms": "^0.9.2", + "@gradio/client": "^1.7.0", + "@gradio/icons": "^0.8.0", + "@gradio/utils": "^0.7.0", + "@gradio/wasm": "^0.14.2" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@gradio/utils": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@gradio/utils/-/utils-0.7.0.tgz", + "integrity": "sha512-cPVt/oz+tdEQ3ya1XoDe7VBX+q4Z1KDI68FxmindW5BWHIfOEVNWZ+6KQTIZhrgl+k4kSHqpZjnahGyFpes9+w==", + "dependencies": { + "@gradio/theme": "^0.3.0", + "svelte-i18n": "^3.6.0" + } + }, + "node_modules/@gradio/wasm": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/@gradio/wasm/-/wasm-0.14.2.tgz", + "integrity": "sha512-oaGYQFvD5QFizoJbD/OJzURqBhWXGo/3QTXjh53cL6hlzxee4r2nREw8qHFCJ8kKAKfvuF/9cdBrJkEm9XqLeg==", + "dependencies": { + "@types/path-browserify": "^1.0.0", + "path-browserify": "^1.0.1", + "pyodide": "0.26.1" + }, + "peerDependencies": { + "svelte": "^4.0.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-3.2.0.tgz", + "integrity": "sha512-oOIwPs0Dvq5220Z8lGL/6LHRTEr9TgLHmiI99Rj1PJ1p1czTys+olrgBqZk4E2qC0YTzeHprxSQmoHioVdJ7Lw==", + "dependencies": { + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.2.1.tgz", + "integrity": "sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==", + "dependencies": { + "@inquirer/figures": "^1.0.6", + "@inquirer/type": "^2.0.0", + "@types/mute-stream": "^0.0.4", + "@types/node": "^22.5.5", + "@types/wrap-ansi": "^3.0.0", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^1.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/@inquirer/type": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-2.0.0.tgz", + "integrity": "sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.7.tgz", + "integrity": "sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.35.9", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.35.9.tgz", + "integrity": "sha512-SSnyl/4ni/2ViHKkiZb8eajA/eN1DNFaHjhGiLUdZvDz6PKF4COSf/17xqSz64nOo2Ia29SA6B2KNCsyCbVmaQ==", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==" + }, + "node_modules/@originjs/vite-plugin-commonjs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@originjs/vite-plugin-commonjs/-/vite-plugin-commonjs-1.0.3.tgz", + "integrity": "sha512-KuEXeGPptM2lyxdIEJ4R11+5ztipHoE7hy8ClZt3PYaOVQ/pyngd2alaSrPnwyFeOW1UagRBaQ752aA1dTMdOQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.14.14" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-sucrase": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@rollup/plugin-sucrase/-/plugin-sucrase-5.0.2.tgz", + "integrity": "sha512-4MhIVH9Dy2Hwose1/x5QMs0XF7yn9jDd/yozHqzdIrMWIolgFpGnrnVhQkqTaK1RALY/fpyrEKmwH/04vr1THA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "sucrase": "^3.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.53.1||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.2.tgz", + "integrity": "sha512-/FIdS3PyZ39bjZlwqFnWqCOVnW7o963LtKMwQOD0NhQqw22gSr2YY1afu3FxRip4ZCZNsD5jq6Aaz6QV3D/Njw==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sveltejs/vite-plugin-svelte": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz", + "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==", + "dev": true, + "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^2.1.0", + "debug": "^4.3.4", + "deepmerge": "^4.3.1", + "kleur": "^4.1.5", + "magic-string": "^0.30.10", + "svelte-hmr": "^0.16.0", + "vitefu": "^0.2.5" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz", + "integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==", + "dev": true, + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.0.0 || >=20" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^3.0.0", + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": "^5.0.0" + } + }, + "node_modules/@types/cookie": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", + "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==" + }, + "node_modules/@types/dompurify": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-3.0.5.tgz", + "integrity": "sha512-1Wg0g3BtQF7sSb27fJQAKck1HECM6zV1EB66j8JH9i3LCjYabJa0FSdiSgsD5K/RbrsR0SiraKacLB+T8ZVYAg==", + "dependencies": { + "@types/trusted-types": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + }, + "node_modules/@types/eventsource": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/@types/eventsource/-/eventsource-1.1.15.tgz", + "integrity": "sha512-XQmGcbnxUNa06HR3VBVkc9+A2Vpi9ZyLJcdS5dwaQQ/4ZMWFO+5c90FnMUpbtMZwB/FChoYHwuVg8TvkECacTA==" + }, + "node_modules/@types/katex": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz", + "integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==" + }, + "node_modules/@types/mute-stream": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@types/mute-stream/-/mute-stream-0.0.4.tgz", + "integrity": "sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.7.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.6.tgz", + "integrity": "sha512-/d7Rnj0/ExXDMcioS78/kf1lMzYk4BZV8MZGTBKzTGZ6/406ukkbYlIsZmMPhcR5KlkunDHQLrtAVmSq7r+mSw==", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/path-browserify": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/path-browserify/-/path-browserify-1.0.3.tgz", + "integrity": "sha512-ZmHivEbNCBtAfcrFeBCiTjdIc2dey0l7oCGNGpSuRTy8jP6UVND7oUowlvDujBy8r2Hoa8bfFUOCiPWfmtkfxw==" + }, + "node_modules/@types/prismjs": { + "version": "1.26.4", + "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.4.tgz", + "integrity": "sha512-rlAnzkW2sZOjbqZ743IHUhFcvzaGbqijwOu8QZnZCjfQzBqFE3s4lOTJEsxikImav9uzz/42I+O7YUs1mWgMlg==" + }, + "node_modules/@types/pug": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/@types/pug/-/pug-2.0.10.tgz", + "integrity": "sha512-Sk/uYFOBAB7mb74XcpizmH0KOR2Pv3D2Hmrh1Dmy5BmK3MpdSa5kqZcg6EKBdklU0bFXX9gCfzvpnyUehrPIuA==", + "dev": true + }, + "node_modules/@types/statuses": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", + "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==" + }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==" + }, + "node_modules/@types/which": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/which/-/which-3.0.4.tgz", + "integrity": "sha512-liyfuo/106JdlgSchJzXEQCVArk0CvevqPote8F8HgWgJ3dRCcTHgJIsLDuee0kxk/mhbInzIZk3QWSZJ8R+2w==", + "dev": true + }, + "node_modules/@types/wrap-ansi": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz", + "integrity": "sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==" + }, + "node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/amuchina": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/amuchina/-/amuchina-1.0.12.tgz", + "integrity": "sha512-Itv2NEwpiV53+bkpviJIC12+8SOlCSLR1HgQCv6wD7ldNFNesm4JSk7XjvTFkeVfLYzqKEZcEBZO1X/V2MYg4A==" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "dev": true + }, + "node_modules/assert-never": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/assert-never/-/assert-never-1.3.0.tgz", + "integrity": "sha512-9Z3vxQ+berkL/JJo0dK+EY3Lp0s3NtSnP3VCLsh5HDcZPrh0M+KQRK5sWhUeyPPH+/RCxZqOxLMR+YC6vlviEQ==", + "dev": true + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "atob": "bin/atob.js" + }, + "engines": { + "node": ">= 4.5.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-walk": { + "version": "3.0.0-canary-5", + "resolved": "https://registry.npmjs.org/babel-walk/-/babel-walk-3.0.0-canary-5.tgz", + "integrity": "sha512-GAwkz0AihzY5bkwIY5QDR+LvsRQgB/B+1foMPvi0FZPMl5fjD7ICiznUiBdLYMH1QYe6vqu4gWYytZOccLouFw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.9.6" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/bufferutil": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz", + "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", + "hasInstallScript": true, + "dependencies": { + "node-gyp-build": "^4.3.0" + }, + "engines": { + "node": ">=6.14.2" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/character-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/character-parser/-/character-parser-2.2.0.tgz", + "integrity": "sha512-+UqJQjFEFaTAs3bNsF2j2kEN1baG/zghZbdqoYEDxGZtJo9LBzl1A+m0D4n3qKx8N2FNv8/Xp6yV9mQmBuptaw==", + "dev": true, + "dependencies": { + "is-regex": "^1.0.3" + } + }, + "node_modules/chokidar": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.1.tgz", + "integrity": "sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/cli-color": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/cli-color/-/cli-color-2.0.4.tgz", + "integrity": "sha512-zlnpg0jNcibNrO7GG9IeHH7maWFeCz+Ja1wx/7tZNU5ASSSSZ+/qZciM0/LHCYxSdqv5h2sdbQ/PXYdOuetXvA==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.64", + "es6-iterator": "^2.0.3", + "memoizee": "^0.4.15", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/code-red": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz", + "integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15", + "@types/estree": "^1.0.1", + "acorn": "^8.10.0", + "estree-walker": "^3.0.3", + "periscopic": "^3.1.0" + } + }, + "node_modules/code-red/node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/code-red/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/coffeescript": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/coffeescript/-/coffeescript-2.7.0.tgz", + "integrity": "sha512-hzWp6TUE2d/jCcN67LrW1eh5b/rSDKQK6oD6VMLlggYVUUFexgTH9z3dNYihzX4RMhze5FTUsUmOXViJKFQR/A==", + "dev": true, + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/constantinople": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-4.0.1.tgz", + "integrity": "sha512-vCrqcSIq4//Gx74TXXCGnHpulY1dskqLTFGDmhrGxzeXL8lF8kvXv6mpNWlJj1uD4DW23D4ljAqbY4RRaaUZIw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.6.0", + "@babel/types": "^7.6.1" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cropperjs": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz", + "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==" + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/css/-/css-3.0.0.tgz", + "integrity": "sha512-DG9pFfwOrzc+hawpmqX/dHYHJG+Bsdb0klhyi1sDneOgGOXy9wQIC8hzyVp1e4NRYDBdxcylvywPkkXCHAzTyQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.4", + "source-map": "^0.6.1", + "source-map-resolve": "^0.6.0" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/d": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.2.tgz", + "integrity": "sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==", + "dependencies": { + "es5-ext": "^0.10.64", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==" + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-indent": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/detect-indent/-/detect-indent-6.1.0.tgz", + "integrity": "sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/doctypes": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/doctypes/-/doctypes-1.1.0.tgz", + "integrity": "sha512-LLBi6pEqS6Do3EKQ3J0NqHWV5hhb78Pi8vvESYwyOy2c31ZEZVdtitdzsQsKb7878PEERhzUk0ftqGhG6Mz+pQ==", + "dev": true + }, + "node_modules/dom-parser": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/dom-parser/-/dom-parser-1.1.5.tgz", + "integrity": "sha512-lCiFG48ZUzGXjKN0qhSkxD/i3ndyV6I37zQ3W2VFYLjF1ob8A+QgSsM7Ps2UT0d3LpJxLMmMHiJJ34z5hkKLiA==" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/dompurify": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.1.7.tgz", + "integrity": "sha512-VaTstWtsneJY8xzy7DekmYWEOZcmzIe3Qb3zPd4STve1OBTa+e+WmS1ITQec1fZYXI3HCsOZZiSMpG6oxoWMWQ==" + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es5-ext": { + "version": "0.10.64", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz", + "integrity": "sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==", + "hasInstallScript": true, + "dependencies": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "esniff": "^2.0.1", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/es6-promise": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.3.1.tgz", + "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", + "dev": true + }, + "node_modules/es6-symbol": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz", + "integrity": "sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==", + "dependencies": { + "d": "^1.0.2", + "ext": "^1.7.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/es6-weak-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.3.tgz", + "integrity": "sha512-p5um32HOTO1kP+w7PRnB+5lQ43Z6muuMuIMffvDN8ZB4GcnjLBV6zGStpbASIMk4DCAvEaamhe2zhyCb/QXXsA==", + "dependencies": { + "d": "1", + "es5-ext": "^0.10.46", + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.1" + } + }, + "node_modules/esbuild": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.54.tgz", + "integrity": "sha512-Cy9llcy8DvET5uznocPyqL3BFRrFXSVqbgpMJ9Wz8oVjZlh/zUSNbPRbov0VX7VxN2JH1Oa0uNxZ7eLRb62pJA==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.14.54", + "esbuild-android-64": "0.14.54", + "esbuild-android-arm64": "0.14.54", + "esbuild-darwin-64": "0.14.54", + "esbuild-darwin-arm64": "0.14.54", + "esbuild-freebsd-64": "0.14.54", + "esbuild-freebsd-arm64": "0.14.54", + "esbuild-linux-32": "0.14.54", + "esbuild-linux-64": "0.14.54", + "esbuild-linux-arm": "0.14.54", + "esbuild-linux-arm64": "0.14.54", + "esbuild-linux-mips64le": "0.14.54", + "esbuild-linux-ppc64le": "0.14.54", + "esbuild-linux-riscv64": "0.14.54", + "esbuild-linux-s390x": "0.14.54", + "esbuild-netbsd-64": "0.14.54", + "esbuild-openbsd-64": "0.14.54", + "esbuild-sunos-64": "0.14.54", + "esbuild-windows-32": "0.14.54", + "esbuild-windows-64": "0.14.54", + "esbuild-windows-arm64": "0.14.54" + } + }, + "node_modules/esbuild-android-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.14.54.tgz", + "integrity": "sha512-Tz2++Aqqz0rJ7kYBfz+iqyE3QMycD4vk7LBRyWaAVFgFtQ/O8EJOnVmTOiDWYZ/uYzB4kvP+bqejYdVKzE5lAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-android-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.54.tgz", + "integrity": "sha512-F9E+/QDi9sSkLaClO8SOV6etqPd+5DgJje1F9lOWoNncDdOBL2YF59IhsWATSt0TLZbYCf3pNlTHvVV5VfHdvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.54.tgz", + "integrity": "sha512-jtdKWV3nBviOd5v4hOpkVmpxsBy90CGzebpbO9beiqUYVMBtSc0AL9zGftFuBon7PNDcdvNCEuQqw2x0wP9yug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-darwin-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.54.tgz", + "integrity": "sha512-OPafJHD2oUPyvJMrsCvDGkRrVCar5aVyHfWGQzY1dWnzErjrDuSETxwA2HSsyg2jORLY8yBfzc1MIpUkXlctmw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.54.tgz", + "integrity": "sha512-OKwd4gmwHqOTp4mOGZKe/XUlbDJ4Q9TjX0hMPIDBUWWu/kwhBAudJdBoxnjNf9ocIB6GN6CPowYpR/hRCbSYAg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-freebsd-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.54.tgz", + "integrity": "sha512-sFwueGr7OvIFiQT6WeG0jRLjkjdqWWSrfbVwZp8iMP+8UHEHRBvlaxL6IuKNDwAozNUmbb8nIMXa7oAOARGs1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.54.tgz", + "integrity": "sha512-1ZuY+JDI//WmklKlBgJnglpUL1owm2OX+8E1syCD6UAxcMM/XoWd76OHSjl/0MR0LisSAXDqgjT3uJqT67O3qw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.54.tgz", + "integrity": "sha512-EgjAgH5HwTbtNsTqQOXWApBaPVdDn7XcK+/PtJwZLT1UmpLoznPd8c5CxqsH2dQK3j05YsB3L17T8vE7cp4cCg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.54.tgz", + "integrity": "sha512-qqz/SjemQhVMTnvcLGoLOdFpCYbz4v4fUo+TfsWG+1aOu70/80RV6bgNpR2JCrppV2moUQkww+6bWxXRL9YMGw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.54.tgz", + "integrity": "sha512-WL71L+0Rwv+Gv/HTmxTEmpv0UgmxYa5ftZILVi2QmZBgX3q7+tDeOQNqGtdXSdsL8TQi1vIaVFHUPDe0O0kdig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-mips64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.54.tgz", + "integrity": "sha512-qTHGQB8D1etd0u1+sB6p0ikLKRVuCWhYQhAHRPkO+OF3I/iSlTKNNS0Lh2Oc0g0UFGguaFZZiPJdJey3AGpAlw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-ppc64le": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.54.tgz", + "integrity": "sha512-j3OMlzHiqwZBDPRCDFKcx595XVfOfOnv68Ax3U4UKZ3MTYQB5Yz3X1mn5GnodEVYzhtZgxEBidLWeIs8FDSfrQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-riscv64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.54.tgz", + "integrity": "sha512-y7Vt7Wl9dkOGZjxQZnDAqqn+XOqFD7IMWiewY5SPlNlzMX39ocPQlOaoxvT4FllA5viyV26/QzHtvTjVNOxHZg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-linux-s390x": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.54.tgz", + "integrity": "sha512-zaHpW9dziAsi7lRcyV4r8dhfG1qBidQWUXweUjnw+lliChJqQr+6XD71K41oEIC3Mx1KStovEmlzm+MkGZHnHA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-netbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.54.tgz", + "integrity": "sha512-PR01lmIMnfJTgeU9VJTDY9ZerDWVFIUzAtJuDHwwceppW7cQWjBBqP48NdeRtoP04/AtO9a7w3viI+PIDr6d+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-openbsd-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.54.tgz", + "integrity": "sha512-Qyk7ikT2o7Wu76UsvvDS5q0amJvmRzDyVlL0qf5VLsLchjCa1+IAvd8kTBgUxD7VBUUVgItLkk609ZHUc1oCaw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-sunos-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.54.tgz", + "integrity": "sha512-28GZ24KmMSeKi5ueWzMcco6EBHStL3B6ubM7M51RmPwXQGLe0teBGJocmWhgwccA1GeFXqxzILIxXpHbl9Q/Kw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-32": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.54.tgz", + "integrity": "sha512-T+rdZW19ql9MjS7pixmZYVObd9G7kcaZo+sETqNH4RCkuuYSuv9AGHUVnPoP9hhuE1WM1ZimHz1CIBHBboLU7w==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.54.tgz", + "integrity": "sha512-AoHTRBUuYwXtZhjXZbA1pGfTo8cJo3vZIcWGLiUcTNgHpJJMC1rVA44ZereBHMJtotyN71S8Qw0npiCIkW96cQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-windows-arm64": { + "version": "0.14.54", + "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.54.tgz", + "integrity": "sha512-M0kuUvXhot1zOISQGXwWn6YtS+Y/1RT9WrVIOywZnJHo3jCDyewAc79aKNQWFCQm+xNHVTq9h8dZKvygoXQQRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esniff": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz", + "integrity": "sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==", + "dependencies": { + "d": "^1.0.1", + "es5-ext": "^0.10.62", + "event-emitter": "^0.3.5", + "type": "^2.7.2" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" + }, + "node_modules/event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==", + "dependencies": { + "d": "1", + "es5-ext": "~0.10.14" + } + }, + "node_modules/eventsource": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", + "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ext": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz", + "integrity": "sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==", + "dependencies": { + "type": "^2.7.2" + } + }, + "node_modules/fetch-event-stream": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/fetch-event-stream/-/fetch-event-stream-0.1.5.tgz", + "integrity": "sha512-V1PWovkspxQfssq/NnxoEyQo1DV+MRK/laPuPblIZmSjMN8P5u46OhlFQznSr9p/t0Sp8Uc6SbM3yCMfr0KU8g==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==" + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/graphql": { + "version": "16.9.0", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.9.0.tgz", + "integrity": "sha512-GGTKBX4SD7Wdb8mqeDLni2oaRGYQWjWHGKPQ24ZMnUtKfcsVoiv4uX8+LJr1K6U5VW2Lu1BwJnj7uiori0YtRw==", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz", + "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==" + }, + "node_modules/hls.js": { + "version": "1.5.16", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.5.16.tgz", + "integrity": "sha512-+wAWr4aeRq9ODN8/Vgz0Cee1Cw6Ysr7vyEkZJCwOJYNwlld7CNmhKE+dLwfpUO2UuotYLGF0of6UFiN6zA7mig==" + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz", + "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", + "dev": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/intl-messageformat": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-9.13.0.tgz", + "integrity": "sha512-7sGC7QnSQGa5LZP7bXLDhVDtQOeKGeBFGHF2Y8LVBwYZoQZCgWeKoPGTa5GMG8g/TzDgeXuYJQis7Ggiw2xTOw==", + "dependencies": { + "@formatjs/ecma402-abstract": "1.11.4", + "@formatjs/fast-memoize": "1.2.1", + "@formatjs/icu-messageformat-parser": "2.1.0", + "tslib": "^2.1.0" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-expression": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-4.0.0.tgz", + "integrity": "sha512-zMIXX63sxzG3XrkHkrAPvm/OVZVSCPNkwMHU8oTX7/U3AL78I0QXCEICXUM13BIa8TYGZ68PiTKfQz3yaTNr4A==", + "dev": true, + "dependencies": { + "acorn": "^7.1.1", + "object-assign": "^4.1.1" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==" + }, + "node_modules/is-promise": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", + "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==" + }, + "node_modules/is-reference": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz", + "integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/isomorphic-dompurify": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.16.0.tgz", + "integrity": "sha512-cXhX2owp8rPxafCr0ywqy2CGI/4ceLNgWkWBEvUz64KTbtg3oRL2ZRqq/zW0pzt4YtDjkHLbwcp/lozpKzAQjg==", + "dependencies": { + "@types/dompurify": "^3.0.5", + "dompurify": "^3.1.7", + "jsdom": "^25.0.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/js-stringify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/js-stringify/-/js-stringify-1.0.2.tgz", + "integrity": "sha512-rtS5ATOo2Q5k1G+DADISilDA6lv79zIiwFd6CcjuIxGKLFm5C+RLImRscVap9k55i+MOZwgliw+NejvkLuGD5g==", + "dev": true + }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jstransformer": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/jstransformer/-/jstransformer-1.0.0.tgz", + "integrity": "sha512-C9YK3Rf8q6VAPDCCU9fnqo3mAfOH6vUGnMcP4AQAYIEpWtfGLpwOTmZ+igtdK5y+VvI2n3CyYSzy4Qh34eq24A==", + "dev": true, + "dependencies": { + "is-promise": "^2.0.0", + "promise": "^7.0.1" + } + }, + "node_modules/katex": { + "version": "0.16.11", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.11.tgz", + "integrity": "sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/kleur": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz", + "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/lazy-brush": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazy-brush/-/lazy-brush-1.0.1.tgz", + "integrity": "sha512-xT/iSClTVi7vLoF8dCWTBhCuOWqsLXCMPa6ucVmVAk6hyNCM5JeS1NLhXqIrJktUg+caEYKlqSOUU4u3cpXzKg==" + }, + "node_modules/lightningcss": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.27.0.tgz", + "integrity": "sha512-8f7aNmS1+etYSLHht0fQApPc2kNO8qGRutifN5rVIc6Xo6ABsEbqOr758UwI7ALVbTt4x1fllKt0PYgzD9S3yQ==", + "dev": true, + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-darwin-arm64": "1.27.0", + "lightningcss-darwin-x64": "1.27.0", + "lightningcss-freebsd-x64": "1.27.0", + "lightningcss-linux-arm-gnueabihf": "1.27.0", + "lightningcss-linux-arm64-gnu": "1.27.0", + "lightningcss-linux-arm64-musl": "1.27.0", + "lightningcss-linux-x64-gnu": "1.27.0", + "lightningcss-linux-x64-musl": "1.27.0", + "lightningcss-win32-arm64-msvc": "1.27.0", + "lightningcss-win32-x64-msvc": "1.27.0" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.27.0.tgz", + "integrity": "sha512-Gl/lqIXY+d+ySmMbgDf0pgaWSqrWYxVHoc88q+Vhf2YNzZ8DwoRzGt5NZDVqqIW5ScpSnmmjcgXP87Dn2ylSSQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.27.0.tgz", + "integrity": "sha512-0+mZa54IlcNAoQS9E0+niovhyjjQWEMrwW0p2sSdLRhLDc8LMQ/b67z7+B5q4VmjYCMSfnFi3djAAQFIDuj/Tg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.27.0.tgz", + "integrity": "sha512-n1sEf85fePoU2aDN2PzYjoI8gbBqnmLGEhKq7q0DKLj0UTVmOTwDC7PtLcy/zFxzASTSBlVQYJUhwIStQMIpRA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.27.0.tgz", + "integrity": "sha512-MUMRmtdRkOkd5z3h986HOuNBD1c2lq2BSQA1Jg88d9I7bmPGx08bwGcnB75dvr17CwxjxD6XPi3Qh8ArmKFqCA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.27.0.tgz", + "integrity": "sha512-cPsxo1QEWq2sfKkSq2Bq5feQDHdUEwgtA9KaB27J5AX22+l4l0ptgjMZZtYtUnteBofjee+0oW1wQ1guv04a7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.27.0.tgz", + "integrity": "sha512-rCGBm2ax7kQ9pBSeITfCW9XSVF69VX+fm5DIpvDZQl4NnQoMQyRwhZQm9pd59m8leZ1IesRqWk2v/DntMo26lg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.27.0.tgz", + "integrity": "sha512-Dk/jovSI7qqhJDiUibvaikNKI2x6kWPN79AQiD/E/KeQWMjdGe9kw51RAgoWFDi0coP4jinaH14Nrt/J8z3U4A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.27.0.tgz", + "integrity": "sha512-QKjTxXm8A9s6v9Tg3Fk0gscCQA1t/HMoF7Woy1u68wCk5kS4fR+q3vXa1p3++REW784cRAtkYKrPy6JKibrEZA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.27.0.tgz", + "integrity": "sha512-/wXegPS1hnhkeG4OXQKEMQeJd48RDC3qdh+OA8pCuOPCyvnm/yEayrJdJVqzBsqpy1aJklRCVxscpFur80o6iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.27.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.27.0.tgz", + "integrity": "sha512-/OJLj94Zm/waZShL8nB5jsNj3CfNATLCTyFxZyouilfTmSoLDX7VlVAmhPHoZWVFp4vdmoiEbPEYC8HID3m6yw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-character": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", + "integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/lru-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/lru-queue/-/lru-queue-0.1.0.tgz", + "integrity": "sha512-BpdYkt9EvGl8OfWHDQPISVpcl5xZthb+XPsbELj5AQXxIC8IriDZIQYjBJPEm5rS420sjZ0TLEzRcq5KdBhYrQ==", + "dependencies": { + "es5-ext": "~0.10.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/marked": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-12.0.2.tgz", + "integrity": "sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/marked-gfm-heading-id": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/marked-gfm-heading-id/-/marked-gfm-heading-id-3.2.0.tgz", + "integrity": "sha512-Xfxpr5lXLDLY10XqzSCA9l2dDaiabQUgtYM9hw8yunyVsB/xYBRpiic6BOiY/EAJw1ik1eWr1ET1HKOAPZBhXg==", + "dependencies": { + "github-slugger": "^2.0.0" + }, + "peerDependencies": { + "marked": ">=4 <13" + } + }, + "node_modules/marked-highlight": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/marked-highlight/-/marked-highlight-2.2.0.tgz", + "integrity": "sha512-36LzwtVf7HEbbMITKU4j+iZuyWKgdXJfgYr4F5j27vs79oRPyApuBF3WkS5OsqO1+1lypWxztad7zNRM4qgXFw==", + "peerDependencies": { + "marked": ">=4 <15" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==" + }, + "node_modules/memoizee": { + "version": "0.4.17", + "resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz", + "integrity": "sha512-DGqD7Hjpi/1or4F/aYAspXKNm5Yili0QDAFAY4QYvpqpgiY6+1jOfqpmByzjxbWd/T9mChbCArXAbDAsTm5oXA==", + "dependencies": { + "d": "^1.0.2", + "es5-ext": "^0.10.64", + "es6-weak-map": "^2.0.3", + "event-emitter": "^0.3.5", + "is-promise": "^2.2.2", + "lru-queue": "^0.1.0", + "next-tick": "^1.1.0", + "timers-ext": "^0.1.7" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/msw": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.4.11.tgz", + "integrity": "sha512-TVEw9NOPTc6ufOQLJ53234S9NBRxQbu7xFMxs+OCP43JQcNEIOKiZHxEm2nDzYIrwccoIhUxUf8wr99SukD76A==", + "hasInstallScript": true, + "dependencies": { + "@bundled-es-modules/cookie": "^2.0.0", + "@bundled-es-modules/statuses": "^1.0.1", + "@bundled-es-modules/tough-cookie": "^0.1.6", + "@inquirer/confirm": "^3.0.0", + "@mswjs/interceptors": "^0.35.8", + "@open-draft/until": "^2.1.0", + "@types/cookie": "^0.6.0", + "@types/statuses": "^2.0.4", + "chalk": "^4.1.2", + "graphql": "^16.8.1", + "headers-polyfill": "^4.0.2", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "strict-event-emitter": "^0.5.1", + "type-fest": "^4.26.1", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next-tick": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz", + "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true + }, + "node_modules/node-gyp-build": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.2.tgz", + "integrity": "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw==", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==" + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/parse-srcset": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", + "integrity": "sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==" + }, + "node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==" + }, + "node_modules/periscopic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz", + "integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^3.0.0", + "is-reference": "^3.0.0" + } + }, + "node_modules/periscopic/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prettier": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prismjs": { + "version": "1.29.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", + "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/promise": { + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "dependencies": { + "asap": "~2.0.3" + } + }, + "node_modules/psl": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", + "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" + }, + "node_modules/pug": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug/-/pug-3.0.3.tgz", + "integrity": "sha512-uBi6kmc9f3SZ3PXxqcHiUZLmIXgfgWooKWXcwSGwQd2Zi5Rb0bT14+8CJjJgI8AB+nndLaNgHGrcc6bPIB665g==", + "dev": true, + "dependencies": { + "pug-code-gen": "^3.0.3", + "pug-filters": "^4.0.0", + "pug-lexer": "^5.0.1", + "pug-linker": "^4.0.0", + "pug-load": "^3.0.0", + "pug-parser": "^6.0.0", + "pug-runtime": "^3.0.1", + "pug-strip-comments": "^2.0.0" + } + }, + "node_modules/pug-attrs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-attrs/-/pug-attrs-3.0.0.tgz", + "integrity": "sha512-azINV9dUtzPMFQktvTXciNAfAuVh/L/JCl0vtPCwvOA21uZrC08K/UnmrL+SXGEVc1FwzjW62+xw5S/uaLj6cA==", + "dev": true, + "dependencies": { + "constantinople": "^4.0.1", + "js-stringify": "^1.0.2", + "pug-runtime": "^3.0.0" + } + }, + "node_modules/pug-code-gen": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pug-code-gen/-/pug-code-gen-3.0.3.tgz", + "integrity": "sha512-cYQg0JW0w32Ux+XTeZnBEeuWrAY7/HNE6TWnhiHGnnRYlCgyAUPoyh9KzCMa9WhcJlJ1AtQqpEYHc+vbCzA+Aw==", + "dev": true, + "dependencies": { + "constantinople": "^4.0.1", + "doctypes": "^1.1.0", + "js-stringify": "^1.0.2", + "pug-attrs": "^3.0.0", + "pug-error": "^2.1.0", + "pug-runtime": "^3.0.1", + "void-elements": "^3.1.0", + "with": "^7.0.0" + } + }, + "node_modules/pug-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pug-error/-/pug-error-2.1.0.tgz", + "integrity": "sha512-lv7sU9e5Jk8IeUheHata6/UThZ7RK2jnaaNztxfPYUY+VxZyk/ePVaNZ/vwmH8WqGvDz3LrNYt/+gA55NDg6Pg==", + "dev": true + }, + "node_modules/pug-filters": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-4.0.0.tgz", + "integrity": "sha512-yeNFtq5Yxmfz0f9z2rMXGw/8/4i1cCFecw/Q7+D0V2DdtII5UvqE12VaZ2AY7ri6o5RNXiweGH79OCq+2RQU4A==", + "dev": true, + "dependencies": { + "constantinople": "^4.0.1", + "jstransformer": "1.0.0", + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0", + "resolve": "^1.15.1" + } + }, + "node_modules/pug-lexer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-5.0.1.tgz", + "integrity": "sha512-0I6C62+keXlZPZkOJeVam9aBLVP2EnbeDw3An+k0/QlqdwH6rv8284nko14Na7c0TtqtogfWXcRoFE4O4Ff20w==", + "dev": true, + "dependencies": { + "character-parser": "^2.2.0", + "is-expression": "^4.0.0", + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-linker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-linker/-/pug-linker-4.0.0.tgz", + "integrity": "sha512-gjD1yzp0yxbQqnzBAdlhbgoJL5qIFJw78juN1NpTLt/mfPJ5VgC4BvkoD3G23qKzJtIIXBbcCt6FioLSFLOHdw==", + "dev": true, + "dependencies": { + "pug-error": "^2.0.0", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-load": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pug-load/-/pug-load-3.0.0.tgz", + "integrity": "sha512-OCjTEnhLWZBvS4zni/WUMjH2YSUosnsmjGBB1An7CsKQarYSWQ0GCVyd4eQPMFJqZ8w9xgs01QdiZXKVjk92EQ==", + "dev": true, + "dependencies": { + "object-assign": "^4.1.1", + "pug-walk": "^2.0.0" + } + }, + "node_modules/pug-parser": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-6.0.0.tgz", + "integrity": "sha512-ukiYM/9cH6Cml+AOl5kETtM9NR3WulyVP2y4HOU45DyMim1IeP/OOiyEWRr6qk5I5klpsBnbuHpwKmTx6WURnw==", + "dev": true, + "dependencies": { + "pug-error": "^2.0.0", + "token-stream": "1.0.0" + } + }, + "node_modules/pug-runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-runtime/-/pug-runtime-3.0.1.tgz", + "integrity": "sha512-L50zbvrQ35TkpHwv0G6aLSuueDRwc/97XdY8kL3tOT0FmhgG7UypU3VztfV/LATAvmUfYi4wNxSajhSAeNN+Kg==", + "dev": true + }, + "node_modules/pug-strip-comments": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-2.0.0.tgz", + "integrity": "sha512-zo8DsDpH7eTkPHCXFeAk1xZXJbyoTfdPlNR0bK7rpOMuhBYb0f5qUVCO1xlsitYd3w5FQTK7zpNVKb3rZoUrrQ==", + "dev": true, + "dependencies": { + "pug-error": "^2.0.0" + } + }, + "node_modules/pug-walk": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-2.0.0.tgz", + "integrity": "sha512-yYELe9Q5q9IQhuvqsZNwA5hfPkMJ8u92bQLIMcsMxf/VADjNtEYptU+inlufAFYcWdHlwNfZOEnOOQrZrcyJCQ==", + "dev": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/pyodide": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/pyodide/-/pyodide-0.26.1.tgz", + "integrity": "sha512-P+Gm88nwZqY7uBgjbQH8CqqU6Ei/rDn7pS1t02sNZsbyLJMyE2OVXjgNuqVT3KqYWnyGREUN0DbBUCJqk8R0ew==", + "dependencies": { + "ws": "^8.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/querystringify": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", + "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==" + }, + "node_modules/readdirp": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.0.2.tgz", + "integrity": "sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==", + "dev": true, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resize-observer-polyfill": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", + "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==" + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sander": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/sander/-/sander-0.5.1.tgz", + "integrity": "sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==", + "dev": true, + "dependencies": { + "es6-promise": "^3.1.2", + "graceful-fs": "^4.1.3", + "mkdirp": "^0.5.1", + "rimraf": "^2.5.2" + } + }, + "node_modules/sanitize-html": { + "version": "2.13.1", + "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.13.1.tgz", + "integrity": "sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==", + "dependencies": { + "deepmerge": "^4.2.2", + "escape-string-regexp": "^4.0.0", + "htmlparser2": "^8.0.0", + "is-plain-object": "^5.0.0", + "parse-srcset": "^1.0.2", + "postcss": "^8.3.11" + } + }, + "node_modules/sass": { + "version": "1.80.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.1.tgz", + "integrity": "sha512-9lBwDZ7j3y/1DKj5Ec249EVGo5CVpwnzIyIj+cqlCjKkApLnzsJ/l9SnV4YnORvW9dQwQN+gQvh/mFZ8CnDs7Q==", + "dev": true, + "dependencies": { + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, + "node_modules/semiver": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/semiver/-/semiver-1.1.0.tgz", + "integrity": "sha512-QNI2ChmuioGC1/xjyYwyZYADILWyW6AmS1UH6gDj/SFUUUS4MBAWs/7mxnkRPc/F4iHezDP+O8t0dO8WHiEOdg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sorcery": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/sorcery/-/sorcery-0.11.1.tgz", + "integrity": "sha512-o7npfeJE6wi6J9l0/5LKshFzZ2rMatRiCDwYeDQaOzqdzRJwALhX7mk/A/ecg6wjMu7wdZbmXfD2S/vpOg0bdQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.14", + "buffer-crc32": "^1.0.0", + "minimist": "^1.2.0", + "sander": "^0.5.0" + }, + "bin": { + "sorcery": "bin/sorcery" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-resolve": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.6.0.tgz", + "integrity": "sha512-KXBr9d/fO/bWo97NXsPIAW1bFSBOuCnjbNTBMO7N59hsv5i9yzRDfcYwwt0l04+VqnKC+EwzvJZIP/qkuMgR/w==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stylus": { + "version": "0.55.0", + "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.55.0.tgz", + "integrity": "sha512-MuzIIVRSbc8XxHH7FjkvWqkIcr1BvoMZoR/oFuAJDlh7VSaNJzrB4uJ38GRQa+mWjLXODAMzeDe0xi9GYbGwnw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "css": "^3.0.0", + "debug": "~3.1.0", + "glob": "^7.1.6", + "mkdirp": "~1.0.4", + "safer-buffer": "^2.1.2", + "sax": "~1.2.4", + "semver": "^6.3.0", + "source-map": "^0.7.3" + }, + "bin": { + "stylus": "bin/stylus" + }, + "engines": { + "node": "*" + } + }, + "node_modules/stylus/node_modules/debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/stylus/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stylus/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sugarss": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-4.0.1.tgz", + "integrity": "sha512-WCjS5NfuVJjkQzK10s8WOBY+hhDxxNt/N6ZaGwxFZ+wN3/lKKFSaaKUNecULcTTvE4urLcKaZFQD8vO0mOZujw==", + "dev": true, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.3.3" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svelte": { + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", + "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", + "dependencies": { + "@ampproject/remapping": "^2.2.1", + "@jridgewell/sourcemap-codec": "^1.4.15", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/estree": "^1.0.1", + "acorn": "^8.9.0", + "aria-query": "^5.3.0", + "axobject-query": "^4.0.0", + "code-red": "^1.0.3", + "css-tree": "^2.3.1", + "estree-walker": "^3.0.3", + "is-reference": "^3.0.1", + "locate-character": "^3.0.0", + "magic-string": "^0.30.4", + "periscopic": "^3.1.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/svelte-hmr": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", + "integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==", + "dev": true, + "engines": { + "node": "^12.20 || ^14.13.1 || >= 16" + }, + "peerDependencies": { + "svelte": "^3.19.0 || ^4.0.0" + } + }, + "node_modules/svelte-i18n": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-3.7.4.tgz", + "integrity": "sha512-yGRCNo+eBT4cPuU7IVsYTYjxB7I2V8qgUZPlHnNctJj5IgbJgV78flsRzpjZ/8iUYZrS49oCt7uxlU3AZv/N5Q==", + "dependencies": { + "cli-color": "^2.0.3", + "deepmerge": "^4.2.2", + "esbuild": "^0.19.2", + "estree-walker": "^2", + "intl-messageformat": "^9.13.0", + "sade": "^1.8.1", + "tiny-glob": "^0.2.9" + }, + "bin": { + "svelte-i18n": "dist/cli.js" + }, + "engines": { + "node": ">= 16" + }, + "peerDependencies": { + "svelte": "^3 || ^4" + } + }, + "node_modules/svelte-i18n/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/svelte-i18n/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/svelte-preprocess": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/svelte-preprocess/-/svelte-preprocess-5.1.4.tgz", + "integrity": "sha512-IvnbQ6D6Ao3Gg6ftiM5tdbR6aAETwjhHV+UKGf5bHGYR69RQvF1ho0JKPcbUON4vy4R7zom13jPjgdOWCQ5hDA==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "@types/pug": "^2.0.6", + "detect-indent": "^6.1.0", + "magic-string": "^0.30.5", + "sorcery": "^0.11.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">= 16.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.10.2", + "coffeescript": "^2.5.1", + "less": "^3.11.3 || ^4.0.0", + "postcss": "^7 || ^8", + "postcss-load-config": "^2.1.0 || ^3.0.0 || ^4.0.0 || ^5.0.0", + "pug": "^3.0.0", + "sass": "^1.26.8", + "stylus": "^0.55.0", + "sugarss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "svelte": "^3.23.0 || ^4.0.0-next.0 || ^4.0.0 || ^5.0.0-next.0", + "typescript": ">=3.9.5 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "coffeescript": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "postcss-load-config": { + "optional": true + }, + "pug": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/svelte/node_modules/acorn": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.13.0.tgz", + "integrity": "sha512-8zSiw54Oxrdym50NlZ9sUusyO1Z1ZchgRLWRaK6c86XJFClyCgFKetdowBg5bKxyp/u+CDBJG4Mpp0m3HLZl9w==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/svelte/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==" + }, + "node_modules/textlinestream": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/textlinestream/-/textlinestream-1.1.1.tgz", + "integrity": "sha512-iBHbi7BQxrFmwZUQJsT0SjNzlLLsXhvW/kg7EyOMVMBIrlnj/qYofwo1LVLZi+3GbUEo96Iu2eqToI2+lZoAEQ==" + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/timers-ext": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/timers-ext/-/timers-ext-0.1.8.tgz", + "integrity": "sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==", + "dependencies": { + "es5-ext": "^0.10.64", + "next-tick": "^1.1.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", + "dependencies": { + "globalyzer": "0.1.0", + "globrex": "^0.1.2" + } + }, + "node_modules/tldts": { + "version": "6.1.52", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.52.tgz", + "integrity": "sha512-fgrDJXDjbAverY6XnIt0lNfv8A0cf7maTEaZxNykLGsLG7XP+5xhjBTrt/ieAsFjAlZ+G5nmXomLcZDkxXnDzw==", + "dependencies": { + "tldts-core": "^6.1.52" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.52", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.52.tgz", + "integrity": "sha512-j4OxQI5rc1Ve/4m/9o2WhWSC4jGc4uVbCINdOEJRAraCi0YqTqgMcxUx7DbmuP0G3PCixoof/RZB0Q5Kh9tagw==" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/token-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/token-stream/-/token-stream-1.0.0.tgz", + "integrity": "sha512-VSsyNPPW74RpHwR8Fc21uubwHY7wMDeJLys2IX5zJNih+OnAnaifKHo+1LHT7DAdloQ7apeaaWg8l7qnf/TnEg==", + "dev": true + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.0.tgz", + "integrity": "sha512-jWVzBLplnCmoaTr13V9dYbiQ99wvZRd0vNWaDRg+aVYRcjDF3nDksxFDE/+fkXnKhpnUUkmx5pK/v8mCtLVqZA==" + }, + "node_modules/type": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/type/-/type-2.7.3.tgz", + "integrity": "sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==" + }, + "node_modules/type-fest": { + "version": "4.26.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.26.1.tgz", + "integrity": "sha512-yOGpmOAL7CkKe/91I5O3gPICmJNLJ1G4zFYVAsRHg7M64biSnPtRj0WNQt++bRkjYOqjWXrhnUw1utzmVErAdg==", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + }, + "node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/url-parse": { + "version": "1.5.10", + "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", + "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", + "dependencies": { + "querystringify": "^2.1.1", + "requires-port": "^1.0.0" + } + }, + "node_modules/vite": { + "version": "5.4.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.9.tgz", + "integrity": "sha512-20OVpJHh0PAM0oSOELa5GaZNWeDjcAvQjGXy2Uyr+Tp+/D2/Hdz6NLgpJLsarPTA2QJ6v8mX2P1ZfbsSKvdMkg==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vitefu": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz", + "integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==", + "dev": true, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz", + "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/with": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/with/-/with-7.0.2.tgz", + "integrity": "sha512-RNGKj82nUPg3g5ygxkQl0R937xLyho1J24ItRCBTr/m1YnZkzJy1hUiHUJrc/VlsDQzsCnInEGSg3bci0Lmd4w==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.9.6", + "@babel/types": "^7.9.6", + "assert-never": "^1.2.1", + "babel-walk": "3.0.0-canary-5" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yootils": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/yootils/-/yootils-0.3.1.tgz", + "integrity": "sha512-A7AMeJfGefk317I/3tBoUYRcDcNavKEkpiPN/nQsBz/viI2GvT7BtrqdPD6rGqBFN8Ax7v4obf+Cl32JF9DDVw==", + "dev": true + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..616a7db --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,51 @@ +{ + "name": "gradio_webrtc", + "version": "0.11.0-beta.3", + "description": "Gradio UI packages", + "type": "module", + "author": "", + "license": "ISC", + "private": false, + "dependencies": { + "@ffmpeg/ffmpeg": "^0.12.10", + "@ffmpeg/util": "^0.12.1", + "@gradio/atoms": "0.9.2", + "@gradio/client": "1.7.0", + "@gradio/icons": "0.8.0", + "@gradio/image": "0.16.4", + "@gradio/markdown": "^0.10.3", + "@gradio/statustracker": "0.9.1", + "@gradio/upload": "0.13.3", + "@gradio/utils": "0.7.0", + "@gradio/wasm": "0.14.2", + "hls.js": "^1.5.16", + "mrmime": "^2.0.0" + }, + "devDependencies": { + "@gradio/preview": "0.12.0", + "prettier": "3.3.3" + }, + "exports": { + "./package.json": "./package.json", + ".": { + "gradio": "./index.ts", + "svelte": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./example": { + "gradio": "./Example.svelte", + "svelte": "./dist/Example.svelte", + "types": "./dist/Example.svelte.d.ts" + } + }, + "peerDependencies": { + "svelte": "^4.0.0" + }, + "main": "index.ts", + "main_changeset": true, + "repository": { + "type": "git", + "url": "git+https://github.com/gradio-app/gradio.git", + "directory": "js/video" + } +} diff --git a/frontend/shared/AudioWave.svelte b/frontend/shared/AudioWave.svelte new file mode 100644 index 0000000..bcbb8dc --- /dev/null +++ b/frontend/shared/AudioWave.svelte @@ -0,0 +1,164 @@ + + +
+{#if icon} +
+
+ +
+
+{:else} +
+ {#each Array(numBars) as _} +
+ {/each} +
+{/if} +
+ + \ No newline at end of file diff --git a/frontend/shared/InteractiveAudio.svelte b/frontend/shared/InteractiveAudio.svelte new file mode 100644 index 0000000..5350595 --- /dev/null +++ b/frontend/shared/InteractiveAudio.svelte @@ -0,0 +1,454 @@ + + + +
+
+ + \ No newline at end of file diff --git a/frontend/shared/InteractiveVideo.svelte b/frontend/shared/InteractiveVideo.svelte new file mode 100644 index 0000000..bf07ece --- /dev/null +++ b/frontend/shared/InteractiveVideo.svelte @@ -0,0 +1,89 @@ + + + +
+ + + +
+ + diff --git a/frontend/shared/PulsingIcon.svelte b/frontend/shared/PulsingIcon.svelte new file mode 100644 index 0000000..450ac57 --- /dev/null +++ b/frontend/shared/PulsingIcon.svelte @@ -0,0 +1,151 @@ + + +
+
+ {#if pulseIntensity > 0} + {#each Array(3) as _, i} +
+ {/each} + {/if} + +
+ {#if typeof icon === "string"} + Audio visualization icon + {:else} + + {/if} +
+
+
+ + \ No newline at end of file diff --git a/frontend/shared/StaticAudio.svelte b/frontend/shared/StaticAudio.svelte new file mode 100644 index 0000000..d3d007a --- /dev/null +++ b/frontend/shared/StaticAudio.svelte @@ -0,0 +1,135 @@ + + + +