diff --git a/README.md b/README.md index 554d7b845..9f4535fa7 100644 --- a/README.md +++ b/README.md @@ -6,16 +6,23 @@ [![PyPI](https://img.shields.io/pypi/v/pipecat-ai)](https://pypi.org/project/pipecat-ai) [![Discord](https://img.shields.io/discord/1239284677165056021 )](https://discord.gg/pipecat) +[![Docs](https://img.shields.io/badge/docs-docusaurus)](https://daily-co.github.io/dailyai-docs/docs/intro) -`pipecat` is a framework for building voice (and multimodal) conversational agents. Things like personal coaches, meeting assistants, story-telling toys for kids, customer support bots, and snarky social companions. +`pipecat` is a framework for building voice (and multimodal) conversational agents. Things like personal coaches, meeting assistants, [story-telling toys for kids](https://storytelling-chatbot.fly.dev/), customer support bots, [intake flows](https://www.youtube.com/watch?v=lDevgsp9vn0), and snarky social companions. -Build things like this: +Take a lot at some example apps: -[![AI-powered voice patient intake for healthcare](https://img.youtube.com/vi/lDevgsp9vn0/0.jpg)](https://www.youtube.com/watch?v=lDevgsp9vn0) +

+   + +
+   + +

## Getting started with voice agents -You can get started with Pipecat running on your local machine, then move your agent processes to the cloud when you’re ready. You can also add a telephone number, image output, video input, use different LLMs, and more. +You can get started with Pipecat running on your local machine, then move your agent processes to the cloud when you’re ready. You can also add a 📞 telephone number, 🖼️ image output, 📺 video input, use different LLMs, and more. ```shell # install the module @@ -40,14 +47,9 @@ Your project may or may not need these, so they're made available as optional re There are two directories of examples: -- [foundational](https://github.com/pipecat-ai/pipecat/tree/main/examples/foundational) — examples that build on each other, introducing one or two concepts at a time -- [example apps](https://github.com/pipecat-ai/pipecat-examples) — complete applications that you can use as starting points for development +- [foundational](https://github.com/pipecat-ai/pipecat/tree/main/examples/foundational) — small snippets that build on each other, introducing one or two concepts at a time +- [example apps](https://github.com/pipecat-ai/pipecat/tree/main/examples/) — complete applications that you can use as starting points for development -Before running the examples you need to install the dependencies (which will install all the dependencies to run all of the examples): - -``` -pip install -r requirements.txt -``` ## A simple voice agent running locally If you’re doing AI-related stuff, you probably have an OpenAI API key. @@ -57,65 +59,111 @@ To generate voice output, one service that’s easy to get started with is Eleve So let’s run a really simple agent that’s just a GPT-4 prompt, wired up to voice input and speaker output. ```python -TBC +#app.py + +import asyncio +import aiohttp + +from pipecat.frames.frames import EndFrame, TextFrame +from pipecat.pipeline.pipeline import Pipeline +from pipecat.pipeline.task import PipelineTask +from pipecat.pipeline.runner import PipelineRunner +from pipecat.services.elevenlabs import ElevenLabsTTSService +from pipecat.transports.services.daily import DailyParams, DailyTransport + +async def main(): + async with aiohttp.ClientSession() as session: + # Use Daily as a real-time media transport (WebRTC) + daily_url = ... + daily_token = ... + transport = DailyTransport( + daily_url, daily_token, "Bot Name", DailyParams(audio_out_enabled=True)) + + # Use Eleven Labs for Text-to-Speech + tts = ElevenLabsTTSService( + aiohttp_session=session, + api_key=..., + voice_id=..., + ) + + # Simple pipeline that will process tunr text to speech and output the result + pipeline = Pipeline([tts, transport.output()]) + + # Create Pipecat processor that can run one or more pipelines tasks + runner = PipelineRunner() + + # Assign the task callable to run the pipeline + task = PipelineTask(pipeline) + + # Register an event handler to play audio when a + # participant joins the transport WebRTC session + @transport.event_handler("on_participant_joined") + async def on_new_participant_joined(transport, participant): + participant_name = participant["info"]["userName"] or '' + # Queue a TextFrame that will get spoken by the TTS service (Eleven Labs) + await task.queue_frames([TextFrame(f"Hello there, {participant_name}!"), EndFrame()]) + # Run the pipeline task + await runner.run(task) + +if __name__ == "__main__": + asyncio.run(main()) ``` Run it with: ```shell -TBC +python app.py ``` +Daily provides a prebuilt WebRTC user interface. Whilst the app is running, you can visit at `https://.daily.co/` and listen to the bot say hello! -## Example projects - -We've created a seperate repo [here](https://github.com/pipecat-ai/pipecat-examples) that have fully featured example projects to help you get started. - -```shell -git@github.com:pipecat-ai/pipecat-examples.git -``` - -## WebSockets instead of pipes -To run your agent in the cloud, you can switch the Pipecat transport layer to use a WebSocket instead of Unix pipes. - -```shell -TBC -``` ## WebRTC for production use -WebSockets are fine for server-to-server communication or for initial development. But for production use, you’ll need client-server audio to use a protocol designed for real-time media transport. (For an explanation of the difference between WebSockets and WebRTC, see [this post.]) +WebSockets are fine for server-to-server communication or for initial development. But for production use, you’ll need client-server audio to use a protocol designed for real-time media transport. (For an explanation of the difference between WebSockets and WebRTC, see [this post.](https://www.daily.co/blog/how-to-talk-to-an-llm-with-your-voice/#webrtc)) One way to get up and running quickly with WebRTC is to sign up for a Daily developer account. Daily gives you SDKs and global infrastructure for audio (and video) routing. Every account gets 10,000 audio/video/transcription minutes free each month. -Sign up [here](https://dashboard.daily.co/u/signup) and [create a room](https://docs.daily.co/reference/rest-api/rooms) in the developer Dashboard. Then run the examples, this time connecting via WebRTC instead of a WebSocket. +Sign up [here](https://dashboard.daily.co/u/signup) and [create a room](https://docs.daily.co/reference/rest-api/rooms) in the developer Dashboard. + +### What is VAD? + +Voice Activity Detection — very important for knowing when a user has finished speaking to your bot. If you are not using press-to-talk, and want Pipecat to detect when the user has finished talking, VAD is an essential component for a natural feeling conversation. + +Pipecast makes use of WebRTC VAD by default when using a WebRTC transport layer. Optionally, you can use Silero VAD for improved accuracy at the cost of higher CPU usage. + +```shell +pip install pipecat-ai[silero] +``` + +The first time your run your bot with Silero, startup may take a while whilst it downloads and caches the model in the background. You can check the progress of this in the console. ## Hacking on the framework itself _Note that you may need to set up a virtual environment before following the instructions below. For instance, you might need to run the following from the root of the repo:_ -``` +```shell python3 -m venv venv source venv/bin/activate ``` From the root of this repo, run the following: -``` +```shell pip install -r dev-requirements.txt -r {env}-requirements.txt python -m build ``` This builds the package. To use the package locally (eg to run sample files), run -``` +```shell pip install --editable . ``` If you want to use this package from another directory, you can run: -``` +```shell pip install path_to_this_repo ``` @@ -123,7 +171,7 @@ pip install path_to_this_repo From the root directory, run: -``` +```shell pytest --doctest-modules --ignore-glob="*to_be_updated*" src tests ``` @@ -170,3 +218,9 @@ Install the "--max-line-length=100" ], ``` + +## Getting help + +➡️ [Join our Discord](https://discord.gg/pipecat) + +➡️ [Reach us on Twitter](https://x.com/pipecat_ai) \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 3de18ea09..1c3845cb7 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,10 +8,3 @@ Learn about the thinking behind the framework's design. See how a Frame is processed through a Transport, a Pipeline, and a series of Frame Processors. -## [Example Code](examples/) - -The repository includes several example apps in the `examples` directory. The docs explain how they work. - -## [API Reference](api/) - -Complete documentation of the available classes and methods in the SDK. diff --git a/docs/examples/01-say-one-thing.md b/docs/examples/01-say-one-thing.md deleted file mode 100644 index 3d8231ded..000000000 --- a/docs/examples/01-say-one-thing.md +++ /dev/null @@ -1,119 +0,0 @@ -# 01: Say One Thing - -_video here - youtube?_ - -This example uses a text-to-speech (TTS) service to say one predefined sentence. But first, a quick overview of the general structure of these examples. - -## Running the demos - -All of the demos have something like this at the bottom of the file: - -```python -if __name__ == "__main__": - (url, token) = configure() - asyncio.run(main(url, token)) -``` - -### `configure()` - -The `configure()` function comes from `examples/foundational/support/runner.py`, and it allows you to configure the examples from the command line directly, or using environment variables: - -```bash -python 01-say-one-thing.py -u https://YOUR_DOMAIN.daily.co/YOUR_ROOM -k YOUR_API_KEY -# or -DAILY_ROOM_URL=https://YOUR_DOMAIN.daily.co/YOUR_ROOM DAILY_API_KEY=YOUR_API_KEY python 01-say-one-thing.py -# or set DAILY_ROOM_URL and DAILY_API_KEY in a .env file -python 01-say-one-thing.py -``` - -You'll need a Daily account to run these demos. You can sign up for free at [daily.co](https://daily.co). Once you've signed up you can create a room from the [Dashboard](https://dashboard.daily.co/rooms), and grab [your API key](https://dashboard.daily.co/developers) while you're there. - -Some functionality (such as transcription) requires the bot to have owner privileges in the room. `runner.py` uses the Daily REST API to create a meeting token with owner privileges. You can learn more about meeting tokens in the [Daily docs](https://docs.daily.co/reference/rest-api/meeting-tokens). - -### `asyncio.run()` - -The AI SDK makes heavy use of Python's `asyncio` module. [This is a reasonable intro to the topic](https://builtin.com/data-science/asyncio) if you haven't worked with `asyncio` and coroutines before. - -You can learn a bit more about the specifics of how the Daily AI SDK uses coroutines in the [Architecture Guide](../architecture.md). - -## The `main()` function - -All of the examples have a `main()` function with a similar structure: - -- Configure the transport -- Configure the AI service(s) used in the demo -- Configure any event listeners -- Define a processing pipeline -- Run the example's coroutine(s) - -### Configuring the transport - -The first section of the `main()` function configures the transport object: - -```python -meeting_duration_minutes = 5 -transport = DailyTransportService( - room_url, - None, - "Say One Thing", - meeting_duration_minutes, -) -transport.mic_enabled = True -``` - -The [Architecture Guide](../architecture.md) explains the transport object in more detail. In this case, we're configuring a Daily transport object and enabling the virtual microphone, so our bot can play audio. - -### Configuring the services - -As described in the [Architecture Guide](../architecture.md), 'a 'Service' is a class that processes 'Frames' as part of a 'Pipeline'. In this demo app, we'll only need one service: a text-to-speech generator. We can create an instance of the `ElevenLabsTTSService` class with this line of code: - -```python -tts = ElevenLabsTTSService(aiohttp_session=session, api_key=os.getenv("ELEVENLABS_API_KEY"), voice_id=os.getenv("ELEVENLABS_VOICE_ID")) -``` - -You'll need to make sure and set those environment variables somewhere. The easiest way to do that is to copy the `example.env` file in the repo and rename it to `.env`, and then add your credentials to that file. `runner.py` loads the `python-dotenv` module and initializes it, making the values in that file available in the environment. - -### Configuring event listeners - -This part isn't strictly necessary for an app like this. You could include the contents of the `on_participant_joined` function directly in the body of the `main()` function, and it would run as soon as you started the script from the command line. - -Instead, we can use an event handler to wait to run that code until someone else joins the meeting. We'll define a function called `greet_user()`, and use the `@transport.event_handler("on_participant_joined")` decorator to tell the SDK that we want to run that function whenever a user joins the room. - -```python -@transport.event_handler("on_participant_joined") -async def greet_user(transport, participant): - if participant["info"]["isLocal"]: - return - - await tts.say( - "Hello there, " + participant["info"]["userName"] + "!", - transport.send_queue, - ) - - # wait for the output queue to be empty, then leave the meeting - await transport.stop_when_done() -``` - -### Defining a processing pipeline - -In this example, we don't actually have much of a processing pipeline! In fact, we're doing the whole thing inside the `greet_user()` function already. - -Pipelines usually look like a bunch of nested calls to the `run()` or `run_to_queue()` function from different Services. In this example, we're using the `say()` function from the TTS service. This is effectively a convenience wrapper around the `run_to_queue()` function, which we'll discuss more later. It's important to `await` this function to ensure that the speech frames are queued for playback before the next line of code, because of the `stop_when_done()` function being called immediately afterward. - -The output of the `say()` function goes to the transport's `send_queue`. This queue is the all-important connection between the world of the Services pipeline that's generating frames asynchronously and the ordered playback of audio and visual media in the WebRTC call. - -### Running the coroutines - -In this example, we don't actually have any separate processing pipelines—everything happens as a result of an event from the transport. So we only need to run the transport's coroutine, and await its completion: - -```python -await transport.run() -``` - -In future examples, we'll run more processes in parallel. For now, this script can run until the transport exits—which will happen based on calling `stop_when_done()` in the `greet_user()` function. - -## Next Steps - -Next, we'll start connecting multiple AI services together by building a service pipeline. - -## [02 - LLM Say One Thing »](02-llm-say-one-thing.md) diff --git a/docs/examples/README.md b/docs/examples/README.md deleted file mode 100644 index dea68ffc9..000000000 --- a/docs/examples/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Daily AI SDK Examples - -The docs in this folder pair with the example apps located in `examples/foundational`. They are designed to serve as a quick references for building different kinds of AI apps. But the examples also build on one another, so it can be really helpful to walk through them in order. - -To start, you can learn about the overall structure of the examples in [01 - Say One Thing](01-say-one-thing.md). diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..36b26333a --- /dev/null +++ b/examples/README.md @@ -0,0 +1,93 @@ + + +# Pipecat — Examples + +## Foundational snippets +Small snippets that build on each other, introducing one or two concepts at a time. + +➡️ [Take a look](https://github.com/pipecat-ai/pipecat/tree/main/examples/foundational) + +## Chatbot examples +Collection of self-contained real-time voice and video AI demo applications built with [pipecat](https://github.com/pipecat-ai/pipecat/). + +

+   + +
+   + +

+ + +### Quickstart + +Each project has its own set of dependencies and configuration variables. They intentionally avoids shared code across projects — you can grab whichever demo folder you want to work with as a starting point. + +We recommend you start with a virtual environment: + +```shell +cd pipecat-ai/examples/simple-chatbot + +python -m venv venv + +source venv/bin/activate + +pip install -r requirements.txt +``` + +Next, follow the steps in the README for each demo. + +ℹ️ Make sure you `pip install -r requirements.txt` for each demo project, so you can be sure to have the necessary service dependencies that extend the functionality of Pipecat. You can read more about the framework architecture [here](https://github.com/pipecat-ai/pipecat/tree/main/docs). + +## Projects: + +| Project | Description | Services | +| -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------- | +| [Simple Chatbot](simple-chatbot) | Basic voice-driven conversational bot. A good starting point for learning the flow of the framework. | Deepgram, OpenAI, Daily, Daily Prebuilt UI | +| [Storytelling Chatbot](storytelling-chatbot) | Stitches together multiple third-party services to create a collaborative storytime experience. | Deepgram, ElevenLabs, Open AI, Fal, Daily, Custom UI | +| [Translation Chatbot](translation-chatbot) | Listens for user speech, then translates that speech to Spanish and speaks the translation back. Demonstrates multi-participant use-cases. | Deepgram, Azure, OpenAI, Daily, Daily Prebuilt UI | +| [Moondream Chatbot](moondream-chatbot) | Demonstrates how to add vision capabilities to GPT4. **Note: works best with a GPU** | Deepgram, OpenAI, Moondream, Daily, Daily Prebuilt UI | +| Function-calling Chatbot (TBC) | A chatbot that can call functions in response to user input | Deepgram, OpenAI, Fireworks, Daily, Daily Prebuilt UI | + +> [!IMPORTANT] +> These example projects use Daily as a WebRTC transport and can be joined using their hosted Prebuilt UI. +> It provides a quick way to join a real-time session with your bot and test your ideas without building any frontend code. If you'd like to see an example of a custom UI, try Storybot. + + +## FAQ + +### Deployment + +For each of these demos we've included a `Dockerfile`. Out of the box, this should provide everything needed to get the respective demo running on a VM: + +```shell +docker build username/app:tag . + +docker run -p 7860:7860 --env-file ./.env username/app:tag + +docker push ... +``` + +### SSL + +If you're working with a custom UI (such as with the Storytelling Chatbot), it's important to ensure your deployment platform supports HTTPS, as accessing user devices such as mics and webcams requires SSL. + +If you try to run a custom UI without SSL, you may see an error in the console telling you that `navigator` is undefined, or no devices are available. + +### Are these examples production ready? + +Yes, kind of. + +These demos attempt to keep things simple and are unopinionated regarding environment or scalability. + +We're using FastAPI to spawn a subprocess for the bots / agents — useful for small tests, but not so great for production grade apps with many concurrent users. You can see how this works in each project's `start` endpoint in `server.py`. + +Creating virtualized worker pools and on-demand instances is out of scope for these examples, but we hope to add some examples to this repo soon! + +For projects that have CUDA as a requirement, such as Moondream Chatbot, be sure to deploy to a GPU-powered platform (such as [fly.io](https://fly.io) or [Runpod](https://runpod.io).) + +## Getting help + +➡️ [Join our Discord](https://discord.gg/pipecat) + +➡️ [Reach us on Twitter](https://x.com/pipecat_ai) diff --git a/examples/image-gen.py b/examples/image-gen.py deleted file mode 100644 index d9b2cdc1e..000000000 --- a/examples/image-gen.py +++ /dev/null @@ -1,122 +0,0 @@ -import argparse -import asyncio -import requests -import time -import urllib.parse -import random - -from pipecat.transports.daily_transport import DailyTransport -from pipecat.services.azure_ai_services import AzureLLMService, AzureTTSService -from pipecat.pipeline.frames import Frame -from pipecat.services.fal_ai_services import FalImageGenService - - -async def main(room_url: str, token): - global transport - global llm - global tts - - transport = DailyTransport( - room_url, - token, - "Imagebot", - 1, - ) - transport._mic_enabled = True - transport._camera_enabled = True - transport._mic_sample_rate = 16000 - transport._camera_width = 1024 - transport._camera_height = 1024 - - llm = AzureLLMService() - tts = AzureTTSService() - img = FalImageGenService() - - async def handle_transcriptions(): - print("handle_transcriptions got called") - - sentence = "" - async for message in transport.get_transcriptions(): - print(f"transcription message: {message}") - if message["session_id"] == transport._my_participant_id: - continue - finder = message["text"].find("start over") - print(f"finder: {finder}") - if finder >= 0: - async for audio in tts.run_tts(f"Resetting."): - transport.output_queue.put( - Frame(FrameType.AUDIO_FRAME, audio)) - sentence = "" - continue - # todo: we could differentiate between transcriptions from - # different participants - sentence += f" {message['text']}" - print(f"sentence is now: {sentence}") - # TODO: Cache this audio - phrase = random.choice( - ["OK.", "Got it.", "Sure.", "You bet.", "Sure thing."]) - async for audio in tts.run_tts(phrase): - transport.output_queue.put(Frame(FrameType.AUDIO_FRAME, audio)) - img_result = img.run_image_gen(sentence, "1024x1024") - awaited_img = await asyncio.gather(img_result) - transport.output_queue.put( - [ - Frame(FrameType.IMAGE_FRAME, awaited_img[0][1]), - ] - ) - - @transport.event_handler("on_participant_joined") - async def on_participant_joined(transport, participant): - print(f"participant joined: {participant['info']['userName']}") - if participant["info"]["isLocal"]: - return - async for audio in tts.run_tts("Describe an image, and I'll create it."): - audio_generator = tts.run_tts( - f"Hello, {participant['info']['userName']}! Describe an image and I'll create it. To start over, just say 'start over'.") - async for audio in audio_generator: - transport.output_queue.put(Frame(FrameType.AUDIO_FRAME, audio)) - - await asyncio.gather(transport.run(), handle_transcriptions()) - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description="Simple Daily Bot Sample") - parser.add_argument( - "-u", - "--url", - type=str, - required=True, - help="URL of the Daily room to join") - parser.add_argument( - "-k", - "--apikey", - type=str, - required=True, - help="Daily API Key (needed to create token)", - ) - - args, unknown = parser.parse_known_args() - - # Create a meeting token for the given room with an expiration 1 hour in - # the future. - room_name: str = urllib.parse.urlparse(args.url).path[1:] - expiration: float = time.time() + 60 * 60 - - res: requests.Response = requests.post( - f"https://api.daily.co/v1/meeting-tokens", - headers={ - "Authorization": f"Bearer {args.apikey}"}, - json={ - "properties": { - "room_name": room_name, - "is_owner": True, - "exp": expiration}}, - ) - - if res.status_code != 200: - raise Exception( - f"Failed to create meeting token: {res.status_code} {res.text}") - - token: str = res.json()["token"] - - asyncio.run(main(args.url, token)) diff --git a/examples/internal/11a-dial-out.py b/examples/internal/11a-dial-out.py deleted file mode 100644 index f5d013eb1..000000000 --- a/examples/internal/11a-dial-out.py +++ /dev/null @@ -1,135 +0,0 @@ -import aiohttp -import asyncio -import os -import wave - -from pipecat.transports.daily_transport import DailyTransport -from pipecat.services.azure_ai_services import AzureLLMService, AzureTTSService -from pipecat.pipeline.aggregators import LLMContextAggregator -from pipecat.services.ai_services import AIService, FrameLogger -from pipecat.pipeline.frames import Frame, AudioFrame, LLMResponseEndFrame, LLMMessagesFrame -from typing import AsyncGenerator - -from runner import configure - -from dotenv import load_dotenv -load_dotenv(override=True) - -sounds = {} -sound_files = [ - 'ding1.wav', - 'ding2.wav' -] - -script_dir = os.path.dirname(__file__) - -for file in sound_files: - # Build the full path to the image file - full_path = os.path.join(script_dir, "assets", file) - # Get the filename without the extension to use as the dictionary key - filename = os.path.splitext(os.path.basename(full_path))[0] - # Open the image and convert it to bytes - with wave.open(full_path) as audio_file: - sounds[file] = audio_file.readframes(-1) - - -class OutboundSoundEffectWrapper(AIService): - def __init__(self): - pass - - async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: - if isinstance(frame, LLMResponseEndFrame): - yield AudioFrame(sounds["ding1.wav"]) - # In case anything else up the stack needs it - yield frame - else: - yield frame - - -class InboundSoundEffectWrapper(AIService): - def __init__(self): - pass - - async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: - if isinstance(frame, LLMMessagesFrame): - yield AudioFrame(sounds["ding2.wav"]) - # In case anything else up the stack needs it - yield frame - else: - yield frame - - -async def main(room_url: str, token, phone): - async with aiohttp.ClientSession() as session: - - global transport - global llm - global tts - - transport = DailyTransport( - room_url, - token, - "Respond bot", - 300, - ) - transport._mic_enabled = True - transport._mic_sample_rate = 16000 - transport._camera_enabled = False - - llm = AzureLLMService() - tts = AzureTTSService() - - @transport.event_handler("on_first_other_participant_joined") - async def on_first_other_participant_joined(transport, participant): - await tts.say("Hi, I'm listening!", transport.send_queue) - await transport.send_queue.put(AudioFrame(sounds["ding1.wav"])) - - async def handle_transcriptions(): - messages = [ - {"role": "system", "content": "You are a helpful LLM in a WebRTC call. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way."}, - ] - - tma_in = LLMContextAggregator( - messages, "user", transport._my_participant_id - ) - tma_out = LLMContextAggregator( - messages, "assistant", transport._my_participant_id - ) - out_sound = OutboundSoundEffectWrapper() - in_sound = InboundSoundEffectWrapper() - fl = FrameLogger("LLM Out") - fl2 = FrameLogger("Transcription In") - await out_sound.run_to_queue( - transport.send_queue, - tts.run( - tma_out.run( - llm.run( - fl2.run( - in_sound.run( - tma_in.run( - transport.get_receive_frames() - ) - ) - ) - ) - ) - ) - ) - - @transport.event_handler("on_participant_joined") - async def pax_joined(transport, pax): - print(f"PARTICIPANT JOINED: {pax}") - - @transport.event_handler("on_call_state_updated") - async def on_call_state_updated(transport, state): - if (state == "joined"): - if (phone): - transport.start_recording() - transport.dialout(phone) - - await asyncio.gather(transport.run(), handle_transcriptions()) - - -if __name__ == "__main__": - (url, token) = configure() - asyncio.run(main(url, token)) diff --git a/examples/moondream-chatbot/.dockerignore b/examples/moondream-chatbot/.dockerignore new file mode 100644 index 000000000..994402bfd --- /dev/null +++ b/examples/moondream-chatbot/.dockerignore @@ -0,0 +1,163 @@ +# flyctl launch added from .gitignore +# Byte-compiled / optimized / DLL files +**/__pycache__ +**/*.py[cod] +**/*$py.class + +# C extensions +**/*.so + +# Distribution / packaging +**/.Python +**/build +**/develop-eggs +**/dist +**/downloads +**/eggs +**/.eggs +**/lib +**/lib64 +**/parts +**/sdist +**/var +**/wheels +**/share/python-wheels +**/*.egg-info +**/.installed.cfg +**/*.egg +**/MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +**/*.manifest +**/*.spec + +# Installer logs +**/pip-log.txt +**/pip-delete-this-directory.txt + +# Unit test / coverage reports +**/htmlcov +**/.tox +**/.nox +**/.coverage +**/.coverage.* +**/.cache +**/nosetests.xml +**/coverage.xml +**/*.cover +**/*.py,cover +**/.hypothesis +**/.pytest_cache +**/cover + +# Translations +**/*.mo +**/*.pot + +# Django stuff: +**/*.log +**/local_settings.py +**/db.sqlite3 +**/db.sqlite3-journal + +# Flask stuff: +**/instance +**/.webassets-cache + +# Scrapy stuff: +**/.scrapy + +# Sphinx documentation +**/docs/_build + +# PyBuilder +**/.pybuilder +**/target + +# Jupyter Notebook +**/.ipynb_checkpoints + +# IPython +**/profile_default +**/ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +**/.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +**/__pypackages__ + +# Celery stuff +**/celerybeat-schedule +**/celerybeat.pid + +# SageMath parsed files +**/*.sage.py + +# Environments +**/.env +**/.venv +**/env +**/venv +**/ENV +**/env.bak +**/venv.bak + +# Spyder project settings +**/.spyderproject +**/.spyproject + +# Rope project settings +**/.ropeproject + +# mkdocs documentation +site + +# mypy +**/.mypy_cache +**/.dmypy.json +**/dmypy.json + +# Pyre type checker +**/.pyre + +# pytype static type analyzer +**/.pytype + +# Cython debug symbols +**/cython_debug + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +**/runpod.toml +fly.toml diff --git a/examples/moondream-chatbot/.gitignore b/examples/moondream-chatbot/.gitignore new file mode 100644 index 000000000..2bc1403d1 --- /dev/null +++ b/examples/moondream-chatbot/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +runpod.toml diff --git a/examples/moondream-chatbot/Dockerfile b/examples/moondream-chatbot/Dockerfile new file mode 100644 index 000000000..d5948d5c1 --- /dev/null +++ b/examples/moondream-chatbot/Dockerfile @@ -0,0 +1,25 @@ +FROM ubuntu:22.04 + +RUN apt-get update && apt-get install -y wget +RUN wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/cuda-keyring_1.1-1_all.deb +RUN dpkg -i cuda-keyring_1.1-1_all.deb + +RUN echo "deb [signed-by=/usr/share/keyrings/cuda-archive-keyring.gpg] https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2204/x86_64/ /" > /etc/apt/sources.list.d/cuda-ubuntu2204-x86_64.list + +RUN apt-get update && apt-get install -y python3 python3-pip +RUN apt-get install -y cuda-nvcc-12-4 libcublas-12-4 libcudnn8 + +RUN mkdir /app +RUN mkdir /app/assets +RUN mkdir /app/utils +COPY *.py /app/ +COPY requirements.txt /app/ +copy assets/* /app/assets/ +copy utils/* /app/utils/ + +WORKDIR /app +RUN pip3 install -r requirements.txt + +EXPOSE 7860 + +CMD ["python3", "server.py"] diff --git a/examples/moondream-chatbot/README.md b/examples/moondream-chatbot/README.md new file mode 100644 index 000000000..f85531c9a --- /dev/null +++ b/examples/moondream-chatbot/README.md @@ -0,0 +1,37 @@ +# Moondream Chatbot + + + + +This app connects you to a chatbot powered by GPT-4, complete with animations generated by Stable Video Diffusion. The chatbot also has vision powers thanks to [Moondream](https://moondream.ai) so you can ask it, for example, "what do you see?". + +ℹ️ The first time, things might take some time to get started since VAD (Voice Activity Detection) and vision models need to be downloaded. + +## Get started + +```python +python3 -m venv env +source env/bin/activate +pip install -r requirements.txt + +cp env.example .env # and add your credentials + +``` + +## Run the server + +```bash +python server.py +``` + +Then, visit `http://localhost:7860/start` in your browser to start a chatbot +session. + +## Build and test the Docker image + +``` +docker build -t moonbot . +docker run --env-file .env -p 7860:7860 moonbot +``` + +You can try to visit `http://localhost:7860/start` again. diff --git a/examples/starter-apps/assets/robot01.png b/examples/moondream-chatbot/assets/robot01.png similarity index 100% rename from examples/starter-apps/assets/robot01.png rename to examples/moondream-chatbot/assets/robot01.png diff --git a/examples/starter-apps/assets/robot010.png b/examples/moondream-chatbot/assets/robot010.png similarity index 100% rename from examples/starter-apps/assets/robot010.png rename to examples/moondream-chatbot/assets/robot010.png diff --git a/examples/starter-apps/assets/robot011.png b/examples/moondream-chatbot/assets/robot011.png similarity index 100% rename from examples/starter-apps/assets/robot011.png rename to examples/moondream-chatbot/assets/robot011.png diff --git a/examples/starter-apps/assets/robot012.png b/examples/moondream-chatbot/assets/robot012.png similarity index 100% rename from examples/starter-apps/assets/robot012.png rename to examples/moondream-chatbot/assets/robot012.png diff --git a/examples/starter-apps/assets/robot013.png b/examples/moondream-chatbot/assets/robot013.png similarity index 100% rename from examples/starter-apps/assets/robot013.png rename to examples/moondream-chatbot/assets/robot013.png diff --git a/examples/starter-apps/assets/robot014.png b/examples/moondream-chatbot/assets/robot014.png similarity index 100% rename from examples/starter-apps/assets/robot014.png rename to examples/moondream-chatbot/assets/robot014.png diff --git a/examples/starter-apps/assets/robot015.png b/examples/moondream-chatbot/assets/robot015.png similarity index 100% rename from examples/starter-apps/assets/robot015.png rename to examples/moondream-chatbot/assets/robot015.png diff --git a/examples/starter-apps/assets/robot016.png b/examples/moondream-chatbot/assets/robot016.png similarity index 100% rename from examples/starter-apps/assets/robot016.png rename to examples/moondream-chatbot/assets/robot016.png diff --git a/examples/starter-apps/assets/robot017.png b/examples/moondream-chatbot/assets/robot017.png similarity index 100% rename from examples/starter-apps/assets/robot017.png rename to examples/moondream-chatbot/assets/robot017.png diff --git a/examples/starter-apps/assets/robot018.png b/examples/moondream-chatbot/assets/robot018.png similarity index 100% rename from examples/starter-apps/assets/robot018.png rename to examples/moondream-chatbot/assets/robot018.png diff --git a/examples/starter-apps/assets/robot019.png b/examples/moondream-chatbot/assets/robot019.png similarity index 100% rename from examples/starter-apps/assets/robot019.png rename to examples/moondream-chatbot/assets/robot019.png diff --git a/examples/starter-apps/assets/robot02.png b/examples/moondream-chatbot/assets/robot02.png similarity index 100% rename from examples/starter-apps/assets/robot02.png rename to examples/moondream-chatbot/assets/robot02.png diff --git a/examples/starter-apps/assets/robot020.png b/examples/moondream-chatbot/assets/robot020.png similarity index 100% rename from examples/starter-apps/assets/robot020.png rename to examples/moondream-chatbot/assets/robot020.png diff --git a/examples/starter-apps/assets/robot021.png b/examples/moondream-chatbot/assets/robot021.png similarity index 100% rename from examples/starter-apps/assets/robot021.png rename to examples/moondream-chatbot/assets/robot021.png diff --git a/examples/starter-apps/assets/robot022.png b/examples/moondream-chatbot/assets/robot022.png similarity index 100% rename from examples/starter-apps/assets/robot022.png rename to examples/moondream-chatbot/assets/robot022.png diff --git a/examples/starter-apps/assets/robot023.png b/examples/moondream-chatbot/assets/robot023.png similarity index 100% rename from examples/starter-apps/assets/robot023.png rename to examples/moondream-chatbot/assets/robot023.png diff --git a/examples/starter-apps/assets/robot024.png b/examples/moondream-chatbot/assets/robot024.png similarity index 100% rename from examples/starter-apps/assets/robot024.png rename to examples/moondream-chatbot/assets/robot024.png diff --git a/examples/starter-apps/assets/robot025.png b/examples/moondream-chatbot/assets/robot025.png similarity index 100% rename from examples/starter-apps/assets/robot025.png rename to examples/moondream-chatbot/assets/robot025.png diff --git a/examples/starter-apps/assets/robot03.png b/examples/moondream-chatbot/assets/robot03.png similarity index 100% rename from examples/starter-apps/assets/robot03.png rename to examples/moondream-chatbot/assets/robot03.png diff --git a/examples/starter-apps/assets/robot04.png b/examples/moondream-chatbot/assets/robot04.png similarity index 100% rename from examples/starter-apps/assets/robot04.png rename to examples/moondream-chatbot/assets/robot04.png diff --git a/examples/starter-apps/assets/robot05.png b/examples/moondream-chatbot/assets/robot05.png similarity index 100% rename from examples/starter-apps/assets/robot05.png rename to examples/moondream-chatbot/assets/robot05.png diff --git a/examples/starter-apps/assets/robot06.png b/examples/moondream-chatbot/assets/robot06.png similarity index 100% rename from examples/starter-apps/assets/robot06.png rename to examples/moondream-chatbot/assets/robot06.png diff --git a/examples/starter-apps/assets/robot07.png b/examples/moondream-chatbot/assets/robot07.png similarity index 100% rename from examples/starter-apps/assets/robot07.png rename to examples/moondream-chatbot/assets/robot07.png diff --git a/examples/starter-apps/assets/robot08.png b/examples/moondream-chatbot/assets/robot08.png similarity index 100% rename from examples/starter-apps/assets/robot08.png rename to examples/moondream-chatbot/assets/robot08.png diff --git a/examples/starter-apps/assets/robot09.png b/examples/moondream-chatbot/assets/robot09.png similarity index 100% rename from examples/starter-apps/assets/robot09.png rename to examples/moondream-chatbot/assets/robot09.png diff --git a/examples/moondream-chatbot/bot.py b/examples/moondream-chatbot/bot.py new file mode 100644 index 000000000..56d5b72da --- /dev/null +++ b/examples/moondream-chatbot/bot.py @@ -0,0 +1,210 @@ +import asyncio + +import aiohttp +import logging +import os +from PIL import Image +from typing import AsyncGenerator + +from dailyai.pipeline.aggregators import ( + LLMUserResponseAggregator, + ParallelPipeline, + VisionImageFrameAggregator, + SentenceAggregator +) +from dailyai.pipeline.frames import ( + ImageFrame, + SpriteFrame, + Frame, + LLMMessagesFrame, + AudioFrame, + PipelineStartedFrame, + TTSEndFrame, + TextFrame, + UserImageFrame, + UserImageRequestFrame, +) +from dailyai.services.moondream_ai_service import MoondreamService +from dailyai.pipeline.pipeline import FrameProcessor, Pipeline +from dailyai.transports.daily_transport import DailyTransport +from dailyai.services.open_ai_services import OpenAILLMService +from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService + +from runner import configure + +from dotenv import load_dotenv +load_dotenv(override=True) + +logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s") +logger = logging.getLogger("dailyai") +logger.setLevel(logging.DEBUG) + +user_request_answer = "Let me take a look." + +sprites = [] + +script_dir = os.path.dirname(__file__) + +for i in range(1, 26): + # Build the full path to the image file + full_path = os.path.join(script_dir, f"assets/robot0{i}.png") + # Get the filename without the extension to use as the dictionary key + # Open the image and convert it to bytes + with Image.open(full_path) as img: + sprites.append(img.tobytes()) + +flipped = sprites[::-1] +sprites.extend(flipped) + +# When the bot isn't talking, show a static image of the cat listening +quiet_frame = ImageFrame(sprites[0], (1024, 576)) +talking_frame = SpriteFrame(images=sprites) + + +class TalkingAnimation(FrameProcessor): + """ + This class starts a talking animation when it receives an first AudioFrame, + and then returns to a "quiet" sprite when it sees a LLMResponseEndFrame. + """ + + def __init__(self): + super().__init__() + self._is_talking = False + + async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: + if isinstance(frame, AudioFrame): + if not self._is_talking: + yield talking_frame + yield frame + self._is_talking = True + else: + yield frame + elif isinstance(frame, TTSEndFrame): + yield quiet_frame + yield frame + self._is_talking = False + else: + yield frame + + +class AnimationInitializer(FrameProcessor): + def __init__(self): + super().__init__() + + async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: + if isinstance(frame, PipelineStartedFrame): + yield quiet_frame + yield frame + else: + yield frame + + +class UserImageRequester(FrameProcessor): + participant_id: str | None + + def __init__(self): + super().__init__() + self.participant_id = None + + def set_participant_id(self, participant_id: str): + self.participant_id = participant_id + + async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: + if self.participant_id and isinstance(frame, TextFrame): + if frame.text == user_request_answer: + yield UserImageRequestFrame(self.participant_id) + yield TextFrame("Describe the image in a short sentence.") + elif isinstance(frame, UserImageFrame): + yield frame + + +class TextFilterProcessor(FrameProcessor): + text: str + + def __init__(self, text: str): + self.text = text + + async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: + if isinstance(frame, TextFrame): + if frame.text != self.text: + yield frame + else: + yield frame + + +class ImageFilterProcessor(FrameProcessor): + async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: + if not isinstance(frame, ImageFrame): + yield frame + + +async def main(room_url: str, token): + async with aiohttp.ClientSession() as session: + transport = DailyTransport( + room_url, + token, + "Chatbot", + duration_minutes=5, + start_transcription=True, + mic_enabled=True, + mic_sample_rate=16000, + camera_enabled=True, + camera_width=1024, + camera_height=576, + vad_enabled=True, + video_rendering_enabled=True + ) + + tts = ElevenLabsTTSService( + aiohttp_session=session, + api_key=os.getenv("ELEVENLABS_API_KEY"), + voice_id="pNInz6obpgDQGcFmaJgB", + ) + + llm = OpenAILLMService( + api_key=os.getenv("OPENAI_API_KEY"), + model="gpt-4-turbo-preview") + + ta = TalkingAnimation() + ai = AnimationInitializer() + + sa = SentenceAggregator() + ir = UserImageRequester() + va = VisionImageFrameAggregator() + # If you run into weird description, try with use_cpu=True + moondream = MoondreamService() + + tf = TextFilterProcessor(user_request_answer) + imgf = ImageFilterProcessor() + + messages = [ + { + "role": "system", + "content": f"You are Chatbot, a friendly, helpful robot. Let the user know that you are capable of chatting or describing what you see. Your goal is to demonstrate your capabilities in a succinct way. Reply with only '{user_request_answer}' if the user asks you to describe what you see. Your output will be converted to audio so never include special characters in your answers. Respond to what the user said in a creative and helpful way, but keep your responses brief. Start by introducing yourself.", + }, + ] + + ura = LLMUserResponseAggregator(messages) + + pipeline = Pipeline([ + ai, ura, llm, ParallelPipeline( + [[sa, ir, va, moondream], [tf, imgf]] + ), + tts, ta + ]) + + @transport.event_handler("on_first_other_participant_joined") + async def on_first_other_participant_joined(transport, participant): + transport.render_participant_video(participant["id"], framerate=0) + ir.set_participant_id(participant["id"]) + await pipeline.queue_frames([LLMMessagesFrame(messages)]) + + transport.transcription_settings["extra"]["endpointing"] = True + transport.transcription_settings["extra"]["punctuate"] = True + + await asyncio.gather(transport.run(pipeline)) + + +if __name__ == "__main__": + (url, token) = configure() + asyncio.run(main(url, token)) diff --git a/examples/moondream-chatbot/env.example b/examples/moondream-chatbot/env.example new file mode 100644 index 000000000..cdad19c2f --- /dev/null +++ b/examples/moondream-chatbot/env.example @@ -0,0 +1,4 @@ +DAILY_SAMPLE_ROOM_URL=https://yourdomain.daily.co/yourroom # (for joining the bot to the same room repeatedly for local dev) +DAILY_API_KEY=7df... +OPENAI_API_KEY=sk-PL... +ELEVENLABS_API_KEY=aeb... diff --git a/examples/moondream-chatbot/image.png b/examples/moondream-chatbot/image.png new file mode 100644 index 000000000..17ba5401f Binary files /dev/null and b/examples/moondream-chatbot/image.png differ diff --git a/examples/moondream-chatbot/requirements.txt b/examples/moondream-chatbot/requirements.txt new file mode 100644 index 000000000..f0dc27585 --- /dev/null +++ b/examples/moondream-chatbot/requirements.txt @@ -0,0 +1,5 @@ +python-dotenv +requests +fastapi[all] +uvicorn +dailyai[daily,moondream,openai,silero] diff --git a/examples/starter-apps/runner.py b/examples/moondream-chatbot/runner.py similarity index 100% rename from examples/starter-apps/runner.py rename to examples/moondream-chatbot/runner.py diff --git a/examples/moondream-chatbot/server.py b/examples/moondream-chatbot/server.py new file mode 100644 index 000000000..fe383c0aa --- /dev/null +++ b/examples/moondream-chatbot/server.py @@ -0,0 +1,124 @@ +import os +import argparse +import subprocess +import atexit + +from fastapi import FastAPI, Request, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.responses import JSONResponse, RedirectResponse + +from utils.daily_helpers import create_room as _create_room, get_token, get_name_from_url + +MAX_BOTS_PER_ROOM = 1 + +# Bot sub-process dict for status reporting and concurrency control +bot_procs = {} + + +def cleanup(): + # Clean up function, just to be extra safe + for proc in bot_procs.values(): + proc.terminate() + proc.wait() + + +atexit.register(cleanup) + + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/start") +async def start_agent(request: Request): + print(f"!!! Creating room") + room_url, room_name = _create_room() + print(f"!!! Room URL: {room_url}") + # Ensure the room property is present + if not room_url: + raise HTTPException( + status_code=500, + detail="Missing 'room' property in request data. Cannot start agent without a target room!") + + # Check if there is already an existing process running in this room + num_bots_in_room = sum( + 1 for proc in bot_procs.values() if proc[1] == room_url and proc[0].poll() is None) + if num_bots_in_room >= MAX_BOTS_PER_ROOM: + raise HTTPException( + status_code=500, detail=f"Max bot limited reach for room: {room_url}") + + # Get the token for the room + token = get_token(room_url) + + if not token: + raise HTTPException( + status_code=500, detail=f"Failed to get token for room: {room_url}") + + # Spawn a new agent, and join the user session + # Note: this is mostly for demonstration purposes (refer to 'deployment' in README) + try: + proc = subprocess.Popen( + [ + f"python3 -m bot -u {room_url} -t {token}" + ], + shell=True, + bufsize=1, + cwd=os.path.dirname(os.path.abspath(__file__)) + ) + bot_procs[proc.pid] = (proc, room_url) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to start subprocess: {e}") + + return RedirectResponse(room_url) + + +@app.get("/status/{pid}") +def get_status(pid: int): + # Look up the subprocess + proc = bot_procs.get(pid) + + # If the subprocess doesn't exist, return an error + if not proc: + raise HTTPException( + status_code=404, detail=f"Bot with process id: {pid} not found") + + # Check the status of the subprocess + if proc[0].poll() is None: + status = "running" + else: + status = "finished" + + return JSONResponse({"bot_id": pid, "status": status}) + + +if __name__ == "__main__": + import uvicorn + + default_host = os.getenv("HOST", "0.0.0.0") + default_port = int(os.getenv("FAST_API_PORT", "7860")) + + parser = argparse.ArgumentParser( + description="Daily Moondream FastAPI server") + parser.add_argument("--host", type=str, + default=default_host, help="Host address") + parser.add_argument("--port", type=int, + default=default_port, help="Port number") + parser.add_argument("--reload", action="store_true", + help="Reload code on change") + + config = parser.parse_args() + + uvicorn.run( + "server:app", + host=config.host, + port=config.port, + reload=config.reload, + ) diff --git a/examples/moondream-chatbot/utils/daily_helpers.py b/examples/moondream-chatbot/utils/daily_helpers.py new file mode 100644 index 000000000..140f710e4 --- /dev/null +++ b/examples/moondream-chatbot/utils/daily_helpers.py @@ -0,0 +1,109 @@ + +import urllib.parse +import os +import time +import urllib +import requests + +from dotenv import load_dotenv +load_dotenv() + + +daily_api_path = os.getenv("DAILY_API_URL") or "api.daily.co/v1" +daily_api_key = os.getenv("DAILY_API_KEY") + + +def create_room() -> tuple[str, str]: + """ + Helper function to create a Daily room. + # See: https://docs.daily.co/reference/rest-api/rooms + + Returns: + tuple: A tuple containing the room URL and room name. + + Raises: + Exception: If the request to create the room fails or if the response does not contain the room URL or room name. + """ + room_props = { + "exp": time.time() + 60 * 60, # 1 hour + "enable_chat": True, + "enable_emoji_reactions": True, + "eject_at_room_exp": True, + "enable_prejoin_ui": False, # Important for the bot to be able to join headlessly + } + res = requests.post( + f"https://{daily_api_path}/rooms", + headers={"Authorization": f"Bearer {daily_api_key}"}, + json={ + "properties": room_props + }, + ) + if res.status_code != 200: + raise Exception(f"Unable to create room: {res.text}") + + data = res.json() + room_url: str = data.get("url") + room_name: str = data.get("name") + if room_url is None or room_name is None: + raise Exception("Missing room URL or room name in response") + + return room_url, room_name + + +def get_name_from_url(room_url: str) -> str: + """ + Extracts the name from a given room URL. + + Args: + room_url (str): The URL of the room. + + Returns: + str: The extracted name from the room URL. + """ + return urllib.parse.urlparse(room_url).path[1:] + + +def get_token(room_url: str) -> str: + """ + Retrieves a meeting token for the specified Daily room URL. + # See: https://docs.daily.co/reference/rest-api/meeting-tokens + + Args: + room_url (str): The URL of the Daily room. + + Returns: + str: The meeting token. + + Raises: + Exception: If no room URL is specified or if no Daily API key is specified. + Exception: If there is an error creating the meeting token. + """ + if not room_url: + raise Exception( + "No Daily room specified. You must specify a Daily room in order a token to be generated.") + + if not daily_api_key: + raise Exception( + "No Daily API key specified. set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers.") + + expiration: float = time.time() + 60 * 60 + room_name = get_name_from_url(room_url) + + res: requests.Response = requests.post( + f"https://{daily_api_path}/meeting-tokens", + headers={ + "Authorization": f"Bearer {daily_api_key}"}, + json={ + "properties": { + "room_name": room_name, + "is_owner": True, # Owner tokens required for transcription + "exp": expiration}}, + ) + + if res.status_code != 200: + raise Exception( + f"Failed to create meeting token: {res.status_code} {res.text}") + + token: str = res.json()["token"] + + return token diff --git a/examples/server/README.md b/examples/server/README.md deleted file mode 100644 index defd17943..000000000 --- a/examples/server/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Server Example - -Use this server app to quickly host a bot on the web: - -``` -flask --app daily-bot-manager.py --debug run -``` - -It's currently configured to serve example apps defined in the APPS constant in the server file: - -``` -chatbot -patient-intake -storybot -translator -``` - -Once the server is started, you can create a bot instance by opening `http://127.0.0.1:5000/start/chatbot` in a browser, and the server will do the following: - -- Create a new, randomly-named Daily room with `DAILY_API_KEY` from your .env file or environment -- Start an instance of `chatbot.py` and connect it to that room -- 301 redirect your browser to the room - -### Options - -The server supports several options, which can be set in the body of a POST request, or as params in the URL of a GET request. - -- `room_url` (default: none): A room URL to join. If empty, the server will create a Daily room and return the URL in the response. - room_properties (none): A JSON object (URL encoded if included as a GET parameter) for overriding default room creation properties, as described here: https://docs.daily.co/reference/rest-api/rooms/create-room This will be ignored if a room_url is provided. -- `token_properties` (none): A JSON object (URL encoded if included as a GET parameter) for overriding default token properties. By default, the server creates an owner token with an expiration time of one hour. -- `duration` (7200 seconds, or two hours): Use this property to set a time limit for the bot, as well as an expiration time for the room (if the server is creating one). This will not add an expiration time to an existing room. Expiration times in `token_properties` or `room_properties` will also take precedence over this value. You can set this property to `0` to disable timeouts, but this isn't recommended. -- `bot_args` (none): A string containing any additional command-line args to pass to the bot. -- `wait_for_bot` (true): Whether to wait for the bot to successfully join the room before returning a response from the server. If true, the server will start the bot script, then poll the room for up to 5 seconds to confirm the bot has joined the room. If it doesn't, the server will stop the bot and return a 500 response. If set to `false`, the server will start the bot, but immediately return a 200 response. This can be useful if the server is creating rooms for you, and you need the room URL to join the user to the room. -- `redirect` (true): Instead of returning a 200 for GET requests, the server will return a 301 redirect to the ROOM_URL. This is handy for testing by creating a bot with a GET request directly in the browser. POST requests will never return redirects. Set to `false` to get 200 responses with info in a JSON object even for GET requests. diff --git a/examples/server/daily-bot-manager.py b/examples/server/daily-bot-manager.py deleted file mode 100644 index 6bea559e1..000000000 --- a/examples/server/daily-bot-manager.py +++ /dev/null @@ -1,165 +0,0 @@ -import os -import requests -import urllib -import subprocess -import time - -from flask import Flask, jsonify, redirect, request -from flask_cors import CORS - -from dotenv import load_dotenv -load_dotenv(override=True) - -app = Flask(__name__) -CORS(app) - -APPS = { - "chatbot": "../starter-apps/chatbot.py", - "patient-intake": "../starter-apps/patient-intake.py", - "storybot": "../starter-apps/storybot.py", - "translator": "../starter-apps/translator.py" -} - -daily_api_key = os.getenv("DAILY_API_KEY") -api_path = os.getenv("DAILY_API_PATH") or "https://api.daily.co/v1" - - -def get_room_name(room_url): - return urllib.parse.urlparse(room_url).path[1:] - - -def create_room(room_properties, exp): - room_props = { - "exp": exp, - "enable_chat": True, - "enable_emoji_reactions": True, - "eject_at_room_exp": True, - "enable_prejoin_ui": False, - "enable_recording": "cloud" - } - if room_properties: - room_props |= room_properties - - res = requests.post( - f"{api_path}/rooms", - headers={"Authorization": f"Bearer {daily_api_key}"}, - json={ - "properties": room_props - }, - ) - if res.status_code != 200: - raise Exception(f"Unable to create room: {res.text}") - - room_url = res.json()["url"] - room_name = res.json()["name"] - return (room_url, room_name) - - -def create_token(room_name, token_properties, exp): - token_props = {"exp": exp, "is_owner": True} - if token_properties: - token_props |= token_properties - # Force the token to be limited to the room - token_props |= {"room_name": room_name} - res = requests.post( - f'{api_path}/meeting-tokens', - headers={ - 'Authorization': f'Bearer {daily_api_key}'}, - json={ - 'properties': token_props}) - if res.status_code != 200: - if res.status_code != 200: - raise Exception(f"Unable to create meeting token: {res.text}") - - meeting_token = res.json()['token'] - return meeting_token - - -def start_bot(*, bot_path, room_url, token, bot_args, wait_for_bot): - - room_name = get_room_name(room_url) - proc = subprocess.Popen( - [f"python {bot_path} -u {room_url} -t {token} -k {daily_api_key} {bot_args}"], - shell=True, - bufsize=1, - ) - - if wait_for_bot: - # Don't return until the bot has joined the room, but wait for at most 5 - # seconds. - attempts = 0 - while attempts < 50: - time.sleep(0.1) - attempts += 1 - res = requests.get( - f"{api_path}/rooms/{room_name}/get-session-data", - headers={"Authorization": f"Bearer {daily_api_key}"}, - ) - if res.status_code == 200: - print(f"Took {attempts} attempts to join room {room_name}") - return True - - # If we don't break from the loop, that means we never found the bot in the room - raise Exception("The bot was unable to join the room. Please try again.") - - return True - - -@app.route("/start/", methods=["GET", "POST"]) -def start(botname): - try: - if botname not in APPS: - raise Exception(f"Bot '{botname}' is not in the allowlist.") - - bot_path = APPS[botname] - props = { - "room_url": None, - "room_properties": None, - "token_properties": None, - "bot_args": None, - "wait_for_bot": True, - "duration": None, - "redirect": True - } - props |= request.values.to_dict() # gets URL params as well as plaintext POST body - try: - props |= request.json - except BaseException: - pass - if props['redirect'] == "false": - props['redirect'] = False - if props['wait_for_bot'] == "false": - props['wait_for_bot'] = False - - duration = int(os.getenv("DAILY_BOT_DURATION") or 7200) - if props['duration']: - duration = props['duration'] - exp = time.time() + duration - if (props['room_url']): - room_url = props['room_url'] - try: - room_name = get_room_name(room_url) - except ValueError: - raise Exception( - "There was a problem detecting the room name. Please double-check the value of room_url.") - else: - room_url, room_name = create_room(props['room_properties'], exp) - token = create_token(room_name, props['token_properties'], exp) - bot = start_bot( - room_url=room_url, - bot_path=bot_path, - token=token, - bot_args=props['bot_args'], - wait_for_bot=props['wait_for_bot']) - - if props['redirect'] and request.method == "GET": - return redirect(room_url, 302) - else: - return jsonify({"room_url": room_url, "token": token}) - except BaseException as e: - return f"There was a problem starting the bot: {e}", 500 - - -@app.route("/healthz") -def health_check(): - return "ok", 200 diff --git a/examples/simple-chatbot/.gitignore b/examples/simple-chatbot/.gitignore new file mode 100644 index 000000000..2bc1403d1 --- /dev/null +++ b/examples/simple-chatbot/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +runpod.toml diff --git a/examples/simple-chatbot/Dockerfile b/examples/simple-chatbot/Dockerfile new file mode 100644 index 000000000..704080eec --- /dev/null +++ b/examples/simple-chatbot/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.10-bullseye + +RUN mkdir /app +RUN mkdir /app/assets +RUN mkdir /app/utils +COPY *.py /app/ +COPY requirements.txt /app/ +copy assets/* /app/assets/ +copy utils/* /app/utils/ + +WORKDIR /app +RUN pip3 install -r requirements.txt + +EXPOSE 7860 + +CMD ["python3", "server.py"] \ No newline at end of file diff --git a/examples/simple-chatbot/README.md b/examples/simple-chatbot/README.md new file mode 100644 index 000000000..02e4109f5 --- /dev/null +++ b/examples/simple-chatbot/README.md @@ -0,0 +1,37 @@ +# Simple Chatbot + + + +This app connects you to a chatbot powered by GPT-4, complete with animations generated by Stable Video Diffusion. + +See a video of it in action: https://x.com/kwindla/status/1778628911817183509 + +And a quick video walkthrough of the code: https://www.loom.com/share/13df1967161f4d24ade054e7f8753416 + +ℹ️ The first time, things might take extra time to get started since VAD (Voice Activity Detection) model needs to be downloaded. + +## Get started + +```python +python3 -m venv env +source env/bin/activate +pip install -r requirements.txt + +cp env.example .env # and add your credentials + +``` + +## Run the server + +```bash +python server.py +``` + +Then, visit `http://localhost:7860/start` in your browser to start a chatbot session. + +## Build and test the Docker image + +``` +docker build -t chatbot . +docker run --env-file .env -p 7860:7860 chatbot +``` diff --git a/examples/simple-chatbot/assets/robot01.png b/examples/simple-chatbot/assets/robot01.png new file mode 100644 index 000000000..3864411dc Binary files /dev/null and b/examples/simple-chatbot/assets/robot01.png differ diff --git a/examples/simple-chatbot/assets/robot010.png b/examples/simple-chatbot/assets/robot010.png new file mode 100644 index 000000000..e389e0933 Binary files /dev/null and b/examples/simple-chatbot/assets/robot010.png differ diff --git a/examples/simple-chatbot/assets/robot011.png b/examples/simple-chatbot/assets/robot011.png new file mode 100644 index 000000000..c0f0633f3 Binary files /dev/null and b/examples/simple-chatbot/assets/robot011.png differ diff --git a/examples/simple-chatbot/assets/robot012.png b/examples/simple-chatbot/assets/robot012.png new file mode 100644 index 000000000..e5fb2a7d1 Binary files /dev/null and b/examples/simple-chatbot/assets/robot012.png differ diff --git a/examples/simple-chatbot/assets/robot013.png b/examples/simple-chatbot/assets/robot013.png new file mode 100644 index 000000000..cd62a2005 Binary files /dev/null and b/examples/simple-chatbot/assets/robot013.png differ diff --git a/examples/simple-chatbot/assets/robot014.png b/examples/simple-chatbot/assets/robot014.png new file mode 100644 index 000000000..516ca4e8b Binary files /dev/null and b/examples/simple-chatbot/assets/robot014.png differ diff --git a/examples/simple-chatbot/assets/robot015.png b/examples/simple-chatbot/assets/robot015.png new file mode 100644 index 000000000..9b9242691 Binary files /dev/null and b/examples/simple-chatbot/assets/robot015.png differ diff --git a/examples/simple-chatbot/assets/robot016.png b/examples/simple-chatbot/assets/robot016.png new file mode 100644 index 000000000..cbd2d9d6f Binary files /dev/null and b/examples/simple-chatbot/assets/robot016.png differ diff --git a/examples/simple-chatbot/assets/robot017.png b/examples/simple-chatbot/assets/robot017.png new file mode 100644 index 000000000..5780fa27a Binary files /dev/null and b/examples/simple-chatbot/assets/robot017.png differ diff --git a/examples/simple-chatbot/assets/robot018.png b/examples/simple-chatbot/assets/robot018.png new file mode 100644 index 000000000..5c983704d Binary files /dev/null and b/examples/simple-chatbot/assets/robot018.png differ diff --git a/examples/simple-chatbot/assets/robot019.png b/examples/simple-chatbot/assets/robot019.png new file mode 100644 index 000000000..c0f9bef58 Binary files /dev/null and b/examples/simple-chatbot/assets/robot019.png differ diff --git a/examples/simple-chatbot/assets/robot02.png b/examples/simple-chatbot/assets/robot02.png new file mode 100644 index 000000000..267969849 Binary files /dev/null and b/examples/simple-chatbot/assets/robot02.png differ diff --git a/examples/simple-chatbot/assets/robot020.png b/examples/simple-chatbot/assets/robot020.png new file mode 100644 index 000000000..88bcfa04a Binary files /dev/null and b/examples/simple-chatbot/assets/robot020.png differ diff --git a/examples/simple-chatbot/assets/robot021.png b/examples/simple-chatbot/assets/robot021.png new file mode 100644 index 000000000..5d30e6029 Binary files /dev/null and b/examples/simple-chatbot/assets/robot021.png differ diff --git a/examples/simple-chatbot/assets/robot022.png b/examples/simple-chatbot/assets/robot022.png new file mode 100644 index 000000000..0e2d412ed Binary files /dev/null and b/examples/simple-chatbot/assets/robot022.png differ diff --git a/examples/simple-chatbot/assets/robot023.png b/examples/simple-chatbot/assets/robot023.png new file mode 100644 index 000000000..d4bc03938 Binary files /dev/null and b/examples/simple-chatbot/assets/robot023.png differ diff --git a/examples/simple-chatbot/assets/robot024.png b/examples/simple-chatbot/assets/robot024.png new file mode 100644 index 000000000..62f60c815 Binary files /dev/null and b/examples/simple-chatbot/assets/robot024.png differ diff --git a/examples/simple-chatbot/assets/robot025.png b/examples/simple-chatbot/assets/robot025.png new file mode 100644 index 000000000..c2ac6639e Binary files /dev/null and b/examples/simple-chatbot/assets/robot025.png differ diff --git a/examples/simple-chatbot/assets/robot03.png b/examples/simple-chatbot/assets/robot03.png new file mode 100644 index 000000000..1cb8c76d5 Binary files /dev/null and b/examples/simple-chatbot/assets/robot03.png differ diff --git a/examples/simple-chatbot/assets/robot04.png b/examples/simple-chatbot/assets/robot04.png new file mode 100644 index 000000000..155d19f47 Binary files /dev/null and b/examples/simple-chatbot/assets/robot04.png differ diff --git a/examples/simple-chatbot/assets/robot05.png b/examples/simple-chatbot/assets/robot05.png new file mode 100644 index 000000000..b5a5c4b79 Binary files /dev/null and b/examples/simple-chatbot/assets/robot05.png differ diff --git a/examples/simple-chatbot/assets/robot06.png b/examples/simple-chatbot/assets/robot06.png new file mode 100644 index 000000000..b5733db5f Binary files /dev/null and b/examples/simple-chatbot/assets/robot06.png differ diff --git a/examples/simple-chatbot/assets/robot07.png b/examples/simple-chatbot/assets/robot07.png new file mode 100644 index 000000000..8b5d57655 Binary files /dev/null and b/examples/simple-chatbot/assets/robot07.png differ diff --git a/examples/simple-chatbot/assets/robot08.png b/examples/simple-chatbot/assets/robot08.png new file mode 100644 index 000000000..f7600a559 Binary files /dev/null and b/examples/simple-chatbot/assets/robot08.png differ diff --git a/examples/simple-chatbot/assets/robot09.png b/examples/simple-chatbot/assets/robot09.png new file mode 100644 index 000000000..16c1a98ba Binary files /dev/null and b/examples/simple-chatbot/assets/robot09.png differ diff --git a/examples/starter-apps/chatbot.py b/examples/simple-chatbot/bot.py similarity index 81% rename from examples/starter-apps/chatbot.py rename to examples/simple-chatbot/bot.py index 19d144f61..39f1b46a4 100644 --- a/examples/starter-apps/chatbot.py +++ b/examples/simple-chatbot/bot.py @@ -5,24 +5,24 @@ import os from PIL import Image from typing import AsyncGenerator -from pipecat.pipeline.aggregators import ( +from dailyai.pipeline.aggregators import ( LLMAssistantResponseAggregator, LLMUserResponseAggregator, ) -from pipecat.pipeline.frames import ( +from dailyai.pipeline.frames import ( ImageFrame, SpriteFrame, Frame, - LLMResponseEndFrame, LLMMessagesFrame, AudioFrame, PipelineStartedFrame, + TTSEndFrame, ) -from pipecat.services.ai_services import AIService -from pipecat.pipeline.pipeline import Pipeline -from pipecat.transports.daily_transport import DailyTransport -from pipecat.services.open_ai_services import OpenAILLMService -from pipecat.services.elevenlabs_ai_services import ElevenLabsTTSService +from dailyai.services.ai_services import AIService +from dailyai.pipeline.pipeline import Pipeline +from dailyai.transports.daily_transport import DailyTransport +from dailyai.services.open_ai_services import OpenAILLMService +from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService from runner import configure @@ -30,7 +30,7 @@ from dotenv import load_dotenv load_dotenv(override=True) logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s") -logger = logging.getLogger("pipecat") +logger = logging.getLogger("dailyai") logger.setLevel(logging.DEBUG) sprites = [] @@ -47,6 +47,7 @@ for i in range(1, 26): flipped = sprites[::-1] sprites.extend(flipped) + # When the bot isn't talking, show a static image of the cat listening quiet_frame = ImageFrame(sprites[0], (1024, 576)) talking_frame = SpriteFrame(images=sprites) @@ -70,7 +71,7 @@ class TalkingAnimation(AIService): self._is_talking = True else: yield frame - elif isinstance(frame, LLMResponseEndFrame): + elif isinstance(frame, TTSEndFrame): yield quiet_frame yield frame self._is_talking = False @@ -79,6 +80,8 @@ class TalkingAnimation(AIService): class AnimationInitializer(AIService): + def __init__(self): + super().__init__() async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: if isinstance(frame, PipelineStartedFrame): @@ -120,7 +123,7 @@ async def main(room_url: str, token): messages = [ { "role": "system", - "content": "You are Chatbot, a friendly, helpful robot. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio. Respond to what the user said in a creative and helpful way, but keep your responses brief. Start by introducing yourself.", + "content": "You are Chatbot, a friendly, helpful robot. Your goal is to demonstrate your capabilities in a succinct way. Your output will be converted to audio so don't include special characters in your answers. Respond to what the user said in a creative and helpful way, but keep your responses brief. Start by introducing yourself.", }, ] @@ -137,6 +140,8 @@ async def main(room_url: str, token): pre_processor=LLMUserResponseAggregator(messages), ) + transport.transcription_settings["extra"]["endpointing"] = True + transport.transcription_settings["extra"]["punctuate"] = True await asyncio.gather(transport.run(), run_conversation()) diff --git a/examples/simple-chatbot/env.example b/examples/simple-chatbot/env.example new file mode 100644 index 000000000..d368ae510 --- /dev/null +++ b/examples/simple-chatbot/env.example @@ -0,0 +1,4 @@ +DAILY_SAMPLE_ROOM_URL=https://yourdomain.daily.co/yourroom # (for joining the bot to the same room repeatedly for local dev) +DAILY_API_KEY=7df... +OPENAI_API_KEY=sk-PL... +ELEVENLABS_API_KEY=aeb... \ No newline at end of file diff --git a/examples/simple-chatbot/image.png b/examples/simple-chatbot/image.png new file mode 100644 index 000000000..93814fd1e Binary files /dev/null and b/examples/simple-chatbot/image.png differ diff --git a/examples/simple-chatbot/requirements.txt b/examples/simple-chatbot/requirements.txt new file mode 100644 index 000000000..c8cda62cb --- /dev/null +++ b/examples/simple-chatbot/requirements.txt @@ -0,0 +1,5 @@ +python-dotenv +requests +fastapi[all] +uvicorn +dailyai[daily,openai] diff --git a/examples/simple-chatbot/runner.py b/examples/simple-chatbot/runner.py new file mode 100644 index 000000000..6d1a8113d --- /dev/null +++ b/examples/simple-chatbot/runner.py @@ -0,0 +1,58 @@ +import argparse +import os +import time +import urllib +import requests + + +def configure(): + parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample") + parser.add_argument( + "-u", + "--url", + type=str, + required=False, + help="URL of the Daily room to join") + parser.add_argument( + "-k", + "--apikey", + type=str, + required=False, + help="Daily API Key (needed to create an owner token for the room)", + ) + + args, unknown = parser.parse_known_args() + + url = args.url or os.getenv("DAILY_SAMPLE_ROOM_URL") + key = args.apikey or os.getenv("DAILY_API_KEY") + + if not url: + raise Exception( + "No Daily room specified. use the -u/--url option from the command line, or set DAILY_SAMPLE_ROOM_URL in your environment to specify a Daily room URL.") + + if not key: + raise Exception("No Daily API key specified. use the -k/--apikey option from the command line, or set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers.") + + # Create a meeting token for the given room with an expiration 1 hour in + # the future. + room_name: str = urllib.parse.urlparse(url).path[1:] + expiration: float = time.time() + 60 * 60 + + res: requests.Response = requests.post( + f"https://api.daily.co/v1/meeting-tokens", + headers={ + "Authorization": f"Bearer {key}"}, + json={ + "properties": { + "room_name": room_name, + "is_owner": True, + "exp": expiration}}, + ) + + if res.status_code != 200: + raise Exception( + f"Failed to create meeting token: {res.status_code} {res.text}") + + token: str = res.json()["token"] + + return (url, token) diff --git a/examples/simple-chatbot/server.py b/examples/simple-chatbot/server.py new file mode 100644 index 000000000..1b8928db2 --- /dev/null +++ b/examples/simple-chatbot/server.py @@ -0,0 +1,127 @@ +import os +import argparse +import subprocess +import atexit +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, Request, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse, JSONResponse, RedirectResponse + +from utils.daily_helpers import create_room as _create_room, get_token, get_name_from_url + +MAX_BOTS_PER_ROOM = 1 + +# Bot sub-process dict for status reporting and concurrency control +bot_procs = {} + + +def cleanup(): + # Clean up function, just to be extra safe + for proc in bot_procs.values(): + proc.terminate() + proc.wait() + + +atexit.register(cleanup) + + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/start") +async def start_agent(request: Request): + print(f"!!! Creating room") + room_url, room_name = _create_room() + print(f"!!! Room URL: {room_url}") + # Ensure the room property is present + if not room_url: + raise HTTPException( + status_code=500, + detail="Missing 'room' property in request data. Cannot start agent without a target room!") + + # Check if there is already an existing process running in this room + num_bots_in_room = sum( + 1 for proc in bot_procs.values() if proc[1] == room_url and proc[0].poll() is None) + if num_bots_in_room >= MAX_BOTS_PER_ROOM: + raise HTTPException( + status_code=500, detail=f"Max bot limited reach for room: {room_url}") + + # Get the token for the room + token = get_token(room_url) + + if not token: + raise HTTPException( + status_code=500, detail=f"Failed to get token for room: {room_url}") + + # Spawn a new agent, and join the user session + # Note: this is mostly for demonstration purposes (refer to 'deployment' in README) + try: + proc = subprocess.Popen( + [ + f"python3 -m bot -u {room_url} -t {token}" + ], + shell=True, + bufsize=1, + cwd=os.path.dirname(os.path.abspath(__file__)) + ) + bot_procs[proc.pid] = (proc, room_url) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to start subprocess: {e}") + + return RedirectResponse(room_url) + + +@app.get("/status/{pid}") +def get_status(pid: int): + # Look up the subprocess + proc = bot_procs.get(pid) + + # If the subprocess doesn't exist, return an error + if not proc: + raise HTTPException( + status_code=404, detail=f"Bot with process id: {pid} not found") + + # Check the status of the subprocess + if proc[0].poll() is None: + status = "running" + else: + status = "finished" + + return JSONResponse({"bot_id": pid, "status": status}) + + +if __name__ == "__main__": + import uvicorn + + default_host = os.getenv("HOST", "0.0.0.0") + default_port = int(os.getenv("FAST_API_PORT", "7860")) + + parser = argparse.ArgumentParser( + description="Daily Storyteller FastAPI server") + parser.add_argument("--host", type=str, + default=default_host, help="Host address") + parser.add_argument("--port", type=int, + default=default_port, help="Port number") + parser.add_argument("--reload", action="store_true", + help="Reload code on change") + + config = parser.parse_args() + + uvicorn.run( + "server:app", + host=config.host, + port=config.port, + reload=config.reload, + ) diff --git a/examples/simple-chatbot/utils/daily_helpers.py b/examples/simple-chatbot/utils/daily_helpers.py new file mode 100644 index 000000000..140f710e4 --- /dev/null +++ b/examples/simple-chatbot/utils/daily_helpers.py @@ -0,0 +1,109 @@ + +import urllib.parse +import os +import time +import urllib +import requests + +from dotenv import load_dotenv +load_dotenv() + + +daily_api_path = os.getenv("DAILY_API_URL") or "api.daily.co/v1" +daily_api_key = os.getenv("DAILY_API_KEY") + + +def create_room() -> tuple[str, str]: + """ + Helper function to create a Daily room. + # See: https://docs.daily.co/reference/rest-api/rooms + + Returns: + tuple: A tuple containing the room URL and room name. + + Raises: + Exception: If the request to create the room fails or if the response does not contain the room URL or room name. + """ + room_props = { + "exp": time.time() + 60 * 60, # 1 hour + "enable_chat": True, + "enable_emoji_reactions": True, + "eject_at_room_exp": True, + "enable_prejoin_ui": False, # Important for the bot to be able to join headlessly + } + res = requests.post( + f"https://{daily_api_path}/rooms", + headers={"Authorization": f"Bearer {daily_api_key}"}, + json={ + "properties": room_props + }, + ) + if res.status_code != 200: + raise Exception(f"Unable to create room: {res.text}") + + data = res.json() + room_url: str = data.get("url") + room_name: str = data.get("name") + if room_url is None or room_name is None: + raise Exception("Missing room URL or room name in response") + + return room_url, room_name + + +def get_name_from_url(room_url: str) -> str: + """ + Extracts the name from a given room URL. + + Args: + room_url (str): The URL of the room. + + Returns: + str: The extracted name from the room URL. + """ + return urllib.parse.urlparse(room_url).path[1:] + + +def get_token(room_url: str) -> str: + """ + Retrieves a meeting token for the specified Daily room URL. + # See: https://docs.daily.co/reference/rest-api/meeting-tokens + + Args: + room_url (str): The URL of the Daily room. + + Returns: + str: The meeting token. + + Raises: + Exception: If no room URL is specified or if no Daily API key is specified. + Exception: If there is an error creating the meeting token. + """ + if not room_url: + raise Exception( + "No Daily room specified. You must specify a Daily room in order a token to be generated.") + + if not daily_api_key: + raise Exception( + "No Daily API key specified. set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers.") + + expiration: float = time.time() + 60 * 60 + room_name = get_name_from_url(room_url) + + res: requests.Response = requests.post( + f"https://{daily_api_path}/meeting-tokens", + headers={ + "Authorization": f"Bearer {daily_api_key}"}, + json={ + "properties": { + "room_name": room_name, + "is_owner": True, # Owner tokens required for transcription + "exp": expiration}}, + ) + + if res.status_code != 200: + raise Exception( + f"Failed to create meeting token: {res.status_code} {res.text}") + + token: str = res.json()["token"] + + return token diff --git a/examples/starter-apps/assets/clack-short-quiet.wav b/examples/starter-apps/assets/clack-short-quiet.wav deleted file mode 100644 index f0580d11e..000000000 Binary files a/examples/starter-apps/assets/clack-short-quiet.wav and /dev/null differ diff --git a/examples/starter-apps/assets/clack-short.wav b/examples/starter-apps/assets/clack-short.wav deleted file mode 100644 index 864994b28..000000000 Binary files a/examples/starter-apps/assets/clack-short.wav and /dev/null differ diff --git a/examples/starter-apps/assets/clack.wav b/examples/starter-apps/assets/clack.wav deleted file mode 100644 index 2f36164b3..000000000 Binary files a/examples/starter-apps/assets/clack.wav and /dev/null differ diff --git a/examples/starter-apps/assets/ding.wav b/examples/starter-apps/assets/ding.wav deleted file mode 100644 index b63aa3ada..000000000 Binary files a/examples/starter-apps/assets/ding.wav and /dev/null differ diff --git a/examples/starter-apps/assets/ding2.wav b/examples/starter-apps/assets/ding2.wav deleted file mode 100644 index 3b8ab20d5..000000000 Binary files a/examples/starter-apps/assets/ding2.wav and /dev/null differ diff --git a/examples/starter-apps/assets/grandma-listening.png b/examples/starter-apps/assets/grandma-listening.png deleted file mode 100644 index 2eb17e698..000000000 Binary files a/examples/starter-apps/assets/grandma-listening.png and /dev/null differ diff --git a/examples/starter-apps/assets/grandma-writing.png b/examples/starter-apps/assets/grandma-writing.png deleted file mode 100644 index 6820fabc6..000000000 Binary files a/examples/starter-apps/assets/grandma-writing.png and /dev/null differ diff --git a/examples/starter-apps/patient-intake.py b/examples/starter-apps/patient-intake.py deleted file mode 100644 index 2f3823037..000000000 --- a/examples/starter-apps/patient-intake.py +++ /dev/null @@ -1,352 +0,0 @@ -import copy -import aiohttp -import asyncio -import json -import logging -import os -import re -import wave -from typing import AsyncGenerator, List -from pipecat.pipeline.opeanai_llm_aggregator import ( - OpenAIAssistantContextAggregator, - OpenAIUserContextAggregator, -) - -from pipecat.pipeline.pipeline import Pipeline -from pipecat.transports.daily_transport import DailyTransport -from pipecat.services.openai_llm_context import OpenAILLMContext -from pipecat.services.open_ai_services import OpenAILLMService -# from pipecat.services.deepgram_ai_services import DeepgramTTSService -from pipecat.services.elevenlabs_ai_services import ElevenLabsTTSService -from pipecat.services.fireworks_ai_services import FireworksLLMService -from pipecat.pipeline.frames import ( - Frame, - LLMFunctionCallFrame, - LLMFunctionStartFrame, - AudioFrame, -) -from pipecat.pipeline.openai_frames import OpenAILLMContextFrame -from pipecat.services.ai_services import FrameLogger, AIService -from openai._types import NotGiven, NOT_GIVEN - -from openai.types.chat import ( - ChatCompletionToolParam, -) - -from runner import configure - -from dotenv import load_dotenv -load_dotenv(override=True) - -logging.basicConfig(format="%(levelno)s %(asctime)s %(message)s") -logger = logging.getLogger("pipecat") -logger.setLevel(logging.DEBUG) - -sounds = {} -sound_files = [ - "clack-short.wav", - "clack.wav", - "clack-short-quiet.wav", - "ding.wav", - "ding2.wav", -] - -script_dir = os.path.dirname(__file__) - -for file in sound_files: - # Build the full path to the sound file - full_path = os.path.join(script_dir, "assets", file) - # Get the filename without the extension to use as the dictionary key - filename = os.path.splitext(os.path.basename(full_path))[0] - # Open the sound and convert it to bytes - with wave.open(full_path) as audio_file: - sounds[file] = audio_file.readframes(-1) - - -steps = [{"prompt": "Start by introducing yourself. Then, ask the user to confirm their identity by telling you their birthday, including the year. When they answer with their birthday, call the verify_birthday function.", - "run_async": False, - "failed": "The user provided an incorrect birthday. Ask them for their birthday again. When they answer, call the verify_birthday function.", - "tools": [{"type": "function", - "function": {"name": "verify_birthday", - "description": "Use this function to verify the user has provided their correct birthday.", - "parameters": {"type": "object", - "properties": {"birthday": {"type": "string", - "description": "The user's birthdate, including the year. The user can provide it in any format, but convert it to YYYY-MM-DD format to call this function.", - }}, - }, - }, - }], - }, - {"prompt": "Next, thank the user for confirming their identity, then ask the user to list their current prescriptions. Each prescription needs to have a medication name and a dosage. Do not call the list_prescriptions function with any unknown dosages.", - "run_async": True, - "tools": [{"type": "function", - "function": {"name": "list_prescriptions", - "description": "Once the user has provided a list of their prescription medications, call this function.", - "parameters": {"type": "object", - "properties": {"prescriptions": {"type": "array", - "items": {"type": "object", - "properties": {"medication": {"type": "string", - "description": "The medication's name", - }, - "dosage": {"type": "string", - "description": "The prescription's dosage", - }, - }, - }, - }}, - }, - }, - }], - }, - {"prompt": "Next, ask the user if they have any allergies. Once they have listed their allergies or confirmed they don't have any, call the list_allergies function.", - "run_async": True, - "tools": [{"type": "function", - "function": {"name": "list_allergies", - "description": "Once the user has provided a list of their allergies, call this function.", - "parameters": {"type": "object", - "properties": {"allergies": {"type": "array", - "items": {"type": "object", - "properties": {"name": {"type": "string", - "description": "What the user is allergic to", - }}, - }, - }}, - }, - }, - }], - }, - {"prompt": "Now ask the user if they have any medical conditions the doctor should know about. Once they've answered the question, call the list_conditions function.", - "run_async": True, - "tools": [{"type": "function", - "function": {"name": "list_conditions", - "description": "Once the user has provided a list of their medical conditions, call this function.", - "parameters": {"type": "object", - "properties": {"conditions": {"type": "array", - "items": {"type": "object", - "properties": {"name": {"type": "string", - "description": "The user's medical condition", - }}, - }, - }}, - }, - }, - }, - ], - }, - {"prompt": "Finally, ask the user the reason for their doctor visit today. Once they answer, call the list_visit_reasons function.", - "run_async": True, - "tools": [{"type": "function", - "function": {"name": "list_visit_reasons", - "description": "Once the user has provided a list of the reasons they are visiting a doctor today, call this function.", - "parameters": {"type": "object", - "properties": {"visit_reasons": {"type": "array", - "items": {"type": "object", - "properties": {"name": {"type": "string", - "description": "The user's reason for visiting the doctor", - }}, - }, - }}, - }, - }, - }], - }, - {"prompt": "Now, thank the user and end the conversation.", - "run_async": True, - "tools": [], - }, - {"prompt": "", - "run_async": True, - "tools": []}, - ] -current_step = 0 - - -class ChecklistProcessor(AIService): - - def __init__( - self, - context: OpenAILLMContext, - llm: AIService, - tools: List[ChatCompletionToolParam] | NotGiven = NOT_GIVEN, - *args, - **kwargs, - ): - super().__init__(*args, **kwargs) - self._context: OpenAILLMContext = context - self._llm = llm - self._id = "You are Jessica, an agent for a company called Tri-County Health Services. Your job is to collect important information from the user before their doctor visit. You're talking to Chad Bailey. You should address the user by their first name and be polite and professional. You're not a medical professional, so you shouldn't provide any advice. Keep your responses short. Your job is to collect information to give to a doctor. Don't make assumptions about what values to plug into functions. Ask for clarification if a user response is ambiguous." - self._acks = ["One sec.", "Let me confirm that.", "Thanks.", "OK."] - - # Create an allowlist of functions that the LLM can call - self._functions = [ - "verify_birthday", - "list_prescriptions", - "list_allergies", - "list_conditions", - "list_visit_reasons", - ] - - self._context.add_message( - {"role": "system", "content": f"{self._id} {steps[0]['prompt']}"} - ) - - if tools: - self._context.set_tools(tools) - - def verify_birthday(self, args): - return args["birthday"] == "1983-01-01" - - def list_prescriptions(self, args): - # print(f"--- Prescriptions: {args['prescriptions']}\n") - pass - - def list_allergies(self, args): - # print(f"--- Allergies: {args['allergies']}\n") - pass - - def list_conditions(self, args): - # print(f"--- Medical Conditions: {args['conditions']}") - pass - - def list_visit_reasons(self, args): - # print(f"Visit Reasons: {args['visit_reasons']}") - pass - - async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: - global current_step - this_step = steps[current_step] - self._context.set_tools(this_step["tools"]) - if isinstance(frame, LLMFunctionStartFrame): - print(f"... Preparing function call: {frame.function_name}") - self._function_name = frame.function_name - if this_step["run_async"]: - # Get the LLM talking about the next step before getting the rest - # of the function call completion - current_step += 1 - self._context.add_message( - {"role": "system", "content": steps[current_step]["prompt"]} - ) - yield OpenAILLMContextFrame(self._context) - - local_context = copy.deepcopy(self._context) - local_context.set_tool_choice("none") - async for frame in llm.process_frame( - OpenAILLMContextFrame(local_context) - ): - yield frame - else: - # Insert a quick response while we run the function - yield AudioFrame(sounds["ding2.wav"]) - pass - elif isinstance(frame, LLMFunctionCallFrame): - - if frame.function_name and frame.arguments: - print( - f"--> Calling function: {frame.function_name} with arguments:") - pretty_json = re.sub( - "\n", "\n ", json.dumps( - json.loads( - frame.arguments), indent=2)) - print(f"--> {pretty_json}\n") - if frame.function_name not in self._functions: - raise Exception( - f"Unknown function.") - fn = getattr(self, frame.function_name) - result = fn(json.loads(frame.arguments)) - - if not this_step["run_async"]: - if result: - current_step += 1 - self._context.add_message( - {"role": "system", "content": steps[current_step]["prompt"]} - ) - yield OpenAILLMContextFrame(self._context) - - local_context = copy.deepcopy(self._context) - local_context.set_tool_choice("none") - async for frame in llm.process_frame( - OpenAILLMContextFrame(local_context) - ): - yield frame - else: - self._context.add_message( - {"role": "system", "content": this_step["failed"]} - ) - yield OpenAILLMContextFrame(self._context) - - local_context = copy.deepcopy(self._context) - local_context.set_tool_choice("none") - async for frame in llm.process_frame( - OpenAILLMContextFrame(local_context) - ): - yield frame - print(f"<-- Verify result: {result}\n") - - else: - yield frame - - -async def main(room_url: str, token): - async with aiohttp.ClientSession() as session: - global transport - global llm - global tts - - transport = DailyTransport( - room_url, - token, - "Intake Bot", - 5, - mic_enabled=True, - mic_sample_rate=16000, - camera_enabled=False, - start_transcription=True, - vad_enabled=True, - ) - - messages = [] - - llm = FireworksLLMService( - api_key=os.getenv("FIREWORKS_API_KEY"), - model="accounts/fireworks/models/firefunction-v1" - ) - # tts = DeepgramTTSService( - # aiohttp_session=session, - # api_key=os.getenv("DEEPGRAM_API_KEY"), - # voice="aura-asteria-en", - # ) - tts = ElevenLabsTTSService( - aiohttp_session=session, - api_key=os.getenv("ELEVENLABS_API_KEY"), - voice_id="XrExE9yKIg1WjnnlVkGX", - ) - context = OpenAILLMContext( - messages=messages, - ) - - checklist = ChecklistProcessor(context, llm) - fl = FrameLogger("FRAME LOGGER 1:") - fl2 = FrameLogger("FRAME LOGGER 2:") - pipeline = Pipeline(processors=[fl, llm, fl2, checklist, tts]) - - @transport.event_handler("on_first_other_participant_joined") - async def on_first_other_participant_joined(transport, participant): - await pipeline.queue_frames([OpenAILLMContextFrame(context)]) - - async def handle_intake(): - await transport.run_interruptible_pipeline( - pipeline, - post_processor=OpenAIAssistantContextAggregator(context), - pre_processor=OpenAIUserContextAggregator(context), - ) - - try: - await asyncio.gather(transport.run(), handle_intake()) - except (asyncio.CancelledError, KeyboardInterrupt): - print("whoops") - transport.stop() - - -if __name__ == "__main__": - (url, token) = configure() - asyncio.run(main(url, token)) diff --git a/examples/starter-apps/storybot.py b/examples/starter-apps/storybot.py deleted file mode 100644 index af23c7ec9..000000000 --- a/examples/starter-apps/storybot.py +++ /dev/null @@ -1,294 +0,0 @@ -import aiohttp -import asyncio -import json -import random -import logging -import os -import re -import wave -from typing import AsyncGenerator -from PIL import Image - -from pipecat.pipeline.pipeline import Pipeline -from pipecat.pipeline.frame_processor import FrameProcessor -from pipecat.services.live_stream import LiveStream -from pipecat.transports.daily_transport import DailyTransport -from pipecat.services.azure_ai_services import AzureLLMService, AzureTTSService -from pipecat.services.fal_ai_services import FalImageGenService -from pipecat.services.open_ai_services import OpenAILLMService -from pipecat.services.deepgram_ai_services import DeepgramTTSService -from pipecat.services.elevenlabs_ai_services import ElevenLabsTTSService -from pipecat.pipeline.aggregators import ( - LLMAssistantContextAggregator, - LLMAssistantResponseAggregator, - LLMUserResponseAggregator, -) -from pipecat.pipeline.frames import ( - EndPipeFrame, - LLMMessagesFrame, - Frame, - TextFrame, - LLMResponseEndFrame, - AudioFrame, - ImageFrame, - UserStoppedSpeakingFrame, -) -from pipecat.services.ai_services import FrameLogger, AIService - -from runner import configure - -from dotenv import load_dotenv -load_dotenv(override=True) - -logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s") -logger = logging.getLogger("pipecat") -logger.setLevel(logging.DEBUG) - -sounds = {} -images = {} -sound_files = ["talking.wav", "listening.wav", "ding3.wav"] -image_files = ["grandma-writing.png", "grandma-listening.png"] -script_dir = os.path.dirname(__file__) - -for file in sound_files: - # Build the full path to the sound file - full_path = os.path.join(script_dir, "assets", file) - # Get the filename without the extension to use as the dictionary key - filename = os.path.splitext(os.path.basename(full_path))[0] - # Open the sound and convert it to bytes - with wave.open(full_path) as audio_file: - sounds[file] = audio_file.readframes(-1) - -for file in image_files: - # Build the full path to the image file - full_path = os.path.join(script_dir, "assets", file) - # Get the filename without the extension to use as the dictionary key - filename = os.path.splitext(os.path.basename(full_path))[0] - # Open the image and convert it to bytes - with Image.open(full_path) as img: - images[file] = img.tobytes() - - -class StoryStartFrame(TextFrame): - pass - - -class StoryPageFrame(TextFrame): - pass - - -class StoryPromptFrame(TextFrame): - pass - - -class StoryProcessor(FrameProcessor): - def __init__(self, messages, story): - self._messages = messages - self._text = "" - self._story = story - - async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: - """ - The response from the LLM service looks like: - A comment about the user's choice - [start] (when the cat starts telling parts of the story) - A sentence of the story - [break] (between each sentence/'page' of the story) - [prompt] (when the cat asks the user to make a decision) - Question about the next part of the story - - 1. Catch the frames that are generated by the LLM service - """ - if isinstance(frame, UserStoppedSpeakingFrame): - yield ImageFrame(images["grandma-writing.png"], (1024, 1024)) - yield AudioFrame(sounds["talking.wav"]) - - elif isinstance(frame, TextFrame): - self._text += frame.text - - if re.findall(r".*\[[sS]tart\].*", self._text): - # Then we have the intro. Send it to speech ASAP - self._text = self._text.replace("[Start]", "") - self._text = self._text.replace("[start]", "") - - self._text = self._text.replace("\n", " ") - if len(self._text) > 2: - yield ImageFrame(images["grandma-writing.png"], (1024, 1024)) - yield StoryStartFrame(self._text) - yield AudioFrame(sounds["ding3.wav"]) - self._text = "" - - elif re.findall(r".*\[[bB]reak\].*", self._text): - # Then it's a page of the story. Get an image too - self._text = self._text.replace("[Break]", "") - self._text = self._text.replace("[break]", "") - self._text = self._text.replace("\n", " ") - if len(self._text) > 2: - self._story.append(self._text) - yield StoryPageFrame(self._text) - yield AudioFrame(sounds["ding3.wav"]) - - self._text = "" - elif re.findall(r".*\[[pP]rompt\].*", self._text): - # Then it's question time. Flush any - # text here as a story page, then set - # the var to get to prompt mode - # cb: trying scene now - # self.handle_chunk(self._text) - self._text = self._text.replace("[Prompt]", "") - self._text = self._text.replace("[prompt]", "") - - self._text = self._text.replace("\n", " ") - if len(self._text) > 2: - self._story.append(self._text) - yield StoryPageFrame(self._text) - else: - # After the prompt thing, we'll catch an LLM end to get the - # last bit - pass - elif isinstance(frame, LLMResponseEndFrame): - yield ImageFrame(images["grandma-writing.png"], (1024, 1024)) - yield StoryPromptFrame(self._text) - self._text = "" - yield frame - yield ImageFrame(images["grandma-listening.png"], (1024, 1024)) - yield AudioFrame(sounds["listening.wav"]) - - else: - # pass through everything that's not a TextFrame - yield frame - - -class StoryImageGenerator(FrameProcessor): - def __init__(self, story, llm, img): - self._story = story - self._llm = llm - self._img = img - - async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: - if isinstance(frame, StoryPageFrame): - if len(self._story) == 1: - prompt = f'You are an illustrator for a children\'s story book. Generate a prompt for DALL-E to create an illustration for the first page of the book, which reads: "{self._story[0]}"\n\n Your response should start with the phrase "Children\'s book illustration of".' - else: - prompt = f"You are an illustrator for a children's story book. Here is the story so far:\n\n\"{' '.join(self._story[:-1])}\"\n\nGenerate a prompt for DALL-E to create an illustration for the next page. Here's the sentence for the next page:\n\n\"{self._story[-1:][0]}\"\n\n Your response should start with the phrase \"Children's book illustration of\"." - msgs = [{"role": "system", "content": prompt}] - image_prompt = "" - async for f in self._llm.process_frame(LLMMessagesFrame(msgs)): - if isinstance(f, TextFrame): - image_prompt += f.text - async for f in self._img.process_frame(TextFrame(image_prompt)): - yield f - # Yield the original StoryPageFrame for basic image/audio sync - yield frame - else: - yield frame - - -async def main(room_url: str, token): - async with aiohttp.ClientSession() as session: - messages = [ - { - "role": "system", - "content": "You are a storytelling grandma who loves to make up fantastic, fun, and educational stories for children between the ages of 5 and 10 years old. Your stories are full of friendly, magical creatures. Your stories are never scary. Each sentence of your story will become a page in a storybook. Stop after 3-4 sentences and give the child a choice to make that will influence the next part of the story. Once the child responds, start by saying something nice about the choice they made, then include [start] in your response. Include [break] after each sentence of the story. Include [prompt] between the story and the prompt.", - } - ] - - story = [] - - llm = OpenAILLMService( - api_key=os.getenv("OPENAI_API_KEY"), - model="gpt-4-1106-preview", - ) # gpt-4-1106-preview - tts = ElevenLabsTTSService( - aiohttp_session=session, - api_key=os.getenv("ELEVENLABS_API_KEY"), - voice_id="Xb7hH8MSUJpSbSDYk0k2", - ) # matilda - img = FalImageGenService( - params={ - image_size = "1024x1024", - }, - aiohttp_session=session, - key=os.getenv("FAL_KEY"), - ) - lra = LLMAssistantResponseAggregator(messages) - ura = LLMUserResponseAggregator(messages) - sp = StoryProcessor(messages, story) - sig = StoryImageGenerator(story, llm, img) - - transport = DailyTransport( - room_url, - token, - "Storybot", - 5, - mic_enabled=True, - mic_sample_rate=16000, - camera_enabled=True, - camera_width=1024, - camera_height=1024, - start_transcription=True, - vad_enabled=True, - vad_stop_s=1.5, - ) - - start_story_event = asyncio.Event() - - @transport.event_handler("on_first_other_participant_joined") - async def on_first_other_participant_joined(transport, participant): - start_story_event.set() - - async def storytime(): - await start_story_event.wait() - - # We're being a bit tricky here by using a special system prompt to - # ask the user for a story topic. After their intial response, we'll - # use a different system prompt to create story pages. - intro_messages = [ - { - "role": "system", - "content": "You are a storytelling grandma who loves to make up fantastic, fun, and educational stories for children between the ages of 5 and 10 years old. Your stories are full of friendly, magical creatures. Your stories are never scary. Begin by asking what a child wants you to tell a story about. Keep your reponse to only a few sentences.", - } - ] - lca = LLMAssistantContextAggregator(messages) - local_pipeline = Pipeline( - [llm, lca, tts], sink=transport.send_queue) - await local_pipeline.queue_frames( - [ - ImageFrame(images["grandma-listening.png"], (1024, 1024)), - LLMMessagesFrame(intro_messages), - AudioFrame(sounds["listening.wav"]), - EndPipeFrame(), - ] - ) - await local_pipeline.run_pipeline() - - pipeline = Pipeline([llm, lca, tts, ls_sink]) - pipeline.queue_frames([...]) - pipeline.run() - - fl = FrameLogger("### After Image Generation") - pipeline = Pipeline( - processors=[ - ura, - llm, - sp, - sig, - fl, - tts, - lra, - ] - ) - await transport.run_pipeline( - pipeline, - ) - - try: - await asyncio.gather(transport.run(), storytime()) - except (asyncio.CancelledError, KeyboardInterrupt): - print("whoops") - transport.stop() - - -if __name__ == "__main__": - (url, token) = configure() - asyncio.run(main(url, token)) diff --git a/examples/storytelling-chatbot/.dockerignore b/examples/storytelling-chatbot/.dockerignore new file mode 100644 index 000000000..a6b5f4231 --- /dev/null +++ b/examples/storytelling-chatbot/.dockerignore @@ -0,0 +1,2 @@ +frontend/node_modules +frontend/out \ No newline at end of file diff --git a/examples/storytelling-chatbot/.gitignore b/examples/storytelling-chatbot/.gitignore new file mode 100644 index 000000000..ba9cd4106 --- /dev/null +++ b/examples/storytelling-chatbot/.gitignore @@ -0,0 +1,158 @@ +node_modules/ +.idea/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class +.DS_Store + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +read.html \ No newline at end of file diff --git a/examples/storytelling-chatbot/Dockerfile b/examples/storytelling-chatbot/Dockerfile new file mode 100644 index 000000000..b84ba5431 --- /dev/null +++ b/examples/storytelling-chatbot/Dockerfile @@ -0,0 +1,54 @@ +FROM python:3.11-bullseye + +ARG DEBIAN_FRONTEND=noninteractive +ARG USE_PERSISTENT_DATA +ENV PYTHONUNBUFFERED=1 +ENV NODE_MAJOR=20 + +# Expose FastAPI port +ENV FAST_API_PORT=7860 +EXPOSE 7860 + +# Install system dependencies +RUN apt-get update && apt-get install --no-install-recommends -y \ + build-essential \ + git \ + ffmpeg \ + google-perftools \ + ca-certificates curl gnupg \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +# Install Node.js +RUN mkdir -p /etc/apt/keyrings +RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg +RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_${NODE_MAJOR}.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list > /dev/null +RUN apt-get update && apt-get install nodejs -y + +# Set up a new user named "user" with user ID 1000 +RUN useradd -m -u 1000 user + +# Set home to the user's home directory +ENV HOME=/home/user \ + PATH=/home/user/.local/bin:$PATH \ + PYTHONPATH=$HOME/app \ + PYTHONUNBUFFERED=1 + +# Switch to the "user" user +USER user + +# Set the working directory to the user's home directory +WORKDIR $HOME/app + +# Install Python dependencies +COPY ./requirements.txt requirements.txt +RUN pip3 install --no-cache-dir --upgrade -r requirements.txt + +# Copy everything else +COPY --chown=user ./src/ src/ + +# Copy frontend app and build +COPY --chown=user ./frontend/ frontend/ +RUN cd frontend && npm install && npm run build + +# Start the FastAPI server +CMD python3 src/server.py --port ${FAST_API_PORT} \ No newline at end of file diff --git a/examples/storytelling-chatbot/README.md b/examples/storytelling-chatbot/README.md new file mode 100644 index 000000000..543ee6d46 --- /dev/null +++ b/examples/storytelling-chatbot/README.md @@ -0,0 +1,83 @@ +[![Try](https://img.shields.io/badge/try_it-here-blue)](https://storytelling-chatbot.fly.dev) + +# Storytelling Chatbot + + + +This example shows how to build a voice-driven interactive storytelling experience. +It periodically prompts the user for input for a 'choose your own adventure' style experience. + +We add visual elements to the story by generating images at lightning speed using Fal. + + +--- + +### It uses the following AI services: + +**Deepgram - Speech-to-Text** + +Transcribes inbound participant voice media to text. + +**OpenAI (GPT4) - LLM** + +Our creative writer LLM. You can see the context used to prompt it [here](src/prompts.py) + +**ElevenLabs - Text-to-Speech** + +Converts and streams the LLM response from text to audio + +**Fal.ai - Image Generation** + +Adds pictures to our story (really fast!) Prompting is quite key for style consistency, so we task the LLM to turn each story page into a short image prompt. + +--- + +## Setup + +**Install requirements** + +```shell +pip install -r requirements.txt +``` + +**Create environment file and set variables:** + +```shell +mv env.example .env +``` + +**Build the frontend:** + +This project uses a custom frontend, which needs to built. Note: this is done automatically as part of the Docker deployment. + +```shell +cd frontend/ +npm install / yarn +npm run build +``` + +The build UI files can be found in `frontend/out` + +## Running it locally + +Start the API / bot manager: + +`python src/server.py` + +If you'd like to run a custom domain or port: + +`python src/server.py --host somehost --p 7777` + +➡️ Open the host URL in your browser + +> [!IMPORTANT] +> Whilst working on the frontend code, please `yarn run dev` +> and open the NextJS hosted service vs. the Python server. +> (Usually localhost:3000.) + +--- + +## Improvements to make + +- Wait for track_started event to avoid rushed intro +- Show 5 minute timer on the UI diff --git a/examples/storytelling-chatbot/env.example b/examples/storytelling-chatbot/env.example new file mode 100644 index 000000000..e32c9fe23 --- /dev/null +++ b/examples/storytelling-chatbot/env.example @@ -0,0 +1,6 @@ +ELEVENLABS_API_KEY= +ELEVENLABS_VOICE_ID= +FAL_KEY= +DAILY_API_URL=api.daily.co/v1 +DAILY_API_KEY= +OPENAI_API_KEY= \ No newline at end of file diff --git a/examples/storytelling-chatbot/frontend/.eslintrc.json b/examples/storytelling-chatbot/frontend/.eslintrc.json new file mode 100644 index 000000000..bffb357a7 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/examples/storytelling-chatbot/frontend/.gitignore b/examples/storytelling-chatbot/frontend/.gitignore new file mode 100644 index 000000000..fd3dbb571 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/examples/storytelling-chatbot/frontend/README.md b/examples/storytelling-chatbot/frontend/README.md new file mode 100644 index 000000000..c4033664f --- /dev/null +++ b/examples/storytelling-chatbot/frontend/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/examples/storytelling-chatbot/frontend/app/favicon.ico b/examples/storytelling-chatbot/frontend/app/favicon.ico new file mode 100644 index 000000000..8c49042df Binary files /dev/null and b/examples/storytelling-chatbot/frontend/app/favicon.ico differ diff --git a/examples/storytelling-chatbot/frontend/app/globals.css b/examples/storytelling-chatbot/frontend/app/globals.css new file mode 100644 index 000000000..4ea7806f0 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/app/globals.css @@ -0,0 +1,109 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 224 71.4% 4.1%; + + --card: 0 0% 100%; + --card-foreground: 224 71.4% 4.1%; + + --popover: 0 0% 100%; + --popover-foreground: 224 71.4% 4.1%; + + --primary: 220.9 39.3% 11%; + --primary-foreground: 210 20% 98%; + + --secondary: 220 14.3% 95.9%; + --secondary-foreground: 220.9 39.3% 11%; + + --muted: 220 14.3% 95.9%; + --muted-foreground: 220 8.9% 46.1%; + + --accent: 220 14.3% 95.9%; + --accent-foreground: 220.9 39.3% 11%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 20% 98%; + + --border: 220 13% 91%; + --input: 220 13% 91%; + --ring: 224 71.4% 4.1%; + + --radius: 0.5rem; + } + + .dark { + --background: 224 71.4% 4.1%; + --foreground: 210 20% 98%; + + --card: 224 71.4% 4.1%; + --card-foreground: 210 20% 98%; + + --popover: 224 71.4% 4.1%; + --popover-foreground: 210 20% 98%; + + --primary: 210 20% 98%; + --primary-foreground: 220.9 39.3% 11%; + + --secondary: 215 27.9% 16.9%; + --secondary-foreground: 210 20% 98%; + + --muted: 215 27.9% 16.9%; + --muted-foreground: 217.9 10.6% 64.9%; + + --accent: 215 27.9% 16.9%; + --accent-foreground: 210 20% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 20% 98%; + + --border: 215 27.9% 16.9%; + --input: 215 27.9% 16.9%; + --ring: 216 12.2% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} + +body{ + background: url("/bg.jpg") no-repeat center center; + background-size: cover; +} + +.cardShadow{ + box-shadow: 0px 124px 35px 0px rgba(0, 0, 0, 0.00), 0px 79px 32px 0px rgba(0, 0, 0, 0.01), 0px 45px 27px 0px rgba(0, 0, 0, 0.05), 0px 20px 20px 0px rgba(0, 0, 0, 0.09), 0px 5px 11px 0px rgba(0, 0, 0, 0.10); +} + +@keyframes fadeInSlideUp { + 0% { + opacity: 0; + transform: translateY(50px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +.fade-in { + animation: fadeIn 1s ease-out; +} + +@keyframes fadeIn { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} \ No newline at end of file diff --git a/examples/storytelling-chatbot/frontend/app/layout.tsx b/examples/storytelling-chatbot/frontend/app/layout.tsx new file mode 100644 index 000000000..e7ca11512 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/app/layout.tsx @@ -0,0 +1,46 @@ +import React from "react"; + +import "./globals.css"; +import type { Metadata } from "next"; +import { Space_Grotesk, Space_Mono } from "next/font/google"; + +import { cn } from "@/app/utils"; + +// Font +const sans = Space_Grotesk({ + subsets: ["latin"], + weight: ["400", "500", "600"], + variable: "--font-sans", +}); + +const mono = Space_Mono({ + subsets: ["latin"], + weight: ["400", "700"], + variable: "--font-mono", +}); + +export const metadata: Metadata = { + title: "Storytelling Chatbot - Daily AI", + description: "Built with git.new/ai", + metadataBase: new URL(process.env.SITE_URL || "http://localhost:3000"), +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + +
{children}
+ + + ); +} diff --git a/examples/storytelling-chatbot/frontend/app/opengraph-image.png b/examples/storytelling-chatbot/frontend/app/opengraph-image.png new file mode 100644 index 000000000..777b74296 Binary files /dev/null and b/examples/storytelling-chatbot/frontend/app/opengraph-image.png differ diff --git a/examples/storytelling-chatbot/frontend/app/page.tsx b/examples/storytelling-chatbot/frontend/app/page.tsx new file mode 100644 index 000000000..cb395aaf9 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/app/page.tsx @@ -0,0 +1,16 @@ +"use client"; + +import React from "react"; +import { DailyProvider, useCallObject } from "@daily-co/daily-react"; + +import App from "../components/App"; + +export default function Home() { + const callObject = useCallObject({}); + + return ( + + + + ); +} diff --git a/examples/storytelling-chatbot/frontend/app/twitter-image.png b/examples/storytelling-chatbot/frontend/app/twitter-image.png new file mode 100644 index 000000000..2a747ef9e Binary files /dev/null and b/examples/storytelling-chatbot/frontend/app/twitter-image.png differ diff --git a/examples/storytelling-chatbot/frontend/app/utils.ts b/examples/storytelling-chatbot/frontend/app/utils.ts new file mode 100644 index 000000000..d084ccade --- /dev/null +++ b/examples/storytelling-chatbot/frontend/app/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/examples/storytelling-chatbot/frontend/components.json b/examples/storytelling-chatbot/frontend/components.json new file mode 100644 index 000000000..62fbc68a1 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components.json @@ -0,0 +1,17 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "gray", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/app" + } +} \ No newline at end of file diff --git a/examples/storytelling-chatbot/frontend/components/App.tsx b/examples/storytelling-chatbot/frontend/components/App.tsx new file mode 100644 index 000000000..761787c88 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/App.tsx @@ -0,0 +1,90 @@ +"use client"; + +import React, { useState } from "react"; + +import { useDaily } from "@daily-co/daily-react"; +import Setup from "./Setup"; +import Story from "./Story"; + +type State = + | "idle" + | "connecting" + | "connected" + | "started" + | "finished" + | "error"; + +export default function Call() { + const daily = useDaily(); + + const [state, setState] = useState("idle"); + const [room, setRoom] = useState(null); + + async function start() { + setState("connecting"); + + if (!daily) return; + + // Create a new room for the story session + try { + const response = await fetch("/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + room_url: process.env.NEXT_PUBLIC_ROOM_URL || null, + }), + }); + + const { room_url, token } = await response.json(); + + // Keep a reference to the room url for later + setRoom(room_url); + + // Join the WebRTC session + await daily.join({ + url: room_url, + token, + videoSource: false, + startAudioOff: true, + }); + + setState("connected"); + + // Disable local audio, the bot will say hello first + daily.setLocalAudio(false); + + // Start the bot + const resp = await fetch("/start", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + room_url, + }), + }); + + setState("started"); + } catch (error) { + setState("error"); + leave(); + } + } + + async function leave() { + await daily?.leave(); + setState("finished"); + } + + if (state === "error") { + return
An Error occured
; + } + + if (state === "started") { + return leave()} />; + } + + return start()} />; +} diff --git a/examples/storytelling-chatbot/frontend/components/AudioIndicator/index.tsx b/examples/storytelling-chatbot/frontend/components/AudioIndicator/index.tsx new file mode 100644 index 000000000..09ca4c402 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/AudioIndicator/index.tsx @@ -0,0 +1,69 @@ +import { + useAudioLevel, + useAudioTrack, + useLocalSessionId, +} from "@daily-co/daily-react"; +import { useCallback, useRef } from "react"; + +export const AudioIndicator: React.FC = () => { + const localSessionId = useLocalSessionId(); + const audioTrack = useAudioTrack(localSessionId); + const volRef = useRef(null); + + useAudioLevel( + audioTrack?.persistentTrack, + useCallback((volume) => { + // this volume number will be between 0 and 1 + // give it a minimum scale of 0.15 to not completely disappear 👻 + if (volRef.current) { + const v = volume * 1.75; + volRef.current.style.transform = `scale(${Math.max(0.1, v)})`; + } + }, []) + ); + + // Your audio track's audio volume visualized in a small circle, + // whose size changes depending on the volume level + return ( + <> +
+ + + ); +}; + +export const AudioIndicatorBar: React.FC = () => { + const localSessionId = useLocalSessionId(); + const audioTrack = useAudioTrack(localSessionId); + + const volRef = useRef(null); + + useAudioLevel( + audioTrack?.persistentTrack, + useCallback((volume) => { + if (volRef.current) + volRef.current.style.width = Math.max(2, volume * 100) + "%"; + }, []) + ); + + return ( +
+
+
+ ); +}; + +export default AudioIndicator; diff --git a/examples/storytelling-chatbot/frontend/components/DevicePicker/index.tsx b/examples/storytelling-chatbot/frontend/components/DevicePicker/index.tsx new file mode 100644 index 000000000..6cc69796d --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/DevicePicker/index.tsx @@ -0,0 +1,139 @@ +"use client"; + +import { useEffect } from "react"; +import { DailyMeetingState } from "@daily-co/daily-js"; +import { useDaily, useDevices } from "@daily-co/daily-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { IconMicrophone, IconDeviceSpeaker } from "@tabler/icons-react"; +import { AudioIndicatorBar } from "../AudioIndicator"; + +interface Props {} + +export default function DevicePicker({}: Props) { + const daily = useDaily(); + const { + currentMic, + hasMicError, + micState, + microphones, + setMicrophone, + currentSpeaker, + speakers, + setSpeaker, + } = useDevices(); + + const handleMicrophoneChange = (value: string) => { + setMicrophone(value); + }; + + const handleSpeakerChange = (value: string) => { + setSpeaker(value); + }; + + useEffect(() => { + if (microphones.length > 0 || !daily || daily.isDestroyed()) return; + const meetingState = daily.meetingState(); + const meetingStatesBeforeJoin: DailyMeetingState[] = [ + "new", + "loading", + "loaded", + ]; + if (meetingStatesBeforeJoin.includes(meetingState)) { + daily.startCamera({ startVideoOff: true, startAudioOff: false }); + } + }, [daily, microphones]); + + return ( +
+
+ +
+ +
+ + +
+
+
+ +
+ +
+ + +
+
+ {hasMicError && ( +
+ {micState === "blocked" ? ( +

+ Please check your browser and system permissions. Make sure that + this app is allowed to access your microphone. +

+ ) : micState === "in-use" ? ( +

+ Your microphone is being used by another app. Please close any + other apps using your microphone and restart this app. +

+ ) : micState === "not-found" ? ( +

+ No microphone seems to be connected. Please connect a microphone. +

+ ) : micState === "not-supported" ? ( +

+ This app is not supported on your device. Please update your + software or use a different device. +

+ ) : ( +

+ There seems to be an issue accessing your microphone. Try + restarting the app or consult a system administrator. +

+ )} +
+ )} +
+ ); +} diff --git a/examples/storytelling-chatbot/frontend/components/MicToggle/index.tsx b/examples/storytelling-chatbot/frontend/components/MicToggle/index.tsx new file mode 100644 index 000000000..011e6d06f --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/MicToggle/index.tsx @@ -0,0 +1,34 @@ +import { + useDaily, + useLocalSessionId, + useMediaTrack, +} from "@daily-co/daily-react"; +import { useCallback } from "react"; +import { IconMicrophone, IconMicrophoneOff } from "@tabler/icons-react"; + +export const MicToggle: React.FC = () => { + const daily = useDaily(); + const localSessionId = useLocalSessionId(); + const audioTrack = useMediaTrack(localSessionId, "audio"); + const isMicMuted = + audioTrack.state === "blocked" || audioTrack.state === "off"; + + const handleClick = useCallback(() => { + if (!daily) return; + daily.setLocalAudio(isMicMuted); + }, [daily, isMicMuted]); + + const text = isMicMuted ? ( + + ) : ( + + ); + + return ( + + ); +}; + +export default MicToggle; diff --git a/examples/storytelling-chatbot/frontend/components/Setup.tsx b/examples/storytelling-chatbot/frontend/components/Setup.tsx new file mode 100644 index 000000000..42781e87e --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/Setup.tsx @@ -0,0 +1,93 @@ +import React from "react"; +import { Button } from "@/components/ui/button"; +import DevicePicker from "@/components/DevicePicker"; +import { IconEar, IconLoader2 } from "@tabler/icons-react"; + +type SetupProps = { + handleStart: () => void; +}; + +const buttonLabel = { + intro: "Next", + setup: "Let's begin!", + loading: "Joining...", +}; +export const Setup: React.FC = ({ handleStart }) => { + const [state, setState] = React.useState<"intro" | "setup" | "loading">( + "intro" + ); + + return ( +
+
+
+

+ Welcome to Storytime +

+ + {state === "intro" ? ( + <> +

+ This app demos a voice-controlled storytelling chatbot. It will + start with the bot asking you what kind of story you'd like + to hear (e.g. a fairy tale, a mystery, etc.). After each scene, + the bot will pause to ask for your input. Direct the story any + way you choose! +

+

+ For best results, try in a quiet + environment! +

+ + ) : ( + <> +

+ Since you'll be talking to Storybot, we need to make sure + it can hear you! Please configure your microphone and speakers + below. +

+ + + )} + +
+ + +
+
+ +
+ ); +}; + +export default Setup; diff --git a/examples/storytelling-chatbot/frontend/components/Story.tsx b/examples/storytelling-chatbot/frontend/components/Story.tsx new file mode 100644 index 000000000..100c83fc1 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/Story.tsx @@ -0,0 +1,79 @@ +import React, { useState } from "react"; +import { + useDaily, + useParticipantIds, + useAppMessage, + DailyAudio, +} from "@daily-co/daily-react"; +import { IconLogout, IconLoader2 } from "@tabler/icons-react"; + +import VideoTile from "@/components/VideoTile"; +import { Button } from "@/components/ui/button"; +import UserInputIndicator from "@/components/UserInputIndicator"; +import WaveText from "@/components/WaveText"; + +interface StoryProps { + handleLeave: () => void; +} + +const Story: React.FC = ({ handleLeave }) => { + const daily = useDaily(); + const participantIds = useParticipantIds({ filter: "remote" }); + const [storyState, setStoryState] = useState<"user" | "assistant">( + "assistant" + ); + + useAppMessage({ + onAppMessage: (e) => { + if (!daily || !e.data?.cue) return; + + // Determine the UI state from the cue sent by the bot + if (e.data?.cue === "user_turn") { + // Delay enabling local mic input to avoid feedback from LLM + setTimeout(() => daily.setLocalAudio(true), 500); + setStoryState("user"); + } else { + daily.setLocalAudio(false); + setStoryState("assistant"); + } + }, + }); + + return ( +
+ {/* Absolute elements */} +
+ +
+
+ +
+
+ + {/* Static elements */} +
+ {participantIds.length >= 1 ? ( + + ) : ( + + + + )} + +
+ +
+ ); +}; + +export default Story; diff --git a/examples/storytelling-chatbot/frontend/components/StoryTranscript/StoryTranscript.module.css b/examples/storytelling-chatbot/frontend/components/StoryTranscript/StoryTranscript.module.css new file mode 100644 index 000000000..d8a5816a0 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/StoryTranscript/StoryTranscript.module.css @@ -0,0 +1,42 @@ +.container{ + position: absolute; + color:white; + z-index: 50; + margin: 0 auto; + display: flex; + flex-direction: column; + @apply gap-4 inset-6; + align-items: center; + justify-content: end; + text-align: center; +} + +.transcript{ + @apply font-semibold; +} + +.transcript span{ + box-decoration-break: clone; + @apply bg-gray-900/80 rounded-md px-4 py-2; +} + +.sentence{ + opacity: 1; + animation: fadeOut 2.5s linear forwards; + animation-delay: 1s; +} +@keyframes fadeOut { + 0% { + opacity: 1; + transform: scale(1); + } + 20% { + transform: scale(1); + filter: blur(0); + } + 100% { + transform: scale(0.8) translateY(-50%); + filter: blur(25px); + opacity: 0; + } +} \ No newline at end of file diff --git a/examples/storytelling-chatbot/frontend/components/StoryTranscript/index.tsx b/examples/storytelling-chatbot/frontend/components/StoryTranscript/index.tsx new file mode 100644 index 000000000..520ca142e --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/StoryTranscript/index.tsx @@ -0,0 +1,55 @@ +"use client"; + +import React, { useEffect, useRef, useState } from "react"; +import { useAppMessage } from "@daily-co/daily-react"; +import { DailyEventObjectAppMessage } from "@daily-co/daily-js"; + +import styles from "./StoryTranscript.module.css"; + +export default function StoryTranscript() { + const [partialText, setPartialText] = useState(""); + const [sentences, setSentences] = useState([]); + const intervalRef = useRef(null); + + useEffect(() => { + clearInterval(intervalRef.current); + + intervalRef.current = setInterval(() => { + if (sentences.length > 2) { + setSentences((s) => s.slice(1)); + } + }, 2500); + + return () => clearInterval(intervalRef.current); + }, [sentences]); + + useAppMessage({ + onAppMessage: (e: DailyEventObjectAppMessage) => { + if (e.fromId && e.fromId === "transcription") { + // Check for LLM transcripts only + if (e.data.user_id !== "") { + setPartialText(e.data.text); + if (e.data.is_final) { + setPartialText(""); + setSentences((s) => [...s, e.data.text]); + } + } + } + }, + }); + + return ( +
+ {sentences.map((sentence, index) => ( +

+ {sentence} +

+ ))} + {partialText && ( +

+ {partialText} +

+ )} +
+ ); +} diff --git a/examples/storytelling-chatbot/frontend/components/UserInputIndicator/UserInputIndicator.module.css b/examples/storytelling-chatbot/frontend/components/UserInputIndicator/UserInputIndicator.module.css new file mode 100644 index 000000000..bc57e4d5a --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/UserInputIndicator/UserInputIndicator.module.css @@ -0,0 +1,82 @@ +.panel{ + width: 100%; + pointer-events: none; + color: #ffffff; + text-align: center; + position: relative; + @apply pb-8; +} + +.panel::after{ + content: ""; + position: absolute; + inset: 0px; + z-index: 10; + opacity: 0; + transition: all 0.3s ease; + @apply bg-gradient-to-t from-gray-950 to-transparent; +} + +.active::after{ opacity: 1;} + +.micIcon{ + position: relative; + width: 120px; + height: 120px; + border-radius: 120px; + border: 6px solid; + outline: 6px solid; + display: flex; + justify-content: center; + align-items: center; + margin: 0 auto; + z-index: 20; + transition: all 0.5s ease; + @apply bg-gray-700/60 border-gray-600 outline-gray-900/20; + +} + +.micIcon svg{ + position: relative; + z-index: 20; + opacity: 0.25; + transition: opacity 0.5s ease; +} + + +@keyframes pulse { + 0% { + outline-width: 6px; + @apply outline-teal-500/10; + } + 50% { + outline-width: 24px; + @apply outline-teal-500/50; + } + 100% { + outline-width: 6px; + @apply outline-teal-500/10; + } +} + +.micIconActive{ + @apply bg-teal-950 border-teal-500 outline-teal-500/20; + animation: pulse 2s infinite ease-in-out; +} + +.micIconActive svg{ + opacity: 1; +} + +.transcript{ + flex: 0; + align-self: center; + opacity: 0.25; + transition: opacity 1s ease; + transition-delay: 2.5s; + @apply bg-gray-900/90 font-medium py-1 px-2 rounded-sm mt-4; +} + +.active .transcript{ + opacity: 1; +} \ No newline at end of file diff --git a/examples/storytelling-chatbot/frontend/components/UserInputIndicator/index.tsx b/examples/storytelling-chatbot/frontend/components/UserInputIndicator/index.tsx new file mode 100644 index 000000000..c45d6536d --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/UserInputIndicator/index.tsx @@ -0,0 +1,48 @@ +import React, { useState, useEffect } from "react"; + +import { useAppMessage } from "@daily-co/daily-react"; +import { DailyEventObjectAppMessage } from "@daily-co/daily-js"; +import styles from "./UserInputIndicator.module.css"; +import { IconMicrophone } from "@tabler/icons-react"; +import { TypewriterEffect } from "../ui/typewriter"; +import AudioIndicator from "../AudioIndicator"; + +interface Props { + active: boolean; +} + +export default function UserInputIndicator({ active }: Props) { + const [transcription, setTranscription] = useState([]); + + useAppMessage({ + onAppMessage: (e: DailyEventObjectAppMessage) => { + if (e.fromId && e.fromId === "transcription") { + if (e.data.user_id === "" && e.data.is_final) { + setTranscription((t) => [...t, ...e.data.text.split(" ")]); + } + } + }, + }); + + useEffect(() => { + if (active) return; + const t = setTimeout(() => setTranscription([]), 4000); + return () => clearTimeout(t); + }, [active]); + + return ( +
+
+
+ + {active && } +
+
+ +
+
+
+ ); +} diff --git a/examples/storytelling-chatbot/frontend/components/VideoTile/VideoTile.module.css b/examples/storytelling-chatbot/frontend/components/VideoTile/VideoTile.module.css new file mode 100644 index 000000000..7158ca10d --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/VideoTile/VideoTile.module.css @@ -0,0 +1,34 @@ +.container{ + position: relative; + animation: fadeIn 3s ease; + transition: all 3s ease-out; +} + +.videoTile{ + @apply bg-gray-950; + width: 560px; + height: 560px; + mask-image: url('/alpha-mask.gif'); + mask-size: cover; + mask-repeat: no-repeat; + margin:0 auto; + z-index: 10; + position: relative; +} + +.inactive{ + opacity: 0.7; + filter:blur(3px); + transform: scale(0.95) +} + +@keyframes fadeIn { + 0% { + filter: blur(100px); + opacity: 0; + } + 100% { + filter: blur(0px); + opacity: 1; + } +} \ No newline at end of file diff --git a/examples/storytelling-chatbot/frontend/components/VideoTile/index.tsx b/examples/storytelling-chatbot/frontend/components/VideoTile/index.tsx new file mode 100644 index 000000000..af20b198d --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/VideoTile/index.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import styles from "./VideoTile.module.css"; +import { DailyVideo } from "@daily-co/daily-react"; +import StoryTranscript from "@/components/StoryTranscript"; + +interface Props { + sessionId: string; + inactive: boolean; +} + +const VideoTile = ({ sessionId, inactive }: Props) => { + return ( +
+ + +
+ +
+
+ ); +}; + +export default VideoTile; diff --git a/examples/storytelling-chatbot/frontend/components/WaveText/WaveText.module.css b/examples/storytelling-chatbot/frontend/components/WaveText/WaveText.module.css new file mode 100644 index 000000000..75754a779 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/WaveText/WaveText.module.css @@ -0,0 +1,68 @@ +.waveText{ + color: white; + text-shadow: 3px 3px 0px rgba(0,0,0,0.5); + opacity: 0; + transition: opacity 2s ease; + position: relative; + @apply text-4xl font-bold; +} + +.active{ + opacity: 1; +} + +@keyframes jump { + 0% { + opacity: 0.5; + transform:translateY(0px) + } + 50% { + opacity: 1; + transform:translateY(-30px); + } + 100% { + opacity: 0.55; + transform:translateY(0px) + } + } + +.waveText span{ + display:inline-block; + animation:jump 2s infinite ease-in-out; + +} + +.waveText span:nth-child(1) { + animation-delay:0s; +} + +.waveText span:nth-child(1) { + animation-delay:0.1s; +} +.waveText span:nth-child(2) { + animation-delay:0.2s; +} +.waveText span:nth-child(3) { + animation-delay:0.3s; +} +.waveText span:nth-child(4) { + animation-delay:0.4s; +} +.waveText span:nth-child(5) { + animation-delay:0.5s; +} +.waveText span:nth-child(6) { + animation-delay:0.6s; +} +.waveText span:nth-child(7) { + animation-delay:0.7s; +} +.waveText span:nth-child(8) { + animation-delay:0.8s; +} +.waveText span:nth-child(9) { + animation-delay:0.9s; +} +.waveText span:nth-child(10) { + animation-delay:1s; +} \ No newline at end of file diff --git a/examples/storytelling-chatbot/frontend/components/WaveText/index.tsx b/examples/storytelling-chatbot/frontend/components/WaveText/index.tsx new file mode 100644 index 000000000..1f3e9923c --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/WaveText/index.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import styles from "./WaveText.module.css"; + +interface Props { + active: boolean; +} + +export default function WaveText({ active }: Props) { + return ( +
+ W + h + a + t +    + n + e + x + t + ? +
+ ); +} diff --git a/examples/storytelling-chatbot/frontend/components/ui/button.tsx b/examples/storytelling-chatbot/frontend/components/ui/button.tsx new file mode 100644 index 000000000..f6d102e1c --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/app/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-11 rounded-lg px-6 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-12 rounded-xl px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/examples/storytelling-chatbot/frontend/components/ui/select.tsx b/examples/storytelling-chatbot/frontend/components/ui/select.tsx new file mode 100644 index 000000000..f2b6eafc8 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/ui/select.tsx @@ -0,0 +1,160 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { IconCheck, IconChevronDown, IconChevronUp } from "@tabler/icons-react"; + +import { cn } from "@/app/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + span]:line-clamp-1", + className + )} + {...props} + > + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectScrollUpButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName; + +const SelectScrollDownButton = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +SelectScrollDownButton.displayName = + SelectPrimitive.ScrollDownButton.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + {children} + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, + SelectScrollUpButton, + SelectScrollDownButton, +}; diff --git a/examples/storytelling-chatbot/frontend/components/ui/typewriter.tsx b/examples/storytelling-chatbot/frontend/components/ui/typewriter.tsx new file mode 100644 index 000000000..1e51bf418 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/components/ui/typewriter.tsx @@ -0,0 +1,61 @@ +"use client"; + +import { cn } from "@/app/utils"; +import { motion } from "framer-motion"; + +export const TypewriterEffect = ({ + words, + className, +}: { + words: string[]; + className?: string; + cursorClassName?: string; +}) => { + const renderWords = () => { + return ( +
+ {words.map((word, idx) => { + return ( +
+ {word.split("").map((char, index) => ( + {char} + ))} +   +
+ ); + })} +
+ ); + }; + + return ( +
+ {words.length < 1 ? ( + ... + ) : ( + +
+ {renderWords()} +
+
+ )} +
+ ); +}; diff --git a/examples/storytelling-chatbot/frontend/env.example b/examples/storytelling-chatbot/frontend/env.example new file mode 100644 index 000000000..edad9bc2b --- /dev/null +++ b/examples/storytelling-chatbot/frontend/env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_ROOM_URL= +SITE_URL= \ No newline at end of file diff --git a/examples/storytelling-chatbot/frontend/next.config.mjs b/examples/storytelling-chatbot/frontend/next.config.mjs new file mode 100644 index 000000000..9fc75242c --- /dev/null +++ b/examples/storytelling-chatbot/frontend/next.config.mjs @@ -0,0 +1,15 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + output: "export", + + async rewrites() { + return [ + { + source: "/:path*", + destination: "http://localhost:7860/:path*", + }, + ]; + }, +}; + +export default nextConfig; diff --git a/examples/storytelling-chatbot/frontend/package.json b/examples/storytelling-chatbot/frontend/package.json new file mode 100644 index 000000000..10edbb29a --- /dev/null +++ b/examples/storytelling-chatbot/frontend/package.json @@ -0,0 +1,38 @@ +{ + "name": "frontend", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@daily-co/daily-js": "^0.62.0", + "@daily-co/daily-react": "^0.18.0", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-slot": "^1.0.2", + "@tabler/icons-react": "^3.1.0", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.0", + "framer-motion": "^11.0.27", + "next": "14.1.4", + "react": "^18", + "react-dom": "^18", + "recoil": "^0.7.7", + "tailwind-merge": "^2.2.2", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10.0.1", + "eslint": "^8", + "eslint-config-next": "14.1.4", + "postcss": "^8", + "tailwindcss": "^3.4.3", + "typescript": "^5" + } +} diff --git a/examples/storytelling-chatbot/frontend/postcss.config.js b/examples/storytelling-chatbot/frontend/postcss.config.js new file mode 100644 index 000000000..12a703d90 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/examples/storytelling-chatbot/frontend/public/alpha-mask.gif b/examples/storytelling-chatbot/frontend/public/alpha-mask.gif new file mode 100644 index 000000000..6e97616bb Binary files /dev/null and b/examples/storytelling-chatbot/frontend/public/alpha-mask.gif differ diff --git a/examples/storytelling-chatbot/frontend/public/bg.jpg b/examples/storytelling-chatbot/frontend/public/bg.jpg new file mode 100644 index 000000000..fd4477b19 Binary files /dev/null and b/examples/storytelling-chatbot/frontend/public/bg.jpg differ diff --git a/examples/storytelling-chatbot/frontend/tailwind.config.ts b/examples/storytelling-chatbot/frontend/tailwind.config.ts new file mode 100644 index 000000000..338092dbb --- /dev/null +++ b/examples/storytelling-chatbot/frontend/tailwind.config.ts @@ -0,0 +1,86 @@ +import type { Config } from "tailwindcss"; + +import { fontFamily } from "tailwindcss/defaultTheme"; + +const config = { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + fontFamily: { + sans: ["var(--font-sans)", ...fontFamily.sans], + mono: ["var(--font-mono)", ...fontFamily.mono], + }, + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config; + +export default config; diff --git a/examples/storytelling-chatbot/frontend/tsconfig.json b/examples/storytelling-chatbot/frontend/tsconfig.json new file mode 100644 index 000000000..c672fee60 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/tsconfig.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "build/types/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/examples/storytelling-chatbot/frontend/yarn.lock b/examples/storytelling-chatbot/frontend/yarn.lock new file mode 100644 index 000000000..c29773cf6 --- /dev/null +++ b/examples/storytelling-chatbot/frontend/yarn.lock @@ -0,0 +1,3248 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@aashutoshrathi/word-wrap@^1.2.3": + version "1.2.6" + resolved "https://registry.yarnpkg.com/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz#bd9154aec9983f77b3a034ecaa015c2e4201f6cf" + integrity sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA== + +"@alloc/quick-lru@^5.2.0": + version "5.2.0" + resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" + integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== + +"@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.23.2", "@babel/runtime@^7.24.0": + version "7.24.4" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.24.4.tgz#de795accd698007a66ba44add6cc86542aff1edd" + integrity sha512-dkxf7+hn8mFBwKjs9bvBlArzLVxVbS8usaPUDd5p2a9JCL9tB8OaOVN1isD4+Xyk4ns89/xeOmbQvgdK7IIVdA== + dependencies: + regenerator-runtime "^0.14.0" + +"@daily-co/daily-js@^0.62.0": + version "0.62.0" + resolved "https://registry.yarnpkg.com/@daily-co/daily-js/-/daily-js-0.62.0.tgz#db298f85a3b9fe0a142846bfd4d09d298b16f24b" + integrity sha512-OskaskD0DU44bfoAaW+El5TPnJNTG0FdKx+pjRfgyqkXt4sOuLdo0DoUmUVDkls2iKHTbpTRJUOR8MeW/OdccQ== + dependencies: + "@babel/runtime" "^7.12.5" + "@sentry/browser" "^7.60.1" + bowser "^2.8.1" + dequal "^2.0.3" + events "^3.1.0" + +"@daily-co/daily-react@^0.18.0": + version "0.18.0" + resolved "https://registry.yarnpkg.com/@daily-co/daily-react/-/daily-react-0.18.0.tgz#7b07fcaf34b52a8dc3a5c39f87fca79c28cd5ffc" + integrity sha512-qjoVkBLLEpE51NbctXPpi6n3f645L8tuwlPLVnELFKTUiGuDPcEX7x9oUtRjk0qOrMdineCg9QtcB5Dk6tgmJw== + dependencies: + fast-deep-equal "^3.1.3" + lodash.throttle "^4.1.1" + +"@eslint-community/eslint-utils@^4.2.0": + version "4.4.0" + resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59" + integrity sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA== + dependencies: + eslint-visitor-keys "^3.3.0" + +"@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.6.0" + globals "^13.19.0" + ignore "^5.2.0" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.1.2" + strip-json-comments "^3.1.1" + +"@eslint/js@8.57.0": + version "8.57.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.57.0.tgz#a5417ae8427873f1dd08b70b3574b453e67b5f7f" + integrity sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g== + +"@floating-ui/core@^1.0.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" + integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g== + dependencies: + "@floating-ui/utils" "^0.2.1" + +"@floating-ui/dom@^1.6.1": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef" + integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw== + dependencies: + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" + +"@floating-ui/react-dom@^2.0.0": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d" + integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw== + dependencies: + "@floating-ui/dom" "^1.6.1" + +"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== + +"@humanwhocodes/config-array@^0.11.14": + version "0.11.14" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b" + integrity sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg== + dependencies: + "@humanwhocodes/object-schema" "^2.0.2" + debug "^4.3.1" + minimatch "^3.0.5" + +"@humanwhocodes/module-importer@^1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" + integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== + +"@humanwhocodes/object-schema@^2.0.2": + version "2.0.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz#4a2868d75d6d6963e423bcf90b7fd1be343409d3" + integrity sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + 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" + +"@jridgewell/gen-mapping@^0.3.2": + version "0.3.5" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz#dcce6aff74bdf6dad1a95802b69b04a2fcb1fb36" + 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" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14": + version "1.4.15" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32" + integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg== + +"@jridgewell/trace-mapping@^0.3.24": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@next/env@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/env/-/env-14.1.4.tgz#432e80651733fbd67230bf262aee28be65252674" + integrity sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ== + +"@next/eslint-plugin-next@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.4.tgz#d7372b5ffede0e466af8af2ff534386418827fc8" + integrity sha512-n4zYNLSyCo0Ln5b7qxqQeQ34OZKXwgbdcx6kmkQbywr+0k6M3Vinft0T72R6CDAcDrne2IAgSud4uWCzFgc5HA== + dependencies: + glob "10.3.10" + +"@next/swc-darwin-arm64@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.4.tgz#a3bca0dc4393ac4cf3169bbf24df63441de66bb7" + integrity sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg== + +"@next/swc-darwin-x64@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.4.tgz#ba3683d4e2d30099f3f2864dd7349a4d9f440140" + integrity sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ== + +"@next/swc-linux-arm64-gnu@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.4.tgz#3519969293f16379954b7e196deb0c1eecbb2f8b" + integrity sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA== + +"@next/swc-linux-arm64-musl@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.4.tgz#4bb3196bd402b3f84cf5373ff1021f547264d62f" + integrity sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g== + +"@next/swc-linux-x64-gnu@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.4.tgz#1b3372c98c83dcdab946cdb4ee06e068b8139ba3" + integrity sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw== + +"@next/swc-linux-x64-musl@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.4.tgz#8459088bdc872648ff78f121db596f2533df5808" + integrity sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg== + +"@next/swc-win32-arm64-msvc@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.4.tgz#84280a08c00cc3be24ddd3a12f4617b108e6dea6" + integrity sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag== + +"@next/swc-win32-ia32-msvc@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.4.tgz#23ff7f4bd0a27177428669ef6fa5c3923c738031" + integrity sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw== + +"@next/swc-win32-x64-msvc@14.1.4": + version "14.1.4" + resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.4.tgz#bccf5beccfde66d6c66fa4e2509118c796385eda" + integrity sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w== + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3", "@nodelib/fs.walk@^1.2.8": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@pkgjs/parseargs@^0.11.0": + version "0.11.0" + resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33" + integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== + +"@radix-ui/number@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/number/-/number-1.0.1.tgz#644161a3557f46ed38a042acf4a770e826021674" + integrity sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/primitive@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.0.1.tgz#e46f9958b35d10e9f6dc71c497305c22e3e55dbd" + integrity sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-arrow@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" + integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + +"@radix-ui/react-collection@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.0.3.tgz#9595a66e09026187524a36c6e7e9c7d286469159" + integrity sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + +"@radix-ui/react-compose-refs@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz#7ed868b66946aa6030e580b1ffca386dd4d21989" + integrity sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-context@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.1.tgz#fe46e67c96b240de59187dcb7a1a50ce3e2ec00c" + integrity sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-direction@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.0.1.tgz#9cb61bf2ccf568f3421422d182637b7f47596c9b" + integrity sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-dismissable-layer@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" + integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-escape-keydown" "1.0.3" + +"@radix-ui/react-focus-guards@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" + integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + +"@radix-ui/react-id@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" + integrity sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "1.0.1" + +"@radix-ui/react-popper@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42" + integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w== + dependencies: + "@babel/runtime" "^7.13.10" + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-use-rect" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/rect" "1.0.1" + +"@radix-ui/react-portal@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" + integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + +"@radix-ui/react-primitive@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz#d49ea0f3f0b2fe3ab1cb5667eb03e8b843b914d0" + integrity sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-slot" "1.0.2" + +"@radix-ui/react-select@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-2.0.0.tgz#a3511792a51a7018d6559357323a7f52e0e38887" + integrity sha512-RH5b7af4oHtkcHS7pG6Sgv5rk5Wxa7XI8W5gvB1N/yiuDGZxko1ynvOiVhFM7Cis2A8zxF9bTOUVbRDzPepe6w== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/number" "1.0.1" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-visually-hidden" "1.0.3" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + +"@radix-ui/react-slot@1.0.2", "@radix-ui/react-slot@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.0.2.tgz#a9ff4423eade67f501ffb32ec22064bc9d3099ab" + integrity sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + +"@radix-ui/react-use-callback-ref@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a" + integrity sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-controllable-state@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.0.1.tgz#ecd2ced34e6330caf89a82854aa2f77e07440286" + integrity sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.1" + +"@radix-ui/react-use-escape-keydown@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" + integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.1" + +"@radix-ui/react-use-layout-effect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" + integrity sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-previous@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-previous/-/react-use-previous-1.0.1.tgz#b595c087b07317a4f143696c6a01de43b0d0ec66" + integrity sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-use-rect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2" + integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/rect" "1.0.1" + +"@radix-ui/react-use-size@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" + integrity sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-layout-effect" "1.0.1" + +"@radix-ui/react-visually-hidden@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac" + integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + +"@radix-ui/rect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f" + integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ== + dependencies: + "@babel/runtime" "^7.13.10" + +"@rushstack/eslint-patch@^1.3.3": + version "1.10.1" + resolved "https://registry.yarnpkg.com/@rushstack/eslint-patch/-/eslint-patch-1.10.1.tgz#7ca168b6937818e9a74b47ac4e2112b2e1a024cf" + integrity sha512-S3Kq8e7LqxkA9s7HKLqXGTGck1uwis5vAXan3FnU5yw1Ec5hsSGnq4s/UCaSqABPOnOTg7zASLyst7+ohgWexg== + +"@sentry-internal/feedback@7.109.0": + version "7.109.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.109.0.tgz#4657d7f36a1de3be466f42735d295e212b7eca11" + integrity sha512-EL7N++poxvJP9rYvh6vSu24tsKkOveNCcCj4IM7+irWPjsuD2GLYYlhp/A/Mtt9l7iqO4plvtiQU5HGk7smcTQ== + dependencies: + "@sentry/core" "7.109.0" + "@sentry/types" "7.109.0" + "@sentry/utils" "7.109.0" + +"@sentry-internal/replay-canvas@7.109.0": + version "7.109.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.109.0.tgz#9a00857994a9487428296feed4a9ddf2d62bab84" + integrity sha512-Lh/K60kmloR6lkPUcQP0iamw7B/MdEUEx/ImAx4tUSMrLj+IoUEcq/ECgnnVyQkJq59+8nPEKrVLt7x6PUPEjw== + dependencies: + "@sentry/core" "7.109.0" + "@sentry/replay" "7.109.0" + "@sentry/types" "7.109.0" + "@sentry/utils" "7.109.0" + +"@sentry-internal/tracing@7.109.0": + version "7.109.0" + resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.109.0.tgz#3effaa132c41a65378fa98146aea61228d528953" + integrity sha512-PzK/joC5tCuh2R/PRh+7dp+uuZl7pTsBIjPhVZHMTtb9+ls65WkdZJ1/uKXPouyz8NOo9Xok7aEvEo9seongyw== + dependencies: + "@sentry/core" "7.109.0" + "@sentry/types" "7.109.0" + "@sentry/utils" "7.109.0" + +"@sentry/browser@^7.60.1": + version "7.109.0" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.109.0.tgz#13b2623f43047f292cf7d6070128a7501e008693" + integrity sha512-yx+OFG+Ab9qUDDgV9ZDv8M9O9Mqr0fjKta/LMlWALYLjzkMvxsPlRPFj7oMBlHqOTVLDeg7lFYmsA8wyWQ8Z8g== + dependencies: + "@sentry-internal/feedback" "7.109.0" + "@sentry-internal/replay-canvas" "7.109.0" + "@sentry-internal/tracing" "7.109.0" + "@sentry/core" "7.109.0" + "@sentry/replay" "7.109.0" + "@sentry/types" "7.109.0" + "@sentry/utils" "7.109.0" + +"@sentry/core@7.109.0": + version "7.109.0" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.109.0.tgz#7a02f4af4a676950f6555f552a2a232d4458fcd5" + integrity sha512-xwD4U0IlvvlE/x/g/W1I8b4Cfb16SsCMmiEuBf6XxvAa3OfWBxKoqLifb3GyrbxMC4LbIIZCN/SvLlnGJPgszA== + dependencies: + "@sentry/types" "7.109.0" + "@sentry/utils" "7.109.0" + +"@sentry/replay@7.109.0": + version "7.109.0" + resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.109.0.tgz#f50fb0140c81fce660c44cc93c35988898b8348b" + integrity sha512-hCDjbTNO7ErW/XsaBXlyHFsUhneyBUdTec1Swf98TFEfVqNsTs6q338aUcaR8dGRLbLrJ9YU9D1qKq++v5h2CA== + dependencies: + "@sentry-internal/tracing" "7.109.0" + "@sentry/core" "7.109.0" + "@sentry/types" "7.109.0" + "@sentry/utils" "7.109.0" + +"@sentry/types@7.109.0": + version "7.109.0" + resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.109.0.tgz#d8778358114ed05be734661cc9e1e261f4494947" + integrity sha512-egCBnDv3YpVFoNzRLdP0soVrxVLCQ+rovREKJ1sw3rA2/MFH9WJ+DZZexsX89yeAFzy1IFsCp7/dEqudusml6g== + +"@sentry/utils@7.109.0": + version "7.109.0" + resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.109.0.tgz#7078e1400197abc1b0c436679bef980639500a86" + integrity sha512-3RjxMOLMBwZ5VSiH84+o/3NY2An4Zldjz0EbfEQNRY9yffRiCPJSQiCJID8EoylCFOh/PAhPimBhqbtWJxX6iw== + dependencies: + "@sentry/types" "7.109.0" + +"@swc/helpers@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" + integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== + dependencies: + tslib "^2.4.0" + +"@tabler/icons-react@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@tabler/icons-react/-/icons-react-3.1.0.tgz#5202b5cc7c6e57dde75c41981b618dcfebd69b50" + integrity sha512-k/WTlax2vbj/LpxvaJ+BmaLAAhVUgyLj4Ftgaczz66tUSNzqrAZXCFdOU7cRMYPNVBqyqE2IdQd2rzzhDEJvkw== + dependencies: + "@tabler/icons" "3.1.0" + +"@tabler/icons@3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@tabler/icons/-/icons-3.1.0.tgz#d69d184eae572db6adb452b511562442133cc26d" + integrity sha512-CpZGyS1IVJKFcv88yZ2sYZIpWWhQ6oy76BQKQ5SF0fGgOqgyqKdBGG/YGyyMW632on37MX7VqQIMTzN/uQqmFg== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== + +"@types/node@^20": + version "20.12.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.12.5.tgz#74c4f31ab17955d0b5808cdc8fd2839526ad00b3" + integrity sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw== + dependencies: + undici-types "~5.26.4" + +"@types/prop-types@*": + version "15.7.12" + resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6" + integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q== + +"@types/react-dom@^18": + version "18.2.24" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.24.tgz#8dda8f449ae436a7a6e91efed8035d4ab03ff759" + integrity sha512-cN6upcKd8zkGy4HU9F1+/s98Hrp6D4MOcippK4PoE8OZRngohHZpbJn1GsaDLz87MqvHNoT13nHvNqM9ocRHZg== + dependencies: + "@types/react" "*" + +"@types/react@*", "@types/react@^18": + version "18.2.74" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.74.tgz#2d52eb80e4e7c4ea8812c89181d6d590b53f958c" + integrity sha512-9AEqNZZyBx8OdZpxzQlaFEVCSFUM2YXJH46yPOiOpm078k6ZLOCcuAzGum/zK8YBwY+dbahVNbHrbgrAwIRlqw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + +"@typescript-eslint/parser@^5.4.2 || ^6.0.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-6.21.0.tgz#af8fcf66feee2edc86bc5d1cf45e33b0630bf35b" + integrity sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ== + dependencies: + "@typescript-eslint/scope-manager" "6.21.0" + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/typescript-estree" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + +"@typescript-eslint/scope-manager@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz#ea8a9bfc8f1504a6ac5d59a6df308d3a0630a2b1" + integrity sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + +"@typescript-eslint/types@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-6.21.0.tgz#205724c5123a8fef7ecd195075fa6e85bac3436d" + integrity sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg== + +"@typescript-eslint/typescript-estree@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz#c47ae7901db3b8bddc3ecd73daff2d0895688c46" + integrity sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ== + dependencies: + "@typescript-eslint/types" "6.21.0" + "@typescript-eslint/visitor-keys" "6.21.0" + debug "^4.3.4" + globby "^11.1.0" + is-glob "^4.0.3" + minimatch "9.0.3" + semver "^7.5.4" + ts-api-utils "^1.0.1" + +"@typescript-eslint/visitor-keys@6.21.0": + version "6.21.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz#87a99d077aa507e20e238b11d56cc26ade45fe47" + integrity sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A== + dependencies: + "@typescript-eslint/types" "6.21.0" + eslint-visitor-keys "^3.4.1" + +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + +acorn-jsx@^5.3.2: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.9.0: + version "8.11.3" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.11.3.tgz#71e0b14e13a4ec160724b38fb7b0f233b1b81d7a" + integrity sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg== + +ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" + integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +any-promise@^1.0.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/any-promise/-/any-promise-1.3.0.tgz#abc6afeedcea52e809cdc0376aed3ce39635d17f" + integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +arg@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" + integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +aria-hidden@^1.1.1: + version "1.2.4" + resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522" + integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A== + dependencies: + tslib "^2.0.0" + +aria-query@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e" + integrity sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== + dependencies: + dequal "^2.0.3" + +array-buffer-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz#1e5583ec16763540a27ae52eed99ff899223568f" + integrity sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg== + dependencies: + call-bind "^1.0.5" + is-array-buffer "^3.0.4" + +array-includes@^3.1.6, array-includes@^3.1.7: + version "3.1.8" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.8.tgz#5e370cbe172fdd5dd6530c1d4aadda25281ba97d" + integrity sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.findlast@^1.2.4: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz#3e4fbcb30a15a7f5bf64cf2faae22d139c2e4904" + integrity sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.findlastindex@^1.2.3: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.5.tgz#8c35a755c72908719453f87145ca011e39334d0d" + integrity sha512-zfETvRFA8o7EiNn++N5f/kaCw221hrpGsDmcpndVupkPzEc1Wuf3VgC0qby1BbHs7f5DVYjgtEU2LLh5bqeGfQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-shim-unscopables "^1.0.2" + +array.prototype.flat@^1.3.1, array.prototype.flat@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz#1476217df8cff17d72ee8f3ba06738db5b387d18" + integrity sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.flatmap@^1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz#c9a7c6831db8e719d6ce639190146c24bbd3e527" + integrity sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.toreversed@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz#b989a6bf35c4c5051e1dc0325151bf8088954eba" + integrity sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + es-shim-unscopables "^1.0.0" + +array.prototype.tosorted@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz#c8c89348337e51b8a3c48a9227f9ce93ceedcba8" + integrity sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.1.0" + es-shim-unscopables "^1.0.2" + +arraybuffer.prototype.slice@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz#097972f4255e41bc3425e37dc3f6421cf9aefde6" + integrity sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.5" + define-properties "^1.2.1" + es-abstract "^1.22.3" + es-errors "^1.2.1" + get-intrinsic "^1.2.3" + is-array-buffer "^3.0.4" + is-shared-array-buffer "^1.0.2" + +ast-types-flow@^0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.8.tgz#0a85e1c92695769ac13a428bb653e7538bea27d6" + integrity sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== + +autoprefixer@^10.0.1: + version "10.4.19" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.19.tgz#ad25a856e82ee9d7898c59583c1afeb3fa65f89f" + integrity sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew== + dependencies: + browserslist "^4.23.0" + caniuse-lite "^1.0.30001599" + fraction.js "^4.3.7" + normalize-range "^0.1.2" + picocolors "^1.0.0" + postcss-value-parser "^4.2.0" + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +axe-core@=4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.0.tgz#34ba5a48a8b564f67e103f0aa5768d76e15bbbbf" + integrity sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ== + +axobject-query@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a" + integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg== + dependencies: + dequal "^2.0.3" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +bowser@^2.8.1: + version "2.11.0" + resolved "https://registry.yarnpkg.com/bowser/-/bowser-2.11.0.tgz#5ca3c35757a7aa5771500c70a73a9f91ef420a8f" + integrity sha512-AlcaJBi/pqqJBIQ8U9Mcpc9i8Aqxn88Skv5d+xBX006BY5u8N3mGLHa5Lgppa7L/HfwgwLgZ6NYs+Ag6uUmJRA== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.2, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +browserslist@^4.23.0: + version "4.23.0" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.23.0.tgz#8f3acc2bbe73af7213399430890f86c63a5674ab" + integrity sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ== + dependencies: + caniuse-lite "^1.0.30001587" + electron-to-chromium "^1.4.668" + node-releases "^2.0.14" + update-browserslist-db "^1.0.13" + +busboy@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/busboy/-/busboy-1.6.0.tgz#966ea36a9502e43cdb9146962523b92f531f6893" + integrity sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA== + dependencies: + streamsearch "^1.1.0" + +call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.7.tgz#06016599c40c56498c18769d2730be242b6fa3b9" + integrity sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w== + 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" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase-css@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" + integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== + +caniuse-lite@^1.0.30001579, caniuse-lite@^1.0.30001587, caniuse-lite@^1.0.30001599: + version "1.0.30001606" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001606.tgz#b4d5f67ab0746a3b8b5b6d1f06e39c51beb39a9e" + integrity sha512-LPbwnW4vfpJId225pwjZJOgX1m9sGfbw/RKJvw/t0QhYOOaTXHvkjVGFGPpvwEzufrjvTlsULnVTxdy4/6cqkg== + +chalk@^4.0.0: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.5.3: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +class-variance-authority@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/class-variance-authority/-/class-variance-authority-0.7.0.tgz#1c3134d634d80271b1837452b06d821915954522" + integrity sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A== + dependencies: + clsx "2.0.0" + +client-only@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" + integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== + +clsx@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.0.0.tgz#12658f3fd98fafe62075595a5c30e43d18f3d00b" + integrity sha512-rQ1+kcj+ttHG0MKVGBUXwayCCF1oh39BF5COIpRzuCEv8Mwjv0XucrI2ExNTOn9IlLifGClWQcU9BrZORvtw6Q== + +clsx@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" + integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg== + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^4.0.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-4.1.1.tgz#9fd602bd936294e9e9ef46a3f4d6964044b18068" + integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +cross-spawn@^7.0.0, cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +cssesc@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" + integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== + +csstype@^3.0.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +damerau-levenshtein@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7" + integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== + +data-view-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.1.tgz#8ea6326efec17a2e42620696e671d7d5a8bc66b2" + integrity sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.1.tgz#90721ca95ff280677eb793749fce1011347669e2" + integrity sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +data-view-byte-offset@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz#5e0bbfb4828ed2d1b9b400cd8a7d119bca0ff18a" + integrity sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.3.1, debug@^4.3.2, debug@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" + integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.1.3, define-properties@^1.2.0, define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +dequal@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" + integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== + +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + +didyoumean@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" + integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +dlv@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" + integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +electron-to-chromium@^1.4.668: + version "1.4.729" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.729.tgz#8477d21e2a50993781950885b2731d92ad532c00" + integrity sha512-bx7+5Saea/qu14kmPTDHQxkp2UnziG3iajUQu3BxFvCOnpAJdDbMV4rSl+EqFDkkpNNVUFlR1kDfpL59xfy1HA== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +enhanced-resolve@^5.12.0: + version "5.16.0" + resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.16.0.tgz#65ec88778083056cb32487faa9aef82ed0864787" + integrity sha512-O+QWCviPNSSLAD9Ucn8Awv+poAkqn3T1XY5/N7kR7rQO9yfSGWkYZDwpJ+iKF7B8rxaQKWngSqACpgzeapSyoA== + dependencies: + graceful-fs "^4.2.4" + tapable "^2.2.0" + +es-abstract@^1.22.1, es-abstract@^1.22.3, es-abstract@^1.23.0, es-abstract@^1.23.1, es-abstract@^1.23.2: + version "1.23.3" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.3.tgz#8f0c5a35cd215312573c5a27c87dfd6c881a0aa0" + integrity sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A== + dependencies: + array-buffer-byte-length "^1.0.1" + arraybuffer.prototype.slice "^1.0.3" + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + data-view-buffer "^1.0.1" + data-view-byte-length "^1.0.1" + data-view-byte-offset "^1.0.0" + es-define-property "^1.0.0" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.0.3" + es-to-primitive "^1.2.1" + function.prototype.name "^1.1.6" + get-intrinsic "^1.2.4" + get-symbol-description "^1.0.2" + globalthis "^1.0.3" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + hasown "^2.0.2" + internal-slot "^1.0.7" + is-array-buffer "^3.0.4" + is-callable "^1.2.7" + is-data-view "^1.0.1" + is-negative-zero "^2.0.3" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.3" + is-string "^1.0.7" + is-typed-array "^1.1.13" + is-weakref "^1.0.2" + object-inspect "^1.13.1" + object-keys "^1.1.1" + object.assign "^4.1.5" + regexp.prototype.flags "^1.5.2" + safe-array-concat "^1.1.2" + safe-regex-test "^1.0.3" + string.prototype.trim "^1.2.9" + string.prototype.trimend "^1.0.8" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.2" + typed-array-byte-length "^1.0.1" + typed-array-byte-offset "^1.0.2" + typed-array-length "^1.0.6" + unbox-primitive "^1.0.2" + which-typed-array "^1.1.15" + +es-define-property@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.0.tgz#c7faefbdff8b2696cf5f46921edfb77cc4ba3845" + integrity sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ== + dependencies: + get-intrinsic "^1.2.4" + +es-errors@^1.1.0, es-errors@^1.2.1, es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-iterator-helpers@^1.0.15, es-iterator-helpers@^1.0.17: + version "1.0.18" + resolved "https://registry.yarnpkg.com/es-iterator-helpers/-/es-iterator-helpers-1.0.18.tgz#4d3424f46b24df38d064af6fbbc89274e29ea69d" + integrity sha512-scxAJaewsahbqTYrGKJihhViaM6DDZDDoucfvzNbK0pOren1g/daDQ3IAhzn+1G14rBG7w+i5N+qul60++zlKA== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-errors "^1.3.0" + es-set-tostringtag "^2.0.3" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + has-property-descriptors "^1.0.2" + has-proto "^1.0.3" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + iterator.prototype "^1.1.2" + safe-array-concat "^1.1.2" + +es-object-atoms@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.0.0.tgz#ddb55cd47ac2e240701260bc2a8e31ecb643d941" + integrity sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz#8bb60f0a440c2e4281962428438d58545af39777" + integrity sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ== + dependencies: + get-intrinsic "^1.2.4" + has-tostringtag "^1.0.2" + hasown "^2.0.1" + +es-shim-unscopables@^1.0.0, es-shim-unscopables@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz#1f6942e71ecc7835ed1c8a83006d8771a63a3763" + integrity sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw== + dependencies: + hasown "^2.0.0" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escalade@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.2.tgz#54076e9ab29ea5bf3d8f1ed62acffbb88272df27" + integrity sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA== + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-next@14.1.4: + version "14.1.4" + resolved "https://registry.yarnpkg.com/eslint-config-next/-/eslint-config-next-14.1.4.tgz#22f2ba4c0993e991249d863656a64c204bae542c" + integrity sha512-cihIahbhYAWwXJwZkAaRPpUi5t9aOi/HdfWXOjZeUOqNWXHD8X22kd1KG58Dc3MVaRx3HoR/oMGk2ltcrqDn8g== + dependencies: + "@next/eslint-plugin-next" "14.1.4" + "@rushstack/eslint-patch" "^1.3.3" + "@typescript-eslint/parser" "^5.4.2 || ^6.0.0" + eslint-import-resolver-node "^0.3.6" + eslint-import-resolver-typescript "^3.5.2" + eslint-plugin-import "^2.28.1" + eslint-plugin-jsx-a11y "^6.7.1" + eslint-plugin-react "^7.33.2" + eslint-plugin-react-hooks "^4.5.0 || 5.0.0-canary-7118f5dd7-20230705" + +eslint-import-resolver-node@^0.3.6, eslint-import-resolver-node@^0.3.9: + version "0.3.9" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz#d4eaac52b8a2e7c3cd1903eb00f7e053356118ac" + integrity sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g== + dependencies: + debug "^3.2.7" + is-core-module "^2.13.0" + resolve "^1.22.4" + +eslint-import-resolver-typescript@^3.5.2: + version "3.6.1" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.1.tgz#7b983680edd3f1c5bce1a5829ae0bc2d57fe9efa" + integrity sha512-xgdptdoi5W3niYeuQxKmzVDTATvLYqhpwmykwsh7f6HIOStGWEIL9iqZgQDF9u9OEzrRwR8no5q2VT+bjAujTg== + dependencies: + debug "^4.3.4" + enhanced-resolve "^5.12.0" + eslint-module-utils "^2.7.4" + fast-glob "^3.3.1" + get-tsconfig "^4.5.0" + is-core-module "^2.11.0" + is-glob "^4.0.3" + +eslint-module-utils@^2.7.4, eslint-module-utils@^2.8.0: + version "2.8.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz#52f2404300c3bd33deece9d7372fb337cc1d7c34" + integrity sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q== + dependencies: + debug "^3.2.7" + +eslint-plugin-import@^2.28.1: + version "2.29.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz#d45b37b5ef5901d639c15270d74d46d161150643" + integrity sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlastindex "^1.2.3" + array.prototype.flat "^1.3.2" + array.prototype.flatmap "^1.3.2" + debug "^3.2.7" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.9" + eslint-module-utils "^2.8.0" + hasown "^2.0.0" + is-core-module "^2.13.1" + is-glob "^4.0.3" + minimatch "^3.1.2" + object.fromentries "^2.0.7" + object.groupby "^1.0.1" + object.values "^1.1.7" + semver "^6.3.1" + tsconfig-paths "^3.15.0" + +eslint-plugin-jsx-a11y@^6.7.1: + version "6.8.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz#2fa9c701d44fcd722b7c771ec322432857fcbad2" + integrity sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA== + dependencies: + "@babel/runtime" "^7.23.2" + aria-query "^5.3.0" + array-includes "^3.1.7" + array.prototype.flatmap "^1.3.2" + ast-types-flow "^0.0.8" + axe-core "=4.7.0" + axobject-query "^3.2.1" + damerau-levenshtein "^1.0.8" + emoji-regex "^9.2.2" + es-iterator-helpers "^1.0.15" + hasown "^2.0.0" + jsx-ast-utils "^3.3.5" + language-tags "^1.0.9" + minimatch "^3.1.2" + object.entries "^1.1.7" + object.fromentries "^2.0.7" + +"eslint-plugin-react-hooks@^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": + version "4.6.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz#4c3e697ad95b77e93f8646aaa1630c1ba607edd3" + integrity sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g== + +eslint-plugin-react@^7.33.2: + version "7.34.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-react/-/eslint-plugin-react-7.34.1.tgz#6806b70c97796f5bbfb235a5d3379ece5f4da997" + integrity sha512-N97CxlouPT1AHt8Jn0mhhN2RrADlUAsk1/atcT2KyA/l9Q/E6ll7OIGwNumFmWfZ9skV3XXccYS19h80rHtgkw== + dependencies: + array-includes "^3.1.7" + array.prototype.findlast "^1.2.4" + array.prototype.flatmap "^1.3.2" + array.prototype.toreversed "^1.1.2" + array.prototype.tosorted "^1.1.3" + doctrine "^2.1.0" + es-iterator-helpers "^1.0.17" + estraverse "^5.3.0" + jsx-ast-utils "^2.4.1 || ^3.0.0" + minimatch "^3.1.2" + object.entries "^1.1.7" + object.fromentries "^2.0.7" + object.hasown "^1.1.3" + object.values "^1.1.7" + prop-types "^15.8.1" + resolve "^2.0.0-next.5" + semver "^6.3.1" + string.prototype.matchall "^4.0.10" + +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1, eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8: + version "8.57.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668" + integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ== + dependencies: + "@eslint-community/eslint-utils" "^4.2.0" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.57.0" + "@humanwhocodes/config-array" "^0.11.14" + "@humanwhocodes/module-importer" "^1.0.1" + "@nodelib/fs.walk" "^1.2.8" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" + esquery "^1.4.2" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + find-up "^5.0.0" + glob-parent "^6.0.2" + globals "^13.19.0" + graphemer "^1.4.0" + ignore "^5.2.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + is-path-inside "^3.0.3" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.1.2" + natural-compare "^1.4.0" + optionator "^0.9.3" + strip-ansi "^6.0.1" + text-table "^0.2.0" + +espree@^9.6.0, espree@^9.6.1: + version "9.6.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" + integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== + dependencies: + acorn "^8.9.0" + acorn-jsx "^5.3.2" + eslint-visitor-keys "^3.4.1" + +esquery@^1.4.2: + version "1.5.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" + integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^5.1.0, estraverse@^5.2.0, estraverse@^5.3.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +events@^3.1.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.9, fast-glob@^3.3.0, fast-glob@^3.3.1: + version "3.3.2" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.2.tgz#a904501e57cfdd2ffcded45e99a54fef55e46129" + integrity sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== + +fastq@^1.6.0: + version "1.17.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.17.1.tgz#2a523f07a4e7b1e81a42b91b8bf2254107753b47" + integrity sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w== + dependencies: + reusify "^1.0.4" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc" + integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== + dependencies: + locate-path "^6.0.0" + path-exists "^4.0.0" + +flat-cache@^3.0.4: + version "3.2.0" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.2.0.tgz#2c0c2d5040c99b1632771a9d105725c0115363ee" + integrity sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw== + dependencies: + flatted "^3.2.9" + keyv "^4.5.3" + rimraf "^3.0.2" + +flatted@^3.2.9: + version "3.3.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.3.1.tgz#21db470729a6734d4997002f439cb308987f567a" + integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== + +for-each@^0.3.3: + version "0.3.3" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" + integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== + dependencies: + is-callable "^1.1.3" + +foreground-child@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.1.1.tgz#1d173e776d75d2772fed08efe4a0de1ea1b12d0d" + integrity sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg== + dependencies: + cross-spawn "^7.0.0" + signal-exit "^4.0.1" + +fraction.js@^4.3.7: + version "4.3.7" + resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" + integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== + +framer-motion@^11.0.27: + version "11.0.27" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-11.0.27.tgz#6881cb76e9dbbdc194353e5560ff93f3bd727a5d" + integrity sha512-OmY1hnBXxUfvQTuoPqumAiXYPEt8jY31Fqbmihf/NR29XUL9BkRPHrqVqtJS7TLKriwRt+0pbwiO9tnziZTJzA== + dependencies: + tslib "^2.4.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.5, function.prototype.name@^1.1.6: + version "1.1.6" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.6.tgz#cdf315b7d90ee77a4c6ee216c3c3362da07533fd" + integrity sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.2.0" + es-abstract "^1.22.1" + functions-have-names "^1.2.3" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +get-intrinsic@^1.1.3, get-intrinsic@^1.2.1, get-intrinsic@^1.2.3, get-intrinsic@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.4.tgz#e385f5a4b5227d449c3eabbad05494ef0abbeadd" + integrity sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ== + 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" + +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + +get-symbol-description@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.2.tgz#533744d5aa20aca4e079c8e5daf7fd44202821f5" + integrity sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg== + dependencies: + call-bind "^1.0.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + +get-tsconfig@^4.5.0: + version "4.7.3" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.3.tgz#0498163d98f7b58484dd4906999c0c9d5f103f83" + integrity sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg== + dependencies: + resolve-pkg-maps "^1.0.0" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob@10.3.10: + version "10.3.10" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b" + integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.5" + minimatch "^9.0.1" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + path-scurry "^1.10.1" + +glob@^10.3.10: + version "10.3.12" + resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.12.tgz#3a65c363c2e9998d220338e88a5f6ac97302960b" + integrity sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg== + dependencies: + foreground-child "^3.1.0" + jackspeak "^2.3.6" + minimatch "^9.0.1" + minipass "^7.0.4" + path-scurry "^1.10.2" + +glob@^7.1.3: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + 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" + +globals@^13.19.0: + version "13.24.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.24.0.tgz#8432a19d78ce0c1e833949c36adb345400bb1171" + integrity sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ== + dependencies: + type-fest "^0.20.2" + +globalthis@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.3.tgz#5852882a52b80dc301b0660273e1ed082f0b6ccf" + integrity sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA== + dependencies: + define-properties "^1.1.3" + +globby@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +gopd@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" + integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== + dependencies: + get-intrinsic "^1.1.3" + +graceful-fs@^4.2.11, graceful-fs@^4.2.4: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +graphemer@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/graphemer/-/graphemer-1.4.0.tgz#fb2f1d55e0e3a1849aeffc90c4fa0dd53a0e66c6" + integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag== + +hamt_plus@1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/hamt_plus/-/hamt_plus-1.0.2.tgz#e21c252968c7e33b20f6a1b094cd85787a265601" + integrity sha512-t2JXKaehnMb9paaYA7J0BX8QQAY8lwfQ9Gjf4pg/mk4krt+cmwmU652HOoWonf+7+EQV97ARPMhhVgU1ra2GhA== + +has-bigints@^1.0.1, has-bigints@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.2.tgz#0871bd3e3d51626f6ca0966668ba35d5602d6eaa" + integrity sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.0.1, has-proto@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.0.3.tgz#b31ddfe9b0e6e9914536a6ab286426d0214f77fd" + integrity sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q== + +has-symbols@^1.0.2, has-symbols@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" + integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== + +has-tostringtag@^1.0.0, has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.0, hasown@^2.0.1, hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +ignore@^5.2.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.1.tgz#5073e554cd42c5b33b394375f538b8593e34d4ef" + integrity sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw== + +import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +internal-slot@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.7.tgz#c06dcca3ed874249881007b0a5523b172a190802" + integrity sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.0" + side-channel "^1.0.4" + +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + +is-array-buffer@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.4.tgz#7a1f92b3d61edd2bc65d24f130530ea93d7fae98" + integrity sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.2.1" + +is-async-function@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.0.0.tgz#8e4418efd3e5d3a6ebb0164c05ef5afb69aa9646" + integrity sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA== + dependencies: + has-tostringtag "^1.0.0" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.3, is-callable@^1.1.4, is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.11.0, is-core-module@^2.13.0, is-core-module@^2.13.1: + version "2.13.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.13.1.tgz#ad0d7532c6fea9da1ebdc82742d74525c6273384" + integrity sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw== + dependencies: + hasown "^2.0.0" + +is-data-view@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.1.tgz#4b4d3a511b70f3dc26d42c03ca9ca515d847759f" + integrity sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w== + dependencies: + is-typed-array "^1.1.13" + +is-date-object@^1.0.1, is-date-object@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz#c8749b65f17c133313e661b1289b95ad3dbd62e6" + integrity sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw== + dependencies: + call-bind "^1.0.2" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.10: + version "1.0.10" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" + integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== + dependencies: + has-tostringtag "^1.0.0" + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-negative-zero@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz#ced903a027aca6381b777a5743069d7376a49747" + integrity sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== + +is-number-object@^1.0.4: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.7.tgz#59d50ada4c45251784e9904f5246c742f07a42fc" + integrity sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-path-inside@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/is-path-inside/-/is-path-inside-3.0.3.tgz#d231362e53a07ff2b0e0ea7fed049161ffd16283" + integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.2, is-shared-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz#1237f1cba059cdb62431d378dcc37d9680181688" + integrity sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg== + dependencies: + call-bind "^1.0.7" + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-typed-array@^1.1.13: + version "1.1.13" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.13.tgz#d6c5ca56df62334959322d7d7dd1cca50debe229" + integrity sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw== + dependencies: + which-typed-array "^1.1.14" + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +is-weakset@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.3.tgz#e801519df8c0c43e12ff2834eead84ec9e624007" + integrity sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +iterator.prototype@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/iterator.prototype/-/iterator.prototype-1.1.2.tgz#5e29c8924f01916cb9335f1ff80619dcff22b0c0" + integrity sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w== + dependencies: + define-properties "^1.2.1" + get-intrinsic "^1.2.1" + has-symbols "^1.0.3" + reflect.getprototypeof "^1.0.4" + set-function-name "^2.0.1" + +jackspeak@^2.3.5, jackspeak@^2.3.6: + version "2.3.6" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8" + integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ== + dependencies: + "@isaacs/cliui" "^8.0.2" + optionalDependencies: + "@pkgjs/parseargs" "^0.11.0" + +jiti@^1.21.0: + version "1.21.0" + resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.21.0.tgz#7c97f8fe045724e136a397f7340475244156105d" + integrity sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q== + +"js-tokens@^3.0.0 || ^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-buffer@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" + integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== + +json5@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.2.tgz#63d98d60f21b313b77c4d6da18bfa69d80e1d593" + integrity sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== + dependencies: + minimist "^1.2.0" + +"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.3.5: + version "3.3.5" + resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz#4766bd05a8e2a11af222becd19e15575e52a853a" + integrity sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== + dependencies: + array-includes "^3.1.6" + array.prototype.flat "^1.3.1" + object.assign "^4.1.4" + object.values "^1.1.6" + +keyv@^4.5.3: + version "4.5.4" + resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.4.tgz#a879a99e29452f942439f2a405e3af8b31d4de93" + integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== + dependencies: + json-buffer "3.0.1" + +language-subtag-registry@^0.3.20: + version "0.3.22" + resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d" + integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w== + +language-tags@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.9.tgz#1ffdcd0ec0fafb4b1be7f8b11f306ad0f9c08777" + integrity sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== + dependencies: + language-subtag-registry "^0.3.20" + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +lilconfig@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.1.0.tgz#78e23ac89ebb7e1bfbf25b18043de756548e7f52" + integrity sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ== + +lilconfig@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-3.1.1.tgz#9d8a246fa753106cfc205fd2d77042faca56e5e3" + integrity sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ== + +lines-and-columns@^1.1.6: + version "1.2.4" + resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" + integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== + +locate-path@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286" + integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== + dependencies: + p-locate "^5.0.0" + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.throttle@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" + integrity sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ== + +loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf" + integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== + dependencies: + js-tokens "^3.0.0 || ^4.0.0" + +lru-cache@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.2.0.tgz#0bd445ca57363465900f4d1f9bd8db343a4d95c3" + integrity sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q== + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4, micromatch@^4.0.5: + version "4.0.5" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" + integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== + dependencies: + braces "^3.0.2" + picomatch "^2.3.1" + +minimatch@9.0.3: + version "9.0.3" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.3.tgz#a6e00c3de44c3a542bfaae70abfc22420a6da825" + integrity sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^9.0.1: + version "9.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.4.tgz#8e49c731d1749cbec05050ee5145147b32496a51" + integrity sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.0, minimist@^1.2.6: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.0.4: + version "7.0.4" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.0.4.tgz#dbce03740f50a4786ba994c1fb908844d27b038c" + integrity sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ== + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mz@^2.7.0: + version "2.7.0" + resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" + integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== + dependencies: + any-promise "^1.0.0" + object-assign "^4.0.1" + thenify-all "^1.0.0" + +nanoid@^3.3.6, nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== + +next@14.1.4: + version "14.1.4" + resolved "https://registry.yarnpkg.com/next/-/next-14.1.4.tgz#203310f7310578563fd5c961f0db4729ce7a502d" + integrity sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ== + dependencies: + "@next/env" "14.1.4" + "@swc/helpers" "0.5.2" + busboy "1.6.0" + caniuse-lite "^1.0.30001579" + graceful-fs "^4.2.11" + postcss "8.4.31" + styled-jsx "5.1.1" + optionalDependencies: + "@next/swc-darwin-arm64" "14.1.4" + "@next/swc-darwin-x64" "14.1.4" + "@next/swc-linux-arm64-gnu" "14.1.4" + "@next/swc-linux-arm64-musl" "14.1.4" + "@next/swc-linux-x64-gnu" "14.1.4" + "@next/swc-linux-x64-musl" "14.1.4" + "@next/swc-win32-arm64-msvc" "14.1.4" + "@next/swc-win32-ia32-msvc" "14.1.4" + "@next/swc-win32-x64-msvc" "14.1.4" + +node-releases@^2.0.14: + version "2.0.14" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.14.tgz#2ffb053bceb8b2be8495ece1ab6ce600c4461b0b" + integrity sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +normalize-range@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" + integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== + +object-assign@^4.0.1, object-assign@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + +object-hash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" + integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== + +object-inspect@^1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.1.tgz#b96c6109324ccfef6b12216a956ca4dc2ff94bc2" + integrity sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.4, object.assign@^4.1.5: + version "4.1.5" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.5.tgz#3a833f9ab7fdb80fc9e8d2300c803d216d8fdbb0" + integrity sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ== + dependencies: + call-bind "^1.0.5" + define-properties "^1.2.1" + has-symbols "^1.0.3" + object-keys "^1.1.1" + +object.entries@^1.1.7: + version "1.1.8" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.8.tgz#bffe6f282e01f4d17807204a24f8edd823599c41" + integrity sha512-cmopxi8VwRIAw/fkijJohSfpef5PdN0pMQJN6VC/ZKvn0LIknWD8KtgY6KlQdEc4tIjcQ3HxSMmnvtzIscdaYQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +object.fromentries@^2.0.7: + version "2.0.8" + resolved "https://registry.yarnpkg.com/object.fromentries/-/object.fromentries-2.0.8.tgz#f7195d8a9b97bd95cbc1999ea939ecd1a2b00c65" + integrity sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.groupby@^1.0.1: + version "1.0.3" + resolved "https://registry.yarnpkg.com/object.groupby/-/object.groupby-1.0.3.tgz#9b125c36238129f6f7b61954a1e7176148d5002e" + integrity sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + +object.hasown@^1.1.3: + version "1.1.4" + resolved "https://registry.yarnpkg.com/object.hasown/-/object.hasown-1.1.4.tgz#e270ae377e4c120cdcb7656ce66884a6218283dc" + integrity sha512-FZ9LZt9/RHzGySlBARE3VF+gE26TxR38SdmqOqliuTnl9wrKulaQs+4dee1V+Io8VfxqzAfHu6YuRgUy8OHoTg== + dependencies: + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-object-atoms "^1.0.0" + +object.values@^1.1.6, object.values@^1.1.7: + version "1.2.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.2.0.tgz#65405a9d92cee68ac2d303002e0b8470a4d9ab1b" + integrity sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + +optionator@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.3.tgz#007397d44ed1872fdc6ed31360190f81814e2c64" + integrity sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg== + dependencies: + "@aashutoshrathi/word-wrap" "^1.2.3" + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + +p-limit@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" + integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== + dependencies: + yocto-queue "^0.1.0" + +p-locate@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834" + integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== + dependencies: + p-limit "^3.0.2" + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" + integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^1.10.1, path-scurry@^1.10.2: + version "1.10.2" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-1.10.2.tgz#8f6357eb1239d5fa1da8b9f70e9c080675458ba7" + integrity sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA== + dependencies: + lru-cache "^10.2.0" + minipass "^5.0.0 || ^6.0.2 || ^7.0.0" + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +pify@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== + +pirates@^4.0.1: + version "4.0.6" + resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.6.tgz#3018ae32ecfcff6c29ba2267cbf21166ac1f36b9" + integrity sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg== + +possible-typed-array-names@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz#89bb63c6fada2c3e90adc4a647beeeb39cc7bf8f" + integrity sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q== + +postcss-import@^15.1.0: + version "15.1.0" + resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" + integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== + dependencies: + postcss-value-parser "^4.0.0" + read-cache "^1.0.0" + resolve "^1.1.7" + +postcss-js@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" + integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== + dependencies: + camelcase-css "^2.0.1" + +postcss-load-config@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz#7159dcf626118d33e299f485d6afe4aff7c4a3e3" + integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ== + dependencies: + lilconfig "^3.0.0" + yaml "^2.3.4" + +postcss-nested@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.1.tgz#f83dc9846ca16d2f4fa864f16e9d9f7d0961662c" + integrity sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ== + dependencies: + postcss-selector-parser "^6.0.11" + +postcss-selector-parser@^6.0.11: + version "6.0.16" + resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz#3b88b9f5c5abd989ef4e2fc9ec8eedd34b20fb04" + integrity sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw== + dependencies: + cssesc "^3.0.0" + util-deprecate "^1.0.2" + +postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" + integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== + +postcss@8.4.31: + version "8.4.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d" + integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ== + dependencies: + nanoid "^3.3.6" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +postcss@^8, postcss@^8.4.23: + version "8.4.38" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e" + integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.0" + source-map-js "^1.2.0" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +prop-types@^15.8.1: + version "15.8.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5" + integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== + dependencies: + loose-envify "^1.4.0" + object-assign "^4.1.1" + react-is "^16.13.1" + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +react-dom@^18: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" + integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== + dependencies: + loose-envify "^1.1.0" + scheduler "^0.23.0" + +react-is@^16.13.1: + version "16.13.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" + integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== + +react-remove-scroll-bar@^2.3.3: + version "2.3.6" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz#3e585e9d163be84a010180b18721e851ac81a29c" + integrity sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + +react-remove-scroll@2.5.5: + version "2.5.5" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" + integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== + dependencies: + react-remove-scroll-bar "^2.3.3" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + +react@^18: + version "18.2.0" + resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" + integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== + dependencies: + loose-envify "^1.1.0" + +read-cache@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" + integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== + dependencies: + pify "^2.3.0" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +recoil@^0.7.7: + version "0.7.7" + resolved "https://registry.yarnpkg.com/recoil/-/recoil-0.7.7.tgz#c5f2c843224384c9c09e4a62c060fb4c1454dc8e" + integrity sha512-8Og5KPQW9LwC577Vc7Ug2P0vQshkv1y3zG3tSSkWMqkWSwHmE+by06L8JtnGocjW6gcCvfwB3YtrJG6/tWivNQ== + dependencies: + hamt_plus "1.0.2" + +reflect.getprototypeof@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz#3ab04c32a8390b770712b7a8633972702d278859" + integrity sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.1" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + globalthis "^1.0.3" + which-builtin-type "^1.1.3" + +regenerator-runtime@^0.14.0: + version "0.14.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f" + integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw== + +regexp.prototype.flags@^1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz#138f644a3350f981a858c44f6bb1a61ff59be334" + integrity sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw== + dependencies: + call-bind "^1.0.6" + define-properties "^1.2.1" + es-errors "^1.3.0" + set-function-name "^2.0.1" + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +resolve@^1.1.7, resolve@^1.22.2, resolve@^1.22.4: + version "1.22.8" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.8.tgz#b6c87a9f2aa06dfab52e3d70ac8cde321fa5a48d" + integrity sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +resolve@^2.0.0-next.5: + version "2.0.0-next.5" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-2.0.0-next.5.tgz#6b0ec3107e671e52b68cd068ef327173b90dc03c" + integrity sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA== + dependencies: + is-core-module "^2.13.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-array-concat@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.2.tgz#81d77ee0c4e8b863635227c721278dd524c20edb" + integrity sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q== + dependencies: + call-bind "^1.0.7" + get-intrinsic "^1.2.4" + has-symbols "^1.0.3" + isarray "^2.0.5" + +safe-regex-test@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.0.3.tgz#a5b4c0f06e0ab50ea2c395c14d8371232924c377" + integrity sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw== + dependencies: + call-bind "^1.0.6" + es-errors "^1.3.0" + is-regex "^1.1.4" + +scheduler@^0.23.0: + version "0.23.0" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" + integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== + dependencies: + loose-envify "^1.1.0" + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +semver@^7.5.4: + version "7.6.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.0.tgz#1a46a4db4bffcccd97b743b5005c8325f23d4e2d" + integrity sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg== + dependencies: + lru-cache "^6.0.0" + +set-function-length@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + 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" + +set-function-name@^2.0.1, set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4, side-channel@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.6.tgz#abd25fb7cd24baf45466406b1096b7831c9215f2" + integrity sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + get-intrinsic "^1.2.4" + object-inspect "^1.13.1" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +source-map-js@^1.0.2, source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +streamsearch@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/streamsearch/-/streamsearch-1.1.0.tgz#404dd1e2247ca94af554e841a8ef0eaa238da764" + integrity sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + 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" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + 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" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.matchall@^4.0.10: + version "4.0.11" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz#1092a72c59268d2abaad76582dccc687c0297e0a" + integrity sha512-NUdh0aDavY2og7IbBPenWqR9exH+E26Sv8e0/eTe1tltDGZL+GtBkDAnnyBtmekfK6/Dq3MkcGtzXFEd1LQrtg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.2" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-symbols "^1.0.3" + internal-slot "^1.0.7" + regexp.prototype.flags "^1.5.2" + set-function-name "^2.0.2" + side-channel "^1.0.6" + +string.prototype.trim@^1.2.9: + version "1.2.9" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz#b6fa326d72d2c78b6df02f7759c73f8f6274faa4" + integrity sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-abstract "^1.23.0" + es-object-atoms "^1.0.0" + +string.prototype.trimend@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz#3651b8513719e8a9f48de7f2f77640b26652b229" + integrity sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== + +strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +styled-jsx@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/styled-jsx/-/styled-jsx-5.1.1.tgz#839a1c3aaacc4e735fed0781b8619ea5d0009d1f" + integrity sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw== + dependencies: + client-only "0.0.1" + +sucrase@^3.32.0: + version "3.35.0" + resolved "https://registry.yarnpkg.com/sucrase/-/sucrase-3.35.0.tgz#57f17a3d7e19b36d8995f06679d121be914ae263" + integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA== + 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" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +tailwind-merge@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.2.2.tgz#87341e7604f0e20499939e152cd2841f41f7a3df" + integrity sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw== + dependencies: + "@babel/runtime" "^7.24.0" + +tailwindcss-animate@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz#318b692c4c42676cc9e67b19b78775742388bef4" + integrity sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA== + +tailwindcss@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.4.3.tgz#be48f5283df77dfced705451319a5dffb8621519" + integrity sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A== + dependencies: + "@alloc/quick-lru" "^5.2.0" + arg "^5.0.2" + chokidar "^3.5.3" + didyoumean "^1.2.2" + dlv "^1.1.3" + fast-glob "^3.3.0" + glob-parent "^6.0.2" + is-glob "^4.0.3" + jiti "^1.21.0" + lilconfig "^2.1.0" + micromatch "^4.0.5" + normalize-path "^3.0.0" + object-hash "^3.0.0" + picocolors "^1.0.0" + postcss "^8.4.23" + postcss-import "^15.1.0" + postcss-js "^4.0.1" + postcss-load-config "^4.0.1" + postcss-nested "^6.0.1" + postcss-selector-parser "^6.0.11" + resolve "^1.22.2" + sucrase "^3.32.0" + +tapable@^2.2.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" + integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw== + +thenify-all@^1.0.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/thenify-all/-/thenify-all-1.6.0.tgz#1a1918d402d8fc3f98fbf234db0bcc8cc10e9726" + integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA== + dependencies: + thenify ">= 3.1.0 < 4" + +"thenify@>= 3.1.0 < 4": + version "3.3.1" + resolved "https://registry.yarnpkg.com/thenify/-/thenify-3.3.1.tgz#8932e686a4066038a016dd9e2ca46add9838a95f" + integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw== + dependencies: + any-promise "^1.0.0" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +ts-api-utils@^1.0.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" + integrity sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ== + +ts-interface-checker@^0.1.9: + version "0.1.13" + resolved "https://registry.yarnpkg.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz#784fd3d679722bc103b1b4b8030bcddb5db2a699" + integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA== + +tsconfig-paths@^3.15.0: + version "3.15.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" + integrity sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.2" + minimist "^1.2.6" + strip-bom "^3.0.0" + +tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0: + version "2.6.2" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae" + integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q== + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +typed-array-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz#1867c5d83b20fcb5ccf32649e5e2fc7424474ff3" + integrity sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ== + dependencies: + call-bind "^1.0.7" + es-errors "^1.3.0" + is-typed-array "^1.1.13" + +typed-array-byte-length@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz#d92972d3cff99a3fa2e765a28fcdc0f1d89dec67" + integrity sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-byte-offset@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz#f9ec1acb9259f395093e4567eb3c28a580d02063" + integrity sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + +typed-array-length@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.6.tgz#57155207c76e64a3457482dfdc1c9d1d3c4c73a3" + integrity sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-proto "^1.0.3" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + +typescript@^5: + version "5.4.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.4.4.tgz#eb2471e7b0a5f1377523700a21669dce30c2d952" + integrity sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw== + +unbox-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.2.tgz#29032021057d5e6cdbd08c5129c226dff8ed6f9e" + integrity sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw== + dependencies: + call-bind "^1.0.2" + has-bigints "^1.0.2" + has-symbols "^1.0.3" + which-boxed-primitive "^1.0.2" + +undici-types@~5.26.4: + version "5.26.5" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-5.26.5.tgz#bcd539893d00b56e964fd2657a4866b221a65617" + integrity sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA== + +update-browserslist-db@^1.0.13: + version "1.0.13" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz#3c5e4f5c083661bd38ef64b6328c26ed6c8248c4" + integrity sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg== + dependencies: + escalade "^3.1.1" + picocolors "^1.0.0" + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +use-callback-ref@^1.3.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.2.tgz#6134c7f6ff76e2be0b56c809b17a650c942b1693" + integrity sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA== + dependencies: + tslib "^2.0.0" + +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + +util-deprecate@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which-builtin-type@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.1.3.tgz#b1b8443707cc58b6e9bf98d32110ff0c2cbd029b" + integrity sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw== + dependencies: + function.prototype.name "^1.1.5" + has-tostringtag "^1.0.0" + is-async-function "^2.0.0" + is-date-object "^1.0.5" + is-finalizationregistry "^1.0.2" + is-generator-function "^1.0.10" + is-regex "^1.1.4" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.0.2" + which-collection "^1.0.1" + which-typed-array "^1.1.9" + +which-collection@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.14, which-typed-array@^1.1.15, which-typed-array@^1.1.9: + version "1.1.15" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.15.tgz#264859e9b11a649b388bfaaf4f767df1f779b38d" + integrity sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + has-tostringtag "^1.0.2" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yaml@^2.3.4: + version "2.4.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.4.1.tgz#2e57e0b5e995292c25c75d2658f0664765210eed" + integrity sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg== + +yocto-queue@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" + integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== diff --git a/examples/storytelling-chatbot/image.png b/examples/storytelling-chatbot/image.png new file mode 100644 index 000000000..39a6d8620 Binary files /dev/null and b/examples/storytelling-chatbot/image.png differ diff --git a/examples/storytelling-chatbot/requirements.txt b/examples/storytelling-chatbot/requirements.txt new file mode 100644 index 000000000..7b66f55dd --- /dev/null +++ b/examples/storytelling-chatbot/requirements.txt @@ -0,0 +1,5 @@ +dailyai[daily,openai,fal]==0.0.8 +fastapi +uvicorn +requests +python-dotenv \ No newline at end of file diff --git a/examples/storytelling-chatbot/src/assets/book1.png b/examples/storytelling-chatbot/src/assets/book1.png new file mode 100644 index 000000000..4148dbaa7 Binary files /dev/null and b/examples/storytelling-chatbot/src/assets/book1.png differ diff --git a/examples/storytelling-chatbot/src/assets/book2.png b/examples/storytelling-chatbot/src/assets/book2.png new file mode 100644 index 000000000..c7dee447b Binary files /dev/null and b/examples/storytelling-chatbot/src/assets/book2.png differ diff --git a/examples/starter-apps/assets/ding3.wav b/examples/storytelling-chatbot/src/assets/ding.wav similarity index 100% rename from examples/starter-apps/assets/ding3.wav rename to examples/storytelling-chatbot/src/assets/ding.wav diff --git a/examples/starter-apps/assets/listening.wav b/examples/storytelling-chatbot/src/assets/listening.wav similarity index 100% rename from examples/starter-apps/assets/listening.wav rename to examples/storytelling-chatbot/src/assets/listening.wav diff --git a/examples/starter-apps/assets/talking.wav b/examples/storytelling-chatbot/src/assets/talking.wav similarity index 100% rename from examples/starter-apps/assets/talking.wav rename to examples/storytelling-chatbot/src/assets/talking.wav diff --git a/examples/storytelling-chatbot/src/bot.py b/examples/storytelling-chatbot/src/bot.py new file mode 100644 index 000000000..b9d328e60 --- /dev/null +++ b/examples/storytelling-chatbot/src/bot.py @@ -0,0 +1,172 @@ +import asyncio +import aiohttp +import logging +import os +import argparse + +from dailyai.pipeline.pipeline import Pipeline +from dailyai.pipeline.frames import ( + AudioFrame, + ImageFrame, + EndPipeFrame, + LLMMessagesFrame, + SendAppMessageFrame +) +from dailyai.pipeline.aggregators import ( + LLMUserResponseAggregator, + LLMAssistantResponseAggregator, +) +from dailyai.transports.daily_transport import DailyTransport +from dailyai.services.elevenlabs_ai_service import ElevenLabsTTSService +from dailyai.services.open_ai_services import OpenAILLMService +from dailyai.services.fal_ai_services import FalImageGenService + +from processors import StoryProcessor, StoryImageProcessor +from prompts import LLM_BASE_PROMPT, LLM_INTRO_PROMPT, CUE_USER_TURN +from utils.helpers import load_sounds, load_images + +from dotenv import load_dotenv +load_dotenv(override=True) + +logging.basicConfig(format=f"[STORYBOT] %(levelno)s %(asctime)s %(message)s") +logger = logging.getLogger("dailyai") +logger.setLevel(logging.INFO) + + +sounds = load_sounds(["listening.wav"]) +images = load_images(["book1.png", "book2.png"]) + + +async def main(room_url, token=None): + async with aiohttp.ClientSession() as session: + + # -------------- Transport --------------- # + + transport = DailyTransport( + room_url, + token, + "Storytelling Bot", + duration_minutes=5, + start_transcription=True, + mic_enabled=True, + mic_sample_rate=16000, + vad_enabled=True, + camera_framerate=30, + camera_bitrate=680000, + camera_enabled=True, + camera_width=768, + camera_height=768, + ) + + logger.debug("Transport created for room:" + room_url) + + # -------------- Services --------------- # + + llm_service = OpenAILLMService( + api_key=os.getenv("OPENAI_API_KEY"), + model="gpt-4-turbo" + ) + + tts_service = ElevenLabsTTSService( + aiohttp_session=session, + api_key=os.getenv("ELEVENLABS_API_KEY"), + voice_id=os.getenv("ELEVENLABS_VOICE_ID"), + ) + + fal_service_params = FalImageGenService.InputParams( + image_size={ + "width": 768, + "height": 768 + } + ) + + fal_service = FalImageGenService( + aiohttp_session=session, + model="fal-ai/fast-lightning-sdxl", + params=fal_service_params, + key=os.getenv("FAL_KEY"), + ) + + # --------------- Setup ----------------- # + + message_history = [LLM_BASE_PROMPT] + story_pages = [] + + # We need aggregators to keep track of user and LLM responses + llm_responses = LLMAssistantResponseAggregator(message_history) + user_responses = LLMUserResponseAggregator(message_history) + + # -------------- Processors ------------- # + + story_processor = StoryProcessor(message_history, story_pages) + image_processor = StoryImageProcessor(fal_service) + + # -------------- Story Loop ------------- # + + logger.debug("Waiting for participant...") + + start_storytime_event = asyncio.Event() + + @transport.event_handler("on_first_other_participant_joined") + async def on_first_other_participant_joined(transport, participant): + logger.debug("Participant joined, storytime commence!") + start_storytime_event.set() + + # The storytime coroutine will wait for the start_storytime_event + # to be set before starting the storytime pipeline + async def storytime(): + await start_storytime_event.wait() + + # The intro pipeline is used to start + # the story (as per LLM_INTRO_PROMPT) + intro_pipeline = Pipeline(processors=[ + llm_service, + tts_service, + ], sink=transport.send_queue) + + await intro_pipeline.queue_frames( + [ + ImageFrame(images['book1'], (768, 768)), + LLMMessagesFrame([LLM_INTRO_PROMPT]), + SendAppMessageFrame(CUE_USER_TURN, None), + AudioFrame(sounds["listening"]), + ImageFrame(images['book2'], (768, 768)), + EndPipeFrame(), + ] + ) + + # We start the pipeline as soon as the user joins + await intro_pipeline.run_pipeline() + + # The main story pipeline is used to continue the + # story based on user input + pipeline = Pipeline(processors=[ + user_responses, + llm_service, + story_processor, + image_processor, + tts_service, + llm_responses, + ]) + + await transport.run_pipeline(pipeline) + + transport.transcription_settings["extra"]["endpointing"] = True + transport.transcription_settings["extra"]["punctuate"] = True + + try: + await asyncio.gather(transport.run(), storytime()) + except (asyncio.CancelledError, KeyboardInterrupt): + transport.stop() + + logger.debug("Pipeline finished. Exiting.") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Daily Storyteller Bot") + parser.add_argument("-u", type=str, help="Room URL") + parser.add_argument("-t", type=str, help="Token") + config = parser.parse_args() + + asyncio.run(main(config.u, config.t)) diff --git a/examples/storytelling-chatbot/src/processors.py b/examples/storytelling-chatbot/src/processors.py new file mode 100644 index 000000000..c056355e4 --- /dev/null +++ b/examples/storytelling-chatbot/src/processors.py @@ -0,0 +1,148 @@ +from typing import AsyncGenerator +import re + +from dailyai.pipeline.frames import TextFrame, Frame, AudioFrame +from dailyai.pipeline.frame_processor import FrameProcessor +from dailyai.pipeline.frames import ( + Frame, + TextFrame, + SendAppMessageFrame, + LLMResponseEndFrame, + UserStoppedSpeakingFrame, +) + +from utils.helpers import load_sounds +from prompts import IMAGE_GEN_PROMPT, CUE_USER_TURN, CUE_ASSISTANT_TURN +import asyncio + +sounds = load_sounds(["talking.wav", "listening.wav", "ding.wav"]) + +# -------------- Frame Types ------------- # + + +class StoryPageFrame(TextFrame): + # Frame for each sentence in the story before a [break] + pass + + +class StoryImageFrame(TextFrame): + # Frame for trigger image generation + pass + + +class StoryPromptFrame(TextFrame): + # Frame for prompting the user for input + pass + + +# ------------ Frame Processors ----------- # + +class StoryImageProcessor(FrameProcessor): + """ + Processor for image prompt frames that will be sent to the FAL service. + + This processor is responsible for consuming frames of type `StoryImageFrame`. + It processes the by passing it to the FAL service + The processed frames are then yielded back. + + Attributes: + _fal_service (FALService): The FAL service, generates the images (fast fast!). + """ + + def __init__(self, fal_service): + self._fal_service = fal_service + + async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: + if isinstance(frame, StoryImageFrame): + try: + async with asyncio.timeout(7): + async for i in self._fal_service.process_frame(TextFrame(IMAGE_GEN_PROMPT % frame.text)): + yield i + except TimeoutError: + pass + pass + else: + yield frame + + +class StoryProcessor(FrameProcessor): + """ + Primary frame processor. It takes the frames generated by the LLM + and processes them into image prompts and story pages (sentences.) + For a clearer picture of how this works, reference prompts.py + + Attributes: + _messages (list): A list of llm messages. + _text (str): A buffer to store the text from text frames. + _story (list): A list to store the story sentences, or 'pages'. + + Methods: + process_frame: Processes a frame and removes any [break] or [image] tokens. + """ + + def __init__(self, messages, story): + self._messages = messages + self._text = "" + self._story = story + + async def process_frame(self, frame: Frame) -> AsyncGenerator[Frame, None]: + if isinstance(frame, UserStoppedSpeakingFrame): + # Send an app message to the UI + yield SendAppMessageFrame(CUE_ASSISTANT_TURN, None) + yield AudioFrame(sounds["talking"]) + + elif isinstance(frame, TextFrame): + # We want to look for sentence breaks in the text + # but since TextFrames are streamed from the LLM + # we need to keep a buffer of the text we've seen so far + self._text += frame.text + + # IMAGE PROMPT + # Looking for: < [image prompt] > in the LLM response + # We prompted our LLM to add an image prompt in the response + # so we use regex matching to find it and yield a StoryImageFrame + if re.search(r"<.*?>", self._text): + if not re.search(r"<.*?>.*?>", self._text): + # Pass any frames until we have a closing bracket + # otherwise the image prompt will be passed to TTS + pass + # Extract the image prompt from the text using regex + image_prompt = re.search(r"<(.*?)>", self._text).group(1) + # Remove the image prompt from the text + self._text = re.sub(r"<.*?>", '', self._text, count=1) + # Process the image prompt frame + yield StoryImageFrame(image_prompt) + + # STORY PAGE + # Looking for: [break] in the LLM response + # We prompted our LLM to add a [break] after each sentence + # so we use regex matching to find it in the LLM response + if re.search(r".*\[[bB]reak\].*", self._text): + # Remove the [break] token from the text + # so it isn't spoken out loud by the TTS + self._text = re.sub(r'\[[bB]reak\]', '', + self._text, flags=re.IGNORECASE) + self._text = self._text.replace("\n", " ") + if len(self._text) > 2: + # Append the sentence to the story + self._story.append(self._text) + yield StoryPageFrame(self._text) + # Assert that it's the LLMs turn, until we're finished + yield SendAppMessageFrame(CUE_ASSISTANT_TURN, None) + # Clear the buffer + self._text = "" + + # End of LLM response + # Driven by the prompt, the LLM should have asked the user for input + elif isinstance(frame, LLMResponseEndFrame): + # We use a different frame type, as to avoid image generation ingest + yield StoryPromptFrame(self._text) + self._text = "" + yield frame + # Send an app message to the UI + yield SendAppMessageFrame(CUE_USER_TURN, None) + yield AudioFrame(sounds["listening"]) + + # Anything that is not a TextFrame pass through + else: + yield frame diff --git a/examples/storytelling-chatbot/src/prompts.py b/examples/storytelling-chatbot/src/prompts.py new file mode 100644 index 000000000..9df917234 --- /dev/null +++ b/examples/storytelling-chatbot/src/prompts.py @@ -0,0 +1,35 @@ +LLM_INTRO_PROMPT = { + "role": "system", + "content": "You are a creative storyteller who loves to tell whimsical, fantastical stories. \ + Your goal is to craft an engaging and fun story. \ + Start by asking the user what kind of story they'd like to hear. Don't provide any examples. \ + Keep your reponse to only a few sentences." +} + + +LLM_BASE_PROMPT = { + "role": "system", + "content": "You are a creative storyteller who loves tell whimsical, fantastical stories. \ + Your goal is to craft an engaging and fun story. \ + Keep all responses short and no more than a few sentences. Include [break] after each sentence of the story. \ + \ + Start each sentence with an image prompt, wrapped in triangle braces, that I can use to generate an illustration representing the upcoming scene. \ + Image prompts should always be wrapped in triangle braces, like this: . \ + You should provide as much descriptive detail in your image prompt as you can to help recreate the current scene depicted by the sentence. \ + For any recurring characters, you should provide a description of them in the image prompt each time, for example: . \ + Please do not include any character names in the image prompts, just their descriptions. \ + Image prompts should focus on key visual attributes of all characters each time, for example . \ + Please use the following structure for your image prompts: characters, setting, action, and mood. \ + Image prompts should be less than 150-200 characters and start in lowercase. \ + \ + Responses should use the format: <...> story sentence [break] <...> story sentence [break] ... \ + After each response, ask me how I'd like the story to continue and wait for my input. \ + Please ensure your responses are less than 3-4 sentences long. \ + Please refrain from using any explicit language or content. Do not tell scary stories." +} + + +IMAGE_GEN_PROMPT = "illustrative art of %s. In the style of Studio Ghibli. colorful, whimsical, painterly, concept art." + +CUE_USER_TURN = {"cue": "user_turn"} +CUE_ASSISTANT_TURN = {"cue": "assistant_turn"} diff --git a/examples/storytelling-chatbot/src/server.py b/examples/storytelling-chatbot/src/server.py new file mode 100644 index 000000000..7596af2ed --- /dev/null +++ b/examples/storytelling-chatbot/src/server.py @@ -0,0 +1,175 @@ +import os +import argparse +import subprocess +import atexit +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, Request, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse, JSONResponse, RedirectResponse + +from utils.daily_helpers import create_room as _create_room, get_token, get_name_from_url + +MAX_BOTS_PER_ROOM = 1 + +# Bot sub-process dict for status reporting and concurrency control +bot_procs = {} + + +def cleanup(): + # Clean up function, just to be extra safe + for proc in bot_procs.values(): + proc.terminate() + proc.wait() + + +atexit.register(cleanup) + + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Mount the static directory +STATIC_DIR = "frontend/out" + +app.mount("/static", StaticFiles(directory=STATIC_DIR, html=True), name="static") + + +@app.post("/create") +async def create_room(request: Request) -> JSONResponse: + data = await request.json() + + if data.get('room_url') is not None: + room_url = data.get('room_url') + room_name = get_name_from_url(room_url) + else: + room_url, room_name = _create_room() + + token = get_token(room_url) + + return JSONResponse({"room_url": room_url, "room_name": room_name, "token": token}) + + +@app.post("/start") +async def start_agent(request: Request) -> JSONResponse: + data = await request.json() + + # Is this a webhook creation request? + if "test" in data: + return JSONResponse({"test": True}) + + # Ensure the room property is present + room_url = data.get('room_url') + if not room_url: + raise HTTPException( + status_code=500, + detail="Missing 'room' property in request data. Cannot start agent without a target room!") + + # Check if there is already an existing process running in this room + num_bots_in_room = sum( + 1 for proc in bot_procs.values() if proc[1] == room_url and proc[0].poll() is None) + if num_bots_in_room >= MAX_BOTS_PER_ROOM: + raise HTTPException( + status_code=500, detail=f"Max bot limited reach for room: {room_url}") + + # Get the token for the room + token = get_token(room_url) + + if not token: + raise HTTPException( + status_code=500, detail=f"Failed to get token for room: {room_url}") + + # Spawn a new agent, and join the user session + # Note: this is mostly for demonstration purposes (refer to 'deployment' in README) + try: + proc = subprocess.Popen( + [ + f"python3 -m bot -u {room_url} -t {token}" + ], + shell=True, + bufsize=1, + cwd=os.path.dirname(os.path.abspath(__file__)) + ) + bot_procs[proc.pid] = (proc, room_url) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to start subprocess: {e}") + + return JSONResponse({"bot_id": proc.pid, "room_url": room_url}) + + +@app.get("/status/{pid}") +def get_status(pid: int): + # Look up the subprocess + proc = bot_procs.get(pid) + + # If the subprocess doesn't exist, return an error + if not proc: + raise HTTPException( + status_code=404, detail=f"Bot with process id: {pid} not found") + + # Check the status of the subprocess + if proc[0].poll() is None: + status = "running" + else: + status = "finished" + + return JSONResponse({"bot_id": pid, "status": status}) + + +@app.get("/{path_name:path}", response_class=FileResponse) +async def catch_all(path_name: Optional[str] = ""): + if path_name == "": + return FileResponse(f"{STATIC_DIR}/index.html") + + file_path = Path(STATIC_DIR) / (path_name or "") + + if file_path.is_file(): + return file_path + + html_file_path = file_path.with_suffix(".html") + if html_file_path.is_file(): + return FileResponse(html_file_path) + + raise HTTPException(status_code=450, detail="Incorrect API call") + + +if __name__ == "__main__": + # Check environment variables + required_env_vars = ['OPENAI_API_KEY', 'DAILY_API_KEY', + 'FAL_KEY', 'ELEVENLABS_VOICE_ID', 'ELEVENLABS_API_KEY'] + for env_var in required_env_vars: + if env_var not in os.environ: + raise Exception(f"Missing environment variable: {env_var}.") + + import uvicorn + + default_host = os.getenv("HOST", "0.0.0.0") + default_port = int(os.getenv("FAST_API_PORT", "7860")) + + parser = argparse.ArgumentParser( + description="Daily Storyteller FastAPI server") + parser.add_argument("--host", type=str, + default=default_host, help="Host address") + parser.add_argument("--port", type=int, + default=default_port, help="Port number") + parser.add_argument("--reload", action="store_true", + help="Reload code on change") + + config = parser.parse_args() + + uvicorn.run( + "server:app", + host=config.host, + port=config.port, + reload=config.reload + ) diff --git a/examples/storytelling-chatbot/src/utils/daily_helpers.py b/examples/storytelling-chatbot/src/utils/daily_helpers.py new file mode 100644 index 000000000..0a2b472f6 --- /dev/null +++ b/examples/storytelling-chatbot/src/utils/daily_helpers.py @@ -0,0 +1,109 @@ + +import urllib.parse +import os +import time +import urllib +import requests + +from dotenv import load_dotenv +load_dotenv() + + +daily_api_path = os.getenv("DAILY_API_URL") +daily_api_key = os.getenv("DAILY_API_KEY") + + +def create_room() -> tuple[str, str]: + """ + Helper function to create a Daily room. + # See: https://docs.daily.co/reference/rest-api/rooms + + Returns: + tuple: A tuple containing the room URL and room name. + + Raises: + Exception: If the request to create the room fails or if the response does not contain the room URL or room name. + """ + room_props = { + "exp": time.time() + 60 * 60, # 1 hour + "enable_chat": True, + "enable_emoji_reactions": True, + "eject_at_room_exp": True, + "enable_prejoin_ui": False, # Important for the bot to be able to join headlessly + } + res = requests.post( + f"https://{daily_api_path}/rooms", + headers={"Authorization": f"Bearer {daily_api_key}"}, + json={ + "properties": room_props + }, + ) + if res.status_code != 200: + raise Exception(f"Unable to create room: {res.text}") + + data = res.json() + room_url: str = data.get("url") + room_name: str = data.get("name") + if room_url is None or room_name is None: + raise Exception("Missing room URL or room name in response") + + return room_url, room_name + + +def get_name_from_url(room_url: str) -> str: + """ + Extracts the name from a given room URL. + + Args: + room_url (str): The URL of the room. + + Returns: + str: The extracted name from the room URL. + """ + return urllib.parse.urlparse(room_url).path[1:] + + +def get_token(room_url: str) -> str: + """ + Retrieves a meeting token for the specified Daily room URL. + # See: https://docs.daily.co/reference/rest-api/meeting-tokens + + Args: + room_url (str): The URL of the Daily room. + + Returns: + str: The meeting token. + + Raises: + Exception: If no room URL is specified or if no Daily API key is specified. + Exception: If there is an error creating the meeting token. + """ + if not room_url: + raise Exception( + "No Daily room specified. You must specify a Daily room in order a token to be generated.") + + if not daily_api_key: + raise Exception( + "No Daily API key specified. set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers.") + + expiration: float = time.time() + 60 * 60 + room_name = get_name_from_url(room_url) + + res: requests.Response = requests.post( + f"https://{daily_api_path}/meeting-tokens", + headers={ + "Authorization": f"Bearer {daily_api_key}"}, + json={ + "properties": { + "room_name": room_name, + "is_owner": True, # Owner tokens required for transcription + "exp": expiration}}, + ) + + if res.status_code != 200: + raise Exception( + f"Failed to create meeting token: {res.status_code} {res.text}") + + token: str = res.json()["token"] + + return token diff --git a/examples/storytelling-chatbot/src/utils/helpers.py b/examples/storytelling-chatbot/src/utils/helpers.py new file mode 100644 index 000000000..f152be6e4 --- /dev/null +++ b/examples/storytelling-chatbot/src/utils/helpers.py @@ -0,0 +1,33 @@ +import os +import wave +from PIL import Image + +script_dir = os.path.dirname(__file__) + + +def load_images(image_files): + images = {} + for file in image_files: + # Build the full path to the image file + full_path = os.path.join(script_dir, "../assets", file) + # Get the filename without the extension to use as the dictionary key + filename = os.path.splitext(os.path.basename(full_path))[0] + # Open the image and convert it to bytes + with Image.open(full_path) as img: + images[filename] = img.tobytes() + return images + + +def load_sounds(sound_files): + sounds = {} + + for file in sound_files: + # Build the full path to the sound file + full_path = os.path.join(script_dir, "../assets", file) + # Get the filename without the extension to use as the dictionary key + filename = os.path.splitext(os.path.basename(full_path))[0] + # Open the sound and convert it to bytes + with wave.open(full_path) as audio_file: + sounds[filename] = audio_file.readframes(-1) + + return sounds diff --git a/examples/translation-chatbot/.gitignore b/examples/translation-chatbot/.gitignore new file mode 100644 index 000000000..2bc1403d1 --- /dev/null +++ b/examples/translation-chatbot/.gitignore @@ -0,0 +1,161 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ +runpod.toml diff --git a/examples/translation-chatbot/Dockerfile b/examples/translation-chatbot/Dockerfile new file mode 100644 index 000000000..3dff872cf --- /dev/null +++ b/examples/translation-chatbot/Dockerfile @@ -0,0 +1,14 @@ +FROM python:3.10-bullseye + +RUN mkdir /app +RUN mkdir /app/utils +COPY *.py /app/ +COPY requirements.txt /app/ +copy utils/* /app/utils/ + +WORKDIR /app +RUN pip3 install -r requirements.txt + +EXPOSE 7860 + +CMD ["python3", "server.py"] \ No newline at end of file diff --git a/examples/translation-chatbot/README.md b/examples/translation-chatbot/README.md new file mode 100644 index 000000000..7a141b334 --- /dev/null +++ b/examples/translation-chatbot/README.md @@ -0,0 +1,33 @@ +# Translation Chatbot + + + +This app listens for user speech, then translates that speech to Spanish and speaks the translation back to the user using text-to-speech. It's probably most useful with multiple users talking to each other, along with some manual track subscription management in the Daily call. + +See a quick video walkthrough of the code here: https://www.loom.com/share/59fdddf129534dc2be4dde3cc6ebe8de + +## Get started + +```python +python3 -m venv env +source env/bin/activate +pip install -r requirements.txt + +cp env.example .env # and add your credentials + +``` + +## Run the server + +```bash +python server.py +``` + +Then, visit `http://localhost:7860/start` in your browser to start a translatorbot session. + +## Build and test the Docker image + +``` +docker build -t chatbot . +docker run --env-file .env -p 7860:7860 chatbot +``` diff --git a/examples/starter-apps/translator.py b/examples/translation-chatbot/bot.py similarity index 82% rename from examples/starter-apps/translator.py rename to examples/translation-chatbot/bot.py index 94a0fe1e9..5b8c3286b 100644 --- a/examples/starter-apps/translator.py +++ b/examples/translation-chatbot/bot.py @@ -4,21 +4,21 @@ import logging import os from typing import AsyncGenerator -from pipecat.pipeline.aggregators import ( +from dailyai.pipeline.aggregators import ( SentenceAggregator, ) -from pipecat.pipeline.frames import ( +from dailyai.pipeline.frames import ( Frame, LLMMessagesFrame, TextFrame, SendAppMessageFrame, ) -from pipecat.pipeline.frame_processor import FrameProcessor -from pipecat.pipeline.pipeline import Pipeline -from pipecat.transports.daily_transport import DailyTransport -from pipecat.services.azure_ai_services import AzureTTSService -from pipecat.services.open_ai_services import OpenAILLMService -from pipecat.pipeline.aggregators import LLMFullResponseAggregator +from dailyai.pipeline.frame_processor import FrameProcessor +from dailyai.pipeline.pipeline import Pipeline +from dailyai.transports.daily_transport import DailyTransport +from dailyai.services.azure_ai_services import AzureTTSService +from dailyai.services.open_ai_services import OpenAILLMService +from dailyai.pipeline.aggregators import LLMFullResponseAggregator from runner import configure @@ -28,7 +28,7 @@ from dotenv import load_dotenv load_dotenv(override=True) logging.basicConfig(format=f"%(levelno)s %(asctime)s %(message)s") -logger = logging.getLogger("pipecat") +logger = logging.getLogger("dailyai") logger.setLevel(logging.DEBUG) """ @@ -99,6 +99,8 @@ async def main(room_url: str, token): ts = TranslationSubtitles("spanish") pipeline = Pipeline([sa, tp, llm, lfra, ts, tts]) + transport.transcription_settings["extra"]["endpointing"] = True + transport.transcription_settings["extra"]["punctuate"] = True await transport.run(pipeline) diff --git a/examples/translation-chatbot/env.example b/examples/translation-chatbot/env.example new file mode 100644 index 000000000..42c1083bf --- /dev/null +++ b/examples/translation-chatbot/env.example @@ -0,0 +1,5 @@ +DAILY_SAMPLE_ROOM_URL=https://yourdomain.daily.co/yourroom # (for joining the bot to the same room repeatedly for local dev) +DAILY_API_KEY=7df... +OPENAI_API_KEY=sk-PL... +AZURE_SPEECH_REGION=westus... +AZURE_SPEECH_API_KEY=febf... \ No newline at end of file diff --git a/examples/translation-chatbot/image.png b/examples/translation-chatbot/image.png new file mode 100644 index 000000000..af7ebe0bb Binary files /dev/null and b/examples/translation-chatbot/image.png differ diff --git a/examples/translation-chatbot/requirements.txt b/examples/translation-chatbot/requirements.txt new file mode 100644 index 000000000..cb7ac4232 --- /dev/null +++ b/examples/translation-chatbot/requirements.txt @@ -0,0 +1,4 @@ +python-dotenv +requests +fastapi[all] +dailyai[daily,openai,azure] diff --git a/examples/translation-chatbot/runner.py b/examples/translation-chatbot/runner.py new file mode 100644 index 000000000..6d1a8113d --- /dev/null +++ b/examples/translation-chatbot/runner.py @@ -0,0 +1,58 @@ +import argparse +import os +import time +import urllib +import requests + + +def configure(): + parser = argparse.ArgumentParser(description="Daily AI SDK Bot Sample") + parser.add_argument( + "-u", + "--url", + type=str, + required=False, + help="URL of the Daily room to join") + parser.add_argument( + "-k", + "--apikey", + type=str, + required=False, + help="Daily API Key (needed to create an owner token for the room)", + ) + + args, unknown = parser.parse_known_args() + + url = args.url or os.getenv("DAILY_SAMPLE_ROOM_URL") + key = args.apikey or os.getenv("DAILY_API_KEY") + + if not url: + raise Exception( + "No Daily room specified. use the -u/--url option from the command line, or set DAILY_SAMPLE_ROOM_URL in your environment to specify a Daily room URL.") + + if not key: + raise Exception("No Daily API key specified. use the -k/--apikey option from the command line, or set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers.") + + # Create a meeting token for the given room with an expiration 1 hour in + # the future. + room_name: str = urllib.parse.urlparse(url).path[1:] + expiration: float = time.time() + 60 * 60 + + res: requests.Response = requests.post( + f"https://api.daily.co/v1/meeting-tokens", + headers={ + "Authorization": f"Bearer {key}"}, + json={ + "properties": { + "room_name": room_name, + "is_owner": True, + "exp": expiration}}, + ) + + if res.status_code != 200: + raise Exception( + f"Failed to create meeting token: {res.status_code} {res.text}") + + token: str = res.json()["token"] + + return (url, token) diff --git a/examples/translation-chatbot/server.py b/examples/translation-chatbot/server.py new file mode 100644 index 000000000..1b8928db2 --- /dev/null +++ b/examples/translation-chatbot/server.py @@ -0,0 +1,127 @@ +import os +import argparse +import subprocess +import atexit +from pathlib import Path +from typing import Optional + +from fastapi import FastAPI, Request, HTTPException +from fastapi.middleware.cors import CORSMiddleware +from fastapi.staticfiles import StaticFiles +from fastapi.responses import FileResponse, JSONResponse, RedirectResponse + +from utils.daily_helpers import create_room as _create_room, get_token, get_name_from_url + +MAX_BOTS_PER_ROOM = 1 + +# Bot sub-process dict for status reporting and concurrency control +bot_procs = {} + + +def cleanup(): + # Clean up function, just to be extra safe + for proc in bot_procs.values(): + proc.terminate() + proc.wait() + + +atexit.register(cleanup) + + +app = FastAPI() + +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + + +@app.get("/start") +async def start_agent(request: Request): + print(f"!!! Creating room") + room_url, room_name = _create_room() + print(f"!!! Room URL: {room_url}") + # Ensure the room property is present + if not room_url: + raise HTTPException( + status_code=500, + detail="Missing 'room' property in request data. Cannot start agent without a target room!") + + # Check if there is already an existing process running in this room + num_bots_in_room = sum( + 1 for proc in bot_procs.values() if proc[1] == room_url and proc[0].poll() is None) + if num_bots_in_room >= MAX_BOTS_PER_ROOM: + raise HTTPException( + status_code=500, detail=f"Max bot limited reach for room: {room_url}") + + # Get the token for the room + token = get_token(room_url) + + if not token: + raise HTTPException( + status_code=500, detail=f"Failed to get token for room: {room_url}") + + # Spawn a new agent, and join the user session + # Note: this is mostly for demonstration purposes (refer to 'deployment' in README) + try: + proc = subprocess.Popen( + [ + f"python3 -m bot -u {room_url} -t {token}" + ], + shell=True, + bufsize=1, + cwd=os.path.dirname(os.path.abspath(__file__)) + ) + bot_procs[proc.pid] = (proc, room_url) + except Exception as e: + raise HTTPException( + status_code=500, detail=f"Failed to start subprocess: {e}") + + return RedirectResponse(room_url) + + +@app.get("/status/{pid}") +def get_status(pid: int): + # Look up the subprocess + proc = bot_procs.get(pid) + + # If the subprocess doesn't exist, return an error + if not proc: + raise HTTPException( + status_code=404, detail=f"Bot with process id: {pid} not found") + + # Check the status of the subprocess + if proc[0].poll() is None: + status = "running" + else: + status = "finished" + + return JSONResponse({"bot_id": pid, "status": status}) + + +if __name__ == "__main__": + import uvicorn + + default_host = os.getenv("HOST", "0.0.0.0") + default_port = int(os.getenv("FAST_API_PORT", "7860")) + + parser = argparse.ArgumentParser( + description="Daily Storyteller FastAPI server") + parser.add_argument("--host", type=str, + default=default_host, help="Host address") + parser.add_argument("--port", type=int, + default=default_port, help="Port number") + parser.add_argument("--reload", action="store_true", + help="Reload code on change") + + config = parser.parse_args() + + uvicorn.run( + "server:app", + host=config.host, + port=config.port, + reload=config.reload, + ) diff --git a/examples/translation-chatbot/utils/daily_helpers.py b/examples/translation-chatbot/utils/daily_helpers.py new file mode 100644 index 000000000..140f710e4 --- /dev/null +++ b/examples/translation-chatbot/utils/daily_helpers.py @@ -0,0 +1,109 @@ + +import urllib.parse +import os +import time +import urllib +import requests + +from dotenv import load_dotenv +load_dotenv() + + +daily_api_path = os.getenv("DAILY_API_URL") or "api.daily.co/v1" +daily_api_key = os.getenv("DAILY_API_KEY") + + +def create_room() -> tuple[str, str]: + """ + Helper function to create a Daily room. + # See: https://docs.daily.co/reference/rest-api/rooms + + Returns: + tuple: A tuple containing the room URL and room name. + + Raises: + Exception: If the request to create the room fails or if the response does not contain the room URL or room name. + """ + room_props = { + "exp": time.time() + 60 * 60, # 1 hour + "enable_chat": True, + "enable_emoji_reactions": True, + "eject_at_room_exp": True, + "enable_prejoin_ui": False, # Important for the bot to be able to join headlessly + } + res = requests.post( + f"https://{daily_api_path}/rooms", + headers={"Authorization": f"Bearer {daily_api_key}"}, + json={ + "properties": room_props + }, + ) + if res.status_code != 200: + raise Exception(f"Unable to create room: {res.text}") + + data = res.json() + room_url: str = data.get("url") + room_name: str = data.get("name") + if room_url is None or room_name is None: + raise Exception("Missing room URL or room name in response") + + return room_url, room_name + + +def get_name_from_url(room_url: str) -> str: + """ + Extracts the name from a given room URL. + + Args: + room_url (str): The URL of the room. + + Returns: + str: The extracted name from the room URL. + """ + return urllib.parse.urlparse(room_url).path[1:] + + +def get_token(room_url: str) -> str: + """ + Retrieves a meeting token for the specified Daily room URL. + # See: https://docs.daily.co/reference/rest-api/meeting-tokens + + Args: + room_url (str): The URL of the Daily room. + + Returns: + str: The meeting token. + + Raises: + Exception: If no room URL is specified or if no Daily API key is specified. + Exception: If there is an error creating the meeting token. + """ + if not room_url: + raise Exception( + "No Daily room specified. You must specify a Daily room in order a token to be generated.") + + if not daily_api_key: + raise Exception( + "No Daily API key specified. set DAILY_API_KEY in your environment to specify a Daily API key, available from https://dashboard.daily.co/developers.") + + expiration: float = time.time() + 60 * 60 + room_name = get_name_from_url(room_url) + + res: requests.Response = requests.post( + f"https://{daily_api_path}/meeting-tokens", + headers={ + "Authorization": f"Bearer {daily_api_key}"}, + json={ + "properties": { + "room_name": room_name, + "is_owner": True, # Owner tokens required for transcription + "exp": expiration}}, + ) + + if res.status_code != 200: + raise Exception( + f"Failed to create meeting token: {res.status_code} {res.text}") + + token: str = res.json()["token"] + + return token