diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..f3c0ced --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: [abi] diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..386e7e3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Screenshots of backend AND frontend terminal logs** +If applicable, add screenshots to help explain your problem. diff --git a/.github/ISSUE_TEMPLATE/custom.md b/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000..48d5f81 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,10 @@ +--- +name: Custom issue template +about: Describe this issue template's purpose here. +title: '' +labels: '' +assignees: '' + +--- + + diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..bbcbbe7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.gitignore b/.gitignore index 7b377b1..fc073ff 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,6 @@ backend/backend/* # Env vars frontend/.env.local .env + +# Mac files +.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..bfbce46 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "python.analysis.typeCheckingMode": "strict", + "python.analysis.extraPaths": ["./backend"], + "python.autoComplete.extraPaths": ["./backend"] +} diff --git a/Evaluation.md b/Evaluation.md new file mode 100644 index 0000000..e937b78 --- /dev/null +++ b/Evaluation.md @@ -0,0 +1,19 @@ +## Evaluating models and prompts + +Evaluation dataset consists of 16 screenshots. A Python script for running screenshot-to-code on the dataset and a UI for rating outputs is included. With this set up, we can compare and evaluate various models and prompts. + +### Running evals + +- Input screenshots should be located at `backend/evals_data/inputs` and the outputs will be `backend/evals_data/outputs`. If you want to modify this, modify `EVALS_DIR` in `backend/evals/config.py`. You can download the input screenshot dataset here: TODO. +- Set a stack and model (`STACK` var, `MODEL` var) in `backend/run_evals.py` +- Run `OPENAI_API_KEY=sk-... python run_evals.py` - this runs the screenshot-to-code on the input dataset in parallel but it will still take a few minutes to complete. +- Once the script is done, you can find the outputs in `backend/evals_data/outputs`. + +### Rating evals + +In order to view and rate the outputs, visit your front-end at `/evals`. + +- Rate each output on a scale of 1-4 +- You can also print the page as PDF to share your results with others. + +Generally, I run three tests for each model/prompt + stack combo and take the average score out of those tests to evaluate. diff --git a/README.md b/README.md index 393e027..1762f91 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,48 @@ # screenshot-to-code -This simple app converts a screenshot to code (HTML/Tailwind CSS, or React or Vue or Bootstrap). It uses GPT-4 Vision to generate the code and DALL-E 3 to generate similar-looking images. You can now also enter a URL to clone a live website! +A simple tool to convert screenshots, mockups and Figma designs into clean, functional code using AI. **Now supporting GPT-4O!** https://github.com/abi/screenshot-to-code/assets/23818/6cebadae-2fe3-4986-ac6a-8fb9db030045 -See the [Examples](#examples) section below for more demos. +Supported stacks: -## 🚀 Try It Out! +- HTML + Tailwind +- React + Tailwind +- Vue + Tailwind +- Bootstrap +- Ionic + Tailwind +- SVG -🆕 [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section below for details**). Or see [Getting Started](#-getting-started) below for local install instructions. +Supported AI models: -## 🌟 Recent Updates +- GPT-4O - Best model! +- GPT-4 Turbo (Apr 2024) +- GPT-4 Vision (Nov 2023) +- Claude 3 Sonnet +- DALL-E 3 for image generation -- Nov 28 - 🔥 🔥 🔥 Get output code in React or Bootstrap or TailwindCSS -- Nov 23 - Send in a screenshot of the current replicated version (sometimes improves quality of subsequent generations) -- Nov 21 - Edit code in the code editor and preview changes live thanks to [@clean99](https://github.com/clean99) -- Nov 20 - Paste in a URL to screenshot and clone (requires [ScreenshotOne free API key](https://screenshotone.com?via=screenshot-to-code)) -- Nov 19 - Support for dark/light code editor theme - thanks [@kachbit](https://github.com/kachbit) -- Nov 16 - Added a setting to disable DALL-E image generation if you don't need that -- Nov 16 - View code directly within the app -- Nov 15 - You can now instruct the AI to update the code as you wish. It is helpful if the AI messed up some styles or missed a section. +See the [Examples](#-examples) section below for more demos. + +We also just added experimental support for taking a video/screen recording of a website in action and turning that into a functional prototype. + +![google in app quick 3](https://github.com/abi/screenshot-to-code/assets/23818/8758ffa4-9483-4b9b-bb66-abd6d1594c33) + +[Learn more about video here](https://github.com/abi/screenshot-to-code/wiki/Screen-Recording-to-Code). + +[Follow me on Twitter for updates](https://twitter.com/_abi_). + +## Sponsors + + + + +## 🚀 Try It Out without no install + +[Try it live on the hosted version (paid)](https://screenshottocode.com). ## 🛠 Getting Started -The app has a React/Vite frontend and a FastAPI backend. You will need an OpenAI API key with access to the GPT-4 Vision API. +The app has a React/Vite frontend and a FastAPI backend. You will need an OpenAI API key with access to the GPT-4 Vision API or an Anthropic key if you want to use Claude Sonnet, or for experimental video support. Run the backend (I use Poetry for package management - `pip install poetry` if you don't have it): @@ -35,6 +54,8 @@ poetry shell poetry run uvicorn main:app --reload --port 7001 ``` +If you want to use Anthropic, add the `ANTHROPIC_API_KEY` to `backend/.env` with your API key from Anthropic. + Run the frontend: ```bash @@ -68,6 +89,9 @@ The app will be up and running at http://localhost:5173. Note that you can't dev - **I'm running into an error when setting up the backend. How can I fix it?** [Try this](https://github.com/abi/screenshot-to-code/issues/3#issuecomment-1814777959). If that still doesn't work, open an issue. - **How do I get an OpenAI API key?** See https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md +- **How can I configure an OpenAI proxy?** - If you're not able to access the OpenAI API directly (due to e.g. country restrictions), you can try a VPN or you can configure the OpenAI base URL to use a proxy: Set OPENAI_BASE_URL in the `backend/.env` or directly in the UI in the settings dialog. Make sure the URL has "v1" in the path so it should look like this: `https://xxx.xxxxx.xxx/v1` +- **How can I update the backend host that my front-end connects to?** - Configure VITE_HTTP_BACKEND_URL and VITE_WS_BACKEND_URL in front/.env.local For example, set VITE_HTTP_BACKEND_URL=http://124.10.20.1:7001 +- **Seeing UTF-8 errors when running the backend?** - On windows, open the .env file with notepad++, then go to Encoding and select UTF-8. - **How can I provide feedback?** For feedback, feature requests and bug reports, open an issue or ping me on [Twitter](https://twitter.com/_abi_). ## 📚 Examples @@ -88,6 +112,6 @@ https://github.com/abi/screenshot-to-code/assets/23818/3fec0f77-44e8-4fb3-a769-a ## 🌍 Hosted Version -🆕 [Try it here](https://screenshottocode.com) (bring your own OpenAI key - **your key must have access to GPT-4 Vision. See [FAQ](#%EF%B8%8F-faqs) section for details**). Or see [Getting Started](#-getting-started) for local install instructions. +🆕 [Try it here (paid)](https://screenshottocode.com). Or see [Getting Started](#-getting-started) for local install instructions to use with your own API keys. [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/abiraja) diff --git a/Troubleshooting.md b/Troubleshooting.md index 0704859..89aa3ba 100644 --- a/Troubleshooting.md +++ b/Troubleshooting.md @@ -1,16 +1,22 @@ -### Getting an OpenAI API key +### Getting an OpenAI API key with GPT4-Vision model access You don't need a ChatGPT Pro account. Screenshot to code uses API keys from your OpenAI developer account. In order to get access to the GPT4 Vision model, log into your OpenAI account and then, follow these instructions: 1. Open [OpenAI Dashboard](https://platform.openai.com/) 1. Go to Settings > Billing 1. Click at the Add payment details -285636868-c80deb92-ab47-45cd-988f-deee67fbd44d +285636868-c80deb92-ab47-45cd-988f-deee67fbd44d + 4. You have to buy some credits. The minimum is $5. - 5. Go to Settings > Limits and check at the bottom of the page, your current tier has to be "Tier 1" to have GPT4 access -285636973-da38bd4d-8a78-4904-8027-ca67d729b933 +285636973-da38bd4d-8a78-4904-8027-ca67d729b933 -Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated. +6. Navigate to OpenAI [api keys](https://platform.openai.com/api-keys) page and create and copy a new secret key. +7. Go to Screenshot to code and paste it in the Settings dialog under OpenAI key (gear icon). Your key is only stored in your browser. Never stored on our servers. -If you've followed these steps, and it still doesn't work, feel free to open a Github issue. +## Still not working? + +- Some users have also reported that it can take upto 30 minutes after your credit purchase for the GPT4 vision model to be activated. +- You need to add credits to your account AND set it to renew when credits run out in order to be upgraded to Tier 1. Make sure your "Settings > Limits" page shows that you are at Tier 1. + +If you've followed these steps, and it still doesn't work, feel free to open a Github issue. We only provide support for the open source version since we don't have debugging logs on the hosted version. If you're looking to use the hosted version, we recommend getting a paid subscription on screenshottocode.com diff --git a/backend/.gitignore b/backend/.gitignore index d9005f2..5d03006 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -150,3 +150,11 @@ cython_debug/ # 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/ + + +# Temporary eval output +evals_data + + +# Temporary video evals (Remove before merge) +video_evals diff --git a/backend/.pre-commit-config.yaml b/backend/.pre-commit-config.yaml new file mode 100644 index 0000000..b54da93 --- /dev/null +++ b/backend/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - repo: local + hooks: + - id: poetry-pytest + name: Run pytest with Poetry + entry: poetry run --directory backend pytest + language: system + pass_filenames: false + always_run: true + files: ^backend/ + # - id: poetry-pyright + # name: Run pyright with Poetry + # entry: poetry run --directory backend pyright + # language: system + # pass_filenames: false + # always_run: true + # files: ^backend/ diff --git a/backend/README.md b/backend/README.md index 7c1cad2..155bf46 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,3 +1,7 @@ -Run tests +# Run the type checker -pytest test_prompts.py +poetry run pyright + +# Run tests + +poetry run pytest diff --git a/backend/access_token.py b/backend/access_token.py deleted file mode 100644 index bed7273..0000000 --- a/backend/access_token.py +++ /dev/null @@ -1,25 +0,0 @@ -import json -import os -import httpx - - -async def validate_access_token(access_code: str): - async with httpx.AsyncClient() as client: - url = "http://localhost:8000/screenshot_to_code/validate_access_token" - data = json.dumps( - { - "access_code": access_code, - "secret": os.environ.get("PICO_BACKEND_SECRET"), - } - ) - headers = {"Content-Type": "application/json"} - - response = await client.post(url, content=data, headers=headers) - response_data = response.json() - - if response_data["success"]: - print("Access token is valid.") - return True - else: - print(f"Access token validation failed: {response_data['failure_reason']}") - return False diff --git a/backend/config.py b/backend/config.py new file mode 100644 index 0000000..05592b0 --- /dev/null +++ b/backend/config.py @@ -0,0 +1,16 @@ +# Useful for debugging purposes when you don't want to waste GPT4-Vision credits +# Setting to True will stream a mock response instead of calling the OpenAI API +# TODO: Should only be set to true when value is 'True', not any abitrary truthy value +import os + +ANTHROPIC_API_KEY = os.environ.get("ANTHROPIC_API_KEY", None) + +# Debugging-related + +SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False)) +IS_DEBUG_ENABLED = bool(os.environ.get("IS_DEBUG_ENABLED", False)) +DEBUG_DIR = os.environ.get("DEBUG_DIR", "") + +# Set to True when running in production (on the hosted version) +# Used as a feature flag to enable or disable certain features +IS_PROD = os.environ.get("IS_PROD", False) diff --git a/backend/custom_types.py b/backend/custom_types.py new file mode 100644 index 0000000..b6c9fee --- /dev/null +++ b/backend/custom_types.py @@ -0,0 +1,7 @@ +from typing import Literal + + +InputMode = Literal[ + "image", + "video", +] diff --git a/backend/debug/DebugFileWriter.py b/backend/debug/DebugFileWriter.py new file mode 100644 index 0000000..bbcd77b --- /dev/null +++ b/backend/debug/DebugFileWriter.py @@ -0,0 +1,30 @@ +import os +import logging +import uuid + +from config import DEBUG_DIR, IS_DEBUG_ENABLED + + +class DebugFileWriter: + def __init__(self): + if not IS_DEBUG_ENABLED: + return + + try: + self.debug_artifacts_path = os.path.expanduser( + f"{DEBUG_DIR}/{str(uuid.uuid4())}" + ) + os.makedirs(self.debug_artifacts_path, exist_ok=True) + print(f"Debugging artifacts will be stored in: {self.debug_artifacts_path}") + except: + logging.error("Failed to create debug directory") + + def write_to_file(self, filename: str, content: str) -> None: + try: + with open(os.path.join(self.debug_artifacts_path, filename), "w") as file: + file.write(content) + except Exception as e: + logging.error(f"Failed to write to file: {e}") + + def extract_html_content(self, text: str) -> str: + return str(text.split("")[-1].rsplit("", 1)[0] + "") diff --git a/backend/debug/__init__.py b/backend/debug/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/evals/__init__.py b/backend/evals/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/evals/config.py b/backend/evals/config.py new file mode 100644 index 0000000..7643027 --- /dev/null +++ b/backend/evals/config.py @@ -0,0 +1 @@ +EVALS_DIR = "./evals_data" diff --git a/backend/evals/core.py b/backend/evals/core.py new file mode 100644 index 0000000..5e05362 --- /dev/null +++ b/backend/evals/core.py @@ -0,0 +1,39 @@ +import os +from config import ANTHROPIC_API_KEY + +from llm import Llm, stream_claude_response, stream_openai_response +from prompts import assemble_prompt +from prompts.types import Stack + + +async def generate_code_core(image_url: str, stack: Stack, model: Llm) -> str: + prompt_messages = assemble_prompt(image_url, stack) + openai_api_key = os.environ.get("OPENAI_API_KEY") + anthropic_api_key = ANTHROPIC_API_KEY + openai_base_url = None + + async def process_chunk(content: str): + pass + + if model == Llm.CLAUDE_3_SONNET: + if not anthropic_api_key: + raise Exception("Anthropic API key not found") + + completion = await stream_claude_response( + prompt_messages, + api_key=anthropic_api_key, + callback=lambda x: process_chunk(x), + ) + else: + if not openai_api_key: + raise Exception("OpenAI API key not found") + + completion = await stream_openai_response( + prompt_messages, + api_key=openai_api_key, + base_url=openai_base_url, + callback=lambda x: process_chunk(x), + model=model, + ) + + return completion diff --git a/backend/evals/utils.py b/backend/evals/utils.py new file mode 100644 index 0000000..6a52f88 --- /dev/null +++ b/backend/evals/utils.py @@ -0,0 +1,7 @@ +import base64 + + +async def image_to_data_url(filepath: str): + with open(filepath, "rb") as image_file: + encoded_string = base64.b64encode(image_file.read()).decode() + return f"data:image/png;base64,{encoded_string}" diff --git a/backend/image_generation.py b/backend/image_generation.py index 080334f..b93792c 100644 --- a/backend/image_generation.py +++ b/backend/image_generation.py @@ -1,28 +1,28 @@ import asyncio -import os import re +from typing import Dict, List, Union from openai import AsyncOpenAI from bs4 import BeautifulSoup -async def process_tasks(prompts, api_key): - tasks = [generate_image(prompt, api_key) for prompt in prompts] +async def process_tasks(prompts: List[str], api_key: str, base_url: str): + tasks = [generate_image(prompt, api_key, base_url) for prompt in prompts] results = await asyncio.gather(*tasks, return_exceptions=True) - processed_results = [] + processed_results: List[Union[str, None]] = [] for result in results: if isinstance(result, Exception): print(f"An exception occurred: {result}") processed_results.append(None) else: - processed_results.append(result) + processed_results.append(result) # type: ignore return processed_results -async def generate_image(prompt, api_key): - client = AsyncOpenAI(api_key=api_key) - image_params = { +async def generate_image(prompt: str, api_key: str, base_url: str): + client = AsyncOpenAI(api_key=api_key, base_url=base_url) + image_params: Dict[str, Union[str, int]] = { "model": "dall-e-3", "quality": "standard", "style": "natural", @@ -30,11 +30,12 @@ async def generate_image(prompt, api_key): "size": "1024x1024", "prompt": prompt, } - res = await client.images.generate(**image_params) + res = await client.images.generate(**image_params) # type: ignore + await client.close() return res.data[0].url -def extract_dimensions(url): +def extract_dimensions(url: str): # Regular expression to match numbers in the format '300x200' matches = re.findall(r"(\d+)x(\d+)", url) @@ -47,11 +48,11 @@ def extract_dimensions(url): return (100, 100) -def create_alt_url_mapping(code): +def create_alt_url_mapping(code: str) -> Dict[str, str]: soup = BeautifulSoup(code, "html.parser") images = soup.find_all("img") - mapping = {} + mapping: Dict[str, str] = {} for image in images: if not image["src"].startswith("https://placehold.co"): @@ -60,7 +61,9 @@ def create_alt_url_mapping(code): return mapping -async def generate_images(code, api_key, image_cache): +async def generate_images( + code: str, api_key: str, base_url: Union[str, None], image_cache: Dict[str, str] +): # Find all images soup = BeautifulSoup(code, "html.parser") images = soup.find_all("img") @@ -74,26 +77,26 @@ async def generate_images(code, api_key, image_cache): img["src"].startswith("https://placehold.co") and image_cache.get(img.get("alt")) is None ): - alts.append(img.get("alt", None)) + alts.append(img.get("alt", None)) # type: ignore # Exclude images with no alt text - alts = [alt for alt in alts if alt is not None] + alts = [alt for alt in alts if alt is not None] # type: ignore # Remove duplicates - prompts = list(set(alts)) + prompts = list(set(alts)) # type: ignore # Return early if there are no images to replace - if len(prompts) == 0: + if len(prompts) == 0: # type: ignore return code # Generate images - results = await process_tasks(prompts, api_key) + results = await process_tasks(prompts, api_key, base_url) # type: ignore # Create a dict mapping alt text to image URL - mapped_image_urls = dict(zip(prompts, results)) + mapped_image_urls = dict(zip(prompts, results)) # type: ignore # Merge with image_cache - mapped_image_urls = {**mapped_image_urls, **image_cache} + mapped_image_urls = {**mapped_image_urls, **image_cache} # type: ignore # Replace old image URLs with the generated URLs for img in images: diff --git a/backend/llm.py b/backend/llm.py index b52c3c9..e541046 100644 --- a/backend/llm.py +++ b/backend/llm.py @@ -1,30 +1,217 @@ -import os -from typing import Awaitable, Callable +from enum import Enum +from typing import Any, Awaitable, Callable, List, cast +from anthropic import AsyncAnthropic from openai import AsyncOpenAI +from openai.types.chat import ChatCompletionMessageParam, ChatCompletionChunk +from config import IS_DEBUG_ENABLED +from debug.DebugFileWriter import DebugFileWriter -MODEL_GPT_4_VISION = "gpt-4-vision-preview" +from utils import pprint_prompt + + +# Actual model versions that are passed to the LLMs and stored in our logs +class Llm(Enum): + GPT_4_VISION = "gpt-4-vision-preview" + GPT_4_TURBO_2024_04_09 = "gpt-4-turbo-2024-04-09" + GPT_4O_2024_05_13 = "gpt-4o-2024-05-13" + CLAUDE_3_SONNET = "claude-3-sonnet-20240229" + CLAUDE_3_OPUS = "claude-3-opus-20240229" + CLAUDE_3_HAIKU = "claude-3-haiku-20240307" + + +# Will throw errors if you send a garbage string +def convert_frontend_str_to_llm(frontend_str: str) -> Llm: + if frontend_str == "gpt_4_vision": + return Llm.GPT_4_VISION + elif frontend_str == "claude_3_sonnet": + return Llm.CLAUDE_3_SONNET + else: + return Llm(frontend_str) async def stream_openai_response( - messages, api_key: str, callback: Callable[[str], Awaitable[None]] -): - client = AsyncOpenAI(api_key=api_key) - - model = MODEL_GPT_4_VISION + messages: List[ChatCompletionMessageParam], + api_key: str, + base_url: str | None, + callback: Callable[[str], Awaitable[None]], + model: Llm, +) -> str: + client = AsyncOpenAI(api_key=api_key, base_url=base_url) # Base parameters - params = {"model": model, "messages": messages, "stream": True, "timeout": 600} + params = { + "model": model.value, + "messages": messages, + "stream": True, + "timeout": 600, + "temperature": 0.0, + } - # Add 'max_tokens' only if the model is a GPT4 vision model - if model == MODEL_GPT_4_VISION: + # Add 'max_tokens' only if the model is a GPT4 vision or Turbo model + if ( + model == Llm.GPT_4_VISION + or model == Llm.GPT_4_TURBO_2024_04_09 + or model == Llm.GPT_4O_2024_05_13 + ): params["max_tokens"] = 4096 - params["temperature"] = 0 - completion = await client.chat.completions.create(**params) + stream = await client.chat.completions.create(**params) # type: ignore full_response = "" - async for chunk in completion: + async for chunk in stream: # type: ignore + assert isinstance(chunk, ChatCompletionChunk) content = chunk.choices[0].delta.content or "" full_response += content await callback(content) + await client.close() + return full_response + + +# TODO: Have a seperate function that translates OpenAI messages to Claude messages +async def stream_claude_response( + messages: List[ChatCompletionMessageParam], + api_key: str, + callback: Callable[[str], Awaitable[None]], +) -> str: + + client = AsyncAnthropic(api_key=api_key) + + # Base parameters + model = Llm.CLAUDE_3_SONNET + max_tokens = 4096 + temperature = 0.0 + + # Translate OpenAI messages to Claude messages + system_prompt = cast(str, messages[0].get("content")) + claude_messages = [dict(message) for message in messages[1:]] + for message in claude_messages: + if not isinstance(message["content"], list): + continue + + for content in message["content"]: # type: ignore + if content["type"] == "image_url": + content["type"] = "image" + + # Extract base64 data and media type from data URL + # Example base64 data URL: data:image/png;base64,iVBOR... + image_data_url = cast(str, content["image_url"]["url"]) + media_type = image_data_url.split(";")[0].split(":")[1] + base64_data = image_data_url.split(",")[1] + + # Remove OpenAI parameter + del content["image_url"] + + content["source"] = { + "type": "base64", + "media_type": media_type, + "data": base64_data, + } + + # Stream Claude response + async with client.messages.stream( + model=model.value, + max_tokens=max_tokens, + temperature=temperature, + system=system_prompt, + messages=claude_messages, # type: ignore + ) as stream: + async for text in stream.text_stream: + await callback(text) + + # Return final message + response = await stream.get_final_message() + + # Close the Anthropic client + await client.close() + + return response.content[0].text + + +async def stream_claude_response_native( + system_prompt: str, + messages: list[Any], + api_key: str, + callback: Callable[[str], Awaitable[None]], + include_thinking: bool = False, + model: Llm = Llm.CLAUDE_3_OPUS, +) -> str: + + client = AsyncAnthropic(api_key=api_key) + + # Base model parameters + max_tokens = 4096 + temperature = 0.0 + + # Multi-pass flow + current_pass_num = 1 + max_passes = 2 + + prefix = "" + response = None + + # For debugging + full_stream = "" + debug_file_writer = DebugFileWriter() + + while current_pass_num <= max_passes: + current_pass_num += 1 + + # Set up message depending on whether we have a prefix + messages_to_send = ( + messages + [{"role": "assistant", "content": prefix}] + if include_thinking + else messages + ) + + pprint_prompt(messages_to_send) + + async with client.messages.stream( + model=model.value, + max_tokens=max_tokens, + temperature=temperature, + system=system_prompt, + messages=messages_to_send, # type: ignore + ) as stream: + async for text in stream.text_stream: + print(text, end="", flush=True) + full_stream += text + await callback(text) + + response = await stream.get_final_message() + response_text = response.content[0].text + + # Write each pass's code to .html file and thinking to .txt file + if IS_DEBUG_ENABLED: + debug_file_writer.write_to_file( + f"pass_{current_pass_num - 1}.html", + debug_file_writer.extract_html_content(response_text), + ) + debug_file_writer.write_to_file( + f"thinking_pass_{current_pass_num - 1}.txt", + response_text.split("")[0], + ) + + # Set up messages array for next pass + messages += [ + {"role": "assistant", "content": str(prefix) + response.content[0].text}, + { + "role": "user", + "content": "You've done a good job with a first draft. Improve this further based on the original instructions so that the app is fully functional and looks like the original video of the app we're trying to replicate.", + }, + ] + + print( + f"Token usage: Input Tokens: {response.usage.input_tokens}, Output Tokens: {response.usage.output_tokens}" + ) + + # Close the Anthropic client + await client.close() + + if IS_DEBUG_ENABLED: + debug_file_writer.write_to_file("full_stream.txt", full_stream) + + if not response: + raise Exception("No HTML response found in AI response") + else: + return response.content[0].text diff --git a/backend/main.py b/backend/main.py index 4c5823b..192ef22 100644 --- a/backend/main.py +++ b/backend/main.py @@ -4,23 +4,12 @@ from dotenv import load_dotenv load_dotenv() -import json -import os -import traceback -from datetime import datetime -from fastapi import FastAPI, WebSocket +from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware -from llm import stream_openai_response -from mock import mock_completion -from image_generation import create_alt_url_mapping, generate_images -from prompts import assemble_prompt -from routes import screenshot -from access_token import validate_access_token +from routes import screenshot, generate_code, home, evals app = FastAPI(openapi_url=None, docs_url=None, redoc_url=None) -# Configure CORS - # Configure CORS settings app.add_middleware( CORSMiddleware, @@ -30,150 +19,8 @@ app.add_middleware( allow_headers=["*"], ) - -# Useful for debugging purposes when you don't want to waste GPT4-Vision credits -# Setting to True will stream a mock response instead of calling the OpenAI API -# TODO: Should only be set to true when value is 'True', not any abitrary truthy value -SHOULD_MOCK_AI_RESPONSE = bool(os.environ.get("MOCK", False)) - - +# Add routes +app.include_router(generate_code.router) app.include_router(screenshot.router) - - -def write_logs(prompt_messages, completion): - # Get the logs path from environment, default to the current working directory - logs_path = os.environ.get("LOGS_PATH", os.getcwd()) - - # Create run_logs directory if it doesn't exist within the specified logs path - logs_directory = os.path.join(logs_path, "run_logs") - if not os.path.exists(logs_directory): - os.makedirs(logs_directory) - - print("Writing to logs directory:", logs_directory) - - # Generate a unique filename using the current timestamp within the logs directory - filename = datetime.now().strftime(f"{logs_directory}/messages_%Y%m%d_%H%M%S.json") - - # Write the messages dict into a new file for each run - with open(filename, "w") as f: - f.write(json.dumps({"prompt": prompt_messages, "completion": completion})) - - -@app.websocket("/generate-code") -async def stream_code(websocket: WebSocket): - await websocket.accept() - - print("Incoming websocket connection...") - - params = await websocket.receive_json() - - print("Received params") - - # Read the output settings from the request. Fall back to default if not provided. - output_settings = {"css": "tailwind", "js": "vanilla"} - if params["outputSettings"] and params["outputSettings"]["css"]: - output_settings["css"] = params["outputSettings"]["css"] - if params["outputSettings"] and params["outputSettings"]["js"]: - output_settings["js"] = params["outputSettings"]["js"] - print("Using output settings:", output_settings) - - # Get the OpenAI API key from the request. Fall back to environment variable if not provided. - # If neither is provided, we throw an error. - openai_api_key = None - if "accessCode" in params and params["accessCode"]: - print("Access code - using platform API key") - if await validate_access_token(params["accessCode"]): - openai_api_key = os.environ.get("PLATFORM_OPENAI_API_KEY") - else: - await websocket.send_json( - { - "type": "error", - "value": "Invalid access code or you're out of credits. Please try again.", - } - ) - return - else: - if params["openAiApiKey"]: - openai_api_key = params["openAiApiKey"] - print("Using OpenAI API key from client-side settings dialog") - else: - openai_api_key = os.environ.get("OPENAI_API_KEY") - if openai_api_key: - print("Using OpenAI API key from environment variable") - - if not openai_api_key: - print("OpenAI API key not found") - await websocket.send_json( - { - "type": "error", - "value": "No OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file.", - } - ) - return - - should_generate_images = ( - params["isImageGenerationEnabled"] - if "isImageGenerationEnabled" in params - else True - ) - - print("generating code...") - await websocket.send_json({"type": "status", "value": "Generating code..."}) - - async def process_chunk(content): - await websocket.send_json({"type": "chunk", "value": content}) - - if params.get("resultImage") and params["resultImage"]: - prompt_messages = assemble_prompt( - params["image"], output_settings, params["resultImage"] - ) - else: - prompt_messages = assemble_prompt(params["image"], output_settings) - - # Image cache for updates so that we don't have to regenerate images - image_cache = {} - - if params["generationType"] == "update": - # Transform into message format - # TODO: Move this to frontend - for index, text in enumerate(params["history"]): - prompt_messages += [ - {"role": "assistant" if index % 2 == 0 else "user", "content": text} - ] - - image_cache = create_alt_url_mapping(params["history"][-2]) - - if SHOULD_MOCK_AI_RESPONSE: - completion = await mock_completion(process_chunk) - else: - completion = await stream_openai_response( - prompt_messages, - api_key=openai_api_key, - callback=lambda x: process_chunk(x), - ) - - # Write the messages dict into a log so that we can debug later - write_logs(prompt_messages, completion) - - try: - if should_generate_images: - await websocket.send_json( - {"type": "status", "value": "Generating images..."} - ) - updated_html = await generate_images( - completion, api_key=openai_api_key, image_cache=image_cache - ) - else: - updated_html = completion - await websocket.send_json({"type": "setCode", "value": updated_html}) - await websocket.send_json( - {"type": "status", "value": "Code generation complete."} - ) - except Exception as e: - traceback.print_exc() - print("Image generation failed", e) - await websocket.send_json( - {"type": "status", "value": "Image generation failed but code is complete."} - ) - finally: - await websocket.close() +app.include_router(home.router) +app.include_router(evals.router) diff --git a/backend/mock.py b/backend/mock.py deleted file mode 100644 index 90dc7d3..0000000 --- a/backend/mock.py +++ /dev/null @@ -1,207 +0,0 @@ -import asyncio - - -async def mock_completion(process_chunk): - code_to_return = NO_IMAGES_NYTIMES_MOCK_CODE - - for i in range(0, len(code_to_return), 10): - await process_chunk(code_to_return[i : i + 10]) - await asyncio.sleep(0.01) - - return code_to_return - - -APPLE_MOCK_CODE = """ - - - - Product Showcase - - - - - - - -
-
-
- Brand Logo -

WATCH SERIES 9

-

Smarter. Brighter. Mightier.

- -
-
- Product image of a smartwatch with a pink band and a circular interface displaying various health metrics. - Product image of a smartwatch with a blue band and a square interface showing a classic analog clock face. -
-
-
- -""" - -NYTIMES_MOCK_CODE = """ - - - - - The New York Times - News - - - - - - -
-
-
-
- - -
Tuesday, November 14, 2023
Today's Paper
-
-
- The New York Times Logo -
-
- -
Account
-
-
- -
-
-
-
-
-
-

Israeli Military Raids Gaza’s Largest Hospital

-

Israeli troops have entered the Al-Shifa Hospital complex, where conditions have grown dire and Israel says Hamas fighters are embedded.

- See more updates -
- -
-
-
- Flares and plumes of smoke over the northern Gaza skyline on Tuesday. -

From Elvis to Elopements, the Evolution of the Las Vegas Wedding

-

The glittering city that attracts thousands of couples seeking unconventional nuptials has grown beyond the drive-through wedding.

- 8 MIN READ -
- -
-
-
-
-
- - -""" - -NO_IMAGES_NYTIMES_MOCK_CODE = """ - - - - - The New York Times - News - - - - - - -
-
-
-
- - -
Tuesday, November 14, 2023
Today's Paper
-
-
- -
Account
-
-
- -
-
-
-
-
-
-

Israeli Military Raids Gaza’s Largest Hospital

-

Israeli troops have entered the Al-Shifa Hospital complex, where conditions have grown dire and Israel says Hamas fighters are embedded.

- See more updates -
- -
-
-
-

From Elvis to Elopements, the Evolution of the Las Vegas Wedding

-

The glittering city that attracts thousands of couples seeking unconventional nuptials has grown beyond the drive-through wedding.

- 8 MIN READ -
- -
-
-
-
-
- - -""" diff --git a/backend/mock_llm.py b/backend/mock_llm.py new file mode 100644 index 0000000..b85b1b1 --- /dev/null +++ b/backend/mock_llm.py @@ -0,0 +1,1583 @@ +import asyncio +from typing import Awaitable, Callable + +from custom_types import InputMode + + +STREAM_CHUNK_SIZE = 20 + + +async def mock_completion( + process_chunk: Callable[[str], Awaitable[None]], input_mode: InputMode +) -> str: + code_to_return = ( + TALLY_FORM_VIDEO_PROMPT_MOCK + if input_mode == "video" + else NO_IMAGES_NYTIMES_MOCK_CODE + ) + + for i in range(0, len(code_to_return), STREAM_CHUNK_SIZE): + await process_chunk(code_to_return[i : i + STREAM_CHUNK_SIZE]) + await asyncio.sleep(0.01) + + if input_mode == "video": + # Extract the last block from code_to_return + # because we can have multiple passes + start = code_to_return.rfind("") + len("") + if start != -1 and end != -1: + code_to_return = code_to_return[start:end] + else: + code_to_return = "Error: HTML block not found." + + return code_to_return + + +APPLE_MOCK_CODE = """ + + + + Product Showcase + + + + + + + +
+
+
+ Brand Logo +

WATCH SERIES 9

+

Smarter. Brighter. Mightier.

+ +
+
+ Product image of a smartwatch with a pink band and a circular interface displaying various health metrics. + Product image of a smartwatch with a blue band and a square interface showing a classic analog clock face. +
+
+
+ +""" + +NYTIMES_MOCK_CODE = """ + + + + + The New York Times - News + + + + + + +
+
+
+
+ + +
Tuesday, November 14, 2023
Today's Paper
+
+
+ The New York Times Logo +
+
+ +
Account
+
+
+ +
+
+
+
+
+
+

Israeli Military Raids Gaza’s Largest Hospital

+

Israeli troops have entered the Al-Shifa Hospital complex, where conditions have grown dire and Israel says Hamas fighters are embedded.

+ See more updates +
+ +
+
+
+ Flares and plumes of smoke over the northern Gaza skyline on Tuesday. +

From Elvis to Elopements, the Evolution of the Las Vegas Wedding

+

The glittering city that attracts thousands of couples seeking unconventional nuptials has grown beyond the drive-through wedding.

+ 8 MIN READ +
+ +
+
+
+
+
+ + +""" + +NO_IMAGES_NYTIMES_MOCK_CODE = """ + + + + + The New York Times - News + + + + + + +
+
+
+
+ + +
Tuesday, November 14, 2023
Today's Paper
+
+
+ +
Account
+
+
+ +
+
+
+
+
+
+

Israeli Military Raids Gaza’s Largest Hospital

+

Israeli troops have entered the Al-Shifa Hospital complex, where conditions have grown dire and Israel says Hamas fighters are embedded.

+ See more updates +
+ +
+
+
+

From Elvis to Elopements, the Evolution of the Las Vegas Wedding

+

The glittering city that attracts thousands of couples seeking unconventional nuptials has grown beyond the drive-through wedding.

+ 8 MIN READ +
+ +
+
+
+
+
+ + +""" + +MORTGAGE_CALCULATOR_VIDEO_PROMPT_MOCK = """ + +The user flow in the video seems to be: +1. The calculator starts with some default values for loan amount, loan term, interest rate, etc. +2. The user toggles the "Include taxes & fees" checkbox which shows an explanation tooltip. +3. The user selects different loan terms from the dropdown, which updates the monthly payment amount. +4. The user enters a custom loan amount. +5. The user selects a different loan term (30-yr fixed FHA). +6. The user enters additional details like home price, down payment, state, credit score, property tax, home insurance, and HOA fees. +7. The calculator updates the total monthly payment breakdown. + +To build this: +- Use a container div for the whole calculator +- Have sections for Monthly Payment, Purchase Budget, loan details, additional costs +- Use input fields, dropdowns, and checkboxes for user input +- Update values dynamically using JavaScript when inputs change +- Show/hide explanation tooltip when checkbox is toggled +- Update monthly payment whenever loan amount, interest rate or term is changed +- Allow selecting loan term from a dropdown +- Update total monthly payment breakdown as additional costs are entered +- Style everything to match the screenshots using Tailwind utility classes + + + + + + + + + +
+
Mortgage Calculator
+ +
+
Monthly payment
+
Purchase budget
+
+ +
+ + + + +
+ +
$1,696
+ +
+
Loan amount
+ +
+ +
+
Loan term
+ +
+ +
+
+
Interest
+
7.61 %
+
+ +
+ +
+
+
Home price
+ +
+
+
Down payment
+
+ +
20 %
+
+
+
+ +
+
+
State
+ +
+
+
Credit score
+ +
+
+ +
+
+
Property tax (yearly)
+ +
+
+
Home insurance (yearly)
+ +
+
+
Private mortgage insurance (monthly)
+ +
+
+
Homeowners association (monthly)
+ +
+
+ +
+
+
Total monthly payment
+
$2,036
+
+
+
Loan
+
$1,635
+
Taxes & fees
+
$401
+
+
+ +
+
Disclaimer • Feedback
+
+
+ + + + + + +More things to consider: +- Add validation to input fields to prevent non-numeric values +- Add a reset button to clear all inputs and reset to default values + + + + + + + + + +
+
Mortgage Calculator
+ +
+
Monthly payment
+
Purchase budget
+
+ +
+ + + + +
+ +
$1,696
+ +
+
Loan amount
+ +
+ +
+
Loan term
+ +
+ +
+
+
Interest
+
7.61 %
+
+ +
+ +
+
+
Home price
+ +
+
+
Down payment
+
+ +
20 %
+
+
+
+ +
+
+
State
+ +
+
+
Credit score
+ +
+
+ +
+
+
Property tax (yearly)
+ +
+
+
Home insurance (yearly)
+ +
+
+
Private mortgage insurance (monthly)
+ +
+
+
Homeowners association (monthly)
+ +
+
+ +
+
+
Total monthly payment
+
$2,036
+
+
+
Loan
+
$1,635
+
Taxes & fees
+
$401
+
+
+ +
+
Disclaimer • Feedback
+
+
+ + + + + +""" + +GOOGLE_FORM_VIDEO_PROMPT_MOCK = """ + +User flow: +1. User starts on the Google search page and types in "times" in the search bar +2. As the user types, Google provides autocomplete suggestions related to "times" +3. User selects the "times" suggestion from the autocomplete dropdown +4. The search results page for "times" loads, showing various results related to The New York Times newspaper +5. User clicks the "Generate" button under "Get an AI-powered overview for this search?" +6. An AI-generated overview about The New York Times loads on the right side of the search results + +Code structure: +- HTML structure with header, search bar, autocomplete dropdown, search button +- Search results area to display search results +- Sidebar area to display the AI-generated overview +- Use Tailwind CSS utility classes for styling +- Use jQuery to handle user interactions: + - Typing in search bar to show/filter autocomplete suggestions + - Selecting autocomplete suggestion to populate search bar + - Clicking search button to display search results + - Clicking "Generate" button to display AI overview +- Hardcode search results and AI overview content for demo purposes + + + + + + + + + + +
+
+ Google logo +
+ + +
+ + Profile picture of the user +
+ +
+ + + +
+
+ + + + + + + +To improve the app further and make it fully functional like the original video: + +1. Add more visual styling to match the original, including: + - Google logo and favicon + - User profile icon + - Search bar styling and hover effects + - Search button icon and styling + - Fonts and text sizes + - Spacing and layout adjustments + +2. Implement the search functionality: + - Perform a search when the user presses Enter or clicks the search button + - Show a loading state while searching + - Display the search results dynamically based on the query + - Handle cases when no results are found + +3. Enhance the autocomplete functionality: + - Fetch autocomplete suggestions from a data source based on the user's input + - Highlight the text match in the suggestions + - Allow navigating the suggestions with arrow keys + - Update the search input when a suggestion is selected + +4. Improve the AI-powered overview: + - Fetch the overview content dynamically based on the search query + - Display a loading state while generating the overview + - Enhance the styling of the overview to match the original + +5. Add additional UI elements: + - Header navigation tabs (About, Store, Gmail, Images) + - Footer links (Advertising, Business, How Search works, Privacy, Terms) + - Pagination for search results + - Feedback link + +6. Optimize performance: + - Lazy-load images and content + - Implement caching mechanisms + - Minify and compress assets + +7. Ensure responsiveness: + - Make the layout responsive and mobile-friendly + - Adjust font sizes, padding, and margins for different screen sizes + - Hide/collapse certain elements on smaller screens + +8. Enhance accessibility: + - Add appropriate ARIA attributes + - Ensure keyboard navigation works properly + - Provide alternative text for images + +9. Implement error handling: + - Handle network errors gracefully + - Display user-friendly error messages + - Provide options to retry or refresh + +10. Add smooth animations and transitions: + - Fade in/out effects for loading states + - Smooth sliding animations for autocomplete suggestions + - Subtle hover and click animations for interactive elements + + + + + Google Search + + + + + + + + +
+
+ About + Store + Gmail + Images + Profile picture of the user +
+ +
+
+ Google logo +
+ + +
+
+ + +
+
+
+ + + + + + +
+ + + + + +""" + +TALLY_FORM_VIDEO_PROMPT_MOCK = """ + +The user flow in the video seems to be: +1. The quiz starts by asking "What is the capital of Bulgaria?" with three multiple choice options. The user selects "Sofia". +2. The next question asks "What is the most famous dish in Belgium?" with two image-based options. The user selects "Steak with fries". +3. The next question asks "What is the name of this actor?" with an image of an older man with gray hair and beard. The user types in "Clooney" and submits the answer. +4. The quiz ends showing the user's score of 10/30 and a "Bummer, try again!" message with an arrow to restart. + +To build this as a functional web app: +1. Create the overall page structure with a title, question area, answer options area, and next/submit buttons. +2. Use jQuery to show/hide different questions and update the answer options based on the current question. +3. Store the questions, answer options, and correct answers in a Javascript array. +4. Keep track of the user's selections and compare them to the correct answers to calculate the score. +5. After the last question, show the final score and restart option. +6. Style everything to match the screenshots using Tailwind utility classes. + + + + + + + Online Trivia Quiz + + + + +

Online Trivia Quiz

+ +
+

+
+
+ + + + + + + + + + + + + + +This implements the online trivia quiz app as shown in the video. It uses jQuery to handle the interactions and Tailwind CSS for styling. The questions, options, and answers are defined in the quizData array. The app keeps track of the user's current question and score, allows selecting or entering answers, and displays the final score at the end with the option to restart. + + +To further improve the app to match the original video: + +1. Adjust the styling to more closely match the font, colors, spacing, and layout in the screenshots. +2. Add a cursor pointer when hovering over answer options. +3. Improve the styling of the selected answer option. +4. Make the "Next" button change to an arrow icon on hover. +5. Add a "Back" button to go to the previous question. +6. Show the current question number and total (e.g. "Question 1 of 3"). +7. Animate the transitions between questions. +8. Improve the responsive layout for different screen sizes. +9. Preload the images to avoid flickering. +10. Refactor the code to be more modular and maintainable. + + + + + + + Online Trivia Quiz + + + + + + +

Online Trivia Quiz

+ +
+
+
+ + +
+

+
+
+ + + + + + + + +
+ + + + + + +This updated version makes several improvements to match the original app more closely: + +1. Styling is refined to better match the font, colors, spacing, and layout of the screenshots. +2. Cursor changes to a pointer when hovering over answer options. +3. Selected answer option is highlighted. +4. "Next" button shows an animated arrow on hover. +5. "Back" button added to go to the previous question. +6. Current question number and total are displayed. +7. Fade transitions are added between questions. +8. Layout is made responsive for different screen sizes. +9. Images are preloaded to avoid flickering. +10. Code is refactored to be more modular and maintainable. + +The app should now be fully functional and closely resemble the look and behavior of the original video. Let me know if you would like any further adjustments! +""" diff --git a/backend/poetry.lock b/backend/poetry.lock index 29e7937..d6b5d02 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,63 +1,197 @@ -# This file is automatically @generated by Poetry 1.4.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] -name = "anyio" -version = "3.7.1" -description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" +name = "anthropic" +version = "0.18.1" +description = "The official Python library for the anthropic API" optional = false python-versions = ">=3.7" files = [ - {file = "anyio-3.7.1-py3-none-any.whl", hash = "sha256:91dee416e570e92c64041bd18b900d1d6fa78dff7048769ce5ac5ddad004fbb5"}, - {file = "anyio-3.7.1.tar.gz", hash = "sha256:44a3c9aba0f5defa43261a8b3efb97891f2bd7d804e0e1f56419befa1adfc780"}, + {file = "anthropic-0.18.1-py3-none-any.whl", hash = "sha256:b85aee64f619ce1b1964ba733a09adc4053e7bc4e6d4186001229ec191099dcf"}, + {file = "anthropic-0.18.1.tar.gz", hash = "sha256:f5d1caafd43f6cc933a79753a93531605095f040a384f6a900c3de9c3fb6694e"}, ] [package.dependencies] -exceptiongroup = {version = "*", markers = "python_version < \"3.11\""} -idna = ">=2.8" -sniffio = ">=1.1" +anyio = ">=3.5.0,<5" +distro = ">=1.7.0,<2" +httpx = ">=0.23.0,<1" +pydantic = ">=1.9.0,<3" +sniffio = "*" +tokenizers = ">=0.13.0" +typing-extensions = ">=4.7,<5" [package.extras] -doc = ["Sphinx", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-jquery"] -test = ["anyio[trio]", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] -trio = ["trio (<0.22)"] +bedrock = ["boto3 (>=1.28.57)", "botocore (>=1.31.57)"] +vertex = ["google-auth (>=2,<3)"] + +[[package]] +name = "anyio" +version = "4.3.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.3.0-py3-none-any.whl", hash = "sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8"}, + {file = "anyio-4.3.0.tar.gz", hash = "sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] [[package]] name = "beautifulsoup4" -version = "4.12.2" +version = "4.12.3" description = "Screen-scraping library" -category = "main" optional = false python-versions = ">=3.6.0" files = [ - {file = "beautifulsoup4-4.12.2-py3-none-any.whl", hash = "sha256:bd2520ca0d9d7d12694a53d44ac482d181b4ec1888909b035a3dbf40d0f57d4a"}, - {file = "beautifulsoup4-4.12.2.tar.gz", hash = "sha256:492bbc69dca35d12daac71c4db1bfff0c876c00ef4a2ffacce226d4638eb72da"}, + {file = "beautifulsoup4-4.12.3-py3-none-any.whl", hash = "sha256:b80878c9f40111313e55da8ba20bdba06d8fa3969fc68304167741bbf9e082ed"}, + {file = "beautifulsoup4-4.12.3.tar.gz", hash = "sha256:74e3d1928edc070d21748185c46e3fb33490f22f52a3addee9aee0f4f7781051"}, ] [package.dependencies] soupsieve = ">1.2" [package.extras] +cchardet = ["cchardet"] +chardet = ["chardet"] +charset-normalizer = ["charset-normalizer"] html5lib = ["html5lib"] lxml = ["lxml"] [[package]] name = "certifi" -version = "2023.7.22" +version = "2024.2.2" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, - {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] [[package]] name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -72,7 +206,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -80,28 +213,48 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "decorator" +version = "4.4.2" +description = "Decorators for Humans" +optional = false +python-versions = ">=2.6, !=3.0.*, !=3.1.*" +files = [ + {file = "decorator-4.4.2-py2.py3-none-any.whl", hash = "sha256:41fa54c2a0cc4ba648be4fd43cff00aedf5b9465c9bf18d64325bc225f08f760"}, + {file = "decorator-4.4.2.tar.gz", hash = "sha256:e3a62f0520172440ca0dcc823749319382e377f37f140a0b99ef45fecb84bfe7"}, +] + +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + [[package]] name = "distro" -version = "1.8.0" +version = "1.9.0" description = "Distro - an OS platform information API" -category = "main" optional = false python-versions = ">=3.6" files = [ - {file = "distro-1.8.0-py3-none-any.whl", hash = "sha256:99522ca3e365cac527b44bde033f64c6945d90eb9f769703caaec52b09bbd3ff"}, - {file = "distro-1.8.0.tar.gz", hash = "sha256:02e111d1dc6a50abb8eed6bf31c3e48ed8b0830d1ea2a1b78c61765c2513fdd8"}, + {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, + {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, ] [[package]] name = "exceptiongroup" -version = "1.1.3" +version = "1.2.0" description = "Backport of PEP 654 (exception groups)" -category = "main" optional = false python-versions = ">=3.7" files = [ - {file = "exceptiongroup-1.1.3-py3-none-any.whl", hash = "sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3"}, - {file = "exceptiongroup-1.1.3.tar.gz", hash = "sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9"}, + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, ] [package.extras] @@ -111,7 +264,6 @@ test = ["pytest (>=6)"] name = "fastapi" version = "0.95.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -129,11 +281,61 @@ dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.138)", "uvicorn[standard] (> doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer-cli (>=0.0.13,<0.0.14)", "typer[all] (>=0.6.1,<0.8.0)"] test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==23.1.0)", "coverage[toml] (>=6.5.0,<8.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.7)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.138)", "sqlalchemy (>=1.3.18,<1.4.43)", "types-orjson (==3.6.2)", "types-ujson (==5.7.0.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] +[[package]] +name = "filelock" +version = "3.13.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.13.1-py3-none-any.whl", hash = "sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c"}, + {file = "filelock-3.13.1.tar.gz", hash = "sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.24)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + +[[package]] +name = "fsspec" +version = "2024.2.0" +description = "File-system specification" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fsspec-2024.2.0-py3-none-any.whl", hash = "sha256:817f969556fa5916bc682e02ca2045f96ff7f586d45110fcb76022063ad2c7d8"}, + {file = "fsspec-2024.2.0.tar.gz", hash = "sha256:b6ad1a679f760dda52b1168c859d01b7b80648ea6f7f7c7f5a8a91dc3f3ecb84"}, +] + +[package.extras] +abfs = ["adlfs"] +adl = ["adlfs"] +arrow = ["pyarrow (>=1)"] +dask = ["dask", "distributed"] +devel = ["pytest", "pytest-cov"] +dropbox = ["dropbox", "dropboxdrivefs", "requests"] +full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] +fuse = ["fusepy"] +gcs = ["gcsfs"] +git = ["pygit2"] +github = ["requests"] +gs = ["gcsfs"] +gui = ["panel"] +hdfs = ["pyarrow (>=1)"] +http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] +libarchive = ["libarchive-c"] +oci = ["ocifs"] +s3 = ["s3fs"] +sftp = ["paramiko"] +smb = ["smbprotocol"] +ssh = ["paramiko"] +tqdm = ["tqdm"] + [[package]] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -143,14 +345,13 @@ files = [ [[package]] name = "httpcore" -version = "1.0.2" +version = "1.0.4" description = "A minimal low-level HTTP client." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "httpcore-1.0.2-py3-none-any.whl", hash = "sha256:096cc05bca73b8e459a1fc3dcf585148f63e534eae4339559c9b8a8d6399acc7"}, - {file = "httpcore-1.0.2.tar.gz", hash = "sha256:9fc092e4799b26174648e54b74ed5f683132a464e95643b226e00c2ed2fa6535"}, + {file = "httpcore-1.0.4-py3-none-any.whl", hash = "sha256:ac418c1db41bade2ad53ae2f3834a3a0f5ae76b56cf5aa497d2d033384fc7d73"}, + {file = "httpcore-1.0.4.tar.gz", hash = "sha256:cb2839ccfcba0d2d3c1131d3c3e26dfc327326fbe7a5dc0dbfe9f6c9151bb022"}, ] [package.dependencies] @@ -160,113 +361,459 @@ h11 = ">=0.13,<0.15" [package.extras] asyncio = ["anyio (>=4.0,<5.0)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] -trio = ["trio (>=0.22.0,<0.23.0)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.25.0)"] [[package]] name = "httpx" -version = "0.25.1" +version = "0.25.2" description = "The next generation HTTP client." -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.25.1-py3-none-any.whl", hash = "sha256:fec7d6cc5c27c578a391f7e87b9aa7d3d8fbcd034f6399f9f79b45bcc12a866a"}, - {file = "httpx-0.25.1.tar.gz", hash = "sha256:ffd96d5cf901e63863d9f1b4b6807861dbea4d301613415d9e6e57ead15fc5d0"}, + {file = "httpx-0.25.2-py3-none-any.whl", hash = "sha256:a05d3d052d9b2dfce0e3896636467f8a5342fb2b902c819428e1ac65413ca118"}, + {file = "httpx-0.25.2.tar.gz", hash = "sha256:8b8fcaa0c8ea7b05edd69a094e63a2094c4efcb48129fb757361bc423c0ad9e8"}, ] [package.dependencies] anyio = "*" certifi = "*" -httpcore = "*" +httpcore = "==1.*" idna = "*" sniffio = "*" [package.extras] brotli = ["brotli", "brotlicffi"] -cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<14)"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] -socks = ["socksio (>=1.0.0,<2.0.0)"] +socks = ["socksio (==1.*)"] + +[[package]] +name = "huggingface-hub" +version = "0.21.4" +description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "huggingface_hub-0.21.4-py3-none-any.whl", hash = "sha256:df37c2c37fc6c82163cdd8a67ede261687d80d1e262526d6c0ce73b6b3630a7b"}, + {file = "huggingface_hub-0.21.4.tar.gz", hash = "sha256:e1f4968c93726565a80edf6dc309763c7b546d0cfe79aa221206034d50155531"}, +] + +[package.dependencies] +filelock = "*" +fsspec = ">=2023.5.0" +packaging = ">=20.9" +pyyaml = ">=5.1" +requests = "*" +tqdm = ">=4.42.1" +typing-extensions = ">=3.7.4.3" + +[package.extras] +all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +cli = ["InquirerPy (==0.3.4)"] +dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] +fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] +hf-transfer = ["hf-transfer (>=0.1.4)"] +inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"] +quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"] +tensorflow = ["graphviz", "pydot", "tensorflow"] +testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] +torch = ["safetensors", "torch"] +typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] + +[[package]] +name = "identify" +version = "2.5.35" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"}, + {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"}, +] + +[package.extras] +license = ["ukkonen"] [[package]] name = "idna" -version = "3.4" +version = "3.6" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[package]] +name = "imageio" +version = "2.34.0" +description = "Library for reading and writing a wide range of image, video, scientific, and volumetric data formats." +optional = false +python-versions = ">=3.8" +files = [ + {file = "imageio-2.34.0-py3-none-any.whl", hash = "sha256:08082bf47ccb54843d9c73fe9fc8f3a88c72452ab676b58aca74f36167e8ccba"}, + {file = "imageio-2.34.0.tar.gz", hash = "sha256:ae9732e10acf807a22c389aef193f42215718e16bd06eed0c5bb57e1034a4d53"}, +] + +[package.dependencies] +numpy = "*" +pillow = ">=8.3.2" + +[package.extras] +all-plugins = ["astropy", "av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"] +all-plugins-pypy = ["av", "imageio-ffmpeg", "pillow-heif", "psutil", "tifffile"] +build = ["wheel"] +dev = ["black", "flake8", "fsspec[github]", "pytest", "pytest-cov"] +docs = ["numpydoc", "pydata-sphinx-theme", "sphinx (<6)"] +ffmpeg = ["imageio-ffmpeg", "psutil"] +fits = ["astropy"] +full = ["astropy", "av", "black", "flake8", "fsspec[github]", "gdal", "imageio-ffmpeg", "itk", "numpydoc", "pillow-heif", "psutil", "pydata-sphinx-theme", "pytest", "pytest-cov", "sphinx (<6)", "tifffile", "wheel"] +gdal = ["gdal"] +itk = ["itk"] +linting = ["black", "flake8"] +pillow-heif = ["pillow-heif"] +pyav = ["av"] +test = ["fsspec[github]", "pytest", "pytest-cov"] +tifffile = ["tifffile"] + +[[package]] +name = "imageio-ffmpeg" +version = "0.4.9" +description = "FFMPEG wrapper for Python" +optional = false +python-versions = ">=3.5" +files = [ + {file = "imageio-ffmpeg-0.4.9.tar.gz", hash = "sha256:39bcd1660118ef360fa4047456501071364661aa9d9021d3d26c58f1ee2081f5"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-macosx_10_9_intel.macosx_10_9_x86_64.macosx_10_10_intel.macosx_10_10_x86_64.whl", hash = "sha256:24095e882a126a0d217197b86265f821b4bb3cf9004104f67c1384a2b4b49168"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-manylinux2010_x86_64.whl", hash = "sha256:2996c64af3e5489227096580269317719ea1a8121d207f2e28d6c24ebc4a253e"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7eead662d2f46d748c0ab446b68f423eb63d2b54d0a8ef96f80607245540866d"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-win32.whl", hash = "sha256:b6de1e18911687c538d5585d8287ab1a23624ca9dc2044fcc4607de667bcf11e"}, + {file = "imageio_ffmpeg-0.4.9-py3-none-win_amd64.whl", hash = "sha256:7e900c695c6541b1cb17feb1baacd4009b30a53a45b81c23d53a67ab13ffb766"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "moviepy" +version = "1.0.3" +description = "Video editing with Python" +optional = false +python-versions = "*" +files = [ + {file = "moviepy-1.0.3.tar.gz", hash = "sha256:2884e35d1788077db3ff89e763c5ba7bfddbd7ae9108c9bc809e7ba58fa433f5"}, +] + +[package.dependencies] +decorator = ">=4.0.2,<5.0" +imageio = {version = ">=2.5,<3.0", markers = "python_version >= \"3.4\""} +imageio_ffmpeg = {version = ">=0.2.0", markers = "python_version >= \"3.4\""} +numpy = {version = ">=1.17.3", markers = "python_version > \"2.7\""} +proglog = "<=1.0.0" +requests = ">=2.8.1,<3.0" +tqdm = ">=4.11.2,<5.0" + +[package.extras] +doc = ["Sphinx (>=1.5.2,<2.0)", "numpydoc (>=0.6.0,<1.0)", "pygame (>=1.9.3,<2.0)", "sphinx_rtd_theme (>=0.1.10b0,<1.0)"] +optional = ["matplotlib (>=2.0.0,<3.0)", "opencv-python (>=3.0,<4.0)", "scikit-image (>=0.13.0,<1.0)", "scikit-learn", "scipy (>=0.19.0,<1.5)", "youtube_dl"] +test = ["coverage (<5.0)", "coveralls (>=1.1,<2.0)", "pytest (>=3.0.0,<4.0)", "pytest-cov (>=2.5.1,<3.0)", "requests (>=2.8.1,<3.0)"] + +[[package]] +name = "nodeenv" +version = "1.8.0" +description = "Node.js virtual environment builder" +optional = false +python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" +files = [ + {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"}, + {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"}, +] + +[package.dependencies] +setuptools = "*" + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, ] [[package]] name = "openai" -version = "1.2.4" +version = "1.14.0" description = "The official Python library for the openai API" -category = "main" optional = false python-versions = ">=3.7.1" files = [ - {file = "openai-1.2.4-py3-none-any.whl", hash = "sha256:53927a2ca276eec0a0efdc1ae829f74a51f49b7d3e14cc6f820aeafb0abfd802"}, - {file = "openai-1.2.4.tar.gz", hash = "sha256:d99a474049376be431d9b4dec3a5c895dd76e19165748c5944e80b7905d1b1ff"}, + {file = "openai-1.14.0-py3-none-any.whl", hash = "sha256:5c9fd3a59f5cbdb4020733ddf79a22f6b7a36d561968cb3f3dd255cdd263d9fe"}, + {file = "openai-1.14.0.tar.gz", hash = "sha256:e287057adf0ec3315abc32ddcc968d095879abd9b68bf51c0402dab13ab5ae9b"}, ] [package.dependencies] -anyio = ">=3.5.0,<4" +anyio = ">=3.5.0,<5" distro = ">=1.7.0,<2" httpx = ">=0.23.0,<1" pydantic = ">=1.9.0,<3" +sniffio = "*" tqdm = ">4" -typing-extensions = ">=4.5,<5" +typing-extensions = ">=4.7,<5" [package.extras] datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] [[package]] -name = "pydantic" -version = "1.10.13" -description = "Data validation and settings management using python type hints" -category = "main" +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.13-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:efff03cc7a4f29d9009d1c96ceb1e7a70a65cfe86e89d34e4a5f2ab1e5693737"}, - {file = "pydantic-1.10.13-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3ecea2b9d80e5333303eeb77e180b90e95eea8f765d08c3d278cd56b00345d01"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1740068fd8e2ef6eb27a20e5651df000978edce6da6803c2bef0bc74540f9548"}, - {file = "pydantic-1.10.13-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84bafe2e60b5e78bc64a2941b4c071a4b7404c5c907f5f5a99b0139781e69ed8"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc0898c12f8e9c97f6cd44c0ed70d55749eaf783716896960b4ecce2edfd2d69"}, - {file = "pydantic-1.10.13-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:654db58ae399fe6434e55325a2c3e959836bd17a6f6a0b6ca8107ea0571d2e17"}, - {file = "pydantic-1.10.13-cp310-cp310-win_amd64.whl", hash = "sha256:75ac15385a3534d887a99c713aa3da88a30fbd6204a5cd0dc4dab3d770b9bd2f"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c553f6a156deb868ba38a23cf0df886c63492e9257f60a79c0fd8e7173537653"}, - {file = "pydantic-1.10.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5e08865bc6464df8c7d61439ef4439829e3ab62ab1669cddea8dd00cd74b9ffe"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e31647d85a2013d926ce60b84f9dd5300d44535a9941fe825dc349ae1f760df9"}, - {file = "pydantic-1.10.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:210ce042e8f6f7c01168b2d84d4c9eb2b009fe7bf572c2266e235edf14bacd80"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8ae5dd6b721459bfa30805f4c25880e0dd78fc5b5879f9f7a692196ddcb5a580"}, - {file = "pydantic-1.10.13-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f8e81fc5fb17dae698f52bdd1c4f18b6ca674d7068242b2aff075f588301bbb0"}, - {file = "pydantic-1.10.13-cp311-cp311-win_amd64.whl", hash = "sha256:61d9dce220447fb74f45e73d7ff3b530e25db30192ad8d425166d43c5deb6df0"}, - {file = "pydantic-1.10.13-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4b03e42ec20286f052490423682016fd80fda830d8e4119f8ab13ec7464c0132"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f59ef915cac80275245824e9d771ee939133be38215555e9dc90c6cb148aaeb5"}, - {file = "pydantic-1.10.13-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a1f9f747851338933942db7af7b6ee8268568ef2ed86c4185c6ef4402e80ba8"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:97cce3ae7341f7620a0ba5ef6cf043975cd9d2b81f3aa5f4ea37928269bc1b87"}, - {file = "pydantic-1.10.13-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:854223752ba81e3abf663d685f105c64150873cc6f5d0c01d3e3220bcff7d36f"}, - {file = "pydantic-1.10.13-cp37-cp37m-win_amd64.whl", hash = "sha256:b97c1fac8c49be29486df85968682b0afa77e1b809aff74b83081cc115e52f33"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c958d053453a1c4b1c2062b05cd42d9d5c8eb67537b8d5a7e3c3032943ecd261"}, - {file = "pydantic-1.10.13-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c5370a7edaac06daee3af1c8b1192e305bc102abcbf2a92374b5bc793818599"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d6f6e7305244bddb4414ba7094ce910560c907bdfa3501e9db1a7fd7eaea127"}, - {file = "pydantic-1.10.13-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d3a3c792a58e1622667a2837512099eac62490cdfd63bd407993aaf200a4cf1f"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c636925f38b8db208e09d344c7aa4f29a86bb9947495dd6b6d376ad10334fb78"}, - {file = "pydantic-1.10.13-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:678bcf5591b63cc917100dc50ab6caebe597ac67e8c9ccb75e698f66038ea953"}, - {file = "pydantic-1.10.13-cp38-cp38-win_amd64.whl", hash = "sha256:6cf25c1a65c27923a17b3da28a0bdb99f62ee04230c931d83e888012851f4e7f"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8ef467901d7a41fa0ca6db9ae3ec0021e3f657ce2c208e98cd511f3161c762c6"}, - {file = "pydantic-1.10.13-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:968ac42970f57b8344ee08837b62f6ee6f53c33f603547a55571c954a4225691"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9849f031cf8a2f0a928fe885e5a04b08006d6d41876b8bbd2fc68a18f9f2e3fd"}, - {file = "pydantic-1.10.13-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:56e3ff861c3b9c6857579de282ce8baabf443f42ffba355bf070770ed63e11e1"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f00790179497767aae6bcdc36355792c79e7bbb20b145ff449700eb076c5f96"}, - {file = "pydantic-1.10.13-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:75b297827b59bc229cac1a23a2f7a4ac0031068e5be0ce385be1462e7e17a35d"}, - {file = "pydantic-1.10.13-cp39-cp39-win_amd64.whl", hash = "sha256:e70ca129d2053fb8b728ee7d1af8e553a928d7e301a311094b8a0501adc8763d"}, - {file = "pydantic-1.10.13-py3-none-any.whl", hash = "sha256:b87326822e71bd5f313e7d3bfdc77ac3247035ac10b0c0618bd99dcf95b1e687"}, - {file = "pydantic-1.10.13.tar.gz", hash = "sha256:32c8b48dcd3b2ac4e78b0ba4af3a2c2eb6048cb75202f0ea7b34feb740efc340"}, + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pillow" +version = "10.2.0" +description = "Python Imaging Library (Fork)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"}, + {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"}, + {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"}, + {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"}, + {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"}, + {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"}, + {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"}, + {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"}, + {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"}, + {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"}, + {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"}, + {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"}, + {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"}, + {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"}, + {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"}, + {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"}, + {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"}, + {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"}, + {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"}, + {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"}, + {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"}, + {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"}, + {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"}, + {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"}, + {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"}, + {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"}, + {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"}, + {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"}, + {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"}, + {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"}, + {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"}, + {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"}, + {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"}, + {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"}, +] + +[package.extras] +docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"] +fpx = ["olefile"] +mic = ["olefile"] +tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"] +typing = ["typing-extensions"] +xmp = ["defusedxml"] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "3.6.2" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.6.2-py2.py3-none-any.whl", hash = "sha256:ba637c2d7a670c10daedc059f5c49b5bd0aadbccfcd7ec15592cf9665117532c"}, + {file = "pre_commit-3.6.2.tar.gz", hash = "sha256:c3ef34f463045c88658c5b99f38c1e297abdcc0ff13f98d3370055fbbfabc67e"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "proglog" +version = "0.1.10" +description = "Log and progress bar manager for console, notebooks, web..." +optional = false +python-versions = "*" +files = [ + {file = "proglog-0.1.10-py3-none-any.whl", hash = "sha256:19d5da037e8c813da480b741e3fa71fb1ac0a5b02bf21c41577c7f327485ec50"}, + {file = "proglog-0.1.10.tar.gz", hash = "sha256:658c28c9c82e4caeb2f25f488fff9ceace22f8d69b15d0c1c86d64275e4ddab4"}, +] + +[package.dependencies] +tqdm = "*" + +[[package]] +name = "pydantic" +version = "1.10.14" +description = "Data validation and settings management using python type hints" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"}, + {file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"}, + {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"}, + {file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"}, + {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"}, + {file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"}, + {file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"}, + {file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"}, + {file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"}, + {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"}, + {file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"}, + {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"}, + {file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"}, + {file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"}, + {file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"}, + {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"}, + {file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"}, + {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"}, + {file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"}, + {file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"}, + {file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"}, + {file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"}, + {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"}, + {file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"}, + {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"}, + {file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"}, + {file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"}, + {file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"}, + {file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"}, + {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"}, + {file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"}, + {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"}, + {file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"}, + {file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"}, + {file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"}, + {file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"}, ] [package.dependencies] @@ -276,38 +823,171 @@ typing-extensions = ">=4.2.0" dotenv = ["python-dotenv (>=0.10.4)"] email = ["email-validator (>=1.0.3)"] +[[package]] +name = "pyright" +version = "1.1.354" +description = "Command line wrapper for pyright" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pyright-1.1.354-py3-none-any.whl", hash = "sha256:f28d61ae8ae035fc52ded1070e8d9e786051a26a4127bbd7a4ba0399b81b37b5"}, + {file = "pyright-1.1.354.tar.gz", hash = "sha256:b1070dc774ff2e79eb0523fe87f4ba9a90550de7e4b030a2bc9e031864029a1f"}, +] + +[package.dependencies] +nodeenv = ">=1.6.0" + +[package.extras] +all = ["twine (>=3.4.1)"] +dev = ["twine (>=3.4.1)"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "python-dotenv" -version = "1.0.0" +version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "python-dotenv-1.0.0.tar.gz", hash = "sha256:a8df96034aae6d2d50a4ebe8216326c61c3eb64836776504fcca410e5937a3ba"}, - {file = "python_dotenv-1.0.0-py3-none-any.whl", hash = "sha256:f5971a9226b701070a4bf2c38c89e5a3f0d64de8debda981d1db98583009122a"}, + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, ] [package.extras] cli = ["click (>=5.0)"] [[package]] -name = "sniffio" -version = "1.3.0" -description = "Sniff out which async library your code is running under" -category = "main" +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." optional = false python-versions = ">=3.7" files = [ - {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, - {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "setuptools" +version = "69.2.0" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"}, + {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, ] [[package]] name = "soupsieve" version = "2.5" description = "A modern CSS selector implementation for Beautiful Soup." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -319,7 +999,6 @@ files = [ name = "starlette" version = "0.27.0" description = "The little ASGI library that shines." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -334,15 +1013,152 @@ anyio = ">=3.4.0,<5" full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] [[package]] -name = "tqdm" -version = "4.66.1" -description = "Fast, Extensible Progress Meter" -category = "main" +name = "tokenizers" +version = "0.15.2" +description = "" optional = false python-versions = ">=3.7" files = [ - {file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, - {file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, + {file = "tokenizers-0.15.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:52f6130c9cbf70544287575a985bf44ae1bda2da7e8c24e97716080593638012"}, + {file = "tokenizers-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:054c1cc9c6d68f7ffa4e810b3d5131e0ba511b6e4be34157aa08ee54c2f8d9ee"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9b9b070fdad06e347563b88c278995735292ded1132f8657084989a4c84a6d5"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea621a7eef4b70e1f7a4e84dd989ae3f0eeb50fc8690254eacc08acb623e82f1"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf7fd9a5141634fa3aa8d6b7be362e6ae1b4cda60da81388fa533e0b552c98fd"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44f2a832cd0825295f7179eaf173381dc45230f9227ec4b44378322d900447c9"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b9ec69247a23747669ec4b0ca10f8e3dfb3545d550258129bd62291aabe8605"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b6a4c78da863ff26dbd5ad9a8ecc33d8a8d97b535172601cf00aee9d7ce9ce"}, + {file = "tokenizers-0.15.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5ab2a4d21dcf76af60e05af8063138849eb1d6553a0d059f6534357bce8ba364"}, + {file = "tokenizers-0.15.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a47acfac7e511f6bbfcf2d3fb8c26979c780a91e06fb5b9a43831b2c0153d024"}, + {file = "tokenizers-0.15.2-cp310-none-win32.whl", hash = "sha256:064ff87bb6acdbd693666de9a4b692add41308a2c0ec0770d6385737117215f2"}, + {file = "tokenizers-0.15.2-cp310-none-win_amd64.whl", hash = "sha256:3b919afe4df7eb6ac7cafd2bd14fb507d3f408db7a68c43117f579c984a73843"}, + {file = "tokenizers-0.15.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:89cd1cb93e4b12ff39bb2d626ad77e35209de9309a71e4d3d4672667b4b256e7"}, + {file = "tokenizers-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfed5c64e5be23d7ee0f0e98081a25c2a46b0b77ce99a4f0605b1ec43dd481fa"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a907d76dcfda37023ba203ab4ceeb21bc5683436ebefbd895a0841fd52f6f6f2"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20ea60479de6fc7b8ae756b4b097572372d7e4032e2521c1bbf3d90c90a99ff0"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48e2b9335be2bc0171df9281385c2ed06a15f5cf121c44094338306ab7b33f2c"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:112a1dd436d2cc06e6ffdc0b06d55ac019a35a63afd26475205cb4b1bf0bfbff"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4620cca5c2817177ee8706f860364cc3a8845bc1e291aaf661fb899e5d1c45b0"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccd73a82751c523b3fc31ff8194702e4af4db21dc20e55b30ecc2079c5d43cb7"}, + {file = "tokenizers-0.15.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:107089f135b4ae7817affe6264f8c7a5c5b4fd9a90f9439ed495f54fcea56fb4"}, + {file = "tokenizers-0.15.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0ff110ecc57b7aa4a594396525a3451ad70988e517237fe91c540997c4e50e29"}, + {file = "tokenizers-0.15.2-cp311-none-win32.whl", hash = "sha256:6d76f00f5c32da36c61f41c58346a4fa7f0a61be02f4301fd30ad59834977cc3"}, + {file = "tokenizers-0.15.2-cp311-none-win_amd64.whl", hash = "sha256:cc90102ed17271cf0a1262babe5939e0134b3890345d11a19c3145184b706055"}, + {file = "tokenizers-0.15.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f86593c18d2e6248e72fb91c77d413a815153b8ea4e31f7cd443bdf28e467670"}, + {file = "tokenizers-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0774bccc6608eca23eb9d620196687c8b2360624619623cf4ba9dc9bd53e8b51"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0222c5b7c9b26c0b4822a82f6a7011de0a9d3060e1da176f66274b70f846b98"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3835738be1de66624fff2f4f6f6684775da4e9c00bde053be7564cbf3545cc66"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0143e7d9dcd811855c1ce1ab9bf5d96d29bf5e528fd6c7824d0465741e8c10fd"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db35825f6d54215f6b6009a7ff3eedee0848c99a6271c870d2826fbbedf31a38"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f5e64b0389a2be47091d8cc53c87859783b837ea1a06edd9d8e04004df55a5c"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e0480c452217edd35eca56fafe2029fb4d368b7c0475f8dfa3c5c9c400a7456"}, + {file = "tokenizers-0.15.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a33ab881c8fe70474980577e033d0bc9a27b7ab8272896e500708b212995d834"}, + {file = "tokenizers-0.15.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a308a607ca9de2c64c1b9ba79ec9a403969715a1b8ba5f998a676826f1a7039d"}, + {file = "tokenizers-0.15.2-cp312-none-win32.whl", hash = "sha256:b8fcfa81bcb9447df582c5bc96a031e6df4da2a774b8080d4f02c0c16b42be0b"}, + {file = "tokenizers-0.15.2-cp312-none-win_amd64.whl", hash = "sha256:38d7ab43c6825abfc0b661d95f39c7f8af2449364f01d331f3b51c94dcff7221"}, + {file = "tokenizers-0.15.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:38bfb0204ff3246ca4d5e726e8cc8403bfc931090151e6eede54d0e0cf162ef0"}, + {file = "tokenizers-0.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c861d35e8286a53e06e9e28d030b5a05bcbf5ac9d7229e561e53c352a85b1fc"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:936bf3842db5b2048eaa53dade907b1160f318e7c90c74bfab86f1e47720bdd6"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:620beacc3373277700d0e27718aa8b25f7b383eb8001fba94ee00aeea1459d89"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2735ecbbf37e52db4ea970e539fd2d450d213517b77745114f92867f3fc246eb"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:473c83c5e2359bb81b0b6fde870b41b2764fcdd36d997485e07e72cc3a62264a"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968fa1fb3c27398b28a4eca1cbd1e19355c4d3a6007f7398d48826bbe3a0f728"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:865c60ae6eaebdde7da66191ee9b7db52e542ed8ee9d2c653b6d190a9351b980"}, + {file = "tokenizers-0.15.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7c0d8b52664ab2d4a8d6686eb5effc68b78608a9008f086a122a7b2996befbab"}, + {file = "tokenizers-0.15.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f33dfbdec3784093a9aebb3680d1f91336c56d86cc70ddf88708251da1fe9064"}, + {file = "tokenizers-0.15.2-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d44ba80988ff9424e33e0a49445072ac7029d8c0e1601ad25a0ca5f41ed0c1d6"}, + {file = "tokenizers-0.15.2-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:dce74266919b892f82b1b86025a613956ea0ea62a4843d4c4237be2c5498ed3a"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0ef06b9707baeb98b316577acb04f4852239d856b93e9ec3a299622f6084e4be"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73e2e74bbb07910da0d37c326869f34113137b23eadad3fc00856e6b3d9930c"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eeb12daf02a59e29f578a865f55d87cd103ce62bd8a3a5874f8fdeaa82e336b"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ba9f6895af58487ca4f54e8a664a322f16c26bbb442effd01087eba391a719e"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccec77aa7150e38eec6878a493bf8c263ff1fa8a62404e16c6203c64c1f16a26"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f40604f5042ff210ba82743dda2b6aa3e55aa12df4e9f2378ee01a17e2855e"}, + {file = "tokenizers-0.15.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5645938a42d78c4885086767c70923abad047163d809c16da75d6b290cb30bbe"}, + {file = "tokenizers-0.15.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:05a77cbfebe28a61ab5c3891f9939cc24798b63fa236d84e5f29f3a85a200c00"}, + {file = "tokenizers-0.15.2-cp37-none-win32.whl", hash = "sha256:361abdc068e8afe9c5b818769a48624687fb6aaed49636ee39bec4e95e1a215b"}, + {file = "tokenizers-0.15.2-cp37-none-win_amd64.whl", hash = "sha256:7ef789f83eb0f9baeb4d09a86cd639c0a5518528f9992f38b28e819df397eb06"}, + {file = "tokenizers-0.15.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4fe1f74a902bee74a3b25aff180fbfbf4f8b444ab37c4d496af7afd13a784ed2"}, + {file = "tokenizers-0.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c4b89038a684f40a6b15d6b09f49650ac64d951ad0f2a3ea9169687bbf2a8ba"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d05a1b06f986d41aed5f2de464c003004b2df8aaf66f2b7628254bcbfb72a438"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508711a108684111ec8af89d3a9e9e08755247eda27d0ba5e3c50e9da1600f6d"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:daa348f02d15160cb35439098ac96e3a53bacf35885072611cd9e5be7d333daa"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494fdbe5932d3416de2a85fc2470b797e6f3226c12845cadf054dd906afd0442"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2d60f5246f4da9373f75ff18d64c69cbf60c3bca597290cea01059c336d2470"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93268e788825f52de4c7bdcb6ebc1fcd4a5442c02e730faa9b6b08f23ead0e24"}, + {file = "tokenizers-0.15.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6fc7083ab404019fc9acafe78662c192673c1e696bd598d16dc005bd663a5cf9"}, + {file = "tokenizers-0.15.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41e39b41e5531d6b2122a77532dbea60e171ef87a3820b5a3888daa847df4153"}, + {file = "tokenizers-0.15.2-cp38-none-win32.whl", hash = "sha256:06cd0487b1cbfabefb2cc52fbd6b1f8d4c37799bd6c6e1641281adaa6b2504a7"}, + {file = "tokenizers-0.15.2-cp38-none-win_amd64.whl", hash = "sha256:5179c271aa5de9c71712e31cb5a79e436ecd0d7532a408fa42a8dbfa4bc23fd9"}, + {file = "tokenizers-0.15.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82f8652a74cc107052328b87ea8b34291c0f55b96d8fb261b3880216a9f9e48e"}, + {file = "tokenizers-0.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:02458bee6f5f3139f1ebbb6d042b283af712c0981f5bc50edf771d6b762d5e4f"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c9a09cd26cca2e1c349f91aa665309ddb48d71636370749414fbf67bc83c5343"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:158be8ea8554e5ed69acc1ce3fbb23a06060bd4bbb09029431ad6b9a466a7121"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ddba9a2b0c8c81633eca0bb2e1aa5b3a15362b1277f1ae64176d0f6eba78ab1"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ef5dd1d39797044642dbe53eb2bc56435308432e9c7907728da74c69ee2adca"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:454c203164e07a860dbeb3b1f4a733be52b0edbb4dd2e5bd75023ffa8b49403a"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cf6b7f1d4dc59af960e6ffdc4faffe6460bbfa8dce27a58bf75755ffdb2526d"}, + {file = "tokenizers-0.15.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2ef09bbc16519f6c25d0c7fc0c6a33a6f62923e263c9d7cca4e58b8c61572afb"}, + {file = "tokenizers-0.15.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c9a2ebdd2ad4ec7a68e7615086e633857c85e2f18025bd05d2a4399e6c5f7169"}, + {file = "tokenizers-0.15.2-cp39-none-win32.whl", hash = "sha256:918fbb0eab96fe08e72a8c2b5461e9cce95585d82a58688e7f01c2bd546c79d0"}, + {file = "tokenizers-0.15.2-cp39-none-win_amd64.whl", hash = "sha256:524e60da0135e106b254bd71f0659be9f89d83f006ea9093ce4d1fab498c6d0d"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9b648a58281c4672212fab04e60648fde574877d0139cd4b4f93fe28ca8944"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7c7d18b733be6bbca8a55084027f7be428c947ddf871c500ee603e375013ffba"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:13ca3611de8d9ddfbc4dc39ef54ab1d2d4aaa114ac8727dfdc6a6ec4be017378"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:237d1bf3361cf2e6463e6c140628e6406766e8b27274f5fcc62c747ae3c6f094"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67a0fe1e49e60c664915e9fb6b0cb19bac082ab1f309188230e4b2920230edb3"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4e022fe65e99230b8fd89ebdfea138c24421f91c1a4f4781a8f5016fd5cdfb4d"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d857be2df69763362ac699f8b251a8cd3fac9d21893de129bc788f8baaef2693"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:708bb3e4283177236309e698da5fcd0879ce8fd37457d7c266d16b550bcbbd18"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64c35e09e9899b72a76e762f9854e8750213f67567787d45f37ce06daf57ca78"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1257f4394be0d3b00de8c9e840ca5601d0a4a8438361ce9c2b05c7d25f6057b"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02272fe48280e0293a04245ca5d919b2c94a48b408b55e858feae9618138aeda"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dc3ad9ebc76eabe8b1d7c04d38be884b8f9d60c0cdc09b0aa4e3bcf746de0388"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:32e16bdeffa7c4f46bf2152172ca511808b952701d13e7c18833c0b73cb5c23f"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fb16ba563d59003028b678d2361a27f7e4ae0ab29c7a80690efa20d829c81fdb"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:2277c36d2d6cdb7876c274547921a42425b6810d38354327dd65a8009acf870c"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1cf75d32e8d250781940d07f7eece253f2fe9ecdb1dc7ba6e3833fa17b82fcbc"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1b3b31884dc8e9b21508bb76da80ebf7308fdb947a17affce815665d5c4d028"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10122d8d8e30afb43bb1fe21a3619f62c3e2574bff2699cf8af8b0b6c5dc4a3"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d88b96ff0fe8e91f6ef01ba50b0d71db5017fa4e3b1d99681cec89a85faf7bf7"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:37aaec5a52e959892870a7c47cef80c53797c0db9149d458460f4f31e2fb250e"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2ea752f2b0fe96eb6e2f3adbbf4d72aaa1272079b0dfa1145507bd6a5d537e6"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b19a808d8799fda23504a5cd31d2f58e6f52f140380082b352f877017d6342b"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64c86e5e068ac8b19204419ed8ca90f9d25db20578f5881e337d203b314f4104"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de19c4dc503c612847edf833c82e9f73cd79926a384af9d801dcf93f110cea4e"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea09acd2fe3324174063d61ad620dec3bcf042b495515f27f638270a7d466e8b"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cf27fd43472e07b57cf420eee1e814549203d56de00b5af8659cb99885472f1f"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7ca22bd897537a0080521445d91a58886c8c04084a6a19e6c78c586e0cfa92a5"}, + {file = "tokenizers-0.15.2.tar.gz", hash = "sha256:e6e9c6e019dd5484be5beafc775ae6c925f4c69a3487040ed09b45e13df2cb91"}, +] + +[package.dependencies] +huggingface_hub = ">=0.16.4,<1.0" + +[package.extras] +dev = ["tokenizers[testing]"] +docs = ["setuptools_rust", "sphinx", "sphinx_rtd_theme"] +testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests"] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "tqdm" +version = "4.66.2" +description = "Fast, Extensible Progress Meter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tqdm-4.66.2-py3-none-any.whl", hash = "sha256:1ee4f8a893eb9bef51c6e35730cebf234d5d0b6bd112b0271e10ed7c24a02bd9"}, + {file = "tqdm-4.66.2.tar.gz", hash = "sha256:6cd52cdf0fef0e0f543299cfc96fec90d7b8a7e88745f411ec33eb44d5ed3531"}, ] [package.dependencies] @@ -356,26 +1172,41 @@ telegram = ["requests"] [[package]] name = "typing-extensions" -version = "4.8.0" +version = "4.10.0" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "typing_extensions-4.8.0-py3-none-any.whl", hash = "sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0"}, - {file = "typing_extensions-4.8.0.tar.gz", hash = "sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef"}, + {file = "typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475"}, + {file = "typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb"}, ] [[package]] -name = "uvicorn" -version = "0.24.0.post1" -description = "The lightning-fast ASGI server." -category = "main" +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"}, - {file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"}, + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.25.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.25.0-py3-none-any.whl", hash = "sha256:ce107f5d9bd02b4636001a77a4e74aab5e1e2b146868ebbad565237145af444c"}, + {file = "uvicorn-0.25.0.tar.gz", hash = "sha256:6dddbad1d7ee0f5140aba5ec138ddc9612c5109399903828b4874c9937f009c2"}, ] [package.dependencies] @@ -386,11 +1217,30 @@ typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} [package.extras] standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "virtualenv" +version = "20.25.1" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"}, + {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "websockets" version = "12.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -471,4 +1321,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "b8d248a44a5eea9638a7726096de77d7a9aa8c00673da806534da2c228ffabb4" +content-hash = "274fed55cf4a2f4e9954b3d196c103b72225409c6050759a939f0ca197ae3f79" diff --git a/backend/prompts.py b/backend/prompts.py deleted file mode 100644 index f01eb7e..0000000 --- a/backend/prompts.py +++ /dev/null @@ -1,127 +0,0 @@ -TAILWIND_SYSTEM_PROMPT = """ -You are an expert Tailwind developer -You take screenshots of a reference web page from the user, and then build single page apps -using Tailwind, HTML and JS. -You might also be given a screenshot(The second image) of a web page that you have already built, and asked to -update it to look more like the reference image(The first image). - -- Make sure the app looks exactly like the screenshot. -- Pay close attention to background color, text color, font size, font family, -padding, margin, border, etc. Match the colors and sizes exactly. -- Use the exact text from the screenshot. -- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. -- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. -- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. - -In terms of libraries, - -- Use this script to include Tailwind: -- You can use Google Fonts -- Font Awesome for icons: - -Return only the full code in tags. -Do not include markdown "```" or "```html" at the start or end. -""" - -BOOTSTRAP_SYSTEM_PROMPT = """ -You are an expert Bootstrap developer -You take screenshots of a reference web page from the user, and then build single page apps -using Bootstrap, HTML and JS. -You might also be given a screenshot(The second image) of a web page that you have already built, and asked to -update it to look more like the reference image(The first image). - -- Make sure the app looks exactly like the screenshot. -- Pay close attention to background color, text color, font size, font family, -padding, margin, border, etc. Match the colors and sizes exactly. -- Use the exact text from the screenshot. -- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. -- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. -- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. - -In terms of libraries, - -- Use this script to include Bootstrap: -- You can use Google Fonts -- Font Awesome for icons: - -Return only the full code in tags. -Do not include markdown "```" or "```html" at the start or end. -""" - -REACT_TAILWIND_SYSTEM_PROMPT = """ -You are an expert React/Tailwind developer -You take screenshots of a reference web page from the user, and then build single page apps -using React and Tailwind CSS. -You might also be given a screenshot(The second image) of a web page that you have already built, and asked to -update it to look more like the reference image(The first image). - -- Make sure the app looks exactly like the screenshot. -- Pay close attention to background color, text color, font size, font family, -padding, margin, border, etc. Match the colors and sizes exactly. -- Use the exact text from the screenshot. -- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. -- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. -- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. - -In terms of libraries, - -- Use these script to include React so that it can run on a standalone page: - - - -- Use this script to include Tailwind: -- You can use Google Fonts -- Font Awesome for icons: - -Return only the full code in tags. -Do not include markdown "```" or "```html" at the start or end. -""" - -USER_PROMPT = """ -Generate code for a web page that looks exactly like this. -""" - - -def assemble_prompt(image_data_url, output_settings: dict, result_image_data_url=None): - # Set the system prompt based on the output settings - chosen_prompt_name = "tailwind" - system_content = TAILWIND_SYSTEM_PROMPT - if output_settings["css"] == "bootstrap": - chosen_prompt_name = "bootstrap" - system_content = BOOTSTRAP_SYSTEM_PROMPT - if output_settings["js"] == "react": - chosen_prompt_name = "react-tailwind" - system_content = REACT_TAILWIND_SYSTEM_PROMPT - - print("Using system prompt:", chosen_prompt_name) - - user_content = [ - { - "type": "image_url", - "image_url": {"url": image_data_url, "detail": "high"}, - }, - { - "type": "text", - "text": USER_PROMPT, - }, - ] - - # Include the result image if it exists - if result_image_data_url: - user_content.insert( - 1, - { - "type": "image_url", - "image_url": {"url": result_image_data_url, "detail": "high"}, - }, - ) - return [ - { - "role": "system", - "content": system_content, - }, - { - "role": "user", - "content": user_content, - }, - ] diff --git a/backend/prompts/__init__.py b/backend/prompts/__init__.py new file mode 100644 index 0000000..4f2e329 --- /dev/null +++ b/backend/prompts/__init__.py @@ -0,0 +1,79 @@ +from typing import List, NoReturn, Union + +from openai.types.chat import ChatCompletionMessageParam, ChatCompletionContentPartParam + +from prompts.imported_code_prompts import IMPORTED_CODE_SYSTEM_PROMPTS +from prompts.screenshot_system_prompts import SYSTEM_PROMPTS +from prompts.types import Stack + + +USER_PROMPT = """ +Generate code for a web page that looks exactly like this. +""" + +SVG_USER_PROMPT = """ +Generate code for a SVG that looks exactly like this. +""" + + +def assemble_imported_code_prompt( + code: str, stack: Stack, result_image_data_url: Union[str, None] = None +) -> List[ChatCompletionMessageParam]: + system_content = IMPORTED_CODE_SYSTEM_PROMPTS[stack] + + user_content = ( + "Here is the code of the app: " + code + if stack != "svg" + else "Here is the code of the SVG: " + code + ) + return [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": user_content, + }, + ] + # TODO: Use result_image_data_url + + +def assemble_prompt( + image_data_url: str, + stack: Stack, + result_image_data_url: Union[str, None] = None, +) -> List[ChatCompletionMessageParam]: + system_content = SYSTEM_PROMPTS[stack] + user_prompt = USER_PROMPT if stack != "svg" else SVG_USER_PROMPT + + user_content: List[ChatCompletionContentPartParam] = [ + { + "type": "image_url", + "image_url": {"url": image_data_url, "detail": "high"}, + }, + { + "type": "text", + "text": user_prompt, + }, + ] + + # Include the result image if it exists + if result_image_data_url: + user_content.insert( + 1, + { + "type": "image_url", + "image_url": {"url": result_image_data_url, "detail": "high"}, + }, + ) + return [ + { + "role": "system", + "content": system_content, + }, + { + "role": "user", + "content": user_content, + }, + ] diff --git a/backend/prompts/claude_prompts.py b/backend/prompts/claude_prompts.py new file mode 100644 index 0000000..3519e49 --- /dev/null +++ b/backend/prompts/claude_prompts.py @@ -0,0 +1,114 @@ +# Not used yet +# References: +# https://github.com/hundredblocks/transcription_demo +# https://docs.anthropic.com/claude/docs/prompt-engineering +# https://github.com/anthropics/anthropic-cookbook/blob/main/multimodal/best_practices_for_vision.ipynb + +VIDEO_PROMPT = """ +You are an expert at building single page, funtional apps using HTML, Jquery and Tailwind CSS. +You also have perfect vision and pay great attention to detail. + +You will be given screenshots in order at consistent intervals from a video of a user interacting with a web app. You need to re-create the same app exactly such that the same user interactions will produce the same results in the app you build. + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- If some fuctionality requires a backend call, just mock the data instead. +- MAKE THE APP FUNCTIONAL using Javascript. Allow the user to interact with the app and get the same behavior as the video. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: +- Use jQuery: + +Before generating the code for the app, think step-by-step: first, about the user flow depicated in the video and then about you how would you build it and how you would structure the code. Do the thinking within tags. Then, provide your code within tags. +""" + +VIDEO_PROMPT_ALPINE_JS = """ +You are an expert at building single page, funtional apps using HTML, Alpine.js and Tailwind CSS. +You also have perfect vision and pay great attention to detail. + +You will be given screenshots in order at consistent intervals from a video of a user interacting with a web app. You need to re-create the same app exactly such that the same user interactions will produce the same results in the app you build. + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- If some fuctionality requires a backend call, just mock the data instead. +- MAKE THE APP FUNCTIONAL using Javascript. Allow the user to interact with the app and get the same behavior as the video. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: +- Use Alpine.js: + +Before generating the code for the app, think step-by-step: first, about the user flow depicated in the video and then about you how would you build it and how you would structure the code. Do the thinking within tags. Then, provide your code within tags. +""" + + +HTML_TAILWIND_CLAUDE_SYSTEM_PROMPT = """ +You have perfect vision and pay great attention to detail which makes you an expert at building single page apps using Tailwind, HTML and JS. +You take screenshots of a reference web page from the user, and then build single page apps +using Tailwind, HTML and JS. +You might also be given a screenshot (The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Do not leave out smaller UI elements. Make sure to include every single thing in the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- In particular, pay attention to background color and overall color scheme. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Make sure to always get the layout right (if things are arranged in a row in the screenshot, they should be in a row in the app as well) +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +# + +REACT_TAILWIND_CLAUDE_SYSTEM_PROMPT = """ +You have perfect vision and pay great attention to detail which makes you an expert at building single page apps using React/Tailwind. +You take screenshots of a reference web page from the user, and then build single page apps +using React and Tailwind CSS. +You might also be given a screenshot (The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Do not leave out smaller UI elements. Make sure to include every single thing in the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- In particular, pay attention to background color and overall color scheme. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Make sure to always get the layout right (if things are arranged in a row in the screenshot, they should be in a row in the app as well) +- CREATE REUSABLE COMPONENTS FOR REPEATING ELEMENTS. For example, if there are 15 similar items in the screenshot, your code should include a reusable component that generates these items. and use loops to instantiate these components as needed. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include React so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" diff --git a/backend/prompts/imported_code_prompts.py b/backend/prompts/imported_code_prompts.py new file mode 100644 index 0000000..8babf78 --- /dev/null +++ b/backend/prompts/imported_code_prompts.py @@ -0,0 +1,136 @@ +from prompts.types import SystemPrompts + + +IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Tailwind developer. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT = """ +You are an expert React/Tailwind developer + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include React so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT = """ +You are an expert Bootstrap developer. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Bootstrap: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Ionic/Tailwind developer. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include Ionic so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- ionicons for icons, add the following + + + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +IMPORTED_CODE_VUE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Vue/Tailwind developer. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include Vue so that it can run on a standalone page: + +- Use Vue using the global build like so: +
{{ message }}
+ +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +The return result must only include the code.""" + +IMPORTED_CODE_SVG_SYSTEM_PROMPT = """ +You are an expert at building SVGs. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- You can use Google Fonts + +Return only the full code in tags. +Do not include markdown "```" or "```svg" at the start or end. +""" + +IMPORTED_CODE_SYSTEM_PROMPTS = SystemPrompts( + html_tailwind=IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT, + react_tailwind=IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT, + bootstrap=IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT, + ionic_tailwind=IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT, + vue_tailwind=IMPORTED_CODE_VUE_TAILWIND_SYSTEM_PROMPT, + svg=IMPORTED_CODE_SVG_SYSTEM_PROMPT, +) diff --git a/backend/prompts/screenshot_system_prompts.py b/backend/prompts/screenshot_system_prompts.py new file mode 100644 index 0000000..fca91ba --- /dev/null +++ b/backend/prompts/screenshot_system_prompts.py @@ -0,0 +1,185 @@ +from prompts.types import SystemPrompts + + +HTML_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Tailwind, HTML and JS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +BOOTSTRAP_SYSTEM_PROMPT = """ +You are an expert Bootstrap developer +You take screenshots of a reference web page from the user, and then build single page apps +using Bootstrap, HTML and JS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Bootstrap: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +REACT_TAILWIND_SYSTEM_PROMPT = """ +You are an expert React/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using React and Tailwind CSS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include React so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +IONIC_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Ionic/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Ionic and Tailwind CSS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include Ionic so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- ionicons for icons, add the following + + + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +VUE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Vue/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Vue and Tailwind CSS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- Use Vue using the global build like so: + +
{{ message }}
+ + +In terms of libraries, + +- Use these script to include Vue so that it can run on a standalone page: + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +The return result must only include the code. +""" + + +SVG_SYSTEM_PROMPT = """ +You are an expert at building SVGs. +You take screenshots of a reference web page from the user, and then build a SVG that looks exactly like the screenshot. + +- Make sure the SVG looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- You can use Google Fonts + +Return only the full code in tags. +Do not include markdown "```" or "```svg" at the start or end. +""" + + +SYSTEM_PROMPTS = SystemPrompts( + html_tailwind=HTML_TAILWIND_SYSTEM_PROMPT, + react_tailwind=REACT_TAILWIND_SYSTEM_PROMPT, + bootstrap=BOOTSTRAP_SYSTEM_PROMPT, + ionic_tailwind=IONIC_TAILWIND_SYSTEM_PROMPT, + vue_tailwind=VUE_TAILWIND_SYSTEM_PROMPT, + svg=SVG_SYSTEM_PROMPT, +) diff --git a/backend/prompts/test_prompts.py b/backend/prompts/test_prompts.py new file mode 100644 index 0000000..42e89c3 --- /dev/null +++ b/backend/prompts/test_prompts.py @@ -0,0 +1,397 @@ +from prompts import assemble_imported_code_prompt, assemble_prompt + +TAILWIND_SYSTEM_PROMPT = """ +You are an expert Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Tailwind, HTML and JS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +BOOTSTRAP_SYSTEM_PROMPT = """ +You are an expert Bootstrap developer +You take screenshots of a reference web page from the user, and then build single page apps +using Bootstrap, HTML and JS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Bootstrap: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +REACT_TAILWIND_SYSTEM_PROMPT = """ +You are an expert React/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using React and Tailwind CSS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include React so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +IONIC_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Ionic/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Ionic and Tailwind CSS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include Ionic so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- ionicons for icons, add the following + + + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +VUE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Vue/Tailwind developer +You take screenshots of a reference web page from the user, and then build single page apps +using Vue and Tailwind CSS. +You might also be given a screenshot(The second image) of a web page that you have already built, and asked to +update it to look more like the reference image(The first image). + +- Make sure the app looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- Use Vue using the global build like so: + +
{{ message }}
+ + +In terms of libraries, + +- Use these script to include Vue so that it can run on a standalone page: + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +The return result must only include the code. +""" + +SVG_SYSTEM_PROMPT = """ +You are an expert at building SVGs. +You take screenshots of a reference web page from the user, and then build a SVG that looks exactly like the screenshot. + +- Make sure the SVG looks exactly like the screenshot. +- Pay close attention to background color, text color, font size, font family, +padding, margin, border, etc. Match the colors and sizes exactly. +- Use the exact text from the screenshot. +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- You can use Google Fonts + +Return only the full code in tags. +Do not include markdown "```" or "```svg" at the start or end. +""" + +IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Tailwind developer. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT = """ +You are an expert React/Tailwind developer + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include React so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT = """ +You are an expert Bootstrap developer. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use this script to include Bootstrap: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + +IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT = """ +You are an expert Ionic/Tailwind developer. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include Ionic so that it can run on a standalone page: + + + +- Use this script to include Tailwind: +- You can use Google Fonts +- ionicons for icons, add the following + + + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +""" + + +IMPORTED_CODE_VUE_TAILWIND_PROMPT = """ +You are an expert Vue/Tailwind developer. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. + +In terms of libraries, + +- Use these script to include Vue so that it can run on a standalone page: + +- Use Vue using the global build like so: +
{{ message }}
+ +- Use this script to include Tailwind: +- You can use Google Fonts +- Font Awesome for icons: + +Return only the full code in tags. +Do not include markdown "```" or "```html" at the start or end. +The return result must only include the code.""" + +IMPORTED_CODE_SVG_SYSTEM_PROMPT = """ +You are an expert at building SVGs. + +- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. +- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. +- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. +- You can use Google Fonts + +Return only the full code in tags. +Do not include markdown "```" or "```svg" at the start or end. +""" + +USER_PROMPT = """ +Generate code for a web page that looks exactly like this. +""" + +SVG_USER_PROMPT = """ +Generate code for a SVG that looks exactly like this. +""" + + +def test_prompts(): + tailwind_prompt = assemble_prompt( + "image_data_url", "html_tailwind", "result_image_data_url" + ) + assert tailwind_prompt[0].get("content") == TAILWIND_SYSTEM_PROMPT + assert tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + react_tailwind_prompt = assemble_prompt( + "image_data_url", "react_tailwind", "result_image_data_url" + ) + assert react_tailwind_prompt[0].get("content") == REACT_TAILWIND_SYSTEM_PROMPT + assert react_tailwind_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + bootstrap_prompt = assemble_prompt( + "image_data_url", "bootstrap", "result_image_data_url" + ) + assert bootstrap_prompt[0].get("content") == BOOTSTRAP_SYSTEM_PROMPT + assert bootstrap_prompt[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + ionic_tailwind = assemble_prompt( + "image_data_url", "ionic_tailwind", "result_image_data_url" + ) + assert ionic_tailwind[0].get("content") == IONIC_TAILWIND_SYSTEM_PROMPT + assert ionic_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + vue_tailwind = assemble_prompt( + "image_data_url", "vue_tailwind", "result_image_data_url" + ) + assert vue_tailwind[0].get("content") == VUE_TAILWIND_SYSTEM_PROMPT + assert vue_tailwind[1]["content"][2]["text"] == USER_PROMPT # type: ignore + + svg_prompt = assemble_prompt("image_data_url", "svg", "result_image_data_url") + assert svg_prompt[0].get("content") == SVG_SYSTEM_PROMPT + assert svg_prompt[1]["content"][2]["text"] == SVG_USER_PROMPT # type: ignore + + +def test_imported_code_prompts(): + tailwind_prompt = assemble_imported_code_prompt( + "code", "html_tailwind", "result_image_data_url" + ) + expected_tailwind_prompt = [ + {"role": "system", "content": IMPORTED_CODE_TAILWIND_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert tailwind_prompt == expected_tailwind_prompt + + react_tailwind_prompt = assemble_imported_code_prompt( + "code", "react_tailwind", "result_image_data_url" + ) + expected_react_tailwind_prompt = [ + {"role": "system", "content": IMPORTED_CODE_REACT_TAILWIND_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert react_tailwind_prompt == expected_react_tailwind_prompt + + bootstrap_prompt = assemble_imported_code_prompt( + "code", "bootstrap", "result_image_data_url" + ) + expected_bootstrap_prompt = [ + {"role": "system", "content": IMPORTED_CODE_BOOTSTRAP_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert bootstrap_prompt == expected_bootstrap_prompt + + ionic_tailwind = assemble_imported_code_prompt( + "code", "ionic_tailwind", "result_image_data_url" + ) + expected_ionic_tailwind = [ + {"role": "system", "content": IMPORTED_CODE_IONIC_TAILWIND_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert ionic_tailwind == expected_ionic_tailwind + + vue_tailwind = assemble_imported_code_prompt( + "code", "vue_tailwind", "result_image_data_url" + ) + expected_vue_tailwind = [ + {"role": "system", "content": IMPORTED_CODE_VUE_TAILWIND_PROMPT}, + {"role": "user", "content": "Here is the code of the app: code"}, + ] + assert vue_tailwind == expected_vue_tailwind + + svg = assemble_imported_code_prompt("code", "svg", "result_image_data_url") + expected_svg = [ + {"role": "system", "content": IMPORTED_CODE_SVG_SYSTEM_PROMPT}, + {"role": "user", "content": "Here is the code of the SVG: code"}, + ] + assert svg == expected_svg diff --git a/backend/prompts/types.py b/backend/prompts/types.py new file mode 100644 index 0000000..9068443 --- /dev/null +++ b/backend/prompts/types.py @@ -0,0 +1,20 @@ +from typing import Literal, TypedDict + + +class SystemPrompts(TypedDict): + html_tailwind: str + react_tailwind: str + bootstrap: str + ionic_tailwind: str + vue_tailwind: str + svg: str + + +Stack = Literal[ + "html_tailwind", + "react_tailwind", + "bootstrap", + "ionic_tailwind", + "vue_tailwind", + "svg", +] diff --git a/backend/pyproject.toml b/backend/pyproject.toml index cacdda9..263b4a4 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -8,12 +8,19 @@ license = "MIT" [tool.poetry.dependencies] python = "^3.10" fastapi = "^0.95.0" -uvicorn = "^0.24.0.post1" +uvicorn = "^0.25.0" websockets = "^12.0" openai = "^1.2.4" python-dotenv = "^1.0.0" beautifulsoup4 = "^4.12.2" httpx = "^0.25.1" +pre-commit = "^3.6.2" +anthropic = "^0.18.0" +moviepy = "^1.0.3" + +[tool.poetry.group.dev.dependencies] +pytest = "^7.4.3" +pyright = "^1.1.352" [build-system] requires = ["poetry-core"] diff --git a/backend/pyrightconfig.json b/backend/pyrightconfig.json new file mode 100644 index 0000000..6e475af --- /dev/null +++ b/backend/pyrightconfig.json @@ -0,0 +1,3 @@ +{ + "exclude": ["image_generation.py"] +} diff --git a/backend/routes/evals.py b/backend/routes/evals.py new file mode 100644 index 0000000..22262cd --- /dev/null +++ b/backend/routes/evals.py @@ -0,0 +1,55 @@ +import os +from fastapi import APIRouter +from pydantic import BaseModel +from evals.utils import image_to_data_url +from evals.config import EVALS_DIR + + +router = APIRouter() + +# Update this if the number of outputs generated per input changes +N = 1 + + +class Eval(BaseModel): + input: str + outputs: list[str] + + +@router.get("/evals") +async def get_evals(): + # Get all evals from EVALS_DIR + input_dir = EVALS_DIR + "/inputs" + output_dir = EVALS_DIR + "/outputs" + + evals: list[Eval] = [] + for file in os.listdir(input_dir): + if file.endswith(".png"): + input_file_path = os.path.join(input_dir, file) + input_file = await image_to_data_url(input_file_path) + + # Construct the corresponding output file names + output_file_names = [ + file.replace(".png", f"_{i}.html") for i in range(0, N) + ] # Assuming 3 outputs for each input + + output_files_data: list[str] = [] + for output_file_name in output_file_names: + output_file_path = os.path.join(output_dir, output_file_name) + # Check if the output file exists + if os.path.exists(output_file_path): + with open(output_file_path, "r") as f: + output_files_data.append(f.read()) + else: + output_files_data.append( + "

Output file not found.

" + ) + + evals.append( + Eval( + input=input_file, + outputs=output_files_data, + ) + ) + + return evals diff --git a/backend/routes/generate_code.py b/backend/routes/generate_code.py new file mode 100644 index 0000000..3fb92f1 --- /dev/null +++ b/backend/routes/generate_code.py @@ -0,0 +1,340 @@ +import os +import traceback +from fastapi import APIRouter, WebSocket +import openai +from config import ANTHROPIC_API_KEY, IS_PROD, SHOULD_MOCK_AI_RESPONSE +from custom_types import InputMode +from llm import ( + Llm, + convert_frontend_str_to_llm, + stream_claude_response, + stream_claude_response_native, + stream_openai_response, +) +from openai.types.chat import ChatCompletionMessageParam +from mock_llm import mock_completion +from typing import Dict, List, cast, get_args +from image_generation import create_alt_url_mapping, generate_images +from prompts import assemble_imported_code_prompt, assemble_prompt +from datetime import datetime +import json +from prompts.claude_prompts import VIDEO_PROMPT +from prompts.types import Stack + +# from utils import pprint_prompt +from video.utils import extract_tag_content, assemble_claude_prompt_video +from ws.constants import APP_ERROR_WEB_SOCKET_CODE # type: ignore + + +router = APIRouter() + + +def write_logs(prompt_messages: List[ChatCompletionMessageParam], completion: str): + # Get the logs path from environment, default to the current working directory + logs_path = os.environ.get("LOGS_PATH", os.getcwd()) + + # Create run_logs directory if it doesn't exist within the specified logs path + logs_directory = os.path.join(logs_path, "run_logs") + if not os.path.exists(logs_directory): + os.makedirs(logs_directory) + + print("Writing to logs directory:", logs_directory) + + # Generate a unique filename using the current timestamp within the logs directory + filename = datetime.now().strftime(f"{logs_directory}/messages_%Y%m%d_%H%M%S.json") + + # Write the messages dict into a new file for each run + with open(filename, "w") as f: + f.write(json.dumps({"prompt": prompt_messages, "completion": completion})) + + +@router.websocket("/generate-code") +async def stream_code(websocket: WebSocket): + await websocket.accept() + + print("Incoming websocket connection...") + + async def throw_error( + message: str, + ): + await websocket.send_json({"type": "error", "value": message}) + await websocket.close(APP_ERROR_WEB_SOCKET_CODE) + + # TODO: Are the values always strings? + params: Dict[str, str] = await websocket.receive_json() + + print("Received params") + + # Read the code config settings from the request. Fall back to default if not provided. + generated_code_config = "" + if "generatedCodeConfig" in params and params["generatedCodeConfig"]: + generated_code_config = params["generatedCodeConfig"] + if not generated_code_config in get_args(Stack): + await throw_error(f"Invalid generated code config: {generated_code_config}") + return + # Cast the variable to the Stack type + valid_stack = cast(Stack, generated_code_config) + + # Validate the input mode + input_mode = params.get("inputMode") + if not input_mode in get_args(InputMode): + await throw_error(f"Invalid input mode: {input_mode}") + raise Exception(f"Invalid input mode: {input_mode}") + # Cast the variable to the right type + validated_input_mode = cast(InputMode, input_mode) + + # Read the model from the request. Fall back to default if not provided. + code_generation_model_str = params.get( + "codeGenerationModel", Llm.GPT_4O_2024_05_13.value + ) + try: + code_generation_model = convert_frontend_str_to_llm(code_generation_model_str) + except: + await throw_error(f"Invalid model: {code_generation_model_str}") + raise Exception(f"Invalid model: {code_generation_model_str}") + exact_llm_version = None + + print( + f"Generating {generated_code_config} code for uploaded {input_mode} using {code_generation_model} model..." + ) + + # Get the OpenAI API key from the request. Fall back to environment variable if not provided. + # If neither is provided, we throw an error. + openai_api_key = None + if params["openAiApiKey"]: + openai_api_key = params["openAiApiKey"] + print("Using OpenAI API key from client-side settings dialog") + else: + openai_api_key = os.environ.get("OPENAI_API_KEY") + if openai_api_key: + print("Using OpenAI API key from environment variable") + + if not openai_api_key and ( + code_generation_model == Llm.GPT_4_VISION + or code_generation_model == Llm.GPT_4_TURBO_2024_04_09 + or code_generation_model == Llm.GPT_4O_2024_05_13 + ): + print("OpenAI API key not found") + await throw_error( + "No OpenAI API key found. Please add your API key in the settings dialog or add it to backend/.env file. If you add it to .env, make sure to restart the backend server." + ) + return + + # Get the Anthropic API key from the request. Fall back to environment variable if not provided. + # If neither is provided, we throw an error later only if Claude is used. + anthropic_api_key = None + if "anthropicApiKey" in params and params["anthropicApiKey"]: + anthropic_api_key = params["anthropicApiKey"] + print("Using Anthropic API key from client-side settings dialog") + else: + anthropic_api_key = ANTHROPIC_API_KEY + if anthropic_api_key: + print("Using Anthropic API key from environment variable") + + # Get the OpenAI Base URL from the request. Fall back to environment variable if not provided. + openai_base_url = None + # Disable user-specified OpenAI Base URL in prod + if not os.environ.get("IS_PROD"): + if "openAiBaseURL" in params and params["openAiBaseURL"]: + openai_base_url = params["openAiBaseURL"] + print("Using OpenAI Base URL from client-side settings dialog") + else: + openai_base_url = os.environ.get("OPENAI_BASE_URL") + if openai_base_url: + print("Using OpenAI Base URL from environment variable") + + if not openai_base_url: + print("Using official OpenAI URL") + + # Get the image generation flag from the request. Fall back to True if not provided. + should_generate_images = ( + params["isImageGenerationEnabled"] + if "isImageGenerationEnabled" in params + else True + ) + + print("generating code...") + await websocket.send_json({"type": "status", "value": "Generating code..."}) + + async def process_chunk(content: str): + await websocket.send_json({"type": "chunk", "value": content}) + + # Image cache for updates so that we don't have to regenerate images + image_cache: Dict[str, str] = {} + + # If this generation started off with imported code, we need to assemble the prompt differently + if params.get("isImportedFromCode") and params["isImportedFromCode"]: + original_imported_code = params["history"][0] + prompt_messages = assemble_imported_code_prompt( + original_imported_code, valid_stack + ) + for index, text in enumerate(params["history"][1:]): + if index % 2 == 0: + message: ChatCompletionMessageParam = { + "role": "user", + "content": text, + } + else: + message: ChatCompletionMessageParam = { + "role": "assistant", + "content": text, + } + prompt_messages.append(message) + else: + # Assemble the prompt + try: + if params.get("resultImage") and params["resultImage"]: + prompt_messages = assemble_prompt( + params["image"], valid_stack, params["resultImage"] + ) + else: + prompt_messages = assemble_prompt(params["image"], valid_stack) + except: + await websocket.send_json( + { + "type": "error", + "value": "Error assembling prompt. Contact support at support@picoapps.xyz", + } + ) + await websocket.close() + return + + if params["generationType"] == "update": + # Transform the history tree into message format + # TODO: Move this to frontend + for index, text in enumerate(params["history"]): + if index % 2 == 0: + message: ChatCompletionMessageParam = { + "role": "assistant", + "content": text, + } + else: + message: ChatCompletionMessageParam = { + "role": "user", + "content": text, + } + prompt_messages.append(message) + + image_cache = create_alt_url_mapping(params["history"][-2]) + + if validated_input_mode == "video": + video_data_url = params["image"] + prompt_messages = await assemble_claude_prompt_video(video_data_url) + + # pprint_prompt(prompt_messages) # type: ignore + + if SHOULD_MOCK_AI_RESPONSE: + completion = await mock_completion( + process_chunk, input_mode=validated_input_mode + ) + else: + try: + if validated_input_mode == "video": + if not anthropic_api_key: + await throw_error( + "Video only works with Anthropic models. No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env or in the settings dialog" + ) + raise Exception("No Anthropic key") + + completion = await stream_claude_response_native( + system_prompt=VIDEO_PROMPT, + messages=prompt_messages, # type: ignore + api_key=anthropic_api_key, + callback=lambda x: process_chunk(x), + model=Llm.CLAUDE_3_OPUS, + include_thinking=True, + ) + exact_llm_version = Llm.CLAUDE_3_OPUS + elif code_generation_model == Llm.CLAUDE_3_SONNET: + if not anthropic_api_key: + await throw_error( + "No Anthropic API key found. Please add the environment variable ANTHROPIC_API_KEY to backend/.env or in the settings dialog" + ) + raise Exception("No Anthropic key") + + completion = await stream_claude_response( + prompt_messages, # type: ignore + api_key=anthropic_api_key, + callback=lambda x: process_chunk(x), + ) + exact_llm_version = code_generation_model + else: + completion = await stream_openai_response( + prompt_messages, # type: ignore + api_key=openai_api_key, + base_url=openai_base_url, + callback=lambda x: process_chunk(x), + model=code_generation_model, + ) + exact_llm_version = code_generation_model + except openai.AuthenticationError as e: + print("[GENERATE_CODE] Authentication failed", e) + error_message = ( + "Incorrect OpenAI key. Please make sure your OpenAI API key is correct, or create a new OpenAI API key on your OpenAI dashboard." + + ( + " Alternatively, you can purchase code generation credits directly on this website." + if IS_PROD + else "" + ) + ) + return await throw_error(error_message) + except openai.NotFoundError as e: + print("[GENERATE_CODE] Model not found", e) + error_message = ( + e.message + + ". Please make sure you have followed the instructions correctly to obtain an OpenAI key with GPT vision access: https://github.com/abi/screenshot-to-code/blob/main/Troubleshooting.md" + + ( + " Alternatively, you can purchase code generation credits directly on this website." + if IS_PROD + else "" + ) + ) + return await throw_error(error_message) + except openai.RateLimitError as e: + print("[GENERATE_CODE] Rate limit exceeded", e) + error_message = ( + "OpenAI error - 'You exceeded your current quota, please check your plan and billing details.'" + + ( + " Alternatively, you can purchase code generation credits directly on this website." + if IS_PROD + else "" + ) + ) + return await throw_error(error_message) + + if validated_input_mode == "video": + completion = extract_tag_content("html", completion) + + print("Exact used model for generation: ", exact_llm_version) + + # Write the messages dict into a log so that we can debug later + write_logs(prompt_messages, completion) # type: ignore + + try: + if should_generate_images: + await websocket.send_json( + {"type": "status", "value": "Generating images..."} + ) + updated_html = await generate_images( + completion, + api_key=openai_api_key, + base_url=openai_base_url, + image_cache=image_cache, + ) + else: + updated_html = completion + await websocket.send_json({"type": "setCode", "value": updated_html}) + await websocket.send_json( + {"type": "status", "value": "Code generation complete."} + ) + except Exception as e: + traceback.print_exc() + print("Image generation failed", e) + # Send set code even if image generation fails since that triggers + # the frontend to update history + await websocket.send_json({"type": "setCode", "value": completion}) + await websocket.send_json( + {"type": "status", "value": "Image generation failed but code is complete."} + ) + + await websocket.close() diff --git a/backend/routes/home.py b/backend/routes/home.py new file mode 100644 index 0000000..c9f66b4 --- /dev/null +++ b/backend/routes/home.py @@ -0,0 +1,12 @@ +from fastapi import APIRouter +from fastapi.responses import HTMLResponse + + +router = APIRouter() + + +@router.get("/") +async def get_status(): + return HTMLResponse( + content="

Your backend is running correctly. Please open the front-end URL (default is http://localhost:5173) to use screenshot-to-code.

" + ) diff --git a/backend/routes/screenshot.py b/backend/routes/screenshot.py index 7efcfb8..258cd7e 100644 --- a/backend/routes/screenshot.py +++ b/backend/routes/screenshot.py @@ -11,7 +11,9 @@ def bytes_to_data_url(image_bytes: bytes, mime_type: str) -> str: return f"data:{mime_type};base64,{base64_image}" -async def capture_screenshot(target_url, api_key, device="desktop") -> bytes: +async def capture_screenshot( + target_url: str, api_key: str, device: str = "desktop" +) -> bytes: api_base_url = "https://api.screenshotone.com/take" params = { diff --git a/backend/run_evals.py b/backend/run_evals.py new file mode 100644 index 0000000..bbf355a --- /dev/null +++ b/backend/run_evals.py @@ -0,0 +1,52 @@ +# Load environment variables first +from dotenv import load_dotenv + +from llm import Llm + +load_dotenv() + +import os +from typing import Any, Coroutine +import asyncio + +from evals.config import EVALS_DIR +from evals.core import generate_code_core +from evals.utils import image_to_data_url + +STACK = "ionic_tailwind" +MODEL = Llm.GPT_4O_2024_05_13 +N = 1 # Number of outputs to generate + + +async def main(): + INPUT_DIR = EVALS_DIR + "/inputs" + OUTPUT_DIR = EVALS_DIR + "/outputs" + + # Get all the files in the directory (only grab pngs) + evals = [f for f in os.listdir(INPUT_DIR) if f.endswith(".png")] + + tasks: list[Coroutine[Any, Any, str]] = [] + for filename in evals: + filepath = os.path.join(INPUT_DIR, filename) + data_url = await image_to_data_url(filepath) + for _ in range(N): # Generate N tasks for each input + task = generate_code_core(image_url=data_url, stack=STACK, model=MODEL) + tasks.append(task) + + results = await asyncio.gather(*tasks) + + os.makedirs(OUTPUT_DIR, exist_ok=True) + + for i, content in enumerate(results): + # Calculate index for filename and output number + eval_index = i // N + output_number = i % N + filename = evals[eval_index] + # File name is derived from the original filename in evals with an added output number + output_filename = f"{os.path.splitext(filename)[0]}_{output_number}.html" + output_filepath = os.path.join(OUTPUT_DIR, output_filename) + with open(output_filepath, "w") as file: + file.write(content) + + +asyncio.run(main()) diff --git a/backend/start.py b/backend/start.py new file mode 100644 index 0000000..a126468 --- /dev/null +++ b/backend/start.py @@ -0,0 +1,4 @@ +import uvicorn + +if __name__ == "__main__": + uvicorn.run("main:app", port=7001, reload=True) diff --git a/backend/test_llm.py b/backend/test_llm.py new file mode 100644 index 0000000..aeb02ab --- /dev/null +++ b/backend/test_llm.py @@ -0,0 +1,41 @@ +import unittest +from llm import convert_frontend_str_to_llm, Llm + + +class TestConvertFrontendStrToLlm(unittest.TestCase): + def test_convert_valid_strings(self): + self.assertEqual( + convert_frontend_str_to_llm("gpt_4_vision"), + Llm.GPT_4_VISION, + "Should convert 'gpt_4_vision' to Llm.GPT_4_VISION", + ) + self.assertEqual( + convert_frontend_str_to_llm("claude_3_sonnet"), + Llm.CLAUDE_3_SONNET, + "Should convert 'claude_3_sonnet' to Llm.CLAUDE_3_SONNET", + ) + self.assertEqual( + convert_frontend_str_to_llm("claude-3-opus-20240229"), + Llm.CLAUDE_3_OPUS, + "Should convert 'claude-3-opus-20240229' to Llm.CLAUDE_3_OPUS", + ) + self.assertEqual( + convert_frontend_str_to_llm("gpt-4-turbo-2024-04-09"), + Llm.GPT_4_TURBO_2024_04_09, + "Should convert 'gpt-4-turbo-2024-04-09' to Llm.GPT_4_TURBO_2024_04_09", + ) + self.assertEqual( + convert_frontend_str_to_llm("gpt-4o-2024-05-13"), + Llm.GPT_4O_2024_05_13, + "Should convert 'gpt-4o-2024-05-13' to Llm.GPT_4O_2024_05_13", + ) + + def test_convert_invalid_string_raises_exception(self): + with self.assertRaises(ValueError): + convert_frontend_str_to_llm("invalid_string") + with self.assertRaises(ValueError): + convert_frontend_str_to_llm("another_invalid_string") + + +if __name__ == "__main__": + unittest.main() diff --git a/backend/test_prompts.py b/backend/test_prompts.py deleted file mode 100644 index 2eaaaf4..0000000 --- a/backend/test_prompts.py +++ /dev/null @@ -1,97 +0,0 @@ -from prompts import assemble_prompt - -TAILWIND_SYSTEM_PROMPT = """ -You are an expert Tailwind developer -You take screenshots of a reference web page from the user, and then build single page apps -using Tailwind, HTML and JS. -You might also be given a screenshot(The second image) of a web page that you have already built, and asked to -update it to look more like the reference image(The first image). - -- Make sure the app looks exactly like the screenshot. -- Pay close attention to background color, text color, font size, font family, -padding, margin, border, etc. Match the colors and sizes exactly. -- Use the exact text from the screenshot. -- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. -- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. -- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. - -In terms of libraries, - -- Use this script to include Tailwind: -- You can use Google Fonts -- Font Awesome for icons: - -Return only the full code in tags. -Do not include markdown "```" or "```html" at the start or end. -""" - -BOOTSTRAP_SYSTEM_PROMPT = """ -You are an expert Bootstrap developer -You take screenshots of a reference web page from the user, and then build single page apps -using Bootstrap, HTML and JS. -You might also be given a screenshot(The second image) of a web page that you have already built, and asked to -update it to look more like the reference image(The first image). - -- Make sure the app looks exactly like the screenshot. -- Pay close attention to background color, text color, font size, font family, -padding, margin, border, etc. Match the colors and sizes exactly. -- Use the exact text from the screenshot. -- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. -- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. -- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. - -In terms of libraries, - -- Use this script to include Bootstrap: -- You can use Google Fonts -- Font Awesome for icons: - -Return only the full code in tags. -Do not include markdown "```" or "```html" at the start or end. -""" - -REACT_TAILWIND_SYSTEM_PROMPT = """ -You are an expert React/Tailwind developer -You take screenshots of a reference web page from the user, and then build single page apps -using React and Tailwind CSS. -You might also be given a screenshot(The second image) of a web page that you have already built, and asked to -update it to look more like the reference image(The first image). - -- Make sure the app looks exactly like the screenshot. -- Pay close attention to background color, text color, font size, font family, -padding, margin, border, etc. Match the colors and sizes exactly. -- Use the exact text from the screenshot. -- Do not add comments in the code such as "" and "" in place of writing the full code. WRITE THE FULL CODE. -- Repeat elements as needed to match the screenshot. For example, if there are 15 items, the code should have 15 items. DO NOT LEAVE comments like "" or bad things will happen. -- For images, use placeholder images from https://placehold.co and include a detailed description of the image in the alt text so that an image generation AI can generate the image later. - -In terms of libraries, - -- Use these script to include React so that it can run on a standalone page: - - - -- Use this script to include Tailwind: -- You can use Google Fonts -- Font Awesome for icons: - -Return only the full code in tags. -Do not include markdown "```" or "```html" at the start or end. -""" - - -def test_prompts(): - tailwind_prompt = assemble_prompt( - "image_data_url", {"css": "tailwind", "js": "vanilla"}, "result_image_data_url" - ) - assert tailwind_prompt[0]["content"] == TAILWIND_SYSTEM_PROMPT - - bootstrap_prompt = assemble_prompt( - "image_data_url", {"css": "bootstrap", "js": "vanilla"}, "result_image_data_url" - ) - assert bootstrap_prompt[0]["content"] == BOOTSTRAP_SYSTEM_PROMPT - - react_tailwind_prompt = assemble_prompt( - "image_data_url", {"css": "tailwind", "js": "react"}, "result_image_data_url" - ) - assert react_tailwind_prompt[0]["content"] == REACT_TAILWIND_SYSTEM_PROMPT diff --git a/backend/utils.py b/backend/utils.py index 88c3a67..6c28e14 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -1,20 +1,30 @@ import copy +import json +from typing import List +from openai.types.chat import ChatCompletionMessageParam -def truncate_data_strings(data): +def pprint_prompt(prompt_messages: List[ChatCompletionMessageParam]): + print(json.dumps(truncate_data_strings(prompt_messages), indent=4)) + + +def truncate_data_strings(data: List[ChatCompletionMessageParam]): # type: ignore # Deep clone the data to avoid modifying the original object cloned_data = copy.deepcopy(data) if isinstance(cloned_data, dict): - for key, value in cloned_data.items(): + for key, value in cloned_data.items(): # type: ignore # Recursively call the function if the value is a dictionary or a list if isinstance(value, (dict, list)): - cloned_data[key] = truncate_data_strings(value) - # Truncate the string if it starts with 'data:' - elif isinstance(value, str) and value.startswith("data:"): - cloned_data[key] = value[:20] - elif isinstance(cloned_data, list): - # Process each item in the list - cloned_data = [truncate_data_strings(item) for item in cloned_data] + cloned_data[key] = truncate_data_strings(value) # type: ignore + # Truncate the string if it it's long and add ellipsis and length + elif isinstance(value, str): + cloned_data[key] = value[:40] # type: ignore + if len(value) > 40: + cloned_data[key] += "..." + f" ({len(value)} chars)" # type: ignore - return cloned_data + elif isinstance(cloned_data, list): # type: ignore + # Process each item in the list + cloned_data = [truncate_data_strings(item) for item in cloned_data] # type: ignore + + return cloned_data # type: ignore diff --git a/backend/video/utils.py b/backend/video/utils.py new file mode 100644 index 0000000..dc772fc --- /dev/null +++ b/backend/video/utils.py @@ -0,0 +1,134 @@ +# Extract HTML content from the completion string +import base64 +import io +import mimetypes +import os +import tempfile +import uuid +from typing import Any, Union, cast +from moviepy.editor import VideoFileClip # type: ignore +from PIL import Image +import math + + +DEBUG = True +TARGET_NUM_SCREENSHOTS = ( + 20 # Should be max that Claude supports (20) - reduce to save tokens on testing +) + + +async def assemble_claude_prompt_video(video_data_url: str) -> list[Any]: + images = split_video_into_screenshots(video_data_url) + + # Save images to tmp if we're debugging + if DEBUG: + save_images_to_tmp(images) + + # Validate number of images + print(f"Number of frames extracted from video: {len(images)}") + if len(images) > 20: + print(f"Too many screenshots: {len(images)}") + raise ValueError("Too many screenshots extracted from video") + + # Convert images to the message format for Claude + content_messages: list[dict[str, Union[dict[str, str], str]]] = [] + for image in images: + + # Convert Image to buffer + buffered = io.BytesIO() + image.save(buffered, format="JPEG") + + # Encode bytes as base64 + base64_data = base64.b64encode(buffered.getvalue()).decode("utf-8") + media_type = "image/jpeg" + + content_messages.append( + { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": base64_data, + }, + } + ) + + return [ + { + "role": "user", + "content": content_messages, + }, + ] + + +# Returns a list of images/frame (RGB format) +def split_video_into_screenshots(video_data_url: str) -> list[Image.Image]: + target_num_screenshots = TARGET_NUM_SCREENSHOTS + + # Decode the base64 URL to get the video bytes + video_encoded_data = video_data_url.split(",")[1] + video_bytes = base64.b64decode(video_encoded_data) + + mime_type = video_data_url.split(";")[0].split(":")[1] + suffix = mimetypes.guess_extension(mime_type) + + with tempfile.NamedTemporaryFile(suffix=suffix, delete=True) as temp_video_file: + print(temp_video_file.name) + temp_video_file.write(video_bytes) + temp_video_file.flush() + clip = VideoFileClip(temp_video_file.name) + images: list[Image.Image] = [] + total_frames = cast(int, clip.reader.nframes) # type: ignore + + # Calculate frame skip interval by dividing total frames by the target number of screenshots + # Ensuring a minimum skip of 1 frame + frame_skip = max(1, math.ceil(total_frames / target_num_screenshots)) + + # Iterate over each frame in the clip + for i, frame in enumerate(clip.iter_frames()): + # Save every nth frame + if i % frame_skip == 0: + frame_image = Image.fromarray(frame) # type: ignore + images.append(frame_image) + # Ensure that we don't capture more than the desired number of frames + if len(images) >= target_num_screenshots: + break + + # Close the video file to release resources + clip.close() + + return images + + +# Save a list of PIL images to a random temporary directory +def save_images_to_tmp(images: list[Image.Image]): + + # Create a unique temporary directory + unique_dir_name = f"screenshots_{uuid.uuid4()}" + tmp_screenshots_dir = os.path.join(tempfile.gettempdir(), unique_dir_name) + os.makedirs(tmp_screenshots_dir, exist_ok=True) + + for idx, image in enumerate(images): + # Generate a unique image filename using index + image_filename = f"screenshot_{idx}.jpg" + tmp_filepath = os.path.join(tmp_screenshots_dir, image_filename) + image.save(tmp_filepath, format="JPEG") + + print("Saved to " + tmp_screenshots_dir) + + +def extract_tag_content(tag: str, text: str) -> str: + """ + Extracts content for a given tag from the provided text. + + :param tag: The tag to search for. + :param text: The text to search within. + :return: The content found within the tag, if any. + """ + tag_start = f"<{tag}>" + tag_end = f"" + start_idx = text.find(tag_start) + end_idx = text.find(tag_end, start_idx) + if start_idx != -1 and end_idx != -1: + return text[start_idx : end_idx + len(tag_end)] + return "" diff --git a/backend/video_to_app.py b/backend/video_to_app.py new file mode 100644 index 0000000..c876804 --- /dev/null +++ b/backend/video_to_app.py @@ -0,0 +1,122 @@ +# Load environment variables first + +from dotenv import load_dotenv + +load_dotenv() + + +import base64 +import mimetypes +import time +import subprocess +import os +import asyncio +from datetime import datetime +from prompts.claude_prompts import VIDEO_PROMPT +from utils import pprint_prompt +from config import ANTHROPIC_API_KEY +from video.utils import extract_tag_content, assemble_claude_prompt_video +from llm import ( + Llm, + stream_claude_response_native, +) + +STACK = "html_tailwind" + +VIDEO_DIR = "./video_evals/videos" +SCREENSHOTS_DIR = "./video_evals/screenshots" +OUTPUTS_DIR = "./video_evals/outputs" + + +async def main(): + video_filename = "shortest.mov" + is_followup = False + + if not ANTHROPIC_API_KEY: + raise ValueError("ANTHROPIC_API_KEY is not set") + + # Get previous HTML + previous_html = "" + if is_followup: + previous_html_file = max( + [ + os.path.join(OUTPUTS_DIR, f) + for f in os.listdir(OUTPUTS_DIR) + if f.endswith(".html") + ], + key=os.path.getctime, + ) + with open(previous_html_file, "r") as file: + previous_html = file.read() + + video_file = os.path.join(VIDEO_DIR, video_filename) + mime_type = mimetypes.guess_type(video_file)[0] + with open(video_file, "rb") as file: + video_content = file.read() + video_data_url = ( + f"data:{mime_type};base64,{base64.b64encode(video_content).decode('utf-8')}" + ) + + prompt_messages = await assemble_claude_prompt_video(video_data_url) + + # Tell the model to continue + # {"role": "assistant", "content": SECOND_MESSAGE}, + # {"role": "user", "content": "continue"}, + + if is_followup: + prompt_messages += [ + {"role": "assistant", "content": previous_html}, + { + "role": "user", + "content": "You've done a good job with a first draft. Improve this further based on the original instructions so that the app is fully functional like in the original video.", + }, + ] # type: ignore + + async def process_chunk(content: str): + print(content, end="", flush=True) + + response_prefix = "" + + pprint_prompt(prompt_messages) # type: ignore + + start_time = time.time() + + completion = await stream_claude_response_native( + system_prompt=VIDEO_PROMPT, + messages=prompt_messages, + api_key=ANTHROPIC_API_KEY, + callback=lambda x: process_chunk(x), + model=Llm.CLAUDE_3_OPUS, + include_thinking=True, + ) + + end_time = time.time() + + # Prepend the response prefix to the completion + completion = response_prefix + completion + + # Extract the outputs + html_content = extract_tag_content("html", completion) + thinking = extract_tag_content("thinking", completion) + + print(thinking) + print(f"Operation took {end_time - start_time} seconds") + + os.makedirs(OUTPUTS_DIR, exist_ok=True) + + # Generate a unique filename based on the current time + timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + filename = f"video_test_output_{timestamp}.html" + output_path = os.path.join(OUTPUTS_DIR, filename) + + # Write the HTML content to the file + with open(output_path, "w") as file: + file.write(html_content) + + print(f"Output file path: {output_path}") + + # Show a notification + subprocess.run(["osascript", "-e", 'display notification "Coding Complete"']) + + +asyncio.run(main()) diff --git a/backend/ws/__init__.py b/backend/ws/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/ws/constants.py b/backend/ws/constants.py new file mode 100644 index 0000000..e992ecb --- /dev/null +++ b/backend/ws/constants.py @@ -0,0 +1,2 @@ +# WebSocket protocol (RFC 6455) allows for the use of custom close codes in the range 4000-4999 +APP_ERROR_WEB_SOCKET_CODE = 4332 diff --git a/blog/evaluating-claude.md b/blog/evaluating-claude.md new file mode 100644 index 0000000..373a343 --- /dev/null +++ b/blog/evaluating-claude.md @@ -0,0 +1,59 @@ +# Claude 3 for converting screenshots to code + +Claude 3 dropped yesterday, claiming to rival GPT-4 on a wide variety of tasks. I maintain a very popular open source project called “screenshot-to-code” (this one!) that uses GPT-4 vision to convert screenshots/designs into clean code. Naturally, I was excited to see how good Claude 3 was at this task. + +**TLDR:** Claude 3 is on par with GPT-4 vision for screenshot to code, better in some ways but worse in others. + +## Evaluation Setup + +I don’t know of a public benchmark for “screenshot to code” so I created simple evaluation setup for the purposes of testing: + +- **Evaluation Dataset**: 16 screenshots with a mix of UI elements, landing pages, dashboards and popular websites. +Screenshot 2024-03-05 at 3 05 52 PM + +- **Evaluation Metric**: Replication accuracy, as in “How close does the generated code look to the screenshot?” While there are other metrics that are important like code quality, speed and so on, this is by far the #1 thing most users of the repo care about. +- **Evaluation Mechanism**: Each output is subjectively rated by a human on a rating scale from 0 to 4. 4 = very close to an exact replica while 0 = nothing like the screenshot. With 16 screenshots, the maximum any model can score is 64. + + +To make the evaluation process easy, I created [a Python script](https://github.com/abi/screenshot-to-code/blob/main/backend/run_evals.py) that runs code for all the inputs in parallel. I also made a simple UI to do a side-by-side comparison of the input and output. + +![Google Chrome](https://github.com/abi/screenshot-to-code/assets/23818/38126f8f-205d-4ed1-b8cf-039e81dcc3d0) + + +## Results + +Quick note about what kind of code we’ll be generating: currently, screenshot-to-code supports generating code in HTML + Tailwind, React, Vue, and several other frameworks. Stacks can impact the replication accuracy quite a bit. For example, because Bootstrap uses a relatively restrictive set of user elements, generations using Bootstrap tend to have a distinct "Bootstrap" style. + +I only ran the evals on HTML/Tailwind here which is the stack where GPT-4 vision tends to perform the best. + +Here are the results (average of 3 runs for each model): + +- GPT-4 Vision obtains a score of **65.10%** - this is what we’re trying to beat +- Claude 3 Sonnet receives a score of **70.31%**, which is a bit better. +- Surprisingly, Claude 3 Opus which is supposed to be the smarter and slower model scores worse than both GPT-4 vision and Claude 3 Sonnet, comes in at **61.46%**. + +Overall, a very strong showing for Claude 3. Obviously, there's a lot of subjectivity involved in this evaluation but Claude 3 is definitely on par with GPT-4 Vision, if not better. + +You can see the [side-by-side comparison for a run of Claude 3 Sonnet here](https://github.com/abi/screenshot-to-code-files/blob/main/sonnet%20results.png). And for [a run of GPT-4 Vision here](https://github.com/abi/screenshot-to-code-files/blob/main/gpt%204%20vision%20results.png). + +Other notes: + +- The prompts used are optimized for GPT-4 vision. Adjusting the prompts a bit for Claude did yield a small improvement. But nothing game-changing and potentially not worth the trade-off of maintaining two sets of prompts. +- All the models excel at code quality - the quality is usually comparable to a human or better. +- Claude 3 is much less lazy than GPT-4 Vision. When asked to recreate Hacker News, GPT-4 Vision will only create two items in the list and leave comments in this code like `` and ``. +Screenshot 2024-03-05 at 9 25 04 PM + +While Claude 3 Sonnet can sometimes be lazy too, most of the time, it does what you ask it to do. + +Screenshot 2024-03-05 at 9 30 23 PM + +- For some reason, all the models struggle with side-by-side "flex" layouts +Screenshot 2024-03-05 at 9 20 58 PM + +- Claude 3 Sonnet is a lot faster +- Claude 3 gets background and text colors wrong quite often! (like in the Hacker News image above) +- My suspicion is that Claude 3 Opus results can be improved to be on par with the other models through better prompting + +Overall, I'm very impressed with Claude 3 Sonnet for this use case. I've added it as an alternative to GPT-4 Vision in the open source repo (hosted version update coming soon). + +If you’d like to contribute to this effort, I have some documentation on [running these evals yourself here](https://github.com/abi/screenshot-to-code/blob/main/Evaluation.md). I'm also working on a better evaluation mechanism with Elo ratings and would love some help on that. diff --git a/design-docs.md b/design-docs.md new file mode 100644 index 0000000..eb38119 --- /dev/null +++ b/design-docs.md @@ -0,0 +1,5 @@ +## Version History + +Version history is stored as a tree on the client-side. + +![Screenshot to Code](https://github.com/abi/screenshot-to-code/assets/23818/e35644aa-b90a-4aa7-8027-b8732796fd7c) diff --git a/frontend/.gitignore b/frontend/.gitignore index 17ceca3..a0d3702 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -25,3 +25,6 @@ dist-ssr # Env files .env* + +# Test files +src/tests/results/ diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 938e2bf..b176926 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -16,4 +16,4 @@ COPY ./ /app/ EXPOSE 5173 # Command to run the application -CMD ["yarn", "dev", "--host", "0.0.0.0"] \ No newline at end of file +CMD ["yarn", "dev", "--host", "0.0.0.0"] diff --git a/frontend/components.json b/frontend/components.json index 88a9a6e..1cd599c 100644 --- a/frontend/components.json +++ b/frontend/components.json @@ -13,4 +13,4 @@ "components": "@/components", "utils": "@/lib/utils" } -} \ No newline at end of file +} diff --git a/frontend/index.html b/frontend/index.html index f747064..39cfccc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,11 +2,7 @@ - + @@ -21,6 +17,34 @@ <%- injectHead %> Screenshot to Code + + + + + + + + + + + + + + + +
diff --git a/frontend/jest.config.js b/frontend/jest.config.js new file mode 100644 index 0000000..310efb5 --- /dev/null +++ b/frontend/jest.config.js @@ -0,0 +1,9 @@ +export default { + preset: "ts-jest", + testEnvironment: "node", + setupFiles: ["/src/setupTests.ts"], + transform: { + "^.+\\.tsx?$": "ts-jest", + }, + testTimeout: 30000, +}; diff --git a/frontend/package.json b/frontend/package.json index 476a72d..4652dc7 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,17 +9,22 @@ "build": "tsc && vite build", "build-hosted": "tsc && vite build --mode prod", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "preview": "vite preview" + "preview": "vite preview", + "test": "jest" }, "dependencies": { "@codemirror/lang-html": "^6.4.6", "@radix-ui/react-accordion": "^1.1.2", "@radix-ui/react-alert-dialog": "^1.0.5", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-collapsible": "^1.0.3", "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-hover-card": "^1.0.7", "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-progress": "^1.0.3", + "@radix-ui/react-scroll-area": "^1.0.5", "@radix-ui/react-select": "^2.0.0", "@radix-ui/react-separator": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", @@ -36,27 +41,37 @@ "react-dropzone": "^14.2.3", "react-hot-toast": "^2.4.1", "react-icons": "^4.12.0", + "react-router-dom": "^6.20.1", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", "thememirror": "^2.0.1", - "vite-plugin-checker": "^0.6.2" + "vite-plugin-checker": "^0.6.2", + "webm-duration-fix": "^1.0.4", + "zustand": "^4.5.2" }, "devDependencies": { + "@types/jest": "^29.5.12", "@types/node": "^20.9.0", + "@types/puppeteer": "^7.0.4", "@types/react": "^18.2.15", "@types/react-dom": "^18.2.7", "@typescript-eslint/eslint-plugin": "^6.0.0", "@typescript-eslint/parser": "^6.0.0", "@vitejs/plugin-react": "^4.0.3", "autoprefixer": "^10.4.16", + "dotenv": "^16.4.5", "eslint": "^8.45.0", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.3", + "jest": "^29.7.0", "postcss": "^8.4.31", + "puppeteer": "^22.6.4", "tailwindcss": "^3.3.5", + "ts-jest": "^29.1.2", "typescript": "^5.0.2", "vite": "^4.4.5", - "vite-plugin-html": "^3.2.0" + "vite-plugin-html": "^3.2.0", + "vitest": "^1.0.1" }, "engines": { "node": ">=14.18.0" diff --git a/frontend/public/brand/twitter-summary-card.png b/frontend/public/brand/twitter-summary-card.png new file mode 100644 index 0000000..6fcedbf Binary files /dev/null and b/frontend/public/brand/twitter-summary-card.png differ diff --git a/frontend/public/favicon/coding.png b/frontend/public/favicon/coding.png new file mode 100644 index 0000000..71a6a0b Binary files /dev/null and b/frontend/public/favicon/coding.png differ diff --git a/frontend/public/favicon/main.png b/frontend/public/favicon/main.png new file mode 100644 index 0000000..4a32069 Binary files /dev/null and b/frontend/public/favicon/main.png differ diff --git a/frontend/src/.env.jest.example b/frontend/src/.env.jest.example new file mode 100644 index 0000000..59bc657 --- /dev/null +++ b/frontend/src/.env.jest.example @@ -0,0 +1,2 @@ +TEST_SCREENSHOTONE_API_KEY= +TEST_ROOT_PATH= diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4680208..f0d86fc 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,8 +1,8 @@ -import { useRef, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import ImageUpload from "./components/ImageUpload"; import CodePreview from "./components/CodePreview"; import Preview from "./components/Preview"; -import { CodeGenerationParams, generateCode } from "./generateCode"; +import { generateCode } from "./generateCode"; import Spinner from "./components/Spinner"; import classNames from "classnames"; import { @@ -18,14 +18,7 @@ import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "./components/ui/tabs"; import SettingsDialog from "./components/SettingsDialog"; -import { - Settings, - EditorTheme, - AppState, - CSSOption, - OutputSettings, - JSFrameworkOption, -} from "./types"; +import { AppState, CodeGenerationParams, EditorTheme, Settings } from "./types"; import { IS_RUNNING_ON_CLOUD } from "./config"; import { PicoBadge } from "./components/PicoBadge"; import { OnboardingNote } from "./components/OnboardingNote"; @@ -36,34 +29,86 @@ import html2canvas from "html2canvas"; import { USER_CLOSE_WEB_SOCKET_CODE } from "./constants"; import CodeTab from "./components/CodeTab"; import OutputSettingsSection from "./components/OutputSettingsSection"; +import { History } from "./components/history/history_types"; +import HistoryDisplay from "./components/history/HistoryDisplay"; +import { extractHistoryTree } from "./components/history/utils"; +import toast from "react-hot-toast"; +import ImportCodeSection from "./components/ImportCodeSection"; +import { Stack } from "./lib/stacks"; +import { CodeGenerationModel } from "./lib/models"; +import ModelSettingsSection from "./components/ModelSettingsSection"; +import { extractHtml } from "./components/preview/extractHtml"; +import useBrowserTabIndicator from "./hooks/useBrowserTabIndicator"; +import TipLink from "./components/core/TipLink"; + +const IS_OPENAI_DOWN = false; function App() { const [appState, setAppState] = useState(AppState.INITIAL); const [generatedCode, setGeneratedCode] = useState(""); + + const [inputMode, setInputMode] = useState<"image" | "video">("image"); + const [referenceImages, setReferenceImages] = useState([]); const [executionConsole, setExecutionConsole] = useState([]); const [updateInstruction, setUpdateInstruction] = useState(""); - const [history, setHistory] = useState([]); + const [isImportedFromCode, setIsImportedFromCode] = useState(false); + + // Settings const [settings, setSettings] = usePersistedState( { openAiApiKey: null, + openAiBaseURL: null, + anthropicApiKey: null, screenshotOneApiKey: null, isImageGenerationEnabled: true, editorTheme: EditorTheme.COBALT, + generatedCodeConfig: Stack.HTML_TAILWIND, + codeGenerationModel: CodeGenerationModel.GPT_4O_2024_05_13, + // Only relevant for hosted version isTermOfServiceAccepted: false, - accessCode: null, }, "setting" ); - const [outputSettings, setOutputSettings] = useState({ - css: CSSOption.TAILWIND, - js: JSFrameworkOption.NO_FRAMEWORK, - }); + + // Code generation model from local storage or the default value + const selectedCodeGenerationModel = + settings.codeGenerationModel || CodeGenerationModel.GPT_4_VISION; + + // App history + const [appHistory, setAppHistory] = useState([]); + // Tracks the currently shown version from app history + const [currentVersion, setCurrentVersion] = useState(null); + const [shouldIncludeResultImage, setShouldIncludeResultImage] = useState(false); const wsRef = useRef(null); + const showReactWarning = + selectedCodeGenerationModel === + CodeGenerationModel.GPT_4_TURBO_2024_04_09 && + settings.generatedCodeConfig === Stack.REACT_TAILWIND; + + const showGpt4OMessage = + selectedCodeGenerationModel !== CodeGenerationModel.GPT_4O_2024_05_13 && + appState === AppState.INITIAL; + + // Indicate coding state using the browser tab's favicon and title + useBrowserTabIndicator(appState === AppState.CODING); + + // When the user already has the settings in local storage, newly added keys + // do not get added to the settings so if it's falsy, we populate it with the default + // value + useEffect(() => { + if (!settings.generatedCodeConfig) { + setSettings((prev) => ({ + ...prev, + generatedCodeConfig: Stack.HTML_TAILWIND, + })); + } + }, [settings.generatedCodeConfig, setSettings]); + const takeScreenshot = async (): Promise => { const iframeElement = document.querySelector( "#preview-desktop" @@ -99,66 +144,187 @@ function App() { setGeneratedCode(""); setReferenceImages([]); setExecutionConsole([]); - setHistory([]); + setUpdateInstruction(""); + setIsImportedFromCode(false); + setAppHistory([]); + setCurrentVersion(null); + setShouldIncludeResultImage(false); }; - const stop = () => { + const regenerate = () => { + if (currentVersion === null) { + toast.error( + "No current version set. Please open a Github issue as this shouldn't happen." + ); + return; + } + + // Retrieve the previous command + const previousCommand = appHistory[currentVersion]; + if (previousCommand.type !== "ai_create") { + toast.error("Only the first version can be regenerated."); + return; + } + + // Re-run the create + doCreate(referenceImages, inputMode); + }; + + const cancelCodeGeneration = () => { wsRef.current?.close?.(USER_CLOSE_WEB_SOCKET_CODE); // make sure stop can correct the state even if the websocket is already closed - setAppState(AppState.CODE_READY); + cancelCodeGenerationAndReset(); }; - function doGenerateCode(params: CodeGenerationParams) { + const previewCode = + inputMode === "video" && appState === AppState.CODING + ? extractHtml(generatedCode) + : generatedCode; + + const cancelCodeGenerationAndReset = () => { + // When this is the first version, reset the entire app state + if (currentVersion === null) { + reset(); + } else { + // Otherwise, revert to the last version + setGeneratedCode(appHistory[currentVersion].code); + setAppState(AppState.CODE_READY); + } + }; + + function doGenerateCode( + params: CodeGenerationParams, + parentVersion: number | null + ) { setExecutionConsole([]); setAppState(AppState.CODING); // Merge settings with params - const updatedParams = { ...params, ...settings, outputSettings }; + const updatedParams = { ...params, ...settings }; generateCode( wsRef, updatedParams, + // On change (token) => setGeneratedCode((prev) => prev + token), - (code) => setGeneratedCode(code), + // On set code + (code) => { + setGeneratedCode(code); + if (params.generationType === "create") { + setAppHistory([ + { + type: "ai_create", + parentIndex: null, + code, + inputs: { image_url: referenceImages[0] }, + }, + ]); + setCurrentVersion(0); + } else { + setAppHistory((prev) => { + // Validate parent version + if (parentVersion === null) { + toast.error( + "No parent version set. Contact support or open a Github issue." + ); + return prev; + } + + const newHistory: History = [ + ...prev, + { + type: "ai_edit", + parentIndex: parentVersion, + code, + inputs: { + prompt: updateInstruction, + }, + }, + ]; + setCurrentVersion(newHistory.length - 1); + return newHistory; + }); + } + }, + // On status update (line) => setExecutionConsole((prev) => [...prev, line]), - () => setAppState(AppState.CODE_READY) + // On cancel + () => { + cancelCodeGenerationAndReset(); + }, + // On complete + () => { + setAppState(AppState.CODE_READY); + } ); } // Initial version creation - function doCreate(referenceImages: string[]) { + function doCreate(referenceImages: string[], inputMode: "image" | "video") { // Reset any existing state reset(); setReferenceImages(referenceImages); + setInputMode(inputMode); if (referenceImages.length > 0) { - doGenerateCode({ - generationType: "create", - image: referenceImages[0], - }); + doGenerateCode( + { + generationType: "create", + image: referenceImages[0], + inputMode, + }, + currentVersion + ); } } // Subsequent updates async function doUpdate() { - const updatedHistory = [...history, generatedCode, updateInstruction]; - if (shouldIncludeResultImage) { - const resultImage = await takeScreenshot(); - doGenerateCode({ - generationType: "update", - image: referenceImages[0], - resultImage: resultImage, - history: updatedHistory, - }); - } else { - doGenerateCode({ - generationType: "update", - image: referenceImages[0], - history: updatedHistory, - }); + if (currentVersion === null) { + toast.error( + "No current version set. Contact support or open a Github issue." + ); + return; + } + + let historyTree; + try { + historyTree = extractHistoryTree(appHistory, currentVersion); + } catch { + toast.error( + "Version history is invalid. This shouldn't happen. Please contact support or open a Github issue." + ); + return; + } + + const updatedHistory = [...historyTree, updateInstruction]; + + if (shouldIncludeResultImage) { + const resultImage = await takeScreenshot(); + doGenerateCode( + { + generationType: "update", + inputMode, + image: referenceImages[0], + resultImage: resultImage, + history: updatedHistory, + isImportedFromCode, + }, + currentVersion + ); + } else { + doGenerateCode( + { + generationType: "update", + inputMode, + image: referenceImages[0], + history: updatedHistory, + isImportedFromCode, + }, + currentVersion + ); } - setHistory(updatedHistory); setGeneratedCode(""); setUpdateInstruction(""); } @@ -170,8 +336,41 @@ function App() { })); }; + function setStack(stack: Stack) { + setSettings((prev) => ({ + ...prev, + generatedCodeConfig: stack, + })); + } + + function setCodeGenerationModel(codeGenerationModel: CodeGenerationModel) { + setSettings((prev) => ({ + ...prev, + codeGenerationModel, + })); + } + + function importFromCode(code: string, stack: Stack) { + setIsImportedFromCode(true); + + // Set up this project + setGeneratedCode(code); + setStack(stack); + setAppHistory([ + { + type: "code_create", + parentIndex: null, + code, + inputs: { code }, + }, + ]); + setCurrentVersion(0); + + setAppState(AppState.CODE_READY); + } + return ( -
+
{IS_RUNNING_ON_CLOUD && } {IS_RUNNING_ON_CLOUD && ( )} -
-
+

Screenshot to Code

setStack(config)} shouldDisableUpdates={ appState === AppState.CODING || appState === AppState.CODE_READY } /> + + + {showReactWarning && ( +
+ Sorry - React is not currently working with GPT-4 Turbo. Please + use GPT-4 Vision or Claude Sonnet. We are working on a fix. +
+ )} + + {showGpt4OMessage && ( +
+

+ Now supporting GPT-4o. Higher quality and 2x faster. Give it a + try! +

+
+ )} + + {appState !== AppState.CODE_READY && } + {IS_RUNNING_ON_CLOUD && !settings.openAiApiKey && } + {IS_OPENAI_DOWN && ( +
+ OpenAI API is currently down. Try back in 30 minutes or later. We + apologize for the inconvenience. +
+ )} + {(appState === AppState.CODING || appState === AppState.CODE_READY) && ( <> {/* Show code preview only when coding */} {appState === AppState.CODING && (
+ {/* Speed disclaimer for video mode */} + {inputMode === "video" && ( +
+ Code generation from videos can take 3-4 minutes. We do + multiple passes to get the best result. Please be patient. +
+ )} +
{executionConsole.slice(-1)[0]}
-
-
-
)} @@ -225,52 +475,69 @@ function App() { value={updateInstruction} />
-
+
Include screenshot of current version?
- +
-
+
-
+
+ +
)} {/* Reference image display */}
-
-
- Reference + {referenceImages.length > 0 && ( +
+
+ {inputMode === "image" && ( + Reference + )} + {inputMode === "video" && ( +
+
+ {inputMode === "video" + ? "Original Video" + : "Original Screenshot"} +
-
- Original Screenshot -
-
+ )}

Console @@ -287,6 +554,23 @@ function App() {

)} + { + { + if ( + index < 0 || + index >= appHistory.length || + !appHistory[index] + ) + return; + setCurrentVersion(index); + setGeneratedCode(appHistory[index].code); + }} + shouldDisableReverts={appState === AppState.CODING} + /> + }
@@ -298,35 +582,58 @@ function App() { doCreate={doCreate} screenshotOneApiKey={settings.screenshotOneApiKey} /> +
)} {(appState === AppState.CODING || appState === AppState.CODE_READY) && (
-
- - - Desktop - - - Mobile - - - - Code - - +
+
+ {appState === AppState.CODE_READY && ( + <> + + + + )} +
+
+ + + Desktop + + + Mobile + + + + Code + + +
- + - + diff --git a/frontend/src/components/ImageUpload.tsx b/frontend/src/components/ImageUpload.tsx index 6cd45b7..bbecd92 100644 --- a/frontend/src/components/ImageUpload.tsx +++ b/frontend/src/components/ImageUpload.tsx @@ -1,7 +1,12 @@ -import { useState, useEffect, useMemo, useCallback } from "react"; +import { useState, useEffect, useMemo } from "react"; +// useCallback import { useDropzone } from "react-dropzone"; // import { PromptImage } from "../../../types"; import { toast } from "react-hot-toast"; +import { URLS } from "../urls"; +import { Badge } from "./ui/badge"; +import ScreenRecorder from "./recording/ScreenRecorder"; +import { ScreenRecorderState } from "../types"; const baseStyle = { flex: 1, @@ -50,19 +55,31 @@ type FileWithPreview = { } & File; interface Props { - setReferenceImages: (referenceImages: string[]) => void; + setReferenceImages: ( + referenceImages: string[], + inputMode: "image" | "video" + ) => void; } function ImageUpload({ setReferenceImages }: Props) { const [files, setFiles] = useState([]); + // TODO: Switch to Zustand + const [screenRecorderState, setScreenRecorderState] = + useState(ScreenRecorderState.INITIAL); + const { getRootProps, getInputProps, isFocused, isDragAccept, isDragReject } = useDropzone({ maxFiles: 1, - maxSize: 1024 * 1024 * 5, // 5 MB + maxSize: 1024 * 1024 * 20, // 20 MB accept: { + // Image formats "image/png": [".png"], "image/jpeg": [".jpeg"], "image/jpg": [".jpg"], + // Video formats + "video/quicktime": [".mov"], + "video/mp4": [".mp4"], + "video/webm": [".webm"], }, onDrop: (acceptedFiles) => { // Set up the preview thumbnail images @@ -77,7 +94,14 @@ function ImageUpload({ setReferenceImages }: Props) { // Convert images to data URLs and set the prompt images state Promise.all(acceptedFiles.map((file) => fileToDataURL(file))) .then((dataUrls) => { - setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string)); + if (dataUrls.length > 0) { + setReferenceImages( + dataUrls.map((dataUrl) => dataUrl as string), + (dataUrls[0] as string).startsWith("data:video") + ? "video" + : "image" + ); + } }) .catch((error) => { toast.error("Error reading files" + error); @@ -89,39 +113,39 @@ function ImageUpload({ setReferenceImages }: Props) { }, }); - const pasteEvent = useCallback( - (event: ClipboardEvent) => { - const clipboardData = event.clipboardData; - if (!clipboardData) return; + // const pasteEvent = useCallback( + // (event: ClipboardEvent) => { + // const clipboardData = event.clipboardData; + // if (!clipboardData) return; - const items = clipboardData.items; - const files = []; - for (let i = 0; i < items.length; i++) { - const file = items[i].getAsFile(); - if (file && file.type.startsWith("image/")) { - files.push(file); - } - } + // const items = clipboardData.items; + // const files = []; + // for (let i = 0; i < items.length; i++) { + // const file = items[i].getAsFile(); + // if (file && file.type.startsWith("image/")) { + // files.push(file); + // } + // } - // Convert images to data URLs and set the prompt images state - Promise.all(files.map((file) => fileToDataURL(file))) - .then((dataUrls) => { - if (dataUrls.length > 0) { - setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string)); - } - }) - .catch((error) => { - // TODO: Display error to user - console.error("Error reading files:", error); - }); - }, - [setReferenceImages] - ); + // // Convert images to data URLs and set the prompt images state + // Promise.all(files.map((file) => fileToDataURL(file))) + // .then((dataUrls) => { + // if (dataUrls.length > 0) { + // setReferenceImages(dataUrls.map((dataUrl) => dataUrl as string)); + // } + // }) + // .catch((error) => { + // // TODO: Display error to user + // console.error("Error reading files:", error); + // }); + // }, + // [setReferenceImages] + // ); // TODO: Make sure we don't listen to paste events in text input components - useEffect(() => { - window.addEventListener("paste", pasteEvent); - }, [pasteEvent]); + // useEffect(() => { + // window.addEventListener("paste", pasteEvent); + // }, [pasteEvent]); useEffect(() => { return () => files.forEach((file) => URL.revokeObjectURL(file.preview)); @@ -139,15 +163,34 @@ function ImageUpload({ setReferenceImages }: Props) { return (
- {/* eslint-disable-next-line @typescript-eslint/no-explicit-any */} -
- -

- Drag & drop a screenshot here,
- or paste from clipboard,
- or click to upload -

-
+ {screenRecorderState === ScreenRecorderState.INITIAL && ( + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ +
+ +

+ Drag & drop a screenshot here,
+ or click to upload +

+
+ )} + {screenRecorderState === ScreenRecorderState.INITIAL && ( +
+ New! Upload a screen recording (.mp4, .mov) or record + your screen to clone a whole app (experimental).{" "} + + Learn more. + +
+ )} +
); } diff --git a/frontend/src/components/ImportCodeSection.tsx b/frontend/src/components/ImportCodeSection.tsx new file mode 100644 index 0000000..c31e753 --- /dev/null +++ b/frontend/src/components/ImportCodeSection.tsx @@ -0,0 +1,76 @@ +import { useState } from "react"; +import { Button } from "./ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "./ui/dialog"; +import { Textarea } from "./ui/textarea"; +import OutputSettingsSection from "./OutputSettingsSection"; +import toast from "react-hot-toast"; +import { Stack } from "../lib/stacks"; + +interface Props { + importFromCode: (code: string, stack: Stack) => void; +} + +function ImportCodeSection({ importFromCode }: Props) { + const [code, setCode] = useState(""); + const [stack, setStack] = useState(undefined); + + const doImport = () => { + if (code === "") { + toast.error("Please paste in some code"); + return; + } + + if (stack === undefined) { + toast.error("Please select your stack"); + return; + } + + importFromCode(code, stack); + }; + return ( + + + + + + + Paste in your HTML code + + Make sure that the code you're importing is valid HTML. + + + +