diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..5003a45 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +.vscode +.git +.gitignore +.husky +*.env + +logs/* +tests/* + +Dockerfile +docker-compose.yml + +ecosystem.config.json \ No newline at end of file diff --git a/.example.env b/.example.env index 22ca934..c30f759 100644 --- a/.example.env +++ b/.example.env @@ -1,2 +1,4 @@ -API_KEY="" # https://300.ya.ru/ -> API -> Получить токен -> Войти как ... -YANDEX_COOKIE="Session_id=XXXX;" # !!! Required Session_id COOKIE! You can log in to your account and then from any request to yandex catch them. AT YOUR OWN RISK !!! \ No newline at end of file +SERVICE_PORT=3312 +USE_WORKER="false" +API_TOKEN="" # For get sharing url +# SESSION_ID_COOKIE="XXXX" # If lib YaHMAC is invalid you can set your cookies and summarize articles/text (video requires valid YaHMAC) \ No newline at end of file diff --git a/.gitignore b/.gitignore index fae4a0c..79475e0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,134 +1,14 @@ -# Editor -.vscode - -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# C extensions -*.so - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -logs/ -backups/ -parts/ -sdist/ -var/ -wheels/ -pip-wheel-metadata/ -share/python-wheels/ -*.egg-info/ -.installed.cfg -*.egg -MANIFEST - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -*.py,cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# PEP 582; used by e.g. github.com/David-OConnor/pyflow -__pypackages__/ - -# Celery stuff -celerybeat-schedule -celerybeat.pid - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ +.vscode +.env + +# dependencies +/node_modules + +# local env files +.env.local +.env.development.local +.env.test.local +.env.production.local + +**/*.log +**/*.bun \ No newline at end of file diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..5ba948b --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,2 @@ + +bunx pretty-quick --staged \ No newline at end of file diff --git a/.oxlintignore b/.oxlintignore new file mode 100644 index 0000000..790a529 --- /dev/null +++ b/.oxlintignore @@ -0,0 +1,3 @@ +**/*.d.ts +ecosystem.config.js +eslint.config.js \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 0000000..a94c25d --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "printWidth": 100, + "tabWidth": 2, + "semi": true, + "trailingComma": "all" +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..bf4b2d3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM oven/bun:latest AS base +WORKDIR /usr/src/app + +FROM base AS release +COPY package.json bun.lock tsconfig.json ./ +COPY src src +RUN bun install + +ENV SERVICE_PORT=3312 +# ENV USE_WORKER="true" +# ENV WORKER_HOST="http://127.0.0.1:7674/browser" +# ENV WORKER_HOST_TH="http://127.0.0.1:7674/th" +# ENV API_TOKEN="" + +ENTRYPOINT [ "bun", "run", "start" ] \ No newline at end of file diff --git a/LICENSE b/LICENSE index ec3080d..4c98a9f 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2023 FOSWLY - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +MIT License + +Copyright (c) 2023 FOSWLY + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index df8a511..e4e687e 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,76 @@ -## [FOSWLY] Summarize Backend +## Summarize Backend -[![Python Version](https://img.shields.io/badge/Python-3.11-blue?logo=python&style=for-the-badge)](https://www.python.org/) [![GitHub Stars](https://img.shields.io/github/stars/FOSWLY/summarize-backend?logo=github&style=for-the-badge)](https://github.com/FOSWLY/summarize-backend/stargazers) [![GitHub Issues](https://img.shields.io/github/issues/FOSWLY/summarize-backend?style=for-the-badge)](https://github.com/FOSWLY/summarize-backend/issues) [![Current Version](https://img.shields.io/github/v/release/FOSWLY/summarize-backend?style=for-the-badge)](https://github.com/FOSWLY/summarize-backend) [![GitHub License](https://img.shields.io/github/license/FOSWLY/summarize-backend?style=for-the-badge)](https://github.com/FOSWLY/summarize-backend/blob/master/LICENSE) -**[FOSWLY] Summarize Backend** - cервер, который реализует Yandex Summarize API для нашего браузерного расширения. Сервер не содержит никакой авторизации и может быть использован для ваших проектов. +**Summarize Backend** - cервер, унифицированные конечные точки для API суммаризации из библиотеки [@toil/neurojs](https://github.com/FOSWLY/neurojs). ## 📝 Функционал -- Суммаризатор статей -- Суммаризатор видео + +- Суммаризация статей +- Суммаризация текста +- Суммаризация видео +- Получение ссылки на суммаризацию статей (необходимо указать токен к оф. апи) +- Получение суммаризации по токену (`https://300.ya.ru/TOKEN`) ## 📦 Деплой -1. Установите Python 3.11 (на других версиях не тестировался) -2. Клонируйте репозиторий -3. Установите зависимости: `pip install -r requirements.txt` -4. Заполните конфиг: `app/settings.py` -5. Переименуйте `.example.env` --> `.env` -6. Заполните: `.env` -7. Запустите сервер: `python3 -OO main.py` - -## ⚙️ Заполнение .env -1. Переименуйте `.example.env` в `.env` -2. Получение `API_KEY`: - 1. Перейдите на сайт [300.ya.ru](https://300.ya.ru/) - 2. Внизу нажмите на "API" - 3. В появившейся панельке нажмите "Получить токен" - 4. Авторизуйтесь, если вы не авторизованы - 5. Вставьте полученный токен в `.env` -3. Получение `YANDEX_COOKIE`: - 1. Перейдите на сайт [300.ya.ru](https://300.ya.ru/) - 2. Авторизуйтесь, если вы не авторизованы. Лучше всего использовать не основной аккаунт, поскольку **все действия выполняются на ваш страх и риск**. - 3. Откройте DevTools (F12 или Ctrl+Shift+I) - 4. Перейдите в Application - - P.S. В некоторых браузерах этого пункта нету. В них вы должны сразу перейти в Storage. - 5. Выберите Storage - 6. Выберите Cookies - 7. Найдите куки с именем `Session_id` и скопируйте ее значение - 8. Вставьте скопированное значение заместо XXXX в `.env` - -## 📖 Зачем нужен свой сервер, почему бы не использовать API напрямую? -В использование API напрямую есть несколько проблемы из-за которых мы от этого отказались: -1. Для работы с Yandex Summarize API нужна авторизация, т.е. необходимо посылать запросы с токеном/куки, которые не хотелось бы лишний раз "палить" на клиенте. -2. У некоторых пользователей могут быть заблокированы сервера Yandex и прямой запрос бы просто не прошёл. \ No newline at end of file + +### С Docker + +1. Установите Docker +2. Соберите образ + +```bash +docker build -t "summarize-backend" . +``` + +3. Запустите контейнер + +```bash +docker run -p 3312:3312 summarize-backend +``` + +### Вручную + +1. Установите [Bun](https://bun.sh/) +2. Клонируйте репозиторий: + +```bash +git clone https://github.com/FOSWLY/summarize-backend +``` + +3. Установите зависимости + +```bash +bun install +``` + +3.1. (опционально) Переименуйте `.example.env` в `.env` и заполните необходимые поля + +4. Запустите сервер + +```bash +bun start +``` + +Если вы хотите использовать PM2: + +1. Установите зависимости: + +```bash +bun install -g pm2-beta && pm2 install pm2-logrotate +``` + +2. Запустите сервер + +```bash +pm2 start ecosystem.config.json +``` + +## 📖 Кому это будет полезно + +1. Если вы хотите использовать логику из [neurojs](https://github.com/FOSWLY/neurojs) с помощью другого языка программирования, но не хотите переносить весь функционал в ваш код +2. Если вы не хотите тянуть зависимости от neurojs +3. Если вы хотите иметь простой унифицированный апи diff --git a/api/summarize.py b/api/summarize.py deleted file mode 100644 index 8c6c950..0000000 --- a/api/summarize.py +++ /dev/null @@ -1,53 +0,0 @@ -import logging -import httpx - -from fastapi import HTTPException, status - -from core.settings import get_settings - -settings = get_settings() - - -class YandexSummarize: - def __init__(self) -> None: - self.logger = logging.getLogger(__name__) - self.domain: str = '300.ya.ru' - self.headers: dict = { - "Content-Type": "application/json", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.5845.967 YaBrowser/23.9.1.967 Yowser/2.5 Safari/537.36", - "Referer": f'https://{self.domain}/summary', - "Origin": f'https://{self.domain}', - "pragma": "no-cache", - "cache-control": "no-cache" - } - - async def request(self, endpoint: str, body: dict, headers: dict): - async with httpx.AsyncClient(http2=True) as client: - r = await client.post( - f'https://{self.domain}/api/{endpoint}', - json = body, - headers = headers - ) - self.logger.debug(f'POST /api/{endpoint}: {r.status_code}') - if r.status_code != status.HTTP_200_OK: - self.logger.error(f'API answer: {r.read()}') - raise HTTPException( - status_code=status.HTTP_503_SERVICE_UNAVAILABLE, - detail='Unable to access Yandex API', - ) - return r.json() - - async def get_sharing_url(self, data: dict): - headers = self.headers.copy() - headers['Authorization'] = f'OAuth {settings.api_key}' - return await self.request('sharing-url', data, headers) - - async def get_sharing_data(self, data: dict): - headers = self.headers.copy() - headers['Cookie'] = settings.yandex_cookie - return await self.request('sharing', data, headers) - - async def generation(self, data: dict): - headers = self.headers.copy() - headers['Cookie'] = settings.yandex_cookie - return await self.request('generation', data, headers) \ No newline at end of file diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..6b84ed3 --- /dev/null +++ b/bun.lock @@ -0,0 +1,394 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "summarize-backend", + "dependencies": { + "@elysiajs/swagger": "^1.2.0", + "@toil/neurojs": "^1.1.1", + "elysia": "1.2.6", + "elysia-http-status-code": "^1.0.9", + "pino": "^9.5.0", + "pino-loki": "^2.4.0", + }, + "devDependencies": { + "@sinclair/typebox": "^0.34.13", + "bun-types": "^1.1.41", + "eslint": "^9.17.0", + "eslint-plugin-oxlint": "^0.15.10", + "husky": "^9.1.7", + "oxlint": "^0.15.10", + "pino-pretty": "^13.0.0", + "typescript-eslint": "^8.18.1", + }, + "peerDependencies": { + "typescript": "^5.6.3", + }, + }, + }, + "packages": { + "@bufbuild/protobuf": ["@bufbuild/protobuf@2.2.3", "", {}, "sha512-tFQoXHJdkEOSwj5tRIZSPNUuXK3RaR7T1nUrPgbYX1pUbvqqaaZAsfo+NXBPsz5rZMSKVFrgK1WL8Q/MSLvprg=="], + + "@elysiajs/swagger": ["@elysiajs/swagger@1.2.0", "", { "dependencies": { "@scalar/themes": "^0.9.52", "@scalar/types": "^0.0.12", "openapi-types": "^12.1.3", "pathe": "^1.1.2" }, "peerDependencies": { "elysia": ">= 1.2.0" } }, "sha512-OPx93DP6rM2VHjA3D44Xiz5MYm9AYlO2NGWPsnSsdyvaOCiL9wJj529583h7arX4iIEYE5LiLB0/A45unqbopw=="], + + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="], + + "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="], + + "@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="], + + "@eslint/core": ["@eslint/core@0.11.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA=="], + + "@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="], + + "@eslint/js": ["@eslint/js@9.20.0", "", {}, "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ=="], + + "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="], + + "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.5", "", { "dependencies": { "@eslint/core": "^0.10.0", "levn": "^0.4.1" } }, "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A=="], + + "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="], + + "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="], + + "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="], + + "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.1", "", {}, "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA=="], + + "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + + "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + + "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + + "@oxlint/darwin-arm64": ["@oxlint/darwin-arm64@0.15.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-cSW5LCqoHAp+zvKNUmzvKXzvh90o0J50HOJj7HARXWes/fqKQ2U2NX36Grc19lOxhP5ItoNeZN6x88opPdVtDw=="], + + "@oxlint/darwin-x64": ["@oxlint/darwin-x64@0.15.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-6iisoeMdIGBvga+dKe6UnAH8jN58lkbwApQh0IAJzSpkS9B0MPFFy2LjT9qq6J4WyHWh8oYnDJhNOJVBGynApQ=="], + + "@oxlint/linux-arm64-gnu": ["@oxlint/linux-arm64-gnu@0.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-3zmkAYm309ZWf0Af3YiQMbx2kV8SKRThyaw32x65NvZje/RfnqDSaUJ/juT32DyWNGgRSI2KaWExbbVKZGj6Bw=="], + + "@oxlint/linux-arm64-musl": ["@oxlint/linux-arm64-musl@0.15.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-kj6t4GxNfYvSkC9HbdnQFyK1AXXmWN+d53lyDVWUKByRNAOLn6hBrzl9WByJ6ZGCTwTOyrkqu1Om4itlILqodA=="], + + "@oxlint/linux-x64-gnu": ["@oxlint/linux-x64-gnu@0.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-1RNUw+nWjv8EXI1wa6A4oc/UXwdCk4l29y3JgCZ7s1aPdZhn3sWLng0SFVruZAf5QFY9bxKS2ffr1s84T1uXhQ=="], + + "@oxlint/linux-x64-musl": ["@oxlint/linux-x64-musl@0.15.11", "", { "os": "linux", "cpu": "x64" }, "sha512-7uUD13t5WUg7TrZlViW0oYwg2npwoFvzA+1wOPtDu9Kyy24WggUIg8dAExTb5OFkj5jxKKAT17EcvtSNxxLdww=="], + + "@oxlint/win32-arm64": ["@oxlint/win32-arm64@0.15.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-6tMc5UYWGwzxa+AsgNQGFktoqewkdV5pmMXlQboGIOUWYYIQfl2/X7owbv+3y3n7EmO7EBniIB2G/5m8teDzGQ=="], + + "@oxlint/win32-x64": ["@oxlint/win32-x64@0.15.11", "", { "os": "win32", "cpu": "x64" }, "sha512-etEXnRNT3Lep/jAvBxgFqHGGAZTnjvRNKSOKSQ0jFNTIzAhaqCpJrH25LKuvIqDPTDOPNA5DjmfuT2AFNhAI0g=="], + + "@scalar/openapi-types": ["@scalar/openapi-types@0.1.1", "", {}, "sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg=="], + + "@scalar/themes": ["@scalar/themes@0.9.66", "", { "dependencies": { "@scalar/types": "0.0.32" } }, "sha512-Fm2dUlIQoWCG83yZ2QNdIG7j+3eHgmSQHSnGOfd59+XIC/JxmCVbiOCYyhzfCXl1Zb8YcPlu6Ka2wY++GlrEeQ=="], + + "@scalar/types": ["@scalar/types@0.0.12", "", { "dependencies": { "@scalar/openapi-types": "0.1.1", "@unhead/schema": "^1.9.5" } }, "sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.25", "", {}, "sha512-gu+tdy9WZIRulrR4CAcGXZAAixwakKszkUXudMJ4EhtNflBEify5Pm5vnVEVqdmMkxnT4tcdfJps5XYqaNeF9Q=="], + + "@toil/neurojs": ["@toil/neurojs@1.1.1", "", { "dependencies": { "@bufbuild/protobuf": "^2.2.3", "@vot.js/core": "^2.2.5", "@vot.js/shared": "^2.2.5" }, "peerDependencies": { "typescript": "^5.7.3" } }, "sha512-wbiGHrxOAOMswKYOL1xJufjDxC3G51tSVPPtQr0Tk1r4DzYuoh66s1LlQVSNKCIGTqYkVp6DchpFFOGceizKog=="], + + "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="], + + "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], + + "@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="], + + "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="], + + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/type-utils": "8.24.0", "@typescript-eslint/utils": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ=="], + + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA=="], + + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0" } }, "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw=="], + + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.0", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.0", "@typescript-eslint/utils": "8.24.0", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA=="], + + "@typescript-eslint/types": ["@typescript-eslint/types@8.24.0", "", {}, "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw=="], + + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "@typescript-eslint/visitor-keys": "8.24.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ=="], + + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.0", "@typescript-eslint/types": "8.24.0", "@typescript-eslint/typescript-estree": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ=="], + + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.0", "", { "dependencies": { "@typescript-eslint/types": "8.24.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg=="], + + "@unhead/schema": ["@unhead/schema@1.11.19", "", { "dependencies": { "hookable": "^5.5.3", "zhead": "^2.2.4" } }, "sha512-7VhYHWK7xHgljdv+C01MepCSYZO2v6OhgsfKWPxRQBDDGfUKCUaChox0XMq3tFvXP6u4zSp6yzcDw2yxCfVMwg=="], + + "@vot.js/core": ["@vot.js/core@2.2.6", "", { "dependencies": { "@vot.js/shared": "^2.2.6" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-cUrvCNlkc2FfmEZZ5Xt1A0Qisc8LnrbP9rcMAH+pMRDtvfX5dPfw+vLNTDu1RBw54tNJaGs5xWWsFlOYHN+P6g=="], + + "@vot.js/shared": ["@vot.js/shared@2.2.6", "", { "dependencies": { "@bufbuild/protobuf": "^2.0.0" }, "peerDependencies": { "typescript": "^5.0.0" } }, "sha512-M4uqKlMFFDFSpzKofywUpdXo/0ZfnzhX1sfwmVDy0+ZVFOboPP7ZRRZLiOkDJlFUHGUnn7tb+FYnJ7LGhfH3lA=="], + + "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + + "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="], + + "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], + + "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="], + + "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], + + "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], + + "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], + + "cookie": ["cookie@1.0.2", "", {}, "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + + "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], + + "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], + + "elysia": ["elysia@1.2.6", "", { "dependencies": { "@sinclair/typebox": "^0.34.13", "cookie": "^1.0.2", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.2.0", "openapi-types": "^12.1.3" }, "peerDependencies": { "typescript": ">= 5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-bIGCPMB4tLA8k06Cr1Cz9TBYGJlXXuivH3iwTgUIK2hqSTyUXW/i4eXbK/E52B3AwXZytl8AUyT79LSTiMjRNw=="], + + "elysia-http-status-code": ["elysia-http-status-code@1.0.9", "", { "peerDependencies": { "elysia": "0.7.12", "typescript": "^5.0.0" } }, "sha512-ae+R6NInUsFbzqxjm+Fax36/cCHnnIL7MfQCx7kyCtl14D16ZwIaSbAiECttX96VGSZEX1KehWmVw/zdwc/XwA=="], + + "end-of-stream": ["end-of-stream@1.4.4", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q=="], + + "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], + + "eslint": ["eslint@9.20.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g=="], + + "eslint-plugin-oxlint": ["eslint-plugin-oxlint@0.15.11", "", { "dependencies": { "jsonc-parser": "^3.3.1" } }, "sha512-sQMNroPApNS85ZPyO5fWjnLVlA6aPuKk7lzP/TpKMVfpFpX/V2FzYtZm/A7RQd0N9ufcP5aKj1rL25OskhvV2g=="], + + "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="], + + "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="], + + "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="], + + "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="], + + "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], + + "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], + + "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="], + + "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="], + + "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], + + "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="], + + "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="], + + "flatted": ["flatted@3.3.2", "", {}, "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA=="], + + "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], + + "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + + "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + + "hookable": ["hookable@5.5.3", "", {}, "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ=="], + + "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + + "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], + + "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], + + "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="], + + "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="], + + "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], + + "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], + + "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], + + "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], + + "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], + + "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="], + + "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + + "memoirist": ["memoirist@0.2.0", "", {}, "sha512-DA1V11OWsKmYjgYHfT1luus0FtTjUbILfI9s5M+ckK29tBLON6GDhH5GwxDz7E1ou4Bdzm9vhbeCaRAWxwG+0g=="], + + "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], + + "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], + + "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], + + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + + "oxlint": ["oxlint@0.15.11", "", { "optionalDependencies": { "@oxlint/darwin-arm64": "0.15.11", "@oxlint/darwin-x64": "0.15.11", "@oxlint/linux-arm64-gnu": "0.15.11", "@oxlint/linux-arm64-musl": "0.15.11", "@oxlint/linux-x64-gnu": "0.15.11", "@oxlint/linux-x64-musl": "0.15.11", "@oxlint/win32-arm64": "0.15.11", "@oxlint/win32-x64": "0.15.11" }, "bin": { "oxlint": "bin/oxlint", "oxc_language_server": "bin/oxc_language_server" } }, "sha512-SvNbuA5KiGzA1/E5TCzbhC0veVFdJRQW0CfeRCUG2AKzfH2j3KkQMmBA8JwVsdOhfPMCjwomAL1xE6+RglyCCA=="], + + "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], + + "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + + "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], + + "picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="], + + "pino": ["pino@9.6.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^4.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-i85pKRCt4qMjZ1+L7sy2Ag4t1atFcdbEt76+7iRJn1g2BvsnRMGu9p8pivl9fs63M2kF/A0OacFZhTub+m/qMg=="], + + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + + "pino-loki": ["pino-loki@2.5.0", "", { "dependencies": { "commander": "^12.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.2" }, "bin": { "pino-loki": "dist/cli.cjs" } }, "sha512-/QCSukecqbjXWcVB58st0bKJFQiXg4ZEGXslnnMCMbcrDr5LT36XvkS0dy0eBvZvI8bWoh3efjLYLmN+94iLSQ=="], + + "pino-pretty": ["pino-pretty@13.0.0", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^2.4.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^3.1.1" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-cQBBIVG3YajgoUjo1FdKVRX6t9XPxwB9lcNJVD5GCnNM4Y6T12YYx8c6zEejxQsU0wrg9TwmDulcE9LR7qcJqA=="], + + "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + + "process-warning": ["process-warning@4.0.1", "", {}, "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q=="], + + "pump": ["pump@3.0.2", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw=="], + + "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + + "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + + "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="], + + "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "secure-json-parse": ["secure-json-parse@2.7.0", "", {}, "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw=="], + + "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + + "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + + "ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="], + + "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + + "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="], + + "typescript-eslint": ["typescript-eslint@8.24.0", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.0", "@typescript-eslint/parser": "8.24.0", "@typescript-eslint/utils": "8.24.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ=="], + + "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="], + + "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + + "zhead": ["zhead@2.2.4", "", {}, "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag=="], + + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], + + "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.10.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw=="], + + "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="], + + "@scalar/themes/@scalar/types": ["@scalar/types@0.0.32", "", { "dependencies": { "@scalar/openapi-types": "0.1.7", "@unhead/schema": "^1.11.11" } }, "sha512-WHMkFQw4cu1mrG4pEiTUXVBBs205kHECdLM/5F7ATI0A7Axv6G1GgofkwbyCAayUjNk82uaCXzSOgPojbq4iGQ=="], + + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], + + "@scalar/themes/@scalar/types/@scalar/openapi-types": ["@scalar/openapi-types@0.1.7", "", {}, "sha512-oOTG3JQifg55U3DhKB7WdNIxFnJzbPJe7rqdyWdio977l8IkxQTVmObftJhdNIMvhV2K+1f/bDoMQGu6yTaD0A=="], + + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="], + } +} diff --git a/changelog.md b/changelog.md index 17465bd..b46e3a5 100644 --- a/changelog.md +++ b/changelog.md @@ -1,12 +1,46 @@ -## 1.2.0 +# 2.0.0 + +> [!CAUTION] +> Эта версия не имеет совместимости с 1.x.x версиями. +> Для обновления до этой версии необходимо выполнить чистую установку + +- Сервер переписан на Bun с использованием фреймворка Elysia +- Основная логика сервера вынесена в библиотеку [@toil/neurojs](https://github.com/FOSWLY/neurojs) +- Произведена полная переработка дизайна эндпоинтов: + + - Добавлен префикс `/v2` ко всем путям. + + - Удален путь `/redoc` + + - Универсальный путь `/generation` был переосмыслен и разделен на: + + - `/summarize/video` + - `/summarize/text` + - `/summarize/article` + +- Добавлена поддержка использования [neuro-worker](https://github.com/FOSWLY/neuro-worker) +- Убраны разные варианты документации. Стандартная документация, теперь, расположена по пути `/v2/docs` +- Переменная окружения `YANDEX_COOKIE` была переименована в `SESSION_ID_COOKIE`. Теперь, она не является обязательной, и больше не требует указания части `Session_id=` в начале и `;` в конце +- Переменная окружения `API_KEY` была переименована в `API_TOKEN`. +- Добавлена возможность установить большую часть значения в конфиге через переменные окружения +- Добавлена поддержка логирования в Loki +- Добавлен докер образ + +# 1.2.0 + - Добавлена возможность суммаризации текста. (В нашем расширение этого не будет) - Обновлена структура ответа на `/health`. Теперь, в ответе так же возвращается версия нашего API сервера. - Теперь, если при ответе сервера Яндекса статус код будет отличным от 200, то будет возвращаться ошибка `{ "detail": "Unable to access Yandex API" }` с 403 статус кодом - Переработана структура настроек. Теперь, все настройки хранятся в `core/settings.py` и `.env` - Обновлены зависимости -## 1.1.0 +# 1.1.0 + - Добавлена поддержка суммаризатора видео - Обновлена структура ответов и запросов - Сервер переименован из `[FOSWLY] Summarize Articles` в `[FOSWLY] Summarize` -- Библиотека `tomli` заменена на `tomlib` (python 3.11+) \ No newline at end of file +- Библиотека `tomli` заменена на `tomlib` (python 3.11+) + +# 1.0.0 + +- Первичный релиз diff --git a/core/app.py b/core/app.py deleted file mode 100644 index 9c42ef3..0000000 --- a/core/app.py +++ /dev/null @@ -1,55 +0,0 @@ -from fastapi import FastAPI -from fastapi.staticfiles import StaticFiles -from fastapi.middleware.cors import CORSMiddleware -from fastapi.openapi.utils import get_openapi - -from core.settings import get_settings - -settings = get_settings() -tags_meta = [ - { - 'name': 'Summarize', - 'description': 'Interaction with Yandex Summarize API without any authorization or restrictions' - }, - { - 'name': 'Health', - 'description': 'Health of our servers' - } -] - -def custom_openapi(): - if app.openapi_schema: - return app.openapi_schema - openapi_schema = get_openapi( - title=settings.app_name, - version=settings.app_version, - description=settings.app_desc, - license_info = { - "name": settings.app_license, - }, - contact = { - "name": "Developer", - "url": settings.app_developer_url, - "email": settings.app_developer_email - }, - routes = app.routes, - tags = tags_meta - ) - openapi_schema["info"]["x-logo"] = { - "url": "/static/assets/logo.svg", - "altText": "logo" - } - app.openapi_schema = openapi_schema - return app.openapi_schema - - -app = FastAPI(openapi_url = '/openapi.json', docs_url = '/docs', redoc_url = '/redoc') -app.mount('/static', StaticFiles(directory = 'static'), name = 'static') -app.add_middleware( - CORSMiddleware, - allow_origins=["*"], - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) -app.openapi = custom_openapi \ No newline at end of file diff --git a/core/logger.py b/core/logger.py deleted file mode 100644 index 3fbd439..0000000 --- a/core/logger.py +++ /dev/null @@ -1,67 +0,0 @@ -import logging -import os -import sys -import rich - -from datetime import datetime -from rich.logging import RichHandler -from rich.theme import Theme -from rich.style import Style - -from core.settings import get_settings - -settings = get_settings() - -def init_logging(): - logger = logging.getLogger() - - if not os.path.isdir('./logs'): - try: - os.mkdir('./logs') - logger.info('Creating log directory') - except OSError as err: - logger.error(f'Failed to create log directory: {err}') - - if settings.log_rich_formatter: - rich_console = rich.get_console() - rich.reconfigure(tab_size = 4) - # Theme from https://github.com/Cog-Creators/Red-DiscordBot/blob/V3/develop/redbot/logging.py - rich_console.push_theme(Theme( - { - 'log.time': Style(dim = True), - 'logging.level.warning': Style(color = 'yellow'), - 'logging.level.critical': Style(color = 'white', bgcolor = 'red'), - 'logging.level.error': Style(color = 'red'), - 'logging.level.verbose': Style(color = 'magenta', italic = True, dim = True), - 'logging.level.trace': Style(color = 'white', italic = True, dim = True), - 'repr.number': Style(color = 'cyan'), - 'repr.url': Style(underline = True, italic = True, bold = False, color = 'cyan'), - } - )) - rich_console.file = sys.stdout - rich_formatter = logging.Formatter('{message}', datefmt = '[%X]', style = '{') - stdout_handler = RichHandler( - rich_tracebacks = True - ) - stdout_handler.setFormatter(rich_formatter) - else: - stdout_handler = logging.StreamHandler(sys.stdout) - - log_handlers = [stdout_handler] - if settings.log_save: - log_name = f'./logs/main{datetime.now().strftime("%Y%m%d")}.log' - filehandler = logging.FileHandler(log_name, encoding='utf-8') - filehandler.setLevel(settings.log_level) - log_handlers.append(filehandler) # type: ignore - - logging.basicConfig( - level = settings.log_level, - datefmt = '%Y-%m-%d %H:%M:%S', - format = '[{asctime}] [{levelname}] {name}: {message}', - style = '{', - handlers = log_handlers - ) - - logging.captureWarnings(True) - - return True diff --git a/core/settings.py b/core/settings.py deleted file mode 100644 index 3dd1979..0000000 --- a/core/settings.py +++ /dev/null @@ -1,33 +0,0 @@ -import logging - -from functools import lru_cache -from pydantic_settings import BaseSettings, SettingsConfigDict - - -class Settings(BaseSettings): - # - server section - - port: int = 3312 - address: str = '0.0.0.0' - - # - yandex section - - api_key: str = '' # ! DO NOT CHANGE THESE FIELDS. You need to change the values in .env - yandex_cookie: str = '' # ! DO NOT CHANGE THESE FIELDS. You need to change the values in .env - - # - logging section - - log_level: int = logging.INFO # level of logs (DEBUG, INFO, WARNING, ERROR, CRITICAL) - log_save: bool = True # save logs to file - log_rich_formatter: bool = True # format logs with rich lib - - # - app section - - app_name: str = '[FOSWLY] Summarize' - app_desc: str = '[FOSWLY] Summarize is Free Yandex Summarize API without any authorization or restrictions.' - app_version: str = '1.2.0' - app_license: str = 'MIT' - app_developer_url: str = 'https://github.com/FOSWLY/summarize-backend' - app_developer_email: str = 'toil.contact@yandex.com' - - model_config = SettingsConfigDict(env_file='.env') - -@lru_cache -def get_settings() -> Settings: - return Settings() \ No newline at end of file diff --git a/ecosystem.config.json b/ecosystem.config.json new file mode 100644 index 0000000..f973330 --- /dev/null +++ b/ecosystem.config.json @@ -0,0 +1,10 @@ +{ + "apps": [ + { + "name": "summarize-backend", + "namespace": "FOSWLY", + "script": "bun", + "args": "run start" + } + ] +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..ea76f8d --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,24 @@ +import js from "@eslint/js"; +// import globals from "globals"; +import oxlint from "eslint-plugin-oxlint"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { + ignores: ["**/*.d.ts", "ecosystem.config.js", "eslint.config.js"], + }, + js.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + ...tseslint.configs.stylisticTypeChecked, + { + languageOptions: { + ecmaVersion: "latest", + sourceType: "module", + parserOptions: { + project: true, + tsconfigDirName: import.meta.dirname, + }, + }, + }, + oxlint.configs["flat/recommended"], // oxlint should be the last one +); diff --git a/hypercorn_config.py b/hypercorn_config.py deleted file mode 100644 index 9b69a97..0000000 --- a/hypercorn_config.py +++ /dev/null @@ -1,10 +0,0 @@ -import logging - -from core.settings import get_settings - -settings = get_settings() - -accesslog = logging.getLogger('server') -errorlog = logging.getLogger('server') -loglevel = logging.getLevelName(settings.log_level) -bind = f'{settings.address}:{settings.port}' \ No newline at end of file diff --git a/main.py b/main.py deleted file mode 100644 index db35d5c..0000000 --- a/main.py +++ /dev/null @@ -1,8 +0,0 @@ -import asyncio -from hypercorn.asyncio import serve - -from server import start - -if __name__ == '__main__': - config, app = asyncio.run(start()) - asyncio.run(serve(app, config)) # type: ignore \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..3561ca4 --- /dev/null +++ b/package.json @@ -0,0 +1,41 @@ +{ + "name": "summarize-backend", + "version": "2.0.0", + "author": "Toil", + "repository": { + "type": "git", + "url": "git+https://github.com/FOSWLY/summarize-backend" + }, + "scripts": { + "dev": "bun run --watch src/index.ts", + "start": "bun src/index.ts", + "lint": "bunx oxlint --ignore-path=.oxlintignore && bunx eslint", + "prepare": "husky" + }, + "dependencies": { + "@elysiajs/swagger": "^1.2.0", + "@toil/neurojs": "^1.1.1", + "elysia": "1.2.6", + "elysia-http-status-code": "^1.0.9", + "pino": "^9.5.0", + "pino-loki": "^2.4.0" + }, + "devDependencies": { + "@sinclair/typebox": "^0.34.13", + "bun-types": "^1.1.41", + "eslint": "^9.17.0", + "eslint-plugin-oxlint": "^0.15.10", + "husky": "^9.1.7", + "oxlint": "^0.15.10", + "pino-pretty": "^13.0.0", + "typescript-eslint": "^8.18.1" + }, + "module": "src/index.js", + "bun-create": { + "start": "bun run src/index.ts" + }, + "type": "module", + "peerDependencies": { + "typescript": "^5.6.3" + } +} diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 46c0441..0000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -fastapi[all]>=0.104.0 -hypercorn>=0.14.4 -pydantic>=2.4.2 -pydantic-settings>=2.0.3 -rich>=13.4.2 -httpx>=0.24.0 \ No newline at end of file diff --git a/routes/__init__.py b/routes/__init__.py deleted file mode 100644 index 1bdda38..0000000 --- a/routes/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -from fastapi import APIRouter - -from routes.endpoints import health, summarize - -api_router = APIRouter() -api_router.include_router(summarize.router, tags = ['Summarize']) -api_router.include_router(health.router, tags = ['Health']) \ No newline at end of file diff --git a/routes/endpoints/__init__.py b/routes/endpoints/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/routes/endpoints/health.py b/routes/endpoints/health.py deleted file mode 100644 index 5c3c631..0000000 --- a/routes/endpoints/health.py +++ /dev/null @@ -1,16 +0,0 @@ -from fastapi import APIRouter - - -from core.settings import get_settings -from schemas.health import HealthResponse - -router = APIRouter() -settings = get_settings() - - -@router.get('/health', response_model=HealthResponse) -async def get_health() -> HealthResponse: - return HealthResponse( - status = 'ok', - version = settings.app_version - ) \ No newline at end of file diff --git a/routes/endpoints/summarize.py b/routes/endpoints/summarize.py deleted file mode 100644 index f5ad4e7..0000000 --- a/routes/endpoints/summarize.py +++ /dev/null @@ -1,36 +0,0 @@ -import logging -from fastapi import APIRouter, HTTPException, status - - -from api.summarize import YandexSummarize -from schemas.summarize_requests import GenerationRequest, GetSummarizeURLRequest, GetSummarizeDataRequest -from schemas.summarize_responses import GetSummarizeURLResponse, GetSummarizeDataResponse, GenerationResponse - -router = APIRouter() -logger = logging.getLogger(__name__) - - -@router.post('/sharing-url', response_model=GetSummarizeURLResponse, response_model_exclude_none=True) -async def get_sharing_url(body: GetSummarizeURLRequest) -> GetSummarizeURLResponse: - return await YandexSummarize().get_sharing_url(body.model_dump()) - - -@router.post('/sharing', response_model=GetSummarizeDataResponse, response_model_exclude_none=True) -async def get_sharing_data(body: GetSummarizeDataRequest) -> GetSummarizeDataResponse: - return await YandexSummarize().get_sharing_data(body.model_dump()) - - -@router.post('/generation', response_model=GenerationResponse, response_model_exclude_none=True) -async def generation(body: GenerationRequest) -> GenerationResponse: - generation_params = body.model_dump(exclude_none=True) - if generation_params == {}: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail='At least one parameter for generation should be provided', - ) - elif 'video_url' in generation_params and 'article_url' in generation_params: - raise HTTPException( - status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, - detail='You can select only 1 parameter: article_url or video_url', - ) - return await YandexSummarize().generation(generation_params) \ No newline at end of file diff --git a/schemas/base.py b/schemas/base.py deleted file mode 100644 index 5a03d3f..0000000 --- a/schemas/base.py +++ /dev/null @@ -1,46 +0,0 @@ -from enum import Enum -from pydantic import BaseModel, Field - - -class YandexPublicStatus(str, Enum): - SUCCESS = 'success' - ERROR = 'error' - - -class YandexPublicResponse(BaseModel): - status: str = Field(examples=[ - YandexPublicStatus.SUCCESS, - YandexPublicStatus.ERROR, - ]) - - -class YandexPrivateErrorStatus(str, Enum): - PAGE_NOT_FOUND = 1 # error_code is 1 or 4 - AI_COULDNT_EXTRACT_TEXT = 2 - ARTICLE_IS_TOO_LONG = 3 - UNKNOWN_ERROR = 5 # error_code is 5 or 7 or 9 - AI_COULDNT_RETELL_ARTICLE = 6 # error_code is 6 or 8 or 11 or 12 - BROWSER_OUTDATED = 10 - VIDEO_TOO_LONG = 21 - - -class YandexPrivateStatus(str, Enum): - SUCCESS_VIDEO = 0 - IN_PROGRESS = 1 - SUCCESS = 2 - ERROR = 3 - NOT_FOUND_IN_CACHE = 4 - - -class YandexPrivateResponse(BaseModel): - status_code: int = Field(examples=[ - YandexPrivateStatus.IN_PROGRESS, - YandexPrivateStatus.SUCCESS, - YandexPrivateStatus.ERROR, - YandexPrivateStatus.NOT_FOUND_IN_CACHE - ]) # 4 - couldn't be found in the cache - - -class YandexType(str, Enum): - VIDEO = 'video' - ARTICLE = 'article' \ No newline at end of file diff --git a/schemas/health.py b/schemas/health.py deleted file mode 100644 index 03abdd9..0000000 --- a/schemas/health.py +++ /dev/null @@ -1,6 +0,0 @@ -from pydantic import BaseModel, Field - - -class HealthResponse(BaseModel): - status: str = Field(description='Returns ok if FOSWLY API is available') - version: str = Field(description='Returns version of FOSWLY API') \ No newline at end of file diff --git a/schemas/summarize_requests.py b/schemas/summarize_requests.py deleted file mode 100644 index 6d238f0..0000000 --- a/schemas/summarize_requests.py +++ /dev/null @@ -1,94 +0,0 @@ -from pydantic import BaseModel, Field - -URL_PATTERN= r'(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])' - - -class GetSummarizeURLRequest(BaseModel): - article_url: str = Field( - pattern = URL_PATTERN, - description = "Link to article" - ) - - model_config = { - "json_schema_extra": { - "examples": [ - { - "article_url": "https://toil.cc", - }, - { - "article_url": "https://bad.toil.cc", - }, - ] - } - } - - -class GetSummarizeDataRequest(BaseModel): - token: str = Field(description="Token from yandex url") - - model_config = { - "json_schema_extra": { - "examples": [ - { - "token": "hQwoyXuM", - }, - { - "token": "1", - }, - ] - } - } - - -class GenerationRequest(BaseModel): - article_url: str = Field( - pattern = URL_PATTERN, - description="Link to article", - examples=[ - "https://toil.cc", - ], - default=None - ) - - video_url: str = Field( - pattern = URL_PATTERN, - description = "Link to video", - examples=[ - "https://www.youtube.com/watch?v=1Nl5APO95Hc", - ], - default = None - ) - - text: str = Field( - min_length=300, - max_length=30000, - description="Text to summarize", - examples=[ - "Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum." - ], - default=None - ) - - session_id: str = Field( - description="Session id of the generation", - examples=[ - "77053a47-731434ec-bc52547c-dedf879a", - ], - default=None - ) - - model_config = { - "json_schema_extra": { - "examples": [ - { - "article_url": "https://toil.cc", - }, - { - "video_url": "https://www.youtube.com/watch?v=1Nl5APO95Hc", - }, - { - "session_id": "77053a47-731434ec-bc52547c-dedf879a", - }, - ] - } - } \ No newline at end of file diff --git a/schemas/summarize_responses.py b/schemas/summarize_responses.py deleted file mode 100644 index 0b37c9b..0000000 --- a/schemas/summarize_responses.py +++ /dev/null @@ -1,432 +0,0 @@ -from typing import Dict, List -from pydantic import Field, BaseModel -from schemas.base import YandexPublicResponse, YandexPublicStatus, YandexPrivateResponse, YandexPrivateStatus, YandexPrivateErrorStatus, YandexType - -URL_PATTERN= r'(http|https):\/\/([\w_-]+(?:(?:\.[\w_-]+)+))([\w.,@?^=%&:\/~+#-]*[\w@?^=%&\/~+#-])' - - -class GetSummarizeURLResponse(YandexPublicResponse): - sharing_url: str = Field( - pattern=URL_PATTERN, - description="Link to summarized article", - default=None, - ) - message: str = Field( - description="Error message descriptions", - default=None, - ) - - model_config = { - "json_schema_extra": { - "examples": [ - { - "status": YandexPublicStatus.SUCCESS, - "sharing_url": "https://300.ya.ru/hQwoyXuM", - }, - { - "status": YandexPublicStatus.ERROR, - "message": "not found", - } - ] - } - } - - -# universal -class ThesisItem(BaseModel): - id: int = Field( - description="Id of the Item", - default=None - ) - content: str = Field( - description="Content of the Item", - default=None - ) - - -class ArticleThesisItem(ThesisItem): - link: str = Field( - description="Link to item", - default=None - ) - - -class VideoSummaryKeyPoints(BaseModel): - id: int = Field( - description="Id of the Item", - default=None - ) - content: str = Field( - description="Content of the Item", - default=None - ) - start_time: int = Field( - description="Time of Item in seconds", - default=None - ) - theses: List[ThesisItem] = Field( - description="Retelling of the segment", - default=None - ) - - -class GetSummaryDataResponse(YandexPrivateResponse): - # Universal - sharing_url: str = Field( - pattern=URL_PATTERN, - description="Link to summarized article", - default=None, - ) - poll_interval_ms: int = Field( - description="The interval with which requests should be sent (in milliseconds)", - default=None - ) - normalized_url: str = Field( - description="Link to the original article", - default=None, - ) - - # Only for articles - thesis: List[ArticleThesisItem] = Field( - description="Retelling of the article", - default=None - ) - title: str = Field( - description="The title of the original site", - default=None - ) - total_parts: int = Field( - description="Count of summarized parts of article", - default=None - ) - - # Only for video - keypoints: List[VideoSummaryKeyPoints] = Field( - description="List of Thesis with times and titles", - default=None - ) - video_title: str = Field( - description="The title of the original video", - default=None - ) - type: str = Field( - # article / video - description="Type of summarize", - default=None - ) - - -class GetSummarizeDataResponse(GetSummaryDataResponse): - # for article - article_age_seconds: int = Field( - ge=0, - description="How long ago the article was retold (seconds)", - default=None - ) - # for video - summary_age_seconds: int = Field( - ge=0, - description="How long ago the video was retold (seconds)", - default=None - ) - - model_config = { - "json_schema_extra": { - "examples": [ - # article example - { - "thesis": [ - { - "id": 0, - "content": "На сайте можно найти информацию о проектах и узнать о создателе.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 1, - "content": "Проекты включают: VOT-CLI, FeimisioDonate, Toiloff Website, LZT Upgrade, Voice Over Translation, SB-MaterialAdmin/NewServer.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 2, - "content": "VOT-CLI - CLI для перевода видео с закадровым озвучиванием.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 3, - "content": "FeimisioDonate - система донатов для CS:GO серверов на Python, NodeJS, NuxtJS, FastAPI, MySQL.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 4, - "content": "Toiloff Website - личный сайт с информацией о создателе и его проектах.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 5, - "content": "LZT Upgrade - реализация полезных скриптов для форума Lolzteam на JavaScript, JQuery, Extensions.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 6, - "content": "Voice Over Translation - расширение для Yandex Browser, добавляющее закадровый перевод видео в другие браузеры.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 7, - "content": "SB-MaterialAdmin/NewServer - плагин для администраторов CS:GO серверов на SourcePawn, MySQL.", - "link": "https://toil.cc/#:~:text=..." - } - ], - "status_code": YandexPrivateStatus.SUCCESS, - "normalized_url": "https://toil.cc", - "sharing_url": "https://300.ya.ru/hQwoyXuM", - "article_age_seconds": 51, - "title": "Главная - Toiloff", - "total_parts": 1, - "type": "article" - }, - # video example - { - "keypoints": [ - { - "id": 1, - "content": "Влюбленность и ревность", - "start_time": 17, - "theses": [ - { - "id": 1, - "content": "В видео автор выражает свои чувства к другому человеку, признаваясь в любви и ревности." - }, - { - "id": 2, - "content": "Он задается вопросом, в чем его вина и почему он продолжает влюбляться в этого человека." - } - ] - }, - { - "id": 2, - "content": "Размышления о любви", - "start_time": 98, - "theses": [ - { - "id": 1, - "content": "Автор размышляет о том, что любовь может быть сложной и причинять боль, но он все равно продолжает влюбляться в этого человека." - }, - { - "id": 2, - "content": "Он предлагает забыть о любви и просто наслаждаться моментом, но в то же время понимает, что это невозможно." - } - ] - }, - { - "id": 3, - "content": "Признание в любви", - "start_time": 150, - "theses": [ - { - "id": 1, - "content": "В конце видео автор признается в любви к этому человеку, несмотря на все сложности и боль, которые они испытывают." - }, - { - "id": 2, - "content": "Он просит этого человека держаться и раздеваться, если он пришел." - } - ] - } - ], - "status_code": YandexPrivateStatus.SUCCESS_VIDEO, - "poll_interval": 500, - "normalized_url": "https://youtu.be/nr1tV3HLQhQ", - "sharing_url": "https://300.ya.ru/v_3d6661EN", - "summary_age_seconds": 148, - "video_title": "к черту любовь - speed up", - "type": "video" - }, - { - "status_code": YandexPrivateStatus.NOT_FOUND_IN_CACHE - } - ] - } - } - - -class GenerationResponse(GetSummaryDataResponse): - # status_code only for articles - status_code: int = Field(examples=[ - YandexPrivateStatus.IN_PROGRESS, - YandexPrivateStatus.SUCCESS, - YandexPrivateStatus.ERROR, - YandexPrivateStatus.NOT_FOUND_IN_CACHE - ], default=None) - - session_id: str = Field( - description="Session id of the generation", - examples=[ - "77053a47-731434ec-bc52547c-dedf879a", - ], - default=None - ) - - error_code: int = Field( - description="Error code ID (see the repository documentation)", - default=None, - examples=[ - YandexPrivateErrorStatus.PAGE_NOT_FOUND, - YandexPrivateErrorStatus.AI_COULDNT_EXTRACT_TEXT, - YandexPrivateErrorStatus.ARTICLE_IS_TOO_LONG, - YandexPrivateErrorStatus.UNKNOWN_ERROR, - YandexPrivateErrorStatus.AI_COULDNT_RETELL_ARTICLE, - YandexPrivateErrorStatus.BROWSER_OUTDATED - ] - ) - - message: str = Field( - description="Error message descriptions (only for video)", - default=None, - ) - - type: str = Field( - # video or article or text - description="Type of content (video or article or text)", - default=None, - ) - - total_parts: int = Field( - description="total_parts", - default=None - ) - - model_config = { - "json_schema_extra": { - "examples": [ - # article example - { - "thesis": [ - { - "id": 0, - "content": "На сайте можно найти информацию о проектах и узнать о создателе.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 1, - "content": "Проекты включают: VOT-CLI, FeimisioDonate, Toiloff Website, LZT Upgrade, Voice Over Translation, SB-MaterialAdmin/NewServer.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 2, - "content": "VOT-CLI - CLI для перевода видео с закадровым озвучиванием.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 3, - "content": "FeimisioDonate - система донатов для CS:GO серверов на Python, NodeJS, NuxtJS, FastAPI, MySQL.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 4, - "content": "Toiloff Website - личный сайт с информацией о создателе и его проектах.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 5, - "content": "LZT Upgrade - реализация полезных скриптов для форума Lolzteam на JavaScript, JQuery, Extensions.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 6, - "content": "Voice Over Translation - расширение для Yandex Browser, добавляющее закадровый перевод видео в другие браузеры.", - "link": "https://toil.cc/#:~:text=..." - }, - { - "id": 7, - "content": "SB-MaterialAdmin/NewServer - плагин для администраторов CS:GO серверов на SourcePawn, MySQL.", - "link": "https://toil.cc/#:~:text=..." - } - ], - "status_code": YandexPrivateStatus.SUCCESS, - "session_id": "77053a47-731434ec-bc52547c-dedf879a", - "normalized_url": "https://toil.cc", - "sharing_url": "https://300.ya.ru/hQwoyXuM", - "poll_interval_ms": 500, - "type": YandexType.ARTICLE, - "title": "Главная - Toiloff", - "total_parts": 1 - }, - # video example - { - "keypoints": [ - { - "id": 1, - "content": "Перчатки с резинкой", - "start_time": 22, - "theses": [ - { - "id": 1, - "content": "Автор рассказывает о своем лайфхаке с пришитыми резинками на перчатках, чтобы они не терялись." - } - ] - }, - { - "id": 2, - "content": "Исторический момент", - "start_time": 45, - "theses": [ - { - "id": 1, - "content": "Автор говорит о том, что он откроет крепость и расскажет об этом своим знакомым." - } - ] - }, - { - "id": 3, - "content": "Вавилон", - "start_time": 69, - "theses": [ - { - "id": 1, - "content": "Автор говорит, что он не сдастся и не успокоится, пока не построит свою крепость." - } - ] - }, - { - "id": 4, - "content": "Финальная точка", - "start_time": 69, - "theses": [ - { - "id": 1, - "content": "Автор говорит, что его крепость будет построена и он будет жить в ней, не обращая внимания на окружающих." - } - ] - } - ], - "status_code": YandexPrivateStatus.SUCCESS_VIDEO, - "session_id": "0abd5e93-d639-400a-a7cb-fd8227ca5b78", - "sharing_url": "https://300.ya.ru/v_HlUtZy1Q", - "poll_interval_ms": 500, - "video_title": "Дайте танк(!) - Крепость [speed up]", - "type": YandexType.VIDEO, - }, - # article in progress - { - "status_code": YandexPrivateStatus.IN_PROGRESS, - "session_id": "77053a47-731434ec-bc52547c-dedf879a", - "poll_interval_ms": 500, - "normalized_url": "https://toil.cc/", - "title": "Главная - Toiloff", - "type": YandexType.ARTICLE, - }, - # article error - { - "status_code": YandexPrivateStatus.ERROR, - "type": YandexType.ARTICLE, - "error_code": YandexPrivateErrorStatus.PAGE_NOT_FOUND - }, - # video error - { - "type": YandexType.VIDEO, - "message": "Not Found" - } - ] - } - } \ No newline at end of file diff --git a/server.py b/server.py deleted file mode 100644 index 1f7d1e7..0000000 --- a/server.py +++ /dev/null @@ -1,12 +0,0 @@ -from hypercorn.config import Config - -from core.app import app -from core.logger import init_logging -from routes import api_router - - -async def start(): - app.include_router(api_router) - config = Config().from_pyfile('hypercorn_config.py') - init_logging() - return config, app \ No newline at end of file diff --git a/settings.py b/settings.py deleted file mode 100644 index 8341422..0000000 --- a/settings.py +++ /dev/null @@ -1,9 +0,0 @@ -import os - -from config.load import load_env - -load_env() - -PORT = os.environ.get('PORT', 3312) -API_KEY = os.environ.get('API_KEY', '') -YANDEX_COOKIE = os.environ.get('YANDEX_COOKIE', '') \ No newline at end of file diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..ac99577 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,40 @@ +import * as path from "node:path"; + +import { Value } from "@sinclair/typebox/value"; + +import { ConfigSchema } from "@/schemas/config"; + +export default Value.Parse(ConfigSchema, { + server: { + port: Bun.env.SERVICE_PORT, + hostname: Bun.env.SERVICE_HOST, + }, + app: { + name: Bun.env.APP_NAME, + desc: Bun.env.APP_DESC, + contact_email: Bun.env.APP_CONTACT_EMAIL, + }, + cors: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "GET, POST, OPTIONS", + "Access-Control-Max-Age": "86400", + }, + logging: { + level: Bun.env.NODE_ENV === "production" ? "info" : "debug", + logPath: path.join(__dirname, "..", "logs"), + loki: { + host: Bun.env.LOKI_HOST, + user: Bun.env.LOKI_USER, + password: Bun.env.LOKI_PASSWORD, + label: Bun.env.LOKI_LABEL, + }, + }, + client: { + useWorker: Bun.env.USE_WORKER === "true", + workerHost: Bun.env.WORKER_HOST, + workerHostTH: Bun.env.WORKER_HOST_TH, + apiToken: Bun.env.API_TOKEN, + sessionId: Bun.env.SESSION_ID_COOKIE, + }, +}); diff --git a/src/controllers/health/index.ts b/src/controllers/health/index.ts new file mode 100644 index 0000000..85ca92c --- /dev/null +++ b/src/controllers/health/index.ts @@ -0,0 +1,23 @@ +import { Elysia } from "elysia"; + +import config from "../../config"; +import { HealthResponse } from "@/models/health.model"; + +export default new Elysia().group("/health", (app) => + app.get( + "/", + () => ({ + version: config.app.version, + status: "ok" as const, + }), + { + response: { + 200: HealthResponse, + }, + detail: { + summary: "Get health", + tags: ["Health"], + }, + }, + ), +); diff --git a/src/controllers/sharing/index.ts b/src/controllers/sharing/index.ts new file mode 100644 index 0000000..32310af --- /dev/null +++ b/src/controllers/sharing/index.ts @@ -0,0 +1,78 @@ +import { Elysia } from "elysia"; + +import { NeuroClient, NeuroWorkerClient } from "@toil/neurojs"; + +import config from "@/config"; +import { + GetSharingBody, + GetSharingResponse, + GetSharingUrlAPITokenNotFound, + GetSharingUrlBody, + GetSharingUrlNotFound, + GetSharingUrlResponse, +} from "@/models/sharing.model"; +import { SharingAPITokenNotFoundError, SharingUrlNotFoundError } from "@/errors"; + +const { + client: { useWorker, workerHost, workerHostTH, sessionId: sessionIdCookie, apiToken }, +} = config; + +const client = useWorker + ? new NeuroWorkerClient({ + host: workerHost, + hostTH: workerHostTH, + sessionIdCookie, + apiToken, + }) + : new NeuroClient({ + sessionIdCookie, + apiToken, + }); + +export default new Elysia() + .post( + "/sharing", + async ({ body: { token } }) => { + return (await client.getSharingContent({ + token, + })) as unknown as GetSharingResponse; + }, + { + body: GetSharingBody, + response: { + 200: GetSharingResponse, + }, + detail: { + summary: "Get sharing content by token", + tags: ["Sharing"], + }, + }, + ) + .post( + "/sharing-url", + async ({ body: { url } }) => { + if (!apiToken) { + throw new SharingAPITokenNotFoundError(); + } + + try { + return await client.getSharingUrl({ + url, + }); + } catch { + throw new SharingUrlNotFoundError(); + } + }, + { + body: GetSharingUrlBody, + response: { + 200: GetSharingUrlResponse, + 404: GetSharingUrlNotFound, + 503: GetSharingUrlAPITokenNotFound, + }, + detail: { + summary: "Get sharing url", + tags: ["Sharing"], + }, + }, + ); diff --git a/src/controllers/summarize/index.ts b/src/controllers/summarize/index.ts new file mode 100644 index 0000000..858ad2b --- /dev/null +++ b/src/controllers/summarize/index.ts @@ -0,0 +1,99 @@ +import { Elysia } from "elysia"; + +import { NeuroClient, NeuroWorkerClient } from "@toil/neurojs"; + +import config from "@/config"; +import { + ArticleSummarizeBody, + TextSummarizeBody, + VideoSummarizeBody, + TextSummarizeResponse, + ArticleSummarizeResponse, + VideoSummarizeResponse, +} from "@/models/summarize.model"; + +const { + client: { useWorker, workerHost, workerHostTH, sessionId: sessionIdCookie }, +} = config; + +const client = useWorker + ? new NeuroWorkerClient({ + host: workerHost, + hostTH: workerHostTH, + sessionIdCookie, + }) + : new NeuroClient({ + sessionIdCookie, + }); + +export default new Elysia().group("/summarize", (app) => + app + .post( + "/text", + async ({ body: { text, sessionId, bypassCache } }) => { + return (await client.summarizeText({ + text, + extraOpts: { + sessionId, + bypassCache, + }, + })) as unknown as TextSummarizeResponse; + }, + { + body: TextSummarizeBody, + response: { + 200: TextSummarizeResponse, + }, + detail: { + summary: "Summarize text", + tags: ["Summarize"], + }, + }, + ) + .post( + "/article", + async ({ body: { url, sessionId, bypassCache } }) => { + return (await client.summarizeArticle({ + url, + extraOpts: { + sessionId, + bypassCache, + }, + })) as unknown as ArticleSummarizeResponse; + }, + { + body: ArticleSummarizeBody, + response: { + 200: ArticleSummarizeResponse, + }, + detail: { + summary: "Summarize article", + tags: ["Summarize"], + }, + }, + ) + .post( + "/video", + async ({ body: { url, language, videoTitle, sessionId, bypassCache } }) => { + return (await client.summarizeVideo({ + url, + language, + extraOpts: { + sessionId, + videoTitle, + bypassCache, + }, + })) as unknown as VideoSummarizeResponse; + }, + { + body: VideoSummarizeBody, + response: { + 200: VideoSummarizeResponse, + }, + detail: { + summary: "Summarize video", + tags: ["Summarize"], + }, + }, + ), +); diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..b4accb3 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,11 @@ +export class SharingUrlNotFoundError extends Error { + constructor() { + super("Sharing url not found"); + } +} + +export class SharingAPITokenNotFoundError extends Error { + constructor() { + super("Server is missing an API token. This endpoint is unavailable"); + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..dcb833d --- /dev/null +++ b/src/index.ts @@ -0,0 +1,94 @@ +import fs from "node:fs/promises"; + +import { Elysia } from "elysia"; +import { swagger } from "@elysiajs/swagger"; +import { HttpStatusCode } from "elysia-http-status-code"; + +import config from "./config"; +import { log } from "./logging"; + +import health from "./controllers/health"; +import summarizeController from "./controllers/summarize"; +import sharingController from "./controllers/sharing"; +import { SharingAPITokenNotFoundError, SharingUrlNotFoundError } from "./errors"; + +if (!(await fs.exists(config.logging.logPath))) { + await fs.mkdir(config.logging.logPath, { recursive: true }); + log.info(`Created log directory`); +} + +const app = new Elysia({ + prefix: "/v2", +}) + .use( + swagger({ + path: "/docs", + scalarCDN: config.app.scalarCDN, + scalarConfig: { + spec: { + url: "/v2/docs/json", + }, + }, + documentation: { + info: { + title: config.app.name, + description: config.app.desc, + version: config.app.version, + license: { + name: config.app.license, + }, + contact: { + name: "Developer", + url: config.app.github_url, + email: config.app.contact_email, + }, + }, + }, + }), + ) + .use(HttpStatusCode()) + .onRequest(({ set }) => { + for (const [key, val] of Object.entries(config.cors)) { + set.headers[key] = val; + } + }) + .error({ + SHARING_URL_NOT_FOUND: SharingUrlNotFoundError, + SHARING_API_TOKEN_NOT_FOUND: SharingAPITokenNotFoundError, + }) + .onError(({ set, code, error, httpStatus }) => { + switch (code) { + case "NOT_FOUND": + return { + detail: "Route not found :(", + }; + case "SHARING_URL_NOT_FOUND": + set.status = httpStatus.HTTP_400_BAD_REQUEST; + break; + case "SHARING_API_TOKEN_NOT_FOUND": + set.status = httpStatus.HTTP_503_SERVICE_UNAVAILABLE; + break; + case "VALIDATION": + return error.all; + } + + log.error( + { + message: error.message, + }, + code, + ); + + return { + error: error.message, + }; + }) + .use(health) + .use(summarizeController) + .use(sharingController) + .listen({ + port: config.server.port, + hostname: config.server.hostname, + }); + +log.info(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`); diff --git a/src/logging.ts b/src/logging.ts new file mode 100644 index 0000000..63dc0ff --- /dev/null +++ b/src/logging.ts @@ -0,0 +1,64 @@ +import * as path from "node:path"; +import { pino, type TransportMultiOptions, type TransportTargetOptions } from "pino"; + +import config from "@/config"; + +const { loki } = config.logging; +const startingDate = new Date().toISOString().split("T")[0]; + +type PinoOpts = Parameters[0] & { + transport: TransportMultiOptions & { targets: TransportTargetOptions[] }; +}; + +// https://github.com/pinojs/pino/issues/1791 +// if don't take out the options separately, it willn't work +const opts: PinoOpts = { + level: config.logging.level, + redact: { + // these just cause clutter + paths: ["pid", "hostname"], + remove: true, + }, + transport: { + targets: [], + }, +}; + +// it may be higher level than global, but it cann't be lower +opts.transport.targets.push({ + level: config.logging.level, + target: "pino-pretty", + options: { + colorized: true, + }, +}); + +opts.transport.targets.push({ + level: config.logging.level, + target: "pino/file", + options: { + destination: path.join(config.logging.logPath, `${startingDate}.log`), + }, +}); + +if (loki.host) { + opts.transport.targets.push({ + level: config.logging.level, + target: "pino-loki", + options: { + batching: true, + interval: 5, + labels: { application: config.logging.loki.label }, + host: loki.host, + basicAuth: + loki.user && loki.password + ? { + username: loki.user, + password: loki.password, + } + : undefined, + }, + }); +} + +export const log = pino(opts); diff --git a/src/models/health.model.ts b/src/models/health.model.ts new file mode 100644 index 0000000..c40c993 --- /dev/null +++ b/src/models/health.model.ts @@ -0,0 +1,8 @@ +import { t } from "elysia"; + +export const HealthResponse = t.Object({ + version: t.String(), + status: t.Literal("ok", { + default: "ok", + }), +}); diff --git a/src/models/sharing.model.ts b/src/models/sharing.model.ts new file mode 100644 index 0000000..0d01b4c --- /dev/null +++ b/src/models/sharing.model.ts @@ -0,0 +1,45 @@ +import { GetSharingUrlSuccess as GetSharingUrlSuccessOG } from "@toil/neurojs/typebox/thapi"; +import { + TextSummarizeResponse as TextSummarizeResponseOG, + ArticleSummarizeResponse as ArticleSummarizeResponseOG, + SharingVideoSummarizeResponse as SharingVideoSummarizeResponseOG, +} from "@toil/neurojs/typebox/yandex"; +import { t, Static } from "elysia"; + +export const GetSharingBody = t.Object({ + token: t.String({ + description: "Token from sharing url", + examples: ["hoOAM7gs"], + }), +}); + +export const GetSharingUrlBody = t.Object({ + url: t.String({ + description: "Url for summarize", + format: "uri", + examples: ["https://habr.com/ru/news/729422"], + }), +}); + +// use composite for fix binding types with elysia typebox +export const GetSharingResponse = t.Union([ + TextSummarizeResponseOG, + ArticleSummarizeResponseOG, + SharingVideoSummarizeResponseOG, +]); +export type GetSharingResponse = Static; + +export const GetSharingUrlResponse = t.Composite([GetSharingUrlSuccessOG]); +export type GetSharingUrlResponse = Static; + +export const GetSharingUrlNotFound = t.Object({ + error: t.Literal("Sharing url not found", { + default: "Sharing url not found", + }), +}); + +export const GetSharingUrlAPITokenNotFound = t.Object({ + error: t.Literal("Server is missing an API token. This endpoint is unavailable", { + default: "Server is missing an API token. This endpoint is unavailable", + }), +}); diff --git a/src/models/summarize.model.ts b/src/models/summarize.model.ts new file mode 100644 index 0000000..d425634 --- /dev/null +++ b/src/models/summarize.model.ts @@ -0,0 +1,65 @@ +import { + TextSummarizeResponse as TextSummarizeResponseOG, + ArticleSummarizeResponse as ArticleSummarizeResponseOG, + VideoSummarizeResponse as VideoSummarizeResponseOG, +} from "@toil/neurojs/typebox/yandex"; +import { t, Static } from "elysia"; + +export const SessionId = t.String({ + description: "Session ID from first response", + examples: ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"], +}); + +export const BypassCache = t.Boolean({ + description: "Bypass cache", + default: false, + examples: [false], +}); + +export const TextSummarizeBody = t.Object({ + text: t.String({ + minLength: 300, + description: "Text for summarize", + examples: ["Very long text with 300+ symbols"], + }), + sessionId: t.Optional(SessionId), + bypassCache: t.Optional(BypassCache), +}); + +export const ArticleSummarizeBody = t.Object({ + url: t.String({ + description: "Url for summarize", + format: "uri", + examples: ["https://toil.cc"], + }), + sessionId: t.Optional(SessionId), + bypassCache: t.Optional(BypassCache), +}); + +export const VideoSummarizeBody = t.Object({ + url: t.String({ + description: "Video url for summarize", + format: "uri", + examples: ["https://youtu.be/dQw4w9WgXcQ"], + }), + language: t.String({ + description: "Video language", + default: "en", + }), + videoTitle: t.Optional( + t.String({ + description: "Video title", + examples: ["Rick Astley - Never Gonna Give You Up (Official Music Video)"], + }), + ), + sessionId: t.Optional(SessionId), + bypassCache: t.Optional(BypassCache), +}); + +// use composite for fix binding types with elysia typebox +export const TextSummarizeResponse = t.Composite([TextSummarizeResponseOG]); +export type TextSummarizeResponse = Static; +export const ArticleSummarizeResponse = t.Composite([ArticleSummarizeResponseOG]); +export type ArticleSummarizeResponse = Static; +export const VideoSummarizeResponse = t.Composite([VideoSummarizeResponseOG]); +export type VideoSummarizeResponse = Static; diff --git a/src/schemas/config.ts b/src/schemas/config.ts new file mode 100644 index 0000000..ef2cfdd --- /dev/null +++ b/src/schemas/config.ts @@ -0,0 +1,69 @@ +import { Type as t, type Static } from "@sinclair/typebox"; + +import { version } from "../../package.json"; + +export const LoggingLevel = t.Union( + [ + t.Literal("info"), + t.Literal("debug"), + t.Literal("fatal"), + t.Literal("error"), + t.Literal("warn"), + t.Literal("trace"), + ], + { + default: "info", + }, +); + +const license = "MIT"; +const scalarCDN = "https://unpkg.com/@scalar/api-reference@1.25.118/dist/browser/standalone.js"; + +export const ConfigSchema = t.Object({ + server: t.Object({ + port: t.Number({ default: 3312 }), + hostname: t.String({ default: "0.0.0.0" }), + }), + app: t.Object({ + name: t.String({ default: "[FOSWLY] Summarize" }), + desc: t.String({ + default: + "[FOSWLY] Summarize is a server that implements unified endpoints for summarize logic from @toil/neurojs library", + }), + version: t.Literal(version, { readOnly: true, default: version }), + license: t.Literal(license, { readOnly: true, default: license }), + github_url: t.String({ + default: "https://github.com/FOSWLY/summarize-backend", + }), + contact_email: t.String({ default: "me@toil.cc" }), + scalarCDN: t.Literal(scalarCDN, { readOnly: true, default: scalarCDN }), + }), + cors: t.Object({ + "Access-Control-Allow-Origin": t.String({ default: "*" }), + "Access-Control-Allow-Headers": t.String({ default: "*" }), + "Access-Control-Allow-Methods": t.String({ default: "POST, GET, OPTIONS" }), + "Access-Control-Max-Age": t.String({ default: "86400" }), + }), + logging: t.Object({ + level: LoggingLevel, + logPath: t.String(), + loki: t.Object({ + host: t.String({ default: "" }), + user: t.String({ default: "" }), + password: t.String({ default: "" }), + label: t.String({ default: "summarize-backend" }), + }), + }), + client: t.Object({ + // use yandex server directly by default + useWorker: t.Boolean({ default: false }), + workerHost: t.String({ default: "http://127.0.0.1:7674/browser" }), + workerHostTH: t.String({ default: "http://127.0.0.1:7674/th" }), + // allow get sharing url + apiToken: t.Optional(t.String()), + // use cookie Session_id instead of YaHMAC (summarize video support only YaHMAC) + sessionId: t.Optional(t.String()), + }), +}); + +export type ConfigSchemaType = Static; diff --git a/src/types/global.d.ts b/src/types/global.d.ts new file mode 100644 index 0000000..264c308 --- /dev/null +++ b/src/types/global.d.ts @@ -0,0 +1,19 @@ +declare module "bun" { + interface Env { + SERVICE_HOST: string; + SERVICE_PORT: number; + APP_NAME: string; + APP_DESC: string; + APP_CONTACT_EMAIL: string; + LOKI_HOST: string; + LOKI_USER: string; + LOKI_PASSWORD: string; + LOKI_LABEL: string; + USE_WORKER: string; + WORKER_HOST: string; + WORKER_HOST_TH: string; + API_TOKEN: string; + SESSION_ID_COOKIE: string; + NODE_ENV: string; + } +} diff --git a/static/assets/logo.svg b/static/assets/logo.svg deleted file mode 100644 index a3e7259..0000000 --- a/static/assets/logo.svg +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..460e4c9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "module": "ES2022" /* Specify what module code is generated. */, + "moduleResolution": "Bundler" /* Specify how TypeScript looks up a file from a given module specifier. */, + "types": [ + "bun-types" + ] /* Specify type package names to be included without being referenced in a source file. */, + "resolveJsonModule": true /* Enable importing .json files. */, + "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */, + "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, + + "strict": true /* Enable all strict type-checking options. */, + "paths": { + "@/*": ["./src/*"] + }, + + "skipLibCheck": true /* Skip type checking all .d.ts files. */ + } +}